pixmatch

package module
v1.1.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jan 28, 2024 License: MIT Imports: 16 Imported by: 1

README

pixmatch

Tests Go Report Card Go Reference

pixmatch is a pixel-level image comparison tool. Heavily inspired by pixelmatch, but rewritten in idiomatic Go, with zero dependencies, to speed up images comparison. Go pixmatch has support for PNG, GIF and JPEG formats. This tool also accurately detects anti-aliasing and may count it as a difference.

Example output

Format Expected Actual Difference
JPEG Hummingbird Hummingbird Hummingbird
GIF Landscape Landscape Landscape
PNG Form Form Form

Install

Library:

go get -u github.com/dknight/go-pixmatch

CLI:

go install github.com/dknight/go-pixmatch/cmd/pixmatch

Library usage

img1, err := NewImageFromPath("./samples/form-a.png")
if err != nil {
    log.Fatalln(err)
}
img2, err := NewImageFromPath("./samples/form-b.png")
if err != nil {
    log.Fatalln(err)
}

// Set some options.
options := NewOptions()
options.SetThreshold(0.05)
options.SetAlpha(0.5)
options.SetDiffColor(color.RGBA{0, 255, 128, 255})
// etc...

diff, err := img1.Compare(img2, options)
if err != nil {
    log.Fatalln(err)
}

fmt.Println(diff)

CLI usage

Usage:

pixmatch [options] image1.png image2.png

Run pixmatch -h for the list of supported options.

Example command:

pixmatch -o diff.png -aa -aacolor=00ffffff -mask ./samples/form-a.png ./samples/form-b.png
Compile binaries

Here is included simple script to compile binaries for some architectures. If you need something special, you can easily adopt it for your needs.

./scripts/makebin.sh

Testing and benchmark

Simple tests:

go test

Tests with update diff images:

UPDATEDIFFS=1 go test

Tests with full coverage:

# Terminal output
UPDATEDIFFS=1 go test -cover

# HTML output
UPDATEDIFFS=1 go test -coverprofile c.out && go tool cover -html c.out

Benchmark scripts (outputFile will be written in logs/ directory):

./scripts/benchmark.sh <outputFile> [iterations=10]

Later, it is easier to analyze it with a cool benchstat tool.

Using with WASM

WASI Preview 1 is very unstable, everything will be changed. This is an rough example how it might work with golang <= 1.12.6.

Compile the WASM file.

cd cmd/pixmatch
GOOS=wasip1 GOARCH=wasm go build -o pixmatch.wasm main.go

Node.js exanoke file (index.js)

const fs = require('node:fs');
const {WASI} = require('node:wasi');
const imgs = ['form-a.png', 'form-b.png'];
const virtulPathWasiPath = '';
const wasi = new WASI({
  version: 'preview1',
  args: [virtulPathWasiPath, '-mask', '-o', 'diff.png', ...imgs],
  preopens: {
    '/': __dirname,
  },
});

const wasmBuffer = fs.readFileSync('./pixmatch.wasm');
WebAssembly.instantiate(wasmBuffer, wasi.getImportObject()).then(
  (wasmModule) => {
    wasi.start(wasmModule.instance);
  }
);

Run node script:

NODE_NO_WARNINGS=1 node index.js

Known issues, bugs, flaws

  • Anti-aliasing detection algorithm can be improved (help appreciated).
  • Because of the nature of the JPEG format, comparing them is not a good idea or play with threshold parameter.
  • I have not tested this tool for 64-bit color images.

Credits

  • To provide 100% compatibility with pixelmatch. Original test files are borrowed from fixtures.
  • Hummingbird is taken from Wiki commons by San Diego Zoo.
  • Someone on the Pixilart platform created this pixel art girl.
  • Form screenshots are made using PureCSS framework.

Contribution

Any help is appreciated. Found a bug, typo, inaccuracy, etc.? Please do not hesitate to make a pull request or file an issue.

License

MIT 2023

Documentation

Overview

Package pixmatch is a pixel-level image comparison tool. Heavily inspired by [Pixelmatch.js], but rewritten in idiomatic Go to speed up images comparison.

Go pixmatch has support for PNG, GIF and JPEG formats. This tool also accurately detects anti-aliasing and may (or may not) count it as a difference. [Pixelmatch.js]: https://github.com/mapbox/pixelmatch

Author: Dmitri Smirnov (https://www.whoop.ee/)

License: MIT 2023

Example
img1, err := NewImageFromPath("./samples/form-a.png")
if err != nil {
	log.Fatalln(err)
}
img2, err := NewImageFromPath("./samples/form-b.png")
if err != nil {
	log.Fatalln(err)
}
options := NewOptions()
options.SetThreshold(0.05)
options.SetAlpha(0.5)
options.SetDiffColor(color.RGBA{0, 255, 127, 255})
// etc...

diff, err := img1.Compare(img2, options)
if err != nil {
	log.Fatalln(err)
}

fmt.Println(diff)
Output:

4933

Index

Examples

Constants

View Source
const (
	// ExitOk when the program exited successfully.
	ExitOk = 0

	// ExitFSFail occurs when there is a problem with the file system.
	ExitFSFail = 100

	// ExitEmptyImage occurs when the image (or both) are empty.
	ExitEmptyImage = 101

	// ExitDimensionsNotEqual occurs when the images dimensions are not equal.
	ExitDimensionsNotEqual = 102

	// ExitInvalidInput input parameters and/or flags are invalid.
	// Check usage.
	ExitInvalidInput = 103

	// ErrMissingImage one or both images are missing.
	ExitMissingImage = 104

	// ExitUnknownFormat if format of the image is not supported.
	ExitUnknownFormat = 105

	// ExitUnknown all other failings.
	ExitUnknown = 199
)

Exit codes that are not defined in the BSD and Linux specifications.

View Source
const (
	// YIQDeltaMax is the value of 35215. This is the maximum possible value
	// for the YIQ difference metric.
	// Read more about YIQ NTSC https://en.wikipedia.org/wiki/YIQ
	YIQDeltaMax = 35215

	// DefaultFormat is used if format is not specified.
	DefaultFormat = FormatPNG

	FormatPNG  = "png"
	FormatGIF  = "gif"
	FormatJPEG = "jpeg"
)

Common constants for pixmatch package.

Variables

View Source
var (
	//ErrDimensionsDoNotMatch represents an error when the dimensions of two
	//images do not match.
	ErrDimensionsDoNotMatch = errors.New("images dimensions do not match")

	// ErrImageIsEmpty occurs when one of the images, or both of them,
	// are empty.
	ErrImageIsEmpty = errors.New("one or both images are empty")

	// ErrCorruptedImage occurs when the data for the image is corrupted and
	// cannot be read or decoded.
	ErrCorruptedImage = errors.New("image data is corrupted")

	// ErrCannotWriteOutputDiff occurs when output cannot be written.
	ErrCannotWriteOutputDiff = errors.New("cannot write diff output")

	// ErrUnknownFormat occurs when the image format is not supported or
	// unknown.
	ErrUnknownFormat = errors.New("unknown image format")

	// ErrInvalidColorFormat occurs when user enter invalid color format.
	ErrInvalidColorFormat = errors.New("invalid color format")

	// ErrMissingImage occurs when one or both images are missing.
	ErrMissingImage = errors.New("one or both images are missing")
)

Functions

func GetVersion

func GetVersion() string

GetVersion gets current version of pixmatch.

func HexStringToColor

func HexStringToColor(hexstr string) (*color.RGBA, error)

HexStringToColor converts hexadecimal string RRGGBBAA of color representation to image/color.RGBA. Input string are case-insensitive. Also strings can be prefixed with '0x' or '0X'.

Examples values are:

  • FF000099
  • ff00ff00
  • 0xff00ff00
  • #ffFF00ff00

Types

type Color

type Color struct {
	R, G, B, A uint32
}

Color represents color structure and its components (R)ed, (G)reen, (B)lue, (Alpha). This is similar to image/color.Color from the standard library.

func NewColor

func NewColor(r, g, b, a uint32) *Color

NewColor creates a new color instance.

func (Color) Blend

func (c Color) Blend(a float64) *Color

Blend is the procedure of blending the color with the alpha factor is known as blending.

func (Color) BlendToGray

func (c Color) BlendToGray(a float64) color.Color

BlendToGray draws gray-scaled color with gray-scaled blending.

func (Color) Equals

func (c Color) Equals(c2 *Color) bool

Equals checks colors' equality, ensuring that all color channels are equal.

func (Color) I

func (c Color) I() float64

I is the RBG to I (chrominance) conversion.

func (Color) Q

func (c Color) Q() float64

Q is the RBG to Q (chrominance) conversion.

func (Color) RGBA

func (c Color) RGBA() (r, g, b, a uint32)

RGBA returns Red, Green, Blue, Alpha channels similar to image/color.RGBA from the standard library, which always returns values as uint32 type.

func (Color) String

func (c Color) String() string

func (Color) Y

func (c Color) Y() float64

Y is the RBG to Y (brightness) conversion.

func (Color) YIQ

func (c Color) YIQ() (float64, float64, float64)

YIQ converts RGB intoto YIQ color space. See the wiki page about YIQ: https://en.wikipedia.org/wiki/YIQ

type Image

type Image struct {
	// Path to the image in file system.
	Path string

	// Format as a string like (PNG, JEPG, GIF).
	Format string

	// PixData contains data for colors as uint32 numbers.
	PixData []uint32

	// BPC is the number of bytes per color.
	BPC int

	// Image is an embedded [image.Image] from the standard library.
	image.Image
}

Image represents the image structure.

func NewImage

func NewImage(w, h int, format string) *Image

NewImage creates a new image instance.

func NewImageFromPath

func NewImageFromPath(path string) (*Image, error)

NewImageFromPath creates a new image instance from the file system path.

Example
img, err := NewImageFromPath("./samples/form-b.png")
if err != nil {
	log.Fatalln(err)
}
fmt.Printf("%T\n", img)
Output:

*pixmatch.Image

func (*Image) Antialiased

func (img *Image) Antialiased(img2 *Image, pt image.Point) bool

Antialiased checks that the point is anti-aliased.

NOTE Probably, better algorithms are required here.

func (*Image) Bytes

func (img *Image) Bytes() []byte

Bytes are the raw bytes of the pixel data. Reflection is never clear (https://go-proverbs.github.io/)

func (*Image) BytesPerColor

func (img *Image) BytesPerColor() int

BytesPerColor resolves the count of the bytes per color: 1, 2, 4, or 8.

func (*Image) ColorDelta

func (img *Image) ColorDelta(img2 *Image, m, n int, onlyY bool) float64

ColorDelta is the squared YUV distance between colors at the pixel's position, returns a negative value if the img2 pixel is darker, and vice versa. If the argument onlyY is true, the only brightness level will be returned (Y component of the YIQ color space).

func (*Image) Compare

func (img *Image) Compare(img2 *Image, opts *Options) (int, error)

Compare returns the number of different pixels between two comparable images. Zero is returned if no difference found.Returns negative values if something went wrong but in this case error also returned.

Looks like process row of the pixel in a single goroutine is the most performant way to do this, but I can mistake here.

Example
options := NewOptions()
options.SetThreshold(0.25)

img1, _ := NewImageFromPath("./samples/form-a.png")
img2, _ := NewImageFromPath("./samples/form-b.png")
diff, _ := img1.Compare(img2, options)

fmt.Println(diff)
Output:

1626

func (*Image) DimensionsEqual

func (img *Image) DimensionsEqual(img2 *Image) bool

DimensionsEqual checks that the dimensions of the two images are equal.

Example
img1, _ := NewImageFromPath("./samples/bird-a.jpg")
img2, _ := NewImageFromPath("./samples/bird-c-small.jpg")
samplesult := img1.DimensionsEqual(img2)
fmt.Println(samplesult)
Output:

false

func (*Image) Empty

func (img *Image) Empty() bool

Empty checks that the image is empty or has a theoretical size of 0 pixels.

Example
img, _ := NewImageFromPath("./samples/form-a.png")
samplesult := img.Empty()
fmt.Println(samplesult)
Output:

false

func (*Image) Identical

func (img *Image) Identical(img2 *Image) bool

Identical determines whether or not images are identical at the byte level. This means that all the bytes of both images are the same.

Alternative possible ways to compare:

loops - the slowest (faster for smaller images)
 reflect.DeepEqual() - slower
 bytes.Compare() - better
 bytes.Equal() - even better
Example
img1, _ := NewImageFromPath("./samples/form-a.png")
img2, _ := NewImageFromPath("./samples/form-a.png")
samplesult := img1.Identical(img2)
fmt.Println(samplesult)
Output:

true

func (*Image) Load

func (img *Image) Load(rd io.Reader) (err error)

Load reads data from the reader.

Example
img := NewImage(0, 0, FormatGIF)
img.Path = "./samples/gray16-b.png"

fp, err := os.Open(img.Path)
if err != nil {
	log.Fatalln(err)
}
defer fp.Close()
if err := img.Load(fp); err != nil {
	log.Fatalln(err)
}
fmt.Printf("%T\n", img)
Output:

*pixmatch.Image

func (*Image) Position

func (img *Image) Position(p image.Point) int

Position is the position of the pixel in the array of bytes.

Formula

(y2-y1)*Stride + (x2-x1)*BPC

func (*Image) SameNeighbors

func (img *Image) SameNeighbors(pt image.Point, n int) bool

SameNeighbors determines whether a pixel has n+ adjacent pixels that are the same color.

func (*Image) Save

func (img *Image) Save(wr io.Writer) (err error)

Save encodes and writes image data to the destination.

func (*Image) Size

func (img *Image) Size() int

Size gives the total size of the image in pixels.

Example
img, _ := NewImageFromPath("./samples/form-a.png")
samplesult := img.Size()
fmt.Println(samplesult)
Output:

51200

func (*Image) Stride

func (img *Image) Stride() int

Stride gets the stride from the image. The default value is 1. Reflection is never clear (https://go-proverbs.github.io/)

func (*Image) Uint32

func (img *Image) Uint32() []uint32

Uint32 converts image.Bytes() into a []uint32 slice. Be careful; this might be an expensive operation, used once and cached in image.PixData on image loading

type Options

type Options struct {
	// Output is structure where final image will be written.
	Output io.Writer

	// Threshold is the threshold of the maximum color delta.
	// Values range [0, 1.0].
	Threshold float64

	// Alpha is the alpha channel factor (multiplier). Values range [0, 1.0].
	// NOTE it is interesting to experiment with overflow and underflow
	// ranges.
	Alpha float64

	// IncludeAA sets anti-aliasing pixels as difference counts.
	IncludeAA bool

	// AAColor is the color to mark anti-aliasing pixels.
	AAColor color.Color

	// DiffColor is the color to highlight the differences.
	DiffColor color.Color

	// DiffColorAlt is the alternative difference color. Used to detect dark
	// and light differences between two images and set an alternative color if
	// required.
	DiffColorAlt color.Color

	// DiffMask sets to use mask, renders the differences without the original
	// image.
	DiffMask bool

	// KeepEmptyDiff removes empty diff files.
	KeepEmptyDiff bool
}

Options is the structure that stores the settings for common comparisons.

func NewOptions

func NewOptions() *Options

NewOptions creates a new Options instance. It is possible to use https://github.com/imdario/mergo in this case. Personally, I try to avoid dependencies whenever possible.

func (*Options) SetAAColor

func (opts *Options) SetAAColor(v color.Color) *Options

SetAAColor sets anti-aliased color to the options.

func (*Options) SetAlpha

func (opts *Options) SetAlpha(v float64) *Options

SetAlpha sets alpha to the options.

func (*Options) SetDiffColor

func (opts *Options) SetDiffColor(v color.Color) *Options

SetDiffColor sets color of differences to the options.

func (*Options) SetDiffColorAlt

func (opts *Options) SetDiffColorAlt(v color.Color) *Options

SetDiffColorAlt sets color of alternative difference to the options.

func (*Options) SetDiffMask

func (opts *Options) SetDiffMask(v bool) *Options

SetDiffMask sets difference mask to the options.

func (*Options) SetIncludeAA

func (opts *Options) SetIncludeAA(v bool) *Options

SetIncludeAA sets anti-aliasing to the options to counts anti-aliased pixels as differences.

func (*Options) SetKeepEmptyDiff

func (opts *Options) SetKeepEmptyDiff(v bool) *Options

SetKeepEmptyDiff sets difference mask to the options.

func (*Options) SetOutput

func (opts *Options) SetOutput(v io.Writer) *Options

SetOutput sets the output as pointer to the options.

func (*Options) SetThreshold

func (opts *Options) SetThreshold(v float64) *Options

SetThreshold sets threshold to the options.

type Version

type Version struct {
	Major int
	Minor int
	Patch int
	Pre   string
}

Version is extremely simple semantic version structure. Personally, I wouldn't like to use external (especially heavy) dependencies to manage versions.

Check out https://pkg.go.dev/github.com/hashicorp/go-version for monstrous version management.

func (Version) String

func (v Version) String() string

Directories

Path Synopsis
cmd
pixmatch Module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL