github.com/Kintar/etxt@v0.0.0-20221224033739-2fc69f000137/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  }