github.com/kintar/etxt@v0.0.9/emask/edge_marker_test.go (about) 1 //go:build gtxt 2 3 package emask 4 5 import "time" 6 import "math" 7 import "math/rand" 8 import "testing" 9 10 import "os" 11 import "strconv" 12 import "image" 13 import "image/png" 14 import "image/color" 15 16 import "golang.org/x/image/math/fixed" 17 18 func TestEdgeAlignedRects(t *testing.T) { 19 tests := []struct { 20 in []float64 // one moveTo + many lineTo coords 21 out []float64 // output buffer (5x4) 22 }{ 23 { // small square 24 in: []float64{1, 1, 1, 3, 3, 3, 3, 1, 1, 1}, 25 out: []float64{ 26 0, 0, 0, 0, 0, 27 0, 1, 0, -1, 0, 28 0, 1, 0, -1, 0, 29 0, 0, 0, 0, 0, 30 }, 31 }, 32 { // full canvas rect 33 in: []float64{0, 0, 0, 4, 5, 4, 5, 0, 0, 0}, 34 out: []float64{ 35 1, 0, 0, 0, 0, 36 1, 0, 0, 0, 0, 37 1, 0, 0, 0, 0, 38 1, 0, 0, 0, 0, 39 }, 40 }, 41 { // large canvas square 42 in: []float64{0, 0, 0, 4, 4, 4, 4, 0, 0, 0}, 43 out: []float64{ 44 1, 0, 0, 0, -1, 45 1, 0, 0, 0, -1, 46 1, 0, 0, 0, -1, 47 1, 0, 0, 0, -1, 48 }, 49 }, 50 { // large outside rect 51 in: []float64{-5, 0, -5, 4, 4, 4, 4, 0, -5, 0}, 52 out: []float64{ 53 1, 0, 0, 0, -1, 54 1, 0, 0, 0, -1, 55 1, 0, 0, 0, -1, 56 1, 0, 0, 0, -1, 57 }, 58 }, 59 { // smaller outside rect 60 in: []float64{-5, 1, -5, 3, 4, 3, 4, 1, -5, 1}, 61 out: []float64{ 62 0, 0, 0, 0, 0, 63 1, 0, 0, 0, -1, 64 1, 0, 0, 0, -1, 65 0, 0, 0, 0, 0, 66 }, 67 }, 68 { // shape 69 in: []float64{0, 1, 0, 3, 1, 3, 1, 4, 2, 4, 2, 2, 3, 2, 3, 1, 0, 1}, 70 out: []float64{ 71 0, 0, 0, 0, 0, 72 1, 0, 0, -1, 0, 73 1, 0, -1, 0, 0, 74 0, 1, -1, 0, 0, 75 }, 76 }, 77 } 78 79 emarker := edgeMarker{} 80 emarker.Buffer.Resize(5, 4) 81 for n, test := range tests { 82 emarker.MoveTo(test.in[0], test.in[1]) 83 for i := 2; i < len(test.in); i += 2 { 84 emarker.LineTo(test.in[i], test.in[i+1]) 85 } 86 if !similarFloat64Slices(test.out, emarker.Buffer.Values) { 87 t.Fatalf("test#%d, on input %v, expected %v, got %v", n, test.in, test.out, emarker.Buffer.Values) 88 } 89 emarker.Buffer.Clear() 90 } 91 } 92 93 func TestEdgeAlignedTriangles(t *testing.T) { 94 tests := []struct { 95 in []float64 // one moveTo + many lineTo coords 96 out []float64 // output buffer (4x4) 97 }{ 98 { // right triangle 99 in: []float64{0, 0, 0, 4, 4, 4, 0, 0}, 100 out: []float64{ 101 0.5, -0.5, 0.0, 0.0, 102 1.0, -0.5, -0.5, 0.0, 103 1.0, 0.0, -0.5, -0.5, 104 1.0, 0.0, 0, -0.5, 105 }, 106 }, 107 { // right triangle, alternative orientation (both shape and def. direction) 108 in: []float64{0, 0, 4, 0, 0, 4, 0, 0}, 109 out: []float64{ 110 -1.0, 0.0, 0.0, 0.5, 111 -1.0, 0.0, 0.5, 0.5, 112 -1.0, 0.5, 0.5, 0.0, 113 -0.5, 0.5, 0.0, 0.0, 114 }, 115 }, 116 { // triangle with wide base 117 in: []float64{0, 0, 2, 4, 4, 0, 0, 0}, 118 out: []float64{ 119 0.75, 0.25, 0.0, -0.25, 120 0.25, 0.75, 0.0, -0.75, 121 0.00, 0.75, 0.0, -0.75, 122 0.00, 0.25, 0.0, -0.25, 123 }, 124 }, 125 { // slimmer right triangle, tricky fill proportions 126 in: []float64{0, 0, 0, 4, 3, 4, 0, 0}, 127 out: []float64{ 128 3 / 8., -3 / 8., 0.0, 0.0, 129 23 / 24., -19 / 24., -4 / 24., 0.0, 130 1.0, -1 / 6., -19 / 24., -1 / 24., 131 1.0, 0.0, -3 / 8., -5 / 8., 132 }, 133 }, 134 { // slimmer right triangle, alternative orientation 135 in: []float64{0, 0, 3, 0, 0, 4, 0, 0}, 136 out: []float64{ 137 -1.0, 0.0, 3 / 8., 5 / 8., 138 -1.0, 1 / 6., 19 / 24., 1 / 24., 139 -23 / 24., 19 / 24., 4 / 24., 0.0, 140 -3 / 8., 3 / 8., 0.0, 0.0, 141 }, 142 }, 143 } 144 145 emarker := edgeMarker{} 146 emarker.Buffer.Resize(4, 4) 147 for n, test := range tests { 148 emarker.MoveTo(test.in[0], test.in[1]) 149 for i := 2; i < len(test.in); i += 2 { 150 emarker.LineTo(test.in[i], test.in[i+1]) 151 } 152 if !similarFloat64Slices(test.out, emarker.Buffer.Values) { 153 t.Fatalf("test#%d, on input %v, expected %v, got %v", n, test.in, test.out, emarker.Buffer.Values) 154 } 155 emarker.Buffer.Clear() 156 } 157 } 158 159 func TestEdgeUnalignedRects(t *testing.T) { 160 tests := []struct { 161 in []float64 // one moveTo + many lineTo coords 162 out []float64 // output buffer (4x4) 163 }{ 164 { // shifted square 165 in: []float64{0.5, 0, 0.5, 4, 2.5, 4, 2.5, 0, 0.5, 0}, 166 out: []float64{ 167 0.5, 0.5, -0.5, -0.5, 168 0.5, 0.5, -0.5, -0.5, 169 0.5, 0.5, -0.5, -0.5, 170 0.5, 0.5, -0.5, -0.5, 171 }, 172 }, 173 { // shifted square, in both axes 174 in: []float64{0.5, 0.5, 0.5, 3.5, 2.5, 3.5, 2.5, 0.5, 0.5, 0.5}, 175 out: []float64{ 176 0.25, 0.25, -0.25, -0.25, 177 0.50, 0.50, -0.50, -0.50, 178 0.50, 0.50, -0.50, -0.50, 179 0.25, 0.25, -0.25, -0.25, 180 }, 181 }, 182 { // slightly shifted square 183 in: []float64{0.2, 0, 0.2, 4, 2.2, 4, 2.2, 0, 0.2, 0}, 184 out: []float64{ 185 0.8, 0.2, -0.8, -0.2, 186 0.8, 0.2, -0.8, -0.2, 187 0.8, 0.2, -0.8, -0.2, 188 0.8, 0.2, -0.8, -0.2, 189 }, 190 }, 191 { // significantly shifted square 192 in: []float64{0.8, 0, 0.8, 4, 2.8, 4, 2.8, 0, 0.8, 0}, 193 out: []float64{ 194 0.2, 0.8, -0.2, -0.8, 195 0.2, 0.8, -0.2, -0.8, 196 0.2, 0.8, -0.2, -0.8, 197 0.2, 0.8, -0.2, -0.8, 198 }, 199 }, 200 } 201 202 emarker := edgeMarker{} 203 emarker.Buffer.Resize(4, 4) 204 for n, test := range tests { 205 emarker.MoveTo(test.in[0], test.in[1]) 206 for i := 2; i < len(test.in); i += 2 { 207 emarker.LineTo(test.in[i], test.in[i+1]) 208 } 209 if !similarFloat64Slices(test.out, emarker.Buffer.Values) { 210 t.Fatalf("test#%d, on input %v, expected %v, got %v", n, test.in, test.out, emarker.Buffer.Values) 211 } 212 emarker.Buffer.Clear() 213 } 214 } 215 216 func TestEdgeSinglePixel(t *testing.T) { 217 tests := []struct { 218 in []float64 // one moveTo + many lineTo coords 219 out []float64 // output buffer (5x4) 220 }{ 221 { // pix square 222 in: []float64{0, 0, 0, 1, 1, 1, 1, 0, 0, 0}, 223 out: []float64{1, -1}, 224 }, 225 { // half-pix square 226 in: []float64{0.5, 0, 0.5, 1, 1, 1, 1, 0, 0.5, 0}, 227 out: []float64{0.5, -0.5}, 228 }, 229 } 230 231 emarker := edgeMarker{} 232 emarker.Buffer.Resize(2, 1) 233 for n, test := range tests { 234 emarker.MoveTo(test.in[0], test.in[1]) 235 for i := 2; i < len(test.in); i += 2 { 236 emarker.LineTo(test.in[i], test.in[i+1]) 237 } 238 if !similarFloat64Slices(test.out, emarker.Buffer.Values) { 239 t.Fatalf("test#%d, on input %v, expected %v, got %v", n, test.in, test.out, emarker.Buffer.Values) 240 } 241 emarker.Buffer.Clear() 242 } 243 } 244 245 func TestCompareEdgeAndStdRasts(t *testing.T) { 246 const useTimeSeed = false 247 const avgCmpTolerance = 2.0 // alpha value per 255 248 const canvasWidth = 80 249 const canvasHeight = 80 250 251 seed := time.Now().UnixNano() 252 if !useTimeSeed { 253 seed = 8623001 254 } 255 rng := rand.New(rand.NewSource(seed)) // * 256 // * Variable time seed works most of the time, but in some 257 // cases there are still differences that are big enough to 258 // be reported as failing tests. 259 // Still, I decided to switch to a static seed in order to make 260 // life more peaceful. Even if there's a bug, I'll wait until 261 // I come across a problematic case that happens in real life... 262 // instead of these synthetic tests. 263 264 stdRasterizer := &DefaultRasterizer{} 265 edgeRasterizer := NewStdEdgeMarkerRasterizer() 266 for n := 0; n < 30; n++ { 267 // create random shape 268 shape := randomShape(rng, 16, canvasWidth, canvasHeight) 269 segments := shape.Segments() 270 271 // rasterize with both rasterizers 272 stdMask, err := Rasterize(segments, stdRasterizer, fixed.Point26_6{}) 273 if err != nil { 274 t.Fatalf("stdRast error: %s", err.Error()) 275 } 276 edgeMask, err := Rasterize(segments, edgeRasterizer, fixed.Point26_6{}) 277 if err != nil { 278 t.Fatalf("edgeRast error: %s", err.Error()) 279 } 280 281 // compare results 282 if len(stdMask.Pix) != len(edgeMask.Pix) { 283 t.Fatalf("len(stdMask.Pix) != len(edgeMask.Pix)") 284 } 285 286 totalDiff := 0 287 for i := 0; i < len(stdMask.Pix); i++ { 288 stdValue := stdMask.Pix[i] 289 edgeValue := edgeMask.Pix[i] 290 if stdValue == edgeValue { 291 continue 292 } 293 var diff uint8 294 if stdValue > edgeValue { 295 diff = stdValue - edgeValue 296 } else { 297 diff = edgeValue - stdValue 298 } 299 300 totalDiff += int(diff) 301 // Note: we could compare pixel values individually here, but 302 // different thresholds and curve segmentation methods 303 // can cause severe value differences in some cases. 304 } 305 306 avgDiff := float64(totalDiff) / (canvasWidth * canvasHeight) 307 if avgDiff > avgCmpTolerance { 308 // Notice: this actually fails sometimes if a variable seed is used. 309 // Different curve segmentation methods are responsible for 310 // it, as far as I have seen. Look at the results yourself 311 // if this ever fails for you. 312 exportTest("cmp_rasts_"+strconv.Itoa(n)+"_edge.png", edgeMask) 313 exportTest("cmp_rasts_"+strconv.Itoa(n)+"_rast.png", stdMask) 314 t.Fatalf("iter %d, totalDiff = %d average tolerance is too big (%f) (written files for visual debug)", n, totalDiff, avgDiff) 315 } 316 } 317 } 318 319 func exportTest(filename string, mask *image.Alpha) { 320 rgba := image.NewRGBA(mask.Rect) 321 r, g, b, a := color.White.RGBA() 322 nrgba := color.NRGBA64{R: uint16(r), G: uint16(g), B: uint16(b), A: 0} 323 for y := mask.Rect.Min.Y; y < mask.Rect.Max.Y; y++ { 324 for x := mask.Rect.Min.X; x < mask.Rect.Max.X; x++ { 325 nrgba.A = uint16((a * uint32(mask.AlphaAt(x, y).A)) / 255) 326 rgba.Set(x, y, mixColors(nrgba, color.Black)) 327 } 328 } 329 330 file, err := os.Create(filename) 331 if err != nil { 332 panic(err) 333 } 334 err = png.Encode(file, rgba) 335 if err != nil { 336 panic(err) 337 } 338 err = file.Close() 339 if err != nil { 340 panic(err) 341 } 342 } 343 344 func randomShape(rng *rand.Rand, lines, w, h int) Shape { 345 fsw, fsh := float64(w)*64, float64(h)*64 346 var makeXY = func() (fixed.Int26_6, fixed.Int26_6) { 347 return fixed.Int26_6(rng.Float64() * fsw), fixed.Int26_6(rng.Float64() * fsh) 348 } 349 startX, startY := makeXY() 350 351 shape := NewShape(lines + 1) 352 shape.MoveToFract(startX, startY) 353 for i := 0; i < lines; i++ { 354 x, y := makeXY() 355 switch rng.Intn(3) { 356 case 0: // LineTo 357 shape.LineToFract(x, y) 358 case 1: // QuadTo 359 cx, cy := makeXY() 360 shape.QuadToFract(cx, cy, x, y) 361 case 2: // CubeTo 362 cx1, cy1 := makeXY() 363 cx2, cy2 := makeXY() 364 shape.CubeToFract(cx1, cy1, cx2, cy2, x, y) 365 } 366 } 367 shape.LineToFract(startX, startY) 368 return shape 369 } 370 371 func similarFloat64Slices(a []float64, b []float64) bool { 372 if len(a) != len(b) { 373 return false 374 } 375 for i, valueA := range a { 376 if valueA != b[i] { 377 diff := math.Abs(valueA - b[i]) 378 if diff > 0.001 { 379 return false 380 } // allow small precision differences 381 } 382 } 383 return true 384 }