Skip to content

Latest commit

 

History

4 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Pixelmatch

A Java port of pixelmatch

Usage

See PixelmatchTest.java

Notable differences from the JavaScript original (v7.2.0):

  • Pixel data is passed as double[] in RGBA order (4 values per pixel, each an integer 0-255), matching the other io.github.t12y image libraries.
  • Options.windowSize <= 0 disables windowed mode (the JavaScript default of Infinity).
  • There is no "wrong data format" error; null image data throws IllegalArgumentException instead.
  • Options.ignoredBoxes (not in upstream pixelmatch) excludes boxes of pixels from the comparison — each {left, right, top, bottom}, all bounds inclusive, the same convention as the other io.github.t12y image libraries. Excluded pixels are never counted (raw, windowed, or cluster-verdict) and draw as background in the diff image.

Cluster verdict (opt-in)

An optional significance layer that distinguishes rendering noise (anti-aliasing halos, sub-pixel shifts, font hinting differences between browser versions) from real visual regressions. Disabled by default — with Options.clusters unset (null), behavior is byte-for-byte identical to plain pixelmatch and no extra code runs.

The idea: rendering noise has no interior; real changes do. The post-anti-aliasing diff mask is eroded by a small radius, which annihilates anything thinner than 2r+1 pixels in any direction — including the sprawling clusters produced by a global 1px layout shift. What survives ("cores") is interior mass, which only real changes have. Two ways a diff region counts as significant:

  • CORE — its eroded interior core is at least minCoreArea pixels.
  • THIN_VIVID — a legitimately thin change (underline, divider, recolored text) is rescued by a safety net: its color difference is vivid (>= hardDelta × the matching threshold) and it sits on a region that was flat in the baseline. Noise is thin, faint, and rides on pre-existing edges; a red underline on white is thin, vivid, and appears where the baseline had no gradient.
Options options = Options.defaults();
options.clusters = ClusterOptions.defaults();

// full analysis:
ClusterAnalysis analysis = Pixelmatch.analyze(baseline, latest, null, width, height, options);

// or through the plain API: with clusters set, the returned count is the
// significant-diff pixel count instead of the raw diff count
int significant = Pixelmatch.pixelmatch(baseline, latest, null, width, height, options);

Reporting semantics: with clusters enabled, the number that drives pass/fail (the returned count / significantPercentage) covers only pixels in significant regions; noise clusters contribute zero. A pure rendering-noise comparison reports ~0.0% and passes any threshold, while a missing button reports its true area share. The raw (unfiltered) numbers stay available in ClusterAnalysis as a diagnostic — compare both while building trust in the filter. Because noise no longer inflates the number, you can and should tighten your percent-difference threshold (a strict value like 0.01% becomes practical once noise reports as 0).

Example ClusterAnalysis.summary() output (from the test suite):

significant: 0.53% (640 px, 2 regions); raw diff: 1.20% (1440 px in 3 clusters, 1 classified as rendering noise)
  region 20x20 at (50, 40): interior core area 324 px (>= 16)
  region 120x2 at (150, 250): thin high-contrast change, mean delta 6.9x threshold on flat background

And when everything is noise (a 1px divider shift):

significant: 0.00%; raw diff: 2.00% (400 px in 1 cluster, all classified as rendering noise)

Tuning

All multipliers apply to the matching threshold (Options.threshold, OKLab HyAB units where black↔white = 1.0). Defaults shown; cannot be combined with windowSize.

Param Default Controls Raising it Lowering it
coreRadius 1 Max thickness (2r+1 px) treated as potentially noise Thicker real changes must rely on the safety net; risk of missing small solid changes rises. Use 2 for 2x/retina captures. (min 1) Thinner noise survives erosion and is counted as significant; false positives return.
minCoreArea 16 Smallest interior "core" that counts as significant Small solid changes (icons, single glyphs) may be ignored → false negatives. Residual noise that survives erosion gets counted → false positives.
hardDelta 2.5 How vivid a thin change must be for the safety net (× threshold) Real thin changes (underlines, dividers, recolored text) missed → false negatives. Strong-ish rendering noise gets rescued as significant → false positives.
flatness 0.5 How flat the baseline must be under a thin change (× threshold) More of the baseline qualifies as "flat" → safety net fires more often. Stricter flatness → thin changes near existing detail are ignored.
flatFraction 0.5 Share of a thin cluster's pixels that must sit on flat baseline Thin changes overlapping existing edges (e.g. text recolor) missed. Noise straddling a flat area may be counted.
minThinArea 8 Smallest thin cluster the safety net will consider Small thin marks (a text caret is ~2x16 px) ignored → false negatives. Isolated vivid speckles (dead pixels, dithering) counted → false positives.

Note: a cluster whose cores are all smaller than minCoreArea is neither CORE nor eligible for the safety net — it demonstrably has some interior, just not enough. That's borderline noise by design.

Dependency

Maven

<dependency>
  <groupId>io.github.t12y</groupId>
  <artifactId>pixelmatch</artifactId>
  <version>1.0.0</version>
</dependency>

Gradle

compile group: 'io.github.t12y', name: 'pixelmatch', version: '1.0.0'

About

A Java port of [pixelmatch](https://github.com/mapbox/pixelmatch)

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages