git/intake/main
internal/artwork/visual.go
116 lines3 KB last change raw · history
package artwork

import (
	"image"
	_ "image/gif"
	_ "image/jpeg"
	_ "image/png"
	"math/bits"
	"os"
)

// visualDistance is how many bits two images may differ by and still count as
// the same picture. Re-encoding and rescaling move a difference hash by a few
// bits; two different covers of one album are typically dozens of bits apart, so
// this stays well clear of merging artwork that merely looks similar. Merging is
// the expensive mistake here: it silently removes a candidate, whereas failing
// to merge only leaves a duplicate in a list.
const visualDistance = 8

// visual is an image reduced to a grid of grey values, with one bit per pair of
// adjacent cells recording which is brighter. Both directions are kept: a hash
// of horizontal differences alone cannot tell two images apart when they share
// their horizontal structure, which is easy to hit by accident.
type visual struct {
	horiz, vert uint64
}

// near reports whether two hashes describe the same picture.
func (v visual) near(o visual) bool {
	d := bits.OnesCount64(v.horiz^o.horiz) + bits.OnesCount64(v.vert^o.vert)
	return d <= visualDistance
}

// visualHash reduces an image to its visual hash.
//
// Hashing the file's bytes cannot tell that Cover Art Archive's 1200x1200 copy
// and its 600x600 copy are one picture, nor that Deezer and iTunes are serving
// re-encodes of it, so the picker offered the same sleeve four times at four
// sizes. This can.
func visualHash(path string) (visual, bool) {
	f, err := os.Open(path)
	if err != nil {
		return visual{}, false
	}
	defer f.Close()
	img, _, err := image.Decode(f)
	if err != nil {
		return visual{}, false
	}

	const cols, rows = 9, 8
	b := img.Bounds()
	if b.Dx() < 1 || b.Dy() < 1 {
		return visual{}, false
	}

	grey := make([]uint32, cols*rows)
	for y := 0; y < rows; y++ {
		y0 := b.Min.Y + y*b.Dy()/rows
		y1 := b.Min.Y + (y+1)*b.Dy()/rows
		if y1 <= y0 {
			y1 = y0 + 1
		}
		for x := 0; x < cols; x++ {
			x0 := b.Min.X + x*b.Dx()/cols
			x1 := b.Min.X + (x+1)*b.Dx()/cols
			if x1 <= x0 {
				x1 = x0 + 1
			}
			// Sampling each cell at a few evenly spaced points rather than
			// every pixel it covers: the same average for a nine by eight grid,
			// without an At call per pixel of a multi-megapixel image.
			var sum, n uint32
			stepY := (y1 - y0) / 4
			if stepY < 1 {
				stepY = 1
			}
			stepX := (x1 - x0) / 4
			if stepX < 1 {
				stepX = 1
			}
			for yy := y0; yy < y1; yy += stepY {
				for xx := x0; xx < x1; xx += stepX {
					r, g, bl, _ := img.At(xx, yy).RGBA()
					sum += (299*(r>>8) + 587*(g>>8) + 114*(bl>>8)) / 1000
					n++
				}
			}
			if n > 0 {
				grey[y*cols+x] = sum / n
			}
		}
	}

	var out visual
	bit := uint(0)
	for y := 0; y < rows; y++ {
		for x := 0; x < cols-1; x++ {
			if grey[y*cols+x] > grey[y*cols+x+1] {
				out.horiz |= 1 << bit
			}
			bit++
		}
	}
	bit = 0
	for x := 0; x < cols; x++ {
		for y := 0; y < rows-1; y++ {
			if grey[y*cols+x] > grey[(y+1)*cols+x] {
				out.vert |= 1 << bit
			}
			bit++
		}
	}
	return out, true
}