Skip to content

Commit

Permalink
internal/cmpimg: add image diffing output
Browse files Browse the repository at this point in the history
  • Loading branch information
kortschak committed Nov 22, 2017
1 parent 6bb95dd commit 991fb6a
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 8 deletions.
41 changes: 36 additions & 5 deletions internal/cmpimg/checkplot.go
Expand Up @@ -5,7 +5,11 @@
package cmpimg

import (
"bytes"
"encoding/base64"
"flag"
"image"
"image/png"
"io/ioutil"
"os"
"path/filepath"
Expand All @@ -22,7 +26,9 @@ func goldenPath(path string) string {
}

// CheckPlot checks a generated plot against a previously created reference.
// If generateTestData = true, it regereates the reference.
// If generateTestData = true, it regenerates the reference.
// For image.Image formats, a base64 encoded png representation is output to
// the testing log when a difference is identified.
func CheckPlot(ExampleFunc func(), t *testing.T, filenames ...string) {
paths := make([]string, len(filenames))
for i, fn := range filenames {
Expand Down Expand Up @@ -50,24 +56,49 @@ func CheckPlot(ExampleFunc func(), t *testing.T, filenames ...string) {
for _, path := range paths {
got, err := ioutil.ReadFile(path)
if err != nil {
t.Errorf("Failed to read %s: %s", path, err)
t.Errorf("Failed to read %s: %v", path, err)
continue
}
golden := goldenPath(path)
want, err := ioutil.ReadFile(golden)
if err != nil {
t.Errorf("Failed to read golden file %s: %s", golden, err)
t.Errorf("Failed to read golden file %s: %v", golden, err)
continue
}
typ := filepath.Ext(path)[1:] // remove the dot in e.g. ".pdf"
ok, err := Equal(typ, got, want)
if err != nil {
t.Errorf("failed to compare image for %s: %v\n", path, err)
t.Errorf("failed to compare image for %s: %v", path, err)
continue
}
if !ok {
t.Errorf("image mismatch for %s\n", path)
continue

switch typ {
case "jpeg", "jpg", "png", "tiff", "tif":
v1, _, err := image.Decode(bytes.NewReader(got))
if err != nil {
t.Errorf("failed to decode %s: %v", path, err)
continue
}
v2, _, err := image.Decode(bytes.NewReader(want))
if err != nil {
t.Errorf("failed to decode %s: %v", golden, err)
continue
}

dst := image.NewRGBA64(v1.Bounds().Union(v2.Bounds()))
rect := Diff(dst, v1, v2)
t.Logf("image bounds union:%+v diff bounds intersection:%+v", dst.Bounds(), rect)

var buf bytes.Buffer
err = png.Encode(&buf, dst)
if err != nil {
t.Errorf("failed to encode difference png: %v", err)
continue
}
t.Log("IMAGE:" + base64.StdEncoding.EncodeToString(buf.Bytes()))
}
}
}
}
89 changes: 86 additions & 3 deletions internal/cmpimg/cmpimg.go
Expand Up @@ -10,13 +10,18 @@ import (
"bytes"
"fmt"
"image"
_ "image/jpeg"
_ "image/png"
"image/color"
"image/draw"
"math"
"reflect"
"strings"

_ "golang.org/x/image/tiff"
"rsc.io/pdf"

_ "image/jpeg"
_ "image/png"

_ "golang.org/x/image/tiff"
)

// Equal takes the raw representation of two images, raw1 and raw2,
Expand Down Expand Up @@ -94,3 +99,81 @@ func cmpPdf(pdf1, pdf2 *pdf.Reader) bool {
t2 := pdf2.Trailer().String()
return t1 == t2
}

// Diff calculates an intensity-scaled difference between images a and b
// and places the result in dst, returning the intersection of a, b and
// dst. It is the responsibility of the caller to construct dst so that
// it will overlap with a and b. For the purposes of Diff, alpha is not
// considered.
//
// Diff is not intended to be used for quantitative analysis of the
// difference between the input images, but rather to highlight differences
// between them for testing purposes, so the calculation is rather naive.
func Diff(dst draw.Image, a, b image.Image) image.Rectangle {
rect := dst.Bounds().Intersect(a.Bounds()).Intersect(b.Bounds())

// Determine greyscale dynamic range.
min := uint16(math.MaxUint16)
max := uint16(0)
for x := rect.Min.X; x < rect.Max.X; x++ {
for y := rect.Min.Y; y < rect.Max.Y; y++ {
p := diffColor{a.At(x, y), b.At(x, y)}
g := color.Gray16Model.Convert(p).(color.Gray16)
if g.Y < min {
min = g.Y
}
if g.Y > max {
max = g.Y
}
}
}

// Render intensity-scaled difference.
for x := rect.Min.X; x < rect.Max.X; x++ {
for y := rect.Min.Y; y < rect.Max.Y; y++ {
dst.Set(x, y, scaledColor{
min: uint32(min), max: uint32(max),
c: diffColor{a.At(x, y), b.At(x, y)},
})
}
}

return rect
}

type diffColor struct {
a, b color.Color
}

func (c diffColor) RGBA() (r, g, b, a uint32) {
ra, ga, ba, _ := c.a.RGBA()
rb, gb, bb, _ := c.b.RGBA()
return diff(ra, rb), diff(ga, gb), diff(ba, bb), math.MaxUint16
}

func diff(a, b uint32) uint32 {
if a < b {
return b - a
}
return a - b
}

type scaledColor struct {
min, max uint32
c color.Color
}

func (c scaledColor) RGBA() (r, g, b, a uint32) {
if c.max == c.min {
return 0, 0, 0, 0
}
f := uint32(math.MaxUint16) / (c.max - c.min)
r, g, b, _ = c.c.RGBA()
r -= c.min
r *= f
g -= c.min
g *= f
b -= c.min
b *= f
return r, g, b, math.MaxUint16
}
53 changes: 53 additions & 0 deletions internal/cmpimg/cmpimg_test.go
@@ -0,0 +1,53 @@
// Copyright ©2017 The gonum Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package cmpimg

import (
"bytes"
"encoding/base64"
"image"
"image/png"
"io/ioutil"
"path/filepath"
"testing"
)

const wantDiffEncoded = `iVBORw0KGgoAAAANSUhEUgAAAZAAAAEzEAIAAADAxR6YAAAHBklEQVR4nOzYjW3kRABA4RWiC0QXUMami2xRSRfrMu76gDKQZQ3+2907xEMg8X3SXZKxdzwZ29JTfrgAAJASWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAMYEFABATWAAAsR//7QX8X12v8//T9E/NvPje+R+tZjvP9882PjV/naZvf+J8lb96xb+/l+uar9d5zfPX8+rnY/PocvTZ9ebj4+j5TmyPjauNM8f8YyXrGh7NM2ZaPjXmHj8dZx4j+zUsax2zr0fHjGOG9frrOfs9WM/bjmx3bnxmP368/2MHzmtfj61j49/2+tv1bO/Ho1UdzzyO7898NsNx/NU+bHfkeMXZzz+Nnz4/3m/rPXi/He/B6r55Tt+evk3btZ73ffsGfH6MPfn8+PL1cvnydVnB779dLrfb/c/fa+zT9imaXhw9P3fH53H9fbdvwH6/lvdv/zSvT/B4e/dzL2e/7d7bZ98vOzqPvG3mWPZ42d375g1f5tzfgePI8Uk8P3GP1nAc+fWXeWT+f5rmOzH2dn5K9nt0/+b8j97ddYXnt+31vq2zHD+/3onz0XGH3m/P3tBnb8szr2Z5vdOvZzyv6Di+fDdNt9t5Dn/BAgCICSwAgJjAAgCICSwAgJjAAgCICSwAgJjAAgCICSwAgJjAAgCICSwAgJjAAgAAAOC/zV+wAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAICawAABiAgsAIPZHAAAA///KAlB2mCuyaAAAAABJRU5ErkJggg==`

func TestDiff(t *testing.T) {
got, err := ioutil.ReadFile(filepath.FromSlash("./testdata/failed_input.png"))
if err != nil {
t.Fatalf("failed to read failed file: %v", err)
}
want, err := ioutil.ReadFile(filepath.FromSlash("./testdata/good_golden.png"))
if err != nil {
t.Fatalf("failed to read golden file: %v", err)
}

v1, _, err := image.Decode(bytes.NewReader(got))
if err != nil {
t.Fatalf("unexpected error decoding failed file: %v", err)
}
v2, _, err := image.Decode(bytes.NewReader(want))
if err != nil {
t.Fatalf("unexpected error decoding golden file: %v", err)
}

dst := image.NewRGBA64(v1.Bounds().Union(v2.Bounds()))
rect := Diff(dst, v1, v2)
if rect != dst.Bounds() {
t.Errorf("unexpected bound for diff: got:%+v want:%+v", rect, dst.Bounds())
}

var buf bytes.Buffer
err = png.Encode(&buf, dst)
if err != nil {
t.Fatalf("failed to encode difference png: %v", err)
}
gotDiff := base64.StdEncoding.EncodeToString(buf.Bytes())
if gotDiff != wantDiffEncoded {
t.Errorf("unexpected encoded diff value:\ngot:%s\nwant:%s", gotDiff, wantDiffEncoded)
}
}
Binary file added internal/cmpimg/testdata/failed_input.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added internal/cmpimg/testdata/good_golden.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit 991fb6a

Please sign in to comment.