QA: Image Diff (SSIM/PSNR)
Prove Your Retouch Only Touched What You Meant It To
- image_a
- image_b
- mask
- diff
- ssim
- psnr
- report
Retouching is a game of invisible changes, and invisible changes are exactly the ones you can't verify by squinting at a preview. Did the skin smoother touch the eyes? Did the sharpen pass survive the resize? Is that "subtle" LUT actually shifting the whole frame?
QA: Image Diff (SSIM/PSNR) (JHPixelProQADiff) answers those with a picture and with numbers. It's the QA node in a 64-node retouch pack, and it's the one that turns "looks fine" into "62% of pixels changed, all inside the mask."
What you get out of it
Four outputs, and all four are useful:
diff- anIMAGEfor every batch item, showing where the two images differ under your chosendiff_mode.ssim- structural similarity, roughly 0 to 1, where 1 means identical. It weights texture and edges more than a flat brightness shift, which is why it's the right metric for "did the structure survive".psnr- peak signal-to-noise ratio in dB, capped at 100. Higher is closer; identical images report the cap.report- aSTRINGof JSON, one entry per batch item, carryingssim,psnr,mae,max_absandchanged_pct.
That last one is the quiet hero. changed_pct is the percentage of pixels that moved by more than 1/255 - a single, honest number for "how much of this image did I actually alter". mae and max_abs tell you the average and worst-case size of those changes. Feed report into a text preview node and you have an audit trail for the graph.
One caveat baked into the outputs: ssim and psnr as wire values describe batch item 0 only, because they're scalars and a batch isn't. The report covers everything. If you're batch-processing and care about image 7, read the report, not the float.
The settings that matter
image_a is the reference (before), image_b is what you're comparing (after). Same width and height - there's no auto-resize, and a mismatch just errors.
diff_mode picks the visualisation. heatmap is a turbo colormap of the amplified difference and is the right default. abs gives you the per-channel absolute difference, which is more literal and less readable. ssim_map gives you 1 − SSIM per region, so bright means "structure changed here" - that's the mode that ignores a uniform exposure nudge and points straight at an edit that broke something.
amplify (1 to 100, default 4) is gain on the difference for heatmap and abs. Crank it when you're hunting a subtle change; it's ignored by ssim_map. window_size (odd, 3 to 31, default 11) is the SSIM window - larger judges more coarse-grained structure, and it's reduced automatically on small images so it won't explode on a crop.
The optional mask restricts all the metrics to a weighted region. That is the killer feature for the loop this pack is built around: run a detail pass through a face mask, then point the same mask at this node. High changed_pct inside, near-zero outside, means the edit stayed where you put it. The diff image itself is not masked, so you still see the whole frame.
How it's computed
SSIM comes from kornia.metrics.ssim over the chosen window, averaged across channels. PSNR is −10·log10(MSE) with a 100 dB cap. With a mask, every metric becomes a mask-weighted mean. All of it is deterministic tensor maths on GPU - no model, no sampling, and identical numbers every run for the same inputs. That determinism is the point: it's the closest thing a retouch graph has to a test assertion.
Install
ComfyUI Manager → search ComfyUI-JH-PixelPro → install → restart. Or:
cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/ComfyUI-JH-PixelPro.git
cd ComfyUI-JH-PixelPro
pip install -r requirements.txt
Restart and look under Add Node → ComfyUI-JH-PixelPro → qa. The pack requires Python 3.10+, kornia>=0.7, MediaPipe ≥ 0.10, OpenCV ≥ 4.8 and SciPy ≥ 1.10; onnxruntime is optional and only needed for the Depth Anything nodes. This node needs none of the heavy parts - the metrics are pure Kornia.
Common issues
SSIM is exactly 1.0 and PSNR is 100. You fed the same tensor to both inputs, or the edit node upstream passed through unchanged (amount 0, mask all black). Also the documented result when the optional mask is entirely black: metrics return neutral values rather than failing, so an accidentally-empty mask looks like a perfect score. Check the mask.
Numbers look great but the image is obviously different. Check window_size and whether you're reading the batch-0 float while the damage is on another item. And check diff_mode: ssim_map deliberately ignores uniform shifts, so a global colour cast can score well structurally.
"must have shape (B,H,W,3)". Alpha channels or a mask where an image should be. Everything in this pack speaks 3-channel IMAGE and 2D-per-item MASK; if a loader hands over RGBA, strip it first.
A very high amplify makes 8-bit rounding look like damage. Compare against the numbers, not just the pretty colours - that's what mae and max_abs are for.
Fair warning about the pack in general: searching the usual subreddits returns nothing for it, so there's no forum folklore to lean on. What exists is per-node documentation - bilingual, with source-level notes - and a claimed 800+ tests. For a deterministic metrics node that's honestly enough; the maths doesn't have opinions.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| image_a | IMAGE | Reference image (e.g. before retouch). | |
| image_b | IMAGE | Image to compare (e.g. after retouch). | |
| diff_mode | COMBO | heatmap | heatmap = turbo colormap of the amplified difference. abs = amplified per-channel |a-b|. ssim_map = 1 - SSIM map (bright = structural change). |
| amplify | FLOAT | 4.01–100 | Gain applied to the difference (heatmap/abs only). |
| window_size | INT | 113–31 | Odd SSIM window size. Larger = coarser structure. Reduced automatically for small images. |
| maskopt | MASK | Restrict all metrics to this region (weighted). |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| diff | IMAGE | Visual difference image (all batch items). |
| ssim | FLOAT | SSIM of batch item 0 only (1.0 = identical); see report for other items. |
| psnr | FLOAT | PSNR in dB of batch item 0 only (capped at 100); see report for other items. |
| report | STRING | JSON list with ssim, psnr, mae, max_abs, changed_pct for every batch item. |