Skip to Content
New: ssim-native brings SSIM, MS-SSIM and Hitchhiker's to Node, and interpret-native ships diff interpretation on its own. Read more โ†’
APIs@blazediff/ssim-native

@blazediff/ssim-native

Native Rust structural-similarity metrics for Node.js: SSIM, MS-SSIM, Hitchhikerโ€™s SSIM and perceptual SSIM, through N-API. Decodes PNG, JPEG and QOI. It is the blazediff-ssim crate with nothing held back.

Installation

npm install @blazediff/ssim-native

The platform binary installs as an optional dependency; there is no compile step.

Why a separate package

@blazediff/core-native is a pixel diff: it answers where two images differ. This package answers how alike they look โ€” a different question, so it is a different package rather than a flag on that one.

It exposes the whole crate: every stability constant, both pooling methods, Hitchhikerโ€™s stride, all of perceptual-ssim, and the local score map itself. The two are independent and share no code but the decoders; installing one does not pull in the other.

Usage

import { compare } from "@blazediff/ssim-native"; const result = await compare("expected.png", "actual.png", "map.png", { metric: "ms-ssim", minScore: 0.99, }); if (result.match) { console.log(`close enough: ${result.score}`); } else if (result.reason === "score-below-threshold") { console.log(`scored ${result.score}, ${result.belowCount} windows below the floor`); }

compare takes two file paths or two encoded buffers (Node Buffer works directly). A third argument renders the local score map to that path as grayscale, dark where the score is low.

Result

SsimResult is a discriminated union:

matchreasonCarries
trueโ€”score, metric, mapWidth, mapHeight
false"score-below-threshold"the above plus belowCount, belowPercentage
false"layout-diff"โ€” the images are different sizes
false"file-not-exists"file

score is pooled similarity in 0..=1, where 1 is identical. belowCount counts map windows scoring under minScore โ€” window counts, not pixel counts.

Metrics

MetricWhat it does
ssim (default)Gaussian-windowed single-scale SSIM, with the automatic downsample to ~256px on the short edge that MATLABโ€™s ssim.m does
ms-ssimSSIM pooled across a 5-octave dyadic pyramid, per msssim.m. Needs โ‰ฅ176px on the short edge
hitchhikers-ssimBox windows over five integral images, pooled by coefficient of variation (Venkataramanan et al. 2021). Every window sum is an O(1) summed-area-table lookup instead of ten 11-tap convolutions
perceptual-ssimThe tunable variant: CIE L*a*b*, chroma weighting, chroma subsampling and MAD pooling, each an independent knob. At its defaults it reduces bit-identically to ms-ssim, which is what makes it usable as an ablation study rather than a second opinion

Raw RGBA

If you already have decoded pixels, skip the codec. These are synchronous:

import { msSsim, renderMap } from "@blazediff/ssim-native"; const result = msSsim(rgba1, rgba2, width, height, { returnMap: true }); const grayscale = renderMap(result.map!, result.mapWidth, result.mapHeight, width, height);

ssim, msSsim, hitchhikersSsim and perceptualSsim all take (base, comparison, width, height, options?). Both buffers are read at the same dimensions, so there is no layout-difference case here โ€” a buffer too short for width * height throws.

Options

interface CompareOptions { metric?: "ssim" | "ms-ssim" | "hitchhikers-ssim" | "perceptual-ssim"; minScore?: number; // identical at or above this. Default: 1 returnMap?: boolean; // include the Float32Array map. Default: false // shared by every metric windowSize?: number; // Default: 11 k1?: number; // Default: 0.01 k2?: number; // Default: 0.03 bitDepth?: number; // Default: 8, so L = 255 msSsim?: { weights?: number[]; method?: "product" | "weighted-sum" }; hitchhikers?: { windowStride?: number; covPooling?: boolean }; perceptual?: { weights?: number[]; method?: "product" | "weighted-sum"; color?: "gamma-luma" | "lab"; chromaWeight?: number; chromaSubsample?: number; pooling?: "mean" | "mad"; deviationWeight?: number; }; compression?: number; // PNG level for a rendered map. Default: 0 quality?: number; // JPEG quality for a rendered map. Default: 90 }

The map is withheld unless returnMap is set โ€” it is one float per window and costs a copy across the binding.

Faster PNG decoding

Decoding is shared with @blazediff/core-native โ€” both sit on the blazediff-shared crate โ€” so the same opt-in applies here. Setting BLAZEDIFF_PNG_ENABLED=1 routes PNG decode through the in-house blazediff-png codec instead of libspng:

BLAZEDIFF_PNG_ENABLED=1 node compare.mjs

Worth roughly 15% off a 4K compare() call, with byte-identical decoded pixels and therefore an unchanged score. It is read once per process, and only affects the path and buffer APIs โ€” the raw RGBA entry points never decode anything.

Accuracy

The Rust implementationโ€™s tap-by-tap accumulation order is frozen to @blazediff/ssim, the TypeScript port whose MATLAB agreement was measured, so this package inherits that agreement instead of drifting from it by an unmeasured amount. Both sides carry tests pinning the two ports to within 5e-6; SSIM lands within 0.03% of MATLAB across the fixture set.

ms-ssim with the default "product" pooling returns NaN for globally anticorrelated content (an inverted image). Both references degenerate the same way โ€” the JS gives NaN, MATLAB gives a complex number. "weighted-sum" stays finite throughout.

Caveats

All three shipped metrics reduce to luma, so a change carried entirely by chroma or by alpha is invisible to them. perceptual-ssim with color: "lab" and a non-zero chromaWeight sees colour.

Scores are pooled over a local map, so these say how much two images differ, not where beyond the mapโ€™s resolution. For exact locations, use @blazediff/core-native.

Unlike @blazediff/core-native there is no CLI to fall back to โ€” this package ships only the .node, so an unsupported platform throws rather than degrading.

Platforms

macOS (arm64, x64), Linux (arm64, x64), Windows (arm64, x64).

Last updated on