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 }