ComfyUI Node

pyiqa FR Score

Get a number for 'is my restored image closer to the original?'

By AnsteinHuynh·Created 7 days ago·Updated 6 days ago· 0
pyiqa FR Score
  • model
  • target
  • reference
  • score
◄memory_managementunload model (VRAM + RAM)►

Full-reference scoring is the simplest idea in image quality: you have a pristine original, you have a processed copy, and you want to know how far the copy drifted. Denoise a photo, upscale it, run it through a VAE twice, restore a scratched scan - the moment you have a ground truth, an FR metric turns "does this look better?" into a float you can sort by.

This node does that for any full-reference metric in pyiqa's registry. You don't pick the metric here; you pick it on (Down)load pyiqa Model and wire the model in.

What you wire in

Three things matter, and one of them is easy to get backwards.

target is the image being judged - your produced, restored, denoised, upscaled version. reference is the pristine original. The tooltips spell it out, and pyiqa calls them x and ref internally.

Here's the part people trip on: for almost every metric, order doesn't actually matter. The pack measured it rather than trusting the docs, scoring every FR metric as f(a,b) and f(b,a). LPIPS, DISTS, MS-SWD, SSIM, MS-SSIM, PSNR, A-FINE FR - identical either way. The handful that aren't: TOPIQ FR (~0.8% drift), WaDIQaM FR (~2%), VIF (~1.5%). So wire them sensibly anyway, because the exceptions are real and because you'll want the same convention across a project.

model comes from the loader, and only a variant labelled FR will score here. Wire an NR variant in and the node refuses with a message naming what would have worked, rather than failing inside pyiqa.

memory_management defaults to unload model (VRAM + RAM) - after scoring, the metric is gutted and the next score reloads it from disk. Keep that default in a generation workflow; the metric has no business holding VRAM between runs. Switch to keep loaded only when you're re-scoring in a loop.

Which metric to actually pick

The variant label tells you direction and range, so start there. LPIPS is the one everyone knows: perceptual distance, 0-1, lower better, and it's still the sane default for "how much did this pipeline mangle my image". DISTS and MS-SWD behave similarly and are less fooled by blur. A-FINE FR is the modern one - lower better, ~-2..2 - and despite its docstring warning that you can't swap its inputs, it measured order-independent here.

For a pair of images where one is a different picture (a generation rather than a restoration), no FR metric is honest: LPIPS between a photo and a rendered image mostly measures content mismatch. That's a job for FGResQ FR Score, which returns a rank and a verdict rather than a distance.

No resolution widgets, on purpose

There is no size input anywhere in this node, and that's a design decision, not an omission. FR metrics are fussy about geometry - pixel-wise ones need matching sizes, A-FINE asserts sides divisible by 32 - so the node resizes the reference to the target's geometry and adjusts the sides where an architecture insists, then scores. Batches pair up by position: target image #1 against reference image #1.

Output is a single score socket, emitted as a list. That detail has a real consequence: each value flows downstream as its own item, so a scalar consumer runs once per image. Ten images in, ten separate scores out.

Where does it go? Anywhere a float goes. A text display if you're eyeballing, a Save Image filename prefix if you're building a leaderboard, or a text-file writer for a CSV of a whole upscaler bake-off. That's the workflow the compare slider can't do - score 200 images across three upscalers and read the table.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/AnsteinHuynh/ComfyUI-pyiqa-toolbox
python -m pip install -r ComfyUI-pyiqa-toolbox/requirements.txt

Or ComfyUI Manager → search pyiqa-toolbox. Restart. Weights download on first score into ComfyUI/models/pyiqa.

Fair warning about the dependency footprint: pyiqa pulls torch, transformers>=5, accelerate, bitsandbytes, openai-clip. The transformers 5 requirement is the usual knife in the ComfyUI ecosystem - install into the same venv as everything else and a pack pinned to transformers 4.x may stop importing. Nothing this pack can do about it; plan for it.

Common issues

Batch mismatch. target and reference need the same batch size. Ten generated images against one reference image is an error, not a broadcast.

My scores are all tiny and I thought lower was worse. Check the label. Half of pyiqa's FR metrics are lower-better; seeing 0.08 from LPIPS is a good result.

A score node you never wired to anything still ran. ComfyUI executes every node whose outputs nothing consumes, so a score node parked off to the side of the canvas is still live - and still pulls its metric into memory. Bypass or delete the ones you're not using.

Big VRAM spike in a generation graph. The score node is lazy but the loader isn't free after scoring unless memory_management is left on unload - or you put Unload Pyiqa Models at the end of the graph.

Categorypyiqa/Generic

Inputs (4)

NameTypeDefaultDescription
modelPYIQA_ANYFrom the matching Loader node. This input (and the loader feeding it) is evaluated LAZILY - only once this node's image inputs exist - so a generation-only run never loads the metric model.
targetIMAGEThe image being judged. In an enhancement workflow this is normally the produced/restored/denoised image, or any distorted version (noisy, blurry, compressed) - the one whose quality you want to measure. pyiqa calls it `x`.
referenceIMAGEThe pristine original / ground-truth image the target is compared against (pyiqa's `ref`). Paired with target by batch position: target image #i is compared to reference image #i. Order-independent: swapping target and reference yields the same score.
memory_managementCOMBOunload model (VRAM + RAM)What happens to this metric after it scores. 'unload model (VRAM + RAM)': the metric is fully unloaded - gone from VRAM and RAM, only the file on disk remains; the next score reloads it (seconds for small models, ~30s for Q-ReAlign Pro). Re-queueing just works: a previously-unloaded model handed back by ComfyUI's output cache is rebuilt transparently. 'keep loaded': stays on the GPU, fastest to re-score, holds its VRAM between runs. With several score nodes sharing one loader, unload only on the LAST one - earlier nodes should 'keep loaded' or you pay a reload between them.

Outputs (1)

NameTypeDescription
scoreFLOATPer-pair score between the target (the produced/distorted image being judged) and its reference (the pristine original), computed by the selected registry metric. Higher- or lower-better depends on the metric (the variant label says which).