blob: efe633ce3986c26112087c69f04e2169aab7143c [file] [log] [blame]
package diff
import (
"image"
"image/color"
"image/png"
"os"
"skia.googlesource.com/buildbot.git/go/util"
)
var (
PixelMatchColor = color.White
// Red from the color blind palette.
PixelDiffColor = color.RGBA{0xE3, 0x1A, 0x1C, 0xFF}
// Grey from the color blind palette.
PixelAlphaDiffColor = color.RGBA{0xB3, 0xB3, 0xB3, 0xFF}
)
type DiffMetrics struct {
NumDiffPixels int
PixelDiffPercent float32
PixelDiffFilePath string
// Contains the maximum difference between the images for each R/G/B channel.
MaxRGBADiffs []int
// True if the dimensions of the compared images are different.
DimDiffer bool
}
type DiffStore interface {
// Get returns the DiffMetrics of the provided dMain digest vs all digests
// specified in dRest.
Get(dMain string, dRest []string) (map[string]*DiffMetrics, error)
// AbsPath returns the paths of the images that correspond to the given
// image digests.
AbsPath(digest []string) map[string]string
}
// OpenImage is a utility function that opens the specified file and returns an
// image.Image
func OpenImage(filePath string) (image.Image, error) {
reader, err := os.Open(filePath)
if err != nil {
return nil, err
}
defer reader.Close()
im, err := png.Decode(reader)
if err != nil {
return nil, err
}
return im, nil
}
// Returns the percentage of pixels that differ, as a float between 0 and 100
// (inclusive).
func getPixelDiffPercent(numDiffPixels, totalPixels int) float32 {
return (float32(numDiffPixels) * 100) / float32(totalPixels)
}
// diffColors compares two color values and returns a color to indicate the
// difference. If the colors differ it updates maxRGBADiffs to contain the
// maximum difference over multiple calls.
// If the RGB channels are identical, but the alpha differ then
// PixelAlphaDiffColor is returned. This allows to distinguish pixels that
// render the same, but have different alpha values.
func diffColors(color1, color2 color.Color, maxRGBADiffs []int) color.Color {
// We compare them before normalizing to non-premultiplied. If one of the
// original images did not have an alpha channel (but the other did) the
// equality will be false.
if color1 == color2 {
return PixelMatchColor
}
// Treat all colors as non-premultiplied.
c1 := color.NRGBAModel.Convert(color1).(color.NRGBA)
c2 := color.NRGBAModel.Convert(color2).(color.NRGBA)
rDiff := util.AbsInt(int(c1.R) - int(c2.R))
gDiff := util.AbsInt(int(c1.G) - int(c2.G))
bDiff := util.AbsInt(int(c1.B) - int(c2.B))
aDiff := util.AbsInt(int(c1.A) - int(c2.A))
maxRGBADiffs[0] = util.MaxInt(maxRGBADiffs[0], rDiff)
maxRGBADiffs[1] = util.MaxInt(maxRGBADiffs[1], gDiff)
maxRGBADiffs[2] = util.MaxInt(maxRGBADiffs[2], bDiff)
maxRGBADiffs[3] = util.MaxInt(maxRGBADiffs[3], aDiff)
// If the color channels differ we mark with the diff color.
if (c1.R != c2.R) || (c1.G != c2.G) || (c1.B != c2.B) {
return PixelDiffColor
}
// If only the alpha channel differs we marke it with the alpha diff color.
return PixelAlphaDiffColor
}
// Diff is a utility function that calculates the DiffMetrics for the provided
// images. Intended to be called from the DiffStore implementations.
func Diff(img1, img2 image.Image, diffFilePath string) (*DiffMetrics, error) {
img1Bounds := img1.Bounds()
img2Bounds := img2.Bounds()
// Get the bounds we want to compare.
cmpWidth := util.MinInt(img1Bounds.Dx(), img2Bounds.Dx())
cmpHeight := util.MinInt(img1Bounds.Dy(), img2Bounds.Dy())
// Get the bounds of the resulting image. If they dimensions match they
// will be identical to the result bounds. Fill the image with black pixels.
resultWidth := util.MaxInt(img1Bounds.Dx(), img2Bounds.Dx())
resultHeight := util.MaxInt(img1Bounds.Dy(), img2Bounds.Dy())
resultImg := image.NewRGBA(image.Rect(0, 0, resultWidth, resultHeight))
totalPixels := resultWidth * resultHeight
// Loop through all points and compare. We start assuming all pixels are
// wrong. This takes care of the case where the images have different sizes
// and there is an area not inspected by the loop.
numDiffPixels := resultWidth * resultHeight
maxRGBADiffs := make([]int, 4)
for x := 0; x < cmpWidth; x++ {
for y := 0; y < cmpHeight; y++ {
color1 := img1.At(x, y)
color2 := img2.At(x, y)
dc := diffColors(color1, color2, maxRGBADiffs)
if dc == PixelMatchColor {
numDiffPixels--
}
resultImg.Set(x, y, dc)
}
}
f, err := os.Create(diffFilePath)
if err != nil {
return nil, err
}
if err := png.Encode(f, resultImg); err != nil {
return nil, err
}
return &DiffMetrics{
NumDiffPixels: numDiffPixels,
PixelDiffPercent: getPixelDiffPercent(numDiffPixels, totalPixels),
PixelDiffFilePath: diffFilePath,
MaxRGBADiffs: maxRGBADiffs,
DimDiffer: (cmpWidth != resultWidth) || (cmpHeight != resultHeight)}, nil
}