...

Source file src/gonum.org/v1/plot/cmpimg/cmpimg.go

Documentation: gonum.org/v1/plot/cmpimg

     1  // Copyright ©2016 The Gonum Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package cmpimg compares the raw representation of images taking into account
     6  // idiosyncracies related to their underlying format (SVG, PDF, PNG, ...).
     7  package cmpimg // import "gonum.org/v1/plot/cmpimg"
     8  
     9  import (
    10  	"bytes"
    11  	"fmt"
    12  	"image"
    13  	"image/color"
    14  	"image/draw"
    15  	"math"
    16  	"reflect"
    17  	"strings"
    18  
    19  	"rsc.io/pdf"
    20  
    21  	_ "image/jpeg"
    22  	_ "image/png"
    23  
    24  	_ "golang.org/x/image/tiff"
    25  )
    26  
    27  // Equal takes the raw representation of two images, raw1 and raw2,
    28  // together with the underlying image type ("eps", "jpeg", "jpg", "pdf", "png", "svg", "tiff"),
    29  // and returns whether the two images are equal or not.
    30  //
    31  // Equal may return an error if the decoding of the raw image somehow failed.
    32  func Equal(typ string, raw1, raw2 []byte) (bool, error) {
    33  	return EqualApprox(typ, raw1, raw2, 0)
    34  }
    35  
    36  // EqualApprox takes the raw representation of two images, raw1 and raw2,
    37  // together with the underlying image type ("eps", "jpeg", "jpg", "pdf", "png", "svg", "tiff"),
    38  // a normalized delta parameter to describe how close the matching should be
    39  // performed (delta=0: perfect match, delta=1, loose match)
    40  // and returns whether the two images are equal or not.
    41  //
    42  // EqualApprox may return an error if the decoding of the raw image somehow failed.
    43  // EqualApprox only uses the normalized delta parameter for "jpeg", "jpg", "png",
    44  // and "tiff" images. It ignores that parameter for other document types.
    45  func EqualApprox(typ string, raw1, raw2 []byte, delta float64) (bool, error) {
    46  	switch {
    47  	case delta < 0:
    48  		delta = 0
    49  	case delta > 1:
    50  		delta = 1
    51  	}
    52  
    53  	switch typ {
    54  	case "svg", "tex":
    55  		return bytes.Equal(raw1, raw2), nil
    56  
    57  	case "eps":
    58  		lines1, lines2 := strings.Split(string(raw1), "\n"), strings.Split(string(raw2), "\n")
    59  		if len(lines1) != len(lines2) {
    60  			return false, nil
    61  		}
    62  		for i, line1 := range lines1 {
    63  			if strings.Contains(line1, "CreationDate") {
    64  				continue
    65  			}
    66  			if line1 != lines2[i] {
    67  				return false, nil
    68  			}
    69  		}
    70  		return true, nil
    71  
    72  	case "pdf":
    73  		pdf1, err := pdf.NewReader(bytes.NewReader(raw1), int64(len(raw1)))
    74  		if err != nil {
    75  			return false, err
    76  		}
    77  
    78  		pdf2, err := pdf.NewReader(bytes.NewReader(raw2), int64(len(raw2)))
    79  		if err != nil {
    80  			return false, err
    81  		}
    82  
    83  		return cmpPdf(pdf1, pdf2), nil
    84  
    85  	case "jpeg", "jpg", "png", "tiff":
    86  		v1, _, err := image.Decode(bytes.NewReader(raw1))
    87  		if err != nil {
    88  			return false, err
    89  		}
    90  		v2, _, err := image.Decode(bytes.NewReader(raw2))
    91  		if err != nil {
    92  			return false, err
    93  		}
    94  		return cmpImg(v1, v2, delta), nil
    95  
    96  	default:
    97  		return false, fmt.Errorf("cmpimg: unknown image type %q", typ)
    98  	}
    99  }
   100  
   101  func cmpPdf(pdf1, pdf2 *pdf.Reader) bool {
   102  	n1 := pdf1.NumPage()
   103  	n2 := pdf2.NumPage()
   104  	if n1 != n2 {
   105  		return false
   106  	}
   107  
   108  	for i := 1; i <= n1; i++ {
   109  		p1 := pdf1.Page(i).Content()
   110  		p2 := pdf2.Page(i).Content()
   111  		if !reflect.DeepEqual(p1, p2) {
   112  			return false
   113  		}
   114  	}
   115  
   116  	t1 := pdf1.Trailer().String()
   117  	t2 := pdf2.Trailer().String()
   118  	return t1 == t2
   119  }
   120  
   121  func cmpImg(v1, v2 image.Image, delta float64) bool {
   122  	img1, ok := v1.(*image.RGBA)
   123  	if !ok {
   124  		img1 = newRGBAFrom(v1)
   125  	}
   126  
   127  	img2, ok := v2.(*image.RGBA)
   128  	if !ok {
   129  		img2 = newRGBAFrom(v2)
   130  	}
   131  
   132  	if len(img1.Pix) != len(img2.Pix) {
   133  		return false
   134  	}
   135  
   136  	max := delta * delta
   137  	bnd := img1.Bounds()
   138  	for x := bnd.Min.X; x < bnd.Max.X; x++ {
   139  		for y := bnd.Min.Y; y < bnd.Max.Y; y++ {
   140  			c1 := img1.RGBAAt(x, y)
   141  			c2 := img2.RGBAAt(x, y)
   142  			if !yiqEqApprox(c1, c2, max) {
   143  				return false
   144  			}
   145  		}
   146  	}
   147  
   148  	return true
   149  }
   150  
   151  // yiqEqApprox compares the colors of 2 pixels, in the NTSC YIQ color space,
   152  // as described in:
   153  //
   154  //	Measuring perceived color difference using YIQ NTSC
   155  //	transmission color space in mobile applications.
   156  //	Yuriy Kotsarenko, Fernando Ramos.
   157  //
   158  // An electronic version is available at:
   159  //
   160  // - http://www.progmat.uaem.mx:8080/artVol2Num2/Articulo3Vol2Num2.pdf
   161  func yiqEqApprox(c1, c2 color.RGBA, d2 float64) bool {
   162  	const max = 35215.0 // difference between 2 maximally different pixels.
   163  
   164  	var (
   165  		r1 = float64(c1.R)
   166  		g1 = float64(c1.G)
   167  		b1 = float64(c1.B)
   168  
   169  		r2 = float64(c2.R)
   170  		g2 = float64(c2.G)
   171  		b2 = float64(c2.B)
   172  
   173  		y1 = r1*0.29889531 + g1*0.58662247 + b1*0.11448223
   174  		i1 = r1*0.59597799 - g1*0.27417610 - b1*0.32180189
   175  		q1 = r1*0.21147017 - g1*0.52261711 + b1*0.31114694
   176  
   177  		y2 = r2*0.29889531 + g2*0.58662247 + b2*0.11448223
   178  		i2 = r2*0.59597799 - g2*0.27417610 - b2*0.32180189
   179  		q2 = r2*0.21147017 - g2*0.52261711 + b2*0.31114694
   180  
   181  		y = y1 - y2
   182  		i = i1 - i2
   183  		q = q1 - q2
   184  
   185  		diff = 0.5053*y*y + 0.299*i*i + 0.1957*q*q
   186  	)
   187  	return diff <= max*d2
   188  }
   189  
   190  func newRGBAFrom(src image.Image) *image.RGBA {
   191  	var (
   192  		bnds = src.Bounds()
   193  		dst  = image.NewRGBA(bnds)
   194  	)
   195  	draw.Draw(dst, bnds, src, image.Point{}, draw.Src)
   196  	return dst
   197  }
   198  
   199  // Diff calculates an intensity-scaled difference between images a and b
   200  // and places the result in dst, returning the intersection of a, b and
   201  // dst. It is the responsibility of the caller to construct dst so that
   202  // it will overlap with a and b. For the purposes of Diff, alpha is not
   203  // considered.
   204  //
   205  // Diff is not intended to be used for quantitative analysis of the
   206  // difference between the input images, but rather to highlight differences
   207  // between them for testing purposes, so the calculation is rather naive.
   208  func Diff(dst draw.Image, a, b image.Image) image.Rectangle {
   209  	rect := dst.Bounds().Intersect(a.Bounds()).Intersect(b.Bounds())
   210  
   211  	// Determine greyscale dynamic range.
   212  	min := uint16(math.MaxUint16)
   213  	max := uint16(0)
   214  	for x := rect.Min.X; x < rect.Max.X; x++ {
   215  		for y := rect.Min.Y; y < rect.Max.Y; y++ {
   216  			p := diffColor{a.At(x, y), b.At(x, y)}
   217  			g := color.Gray16Model.Convert(p).(color.Gray16)
   218  			if g.Y < min {
   219  				min = g.Y
   220  			}
   221  			if g.Y > max {
   222  				max = g.Y
   223  			}
   224  		}
   225  	}
   226  
   227  	// Render intensity-scaled difference.
   228  	for x := rect.Min.X; x < rect.Max.X; x++ {
   229  		for y := rect.Min.Y; y < rect.Max.Y; y++ {
   230  			dst.Set(x, y, scaledColor{
   231  				min: uint32(min), max: uint32(max),
   232  				c: diffColor{a.At(x, y), b.At(x, y)},
   233  			})
   234  		}
   235  	}
   236  
   237  	return rect
   238  }
   239  
   240  type diffColor struct {
   241  	a, b color.Color
   242  }
   243  
   244  func (c diffColor) RGBA() (r, g, b, a uint32) {
   245  	ra, ga, ba, _ := c.a.RGBA()
   246  	rb, gb, bb, _ := c.b.RGBA()
   247  	return diff(ra, rb), diff(ga, gb), diff(ba, bb), math.MaxUint16
   248  }
   249  
   250  func diff(a, b uint32) uint32 {
   251  	if a < b {
   252  		return b - a
   253  	}
   254  	return a - b
   255  }
   256  
   257  type scaledColor struct {
   258  	min, max uint32
   259  	c        color.Color
   260  }
   261  
   262  func (c scaledColor) RGBA() (r, g, b, a uint32) {
   263  	if c.max == c.min {
   264  		return 0, 0, 0, 0
   265  	}
   266  	f := uint32(math.MaxUint16) / (c.max - c.min)
   267  	r, g, b, _ = c.c.RGBA()
   268  	r -= c.min
   269  	r *= f
   270  	g -= c.min
   271  	g *= f
   272  	b -= c.min
   273  	b *= f
   274  	return r, g, b, math.MaxUint16
   275  }
   276  

View as plain text