blob: faa8be4be38ba82310165001899b77114ac99bde [file] [log] [blame]
package fuzzy
import (
"image"
"image/draw"
)
// Matcher is a non-exact image matching algorithm.
//
// It considers two images to be equal if the following two conditions are met:
// - Both images are of equal size.
// - The total number of different pixels is below MaxDifferentPixels.
// - There are no pixels such that dR + dG + dB + dA > PixelDeltaThreshold, where d{R,G,B,A} are
// the per-channel deltas.
// - If IgnoredBorderThickness > 0, then the first/last IgnoredBorderThickness rows/columns will
// be ignored when performing the above pixel-wise comparisons.
//
// It assumes 8-bit channels.
//
// Valid PixelDeltaThreshold values are 0 to 1020 inclusive (0 <= d{R,G,B,A} <= 255, thus
// 0 <= dR + dG + dB + dA <= 255*4 = 1020).
type Matcher struct {
MaxDifferentPixels int
PixelDeltaThreshold int
IgnoredBorderThickness int
// Debug information about the last pair of matched images.
actualNumDifferentPixels int
actualMaxPixelDelta int
}
// Match implements the imagmatching.Matcher interface.
func (m *Matcher) Match(expected, actual image.Image) bool {
// Images must be the same size.
if !expected.Bounds().Eq(actual.Bounds()) {
return false
}
// Convert both images to NRGBA.
bounds := expected.Bounds()
expectedNRGBA := image.NewNRGBA(bounds)
actualNRGBA := image.NewNRGBA(bounds)
draw.Draw(expectedNRGBA, bounds, expected, bounds.Min, draw.Src)
draw.Draw(actualNRGBA, bounds, actual, bounds.Min, draw.Src)
// Reset counters.
m.actualNumDifferentPixels = 0
m.actualMaxPixelDelta = 0
// Iterate over all pixels, with the exception of the ignored border pixels.
b := m.IgnoredBorderThickness
for x := (bounds.Min.X + b); x < (bounds.Max.X - b); x++ {
for y := (bounds.Min.Y + b); y < (bounds.Max.Y - b); y++ {
p1 := expectedNRGBA.NRGBAAt(x, y)
p2 := actualNRGBA.NRGBAAt(x, y)
// Track number of different pixels.
if p1 != p2 {
m.actualNumDifferentPixels++
}
// Track maximum pixel-wise difference.
pixelDelta := absDiff(p1.R, p2.R) + absDiff(p1.G, p2.G) + absDiff(p1.B, p2.B) + absDiff(p1.A, p2.A)
if pixelDelta > m.actualMaxPixelDelta {
m.actualMaxPixelDelta = pixelDelta
}
}
}
// Total number of different pixels must be below the given threshold.
if m.actualNumDifferentPixels > m.MaxDifferentPixels {
return false
}
// Pixel-wise differences must be below the given threshold.
if m.actualMaxPixelDelta > m.PixelDeltaThreshold {
return false
}
return true
}
// NumDifferentPixels returns the number of different pixels between the last two matched images.
func (m *Matcher) NumDifferentPixels() int { return m.actualNumDifferentPixels }
// MaxPixelDelta returns the maximum per-channel delta sum between the last two matched images.
func (m *Matcher) MaxPixelDelta() int { return m.actualMaxPixelDelta }
// absDiff takes two uint8 values m and n, computes |m - n|, and converts the result into an int
// suitable for addition without the risk of overflowing.
func absDiff(m, n uint8) int {
if m > n {
return int(m - n)
}
return int(n - m)
}