go-libwebp

Experimental translation from libwebp to Go source.
Log | Files | Refs | README | LICENSE

corpus.go (9145B)


      1 package main
      2 
      3 import (
      4 	"image"
      5 	"image/color"
      6 	"math"
      7 	"math/rand"
      8 	"runtime"
      9 	"sync"
     10 )
     11 
     12 // Corpus entries are chosen to exercise the code paths a WebP encoder
     13 // actually branches on: predictor effectiveness, palette/colour-cache
     14 // eligibility, the separate alpha plane, and the transform's response to
     15 // smooth versus high-frequency content.
     16 type Entry struct {
     17 	Name string
     18 	Desc string
     19 	Img  *image.NRGBA
     20 }
     21 
     22 func BuildCorpus() []Entry {
     23 	return []Entry{
     24 		{"flat-128", "solid colour, 128x128 — best case for every path",
     25 			flat(128, 128)},
     26 		{"gradient-512", "smooth 2D gradient, 512x384 — spatial predictors win",
     27 			gradient(512, 384)},
     28 		{"photo-512", "synthetic photographic fBm, 512x384 — realistic lossy load",
     29 			photo(512, 384, 1)},
     30 		{"photo-1280", "synthetic photographic fBm, 1280x720 — scaling check",
     31 			photo(1280, 720, 2)},
     32 		{"noise-512", "uniform random RGB, 512x384 — incompressible worst case",
     33 			noise(512, 384, 3)},
     34 		{"noise-128", "uniform random RGB, 128x128 — worst case, small",
     35 			noise(128, 128, 4)},
     36 		{"text-512", "sharp black-on-white line art, 512x384 — hard for lossy",
     37 			text(512, 384)},
     38 		{"palette-512", "16 distinct colours in flat regions, 512x384 — VP8L palette",
     39 			palette(512, 384, 5)},
     40 		{"alpha-512", "photographic RGB under a radial alpha ramp, 512x384",
     41 			alpha(512, 384, 6)},
     42 		{"stripes-512", "1px high-frequency periodic pattern, 512x384",
     43 			stripes(512, 384)},
     44 		{"mandelbrot-512", "escape-time fractal, 512x384 — flat interior, smooth bands, fractal edge",
     45 			mandelbrot(512, 384)},
     46 		{"photo-alpha-1280", "fBm with binary alpha cutout, 1280x720",
     47 			photoAlpha(1280, 720, 7)},
     48 		{"mandelbrot-3840", "escape-time fractal, 3840x2160 — very large, 8.3 MP",
     49 			mandelbrot(3840, 2160)},
     50 		{"photo-3840", "synthetic photographic fBm, 3840x2160 — very large, 8.3 MP",
     51 			photo(3840, 2160, 8)},
     52 	}
     53 }
     54 
     55 func newImg(w, h int) *image.NRGBA { return image.NewNRGBA(image.Rect(0, 0, w, h)) }
     56 
     57 // eachRow runs fn over every row, in parallel. Generators are pure functions
     58 // of (x, y) so the result does not depend on scheduling.
     59 func eachRow(h int, fn func(y int)) {
     60 	workers := runtime.NumCPU()
     61 	if workers > h {
     62 		workers = h
     63 	}
     64 	var wg sync.WaitGroup
     65 	for w := 0; w < workers; w++ {
     66 		wg.Add(1)
     67 		go func(start int) {
     68 			defer wg.Done()
     69 			for y := start; y < h; y += workers {
     70 				fn(y)
     71 			}
     72 		}(w)
     73 	}
     74 	wg.Wait()
     75 }
     76 
     77 // mandelbrot renders the classic view of the set with smooth (continuous)
     78 // escape-time colouring. It is a useful corpus entry because one image holds
     79 // three regimes at once: a large flat interior, wide smooth exterior bands
     80 // that spatial predictors handle well, and a boundary with detail at every
     81 // scale that they cannot.
     82 func mandelbrot(w, h int) *image.NRGBA {
     83 	const maxIter = 512
     84 	m := newImg(w, h)
     85 	// Full set, with the aspect ratio driven by the output size.
     86 	const cx, halfW = -0.7, 1.5
     87 	halfH := halfW * float64(h) / float64(w)
     88 
     89 	eachRow(h, func(py int) {
     90 		ci := (float64(py)/float64(h)*2 - 1) * halfH
     91 		for px := 0; px < w; px++ {
     92 			cr := cx + (float64(px)/float64(w)*2-1)*halfW
     93 			var zr, zi float64
     94 			n := 0
     95 			for ; n < maxIter; n++ {
     96 				zr2, zi2 := zr*zr, zi*zi
     97 				if zr2+zi2 > 4 {
     98 					break
     99 				}
    100 				zr, zi = zr2-zi2+cr, 2*zr*zi+ci
    101 			}
    102 			if n == maxIter {
    103 				m.SetNRGBA(px, py, color.NRGBA{A: 255}) // interior: flat black
    104 				continue
    105 			}
    106 			// Continuous escape time, so the bands are smooth ramps rather
    107 			// than posterised steps.
    108 			mag := math.Sqrt(zr*zr + zi*zi)
    109 			smooth := float64(n) + 1 - math.Log(math.Log(mag))/math.Ln2
    110 			t := smooth / maxIter
    111 			m.SetNRGBA(px, py, color.NRGBA{
    112 				R: uint8(clamp(255 * (0.5 + 0.5*math.Cos(3+t*20)))),
    113 				G: uint8(clamp(255 * (0.5 + 0.5*math.Cos(3+t*20+2.1)))),
    114 				B: uint8(clamp(255 * (0.5 + 0.5*math.Cos(3+t*20+4.2)))),
    115 				A: 255,
    116 			})
    117 		}
    118 	})
    119 	return m
    120 }
    121 
    122 func flat(w, h int) *image.NRGBA {
    123 	m := newImg(w, h)
    124 	for i := 0; i < len(m.Pix); i += 4 {
    125 		m.Pix[i], m.Pix[i+1], m.Pix[i+2], m.Pix[i+3] = 0x3a, 0x7b, 0xd5, 0xff
    126 	}
    127 	return m
    128 }
    129 
    130 func gradient(w, h int) *image.NRGBA {
    131 	m := newImg(w, h)
    132 	for y := 0; y < h; y++ {
    133 		for x := 0; x < w; x++ {
    134 			m.SetNRGBA(x, y, color.NRGBA{
    135 				R: uint8(255 * x / w),
    136 				G: uint8(255 * y / h),
    137 				B: uint8(255 * (x + y) / (w + h)),
    138 				A: 255,
    139 			})
    140 		}
    141 	}
    142 	return m
    143 }
    144 
    145 // fbm is value noise summed over octaves: smooth at low frequency with detail
    146 // at high frequency, which is what makes real photographs compress the way
    147 // they do.
    148 func fbm(x, y float64, seed int64) float64 {
    149 	var sum, amp, norm float64 = 0, 1, 0
    150 	freq := 1.0 / 64.0
    151 	for o := 0; o < 5; o++ {
    152 		sum += amp * valueNoise(x*freq, y*freq, seed+int64(o))
    153 		norm += amp
    154 		amp *= 0.5
    155 		freq *= 2
    156 	}
    157 	return sum / norm
    158 }
    159 
    160 func valueNoise(x, y float64, seed int64) float64 {
    161 	xi, yi := math.Floor(x), math.Floor(y)
    162 	xf, yf := x-xi, y-yi
    163 	sx := xf * xf * (3 - 2*xf) // smoothstep
    164 	sy := yf * yf * (3 - 2*yf)
    165 	n00 := hash2(int64(xi), int64(yi), seed)
    166 	n10 := hash2(int64(xi)+1, int64(yi), seed)
    167 	n01 := hash2(int64(xi), int64(yi)+1, seed)
    168 	n11 := hash2(int64(xi)+1, int64(yi)+1, seed)
    169 	return (n00*(1-sx)+n10*sx)*(1-sy) + (n01*(1-sx)+n11*sx)*sy
    170 }
    171 
    172 func hash2(x, y, seed int64) float64 {
    173 	n := x*374761393 + y*668265263 + seed*1442695040888963407
    174 	n = (n ^ (n >> 13)) * 1274126177
    175 	return float64(uint32(n^(n>>16))) / float64(math.MaxUint32)
    176 }
    177 
    178 func photo(w, h int, seed int64) *image.NRGBA {
    179 	m := newImg(w, h)
    180 	cx, cy := float64(w)/2, float64(h)/2
    181 	maxr := math.Hypot(cx, cy)
    182 	for y := 0; y < h; y++ {
    183 		for x := 0; x < w; x++ {
    184 			fx, fy := float64(x), float64(y)
    185 			// Vignette gives a large-scale luminance ramp on top of the
    186 			// detail, as in a real photo.
    187 			vig := 1 - 0.35*math.Hypot(fx-cx, fy-cy)/maxr
    188 			r := fbm(fx, fy, seed) * vig
    189 			g := fbm(fx+512, fy+512, seed) * vig
    190 			b := fbm(fx+1024, fy+1024, seed) * vig
    191 			m.SetNRGBA(x, y, color.NRGBA{
    192 				R: uint8(clamp(r * 255)), G: uint8(clamp(g * 255)),
    193 				B: uint8(clamp(b * 255)), A: 255,
    194 			})
    195 		}
    196 	}
    197 	return m
    198 }
    199 
    200 func clamp(v float64) float64 { return math.Max(0, math.Min(255, v)) }
    201 
    202 func noise(w, h int, seed int64) *image.NRGBA {
    203 	m := newImg(w, h)
    204 	rng := rand.New(rand.NewSource(seed))
    205 	for i := 0; i < len(m.Pix); i += 4 {
    206 		v := rng.Uint32()
    207 		m.Pix[i], m.Pix[i+1], m.Pix[i+2] = uint8(v), uint8(v>>8), uint8(v>>16)
    208 		m.Pix[i+3] = 0xff
    209 	}
    210 	return m
    211 }
    212 
    213 // text draws axis-aligned bars and diagonals: pure two-tone, hard edges. This
    214 // is the content class lossy WebP handles worst and lossless handles best.
    215 func text(w, h int) *image.NRGBA {
    216 	m := newImg(w, h)
    217 	for i := 0; i < len(m.Pix); i += 4 {
    218 		m.Pix[i], m.Pix[i+1], m.Pix[i+2], m.Pix[i+3] = 0xff, 0xff, 0xff, 0xff
    219 	}
    220 	ink := color.NRGBA{A: 255}
    221 	for row := 0; row < h/24; row++ {
    222 		y0 := row*24 + 6
    223 		for gl := 0; gl < w/16; gl++ {
    224 			x0 := gl * 16
    225 			// Vary the shape per cell so it is not one repeated motif.
    226 			switch (row*7 + gl*3) % 4 {
    227 			case 0:
    228 				fillRect(m, x0+2, y0, 8, 12, ink)
    229 			case 1:
    230 				fillRect(m, x0+2, y0, 2, 12, ink)
    231 				fillRect(m, x0+2, y0+5, 8, 2, ink)
    232 			case 2:
    233 				for d := 0; d < 12; d++ {
    234 					fillRect(m, x0+2+d/2, y0+d, 2, 1, ink)
    235 				}
    236 			case 3:
    237 				fillRect(m, x0+2, y0, 8, 2, ink)
    238 				fillRect(m, x0+6, y0, 2, 12, ink)
    239 			}
    240 		}
    241 	}
    242 	return m
    243 }
    244 
    245 func fillRect(m *image.NRGBA, x, y, w, h int, c color.NRGBA) {
    246 	b := m.Bounds()
    247 	for yy := y; yy < y+h && yy < b.Max.Y; yy++ {
    248 		for xx := x; xx < x+w && xx < b.Max.X; xx++ {
    249 			m.SetNRGBA(xx, yy, c)
    250 		}
    251 	}
    252 }
    253 
    254 // palette builds large flat regions drawn from a 16-entry colour set, which
    255 // is what makes VP8L take its palette / colour-cache path.
    256 func palette(w, h int, seed int64) *image.NRGBA {
    257 	rng := rand.New(rand.NewSource(seed))
    258 	var pal [16]color.NRGBA
    259 	for i := range pal {
    260 		pal[i] = color.NRGBA{
    261 			R: uint8(rng.Intn(256)), G: uint8(rng.Intn(256)),
    262 			B: uint8(rng.Intn(256)), A: 255,
    263 		}
    264 	}
    265 	m := newImg(w, h)
    266 	const cell = 17 // deliberately not a macroblock multiple
    267 	for y := 0; y < h; y++ {
    268 		for x := 0; x < w; x++ {
    269 			m.SetNRGBA(x, y, pal[((x/cell)*5+(y/cell)*3)%16])
    270 		}
    271 	}
    272 	return m
    273 }
    274 
    275 // alpha exercises the separate alpha-plane encoder with a smooth ramp.
    276 func alpha(w, h int, seed int64) *image.NRGBA {
    277 	m := photo(w, h, seed)
    278 	cx, cy := float64(w)/2, float64(h)/2
    279 	maxr := math.Hypot(cx, cy)
    280 	for y := 0; y < h; y++ {
    281 		for x := 0; x < w; x++ {
    282 			d := math.Hypot(float64(x)-cx, float64(y)-cy) / maxr
    283 			m.Pix[m.PixOffset(x, y)+3] = uint8(clamp(255 * (1 - d)))
    284 		}
    285 	}
    286 	return m
    287 }
    288 
    289 // photoAlpha uses a hard-edged alpha cutout rather than a ramp: the alpha
    290 // plane is then highly compressible while the colour plane is not.
    291 func photoAlpha(w, h int, seed int64) *image.NRGBA {
    292 	m := photo(w, h, seed)
    293 	cx, cy := float64(w)/2, float64(h)/2
    294 	r := math.Min(cx, cy) * 0.8
    295 	for y := 0; y < h; y++ {
    296 		for x := 0; x < w; x++ {
    297 			var a uint8
    298 			if math.Hypot(float64(x)-cx, float64(y)-cy) < r {
    299 				a = 255
    300 			}
    301 			m.Pix[m.PixOffset(x, y)+3] = a
    302 		}
    303 	}
    304 	return m
    305 }
    306 
    307 func stripes(w, h int) *image.NRGBA {
    308 	m := newImg(w, h)
    309 	for y := 0; y < h; y++ {
    310 		for x := 0; x < w; x++ {
    311 			v := uint8(0)
    312 			if (x+y)%2 == 0 {
    313 				v = 255
    314 			}
    315 			m.SetNRGBA(x, y, color.NRGBA{R: v, G: uint8(x % 256), B: 255 - v, A: 255})
    316 		}
    317 	}
    318 	return m
    319 }