Nodes/ComfyUI-JH-PixelPro/QA: Cull Score
ComfyUI Node

QA: Cull Score

Let the Node Throw Out the Blurry Ones First

By jetthuangai·Created 6 months ago·Updated a day ago· 6
QA: Cull Score
  • images
  • selected
  • report
  • kept
◄top_k0►
◄min_score0.50►
◄check_eyestrue►

The most expensive step in a retouching pipeline is the part that runs on the frames you were never going to keep. QA: Cull Score (JHPixelProCullScore) sits in front of everything else and drops the rejects: soft shots, blown exposures, closed eyes, near-duplicates. Then you retouch what's left.

It is not a judge of expression or composition. It's a shortlister, and it's honest about that. Look at the picks before you deliver anything.

What it measures

All on a 1024 px proxy per photo, one at a time, so scoring doesn't depend on your original resolution and memory doesn't grow with the batch.

Sharpness is Laplacian energy per tile on an 8×8 grid, taking the 90th percentile - so the sharp subject on a blurred background still scores as sharp, which is the usual way naive sharpness metrics fail. If faces are found (YuNet), the face boxes are measured instead. The value goes on a log scale and - this is the smart bit - when the batch contains other shots of the same scene, half the score is relative to the sharpest of them: a shot 10× blurrier than its siblings scores 0 on that half. A soft-looking portrait doesn't lose to a busy street photo that happened to be in the same folder.

Exposure takes penalties for clipped highlights, crushed shadows, and a mean far from mid-grey.

Closed eyes uses the eye aspect ratio from MediaPipe landmarks (the Soukupova & Cech formulation, limit 0.18) on the two largest faces only, so strangers in the background don't tank a shot. That needs MediaPipe installed; if it isn't, the check is skipped rather than fatal.

Duplicates come from a 64-bit difference hash. Photos within 10 bits of each other are chained into groups, and only the best of each group survives.

The composite score is 0.6 sharpness, 0.4 exposure, minus 0.4 for closed eyes.

Inputs and outputs

images - a batch of photos of the same size. This is the main constraint: mix resolutions and you need to resize first.

top_k (default 0) - keep at most this many; 0 means every photo that passes.

min_score (0–1, default 0.5) - drop anything below this.

check_eyes (default on) - the MediaPipe penalty.

Outputs: selected (the keepers, best first, at least one ever), report (JSON, one entry per input: score, sharpness, exposure, eyes_closed, faces, duplicate_of), and kept (how many).

report is the actual feature

duplicate_of tells you which frame a near-identical shot got folded into. eyes_closed is null when the check is off or MediaPipe is missing - not false, which matters if you're parsing it. Wire report into a text preview and leave it on screen: it's the only way to know why a frame got dropped, and "why" is what tells you whether to trust min_score for this shoot.

Then send selected down the line - into Auto Tone, say - so the retouch chain only ever sees keepers.

Install

cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/ComfyUI-JH-PixelPro.git
cd ComfyUI-JH-PixelPro
pip install -r requirements.txt   # the Python that runs ComfyUI

ComfyUI Manager users: search ComfyUI-JH-PixelPro → Install → restart. Node lives under Add Node → ComfyUI-JH-PixelPro → qa. Deps: kornia>=0.7, mediapipe>=0.10.33, opencv-python-headless>=4.8, scipy>=1.10; PyTorch from ComfyUI. No model files. YuNet weights cache themselves via Kornia on first use; MediaPipe's face landmarker downloads once if you leave check_eyes on.

Where people get burned

  • Mixed sizes in one batch. Hard constraint. If your shoot has a mix, resize first - the node compares across the batch, so different sizes break the comparison as much as the tensor does.
  • min_score too high. You'll lose good frames to a slightly grey exposure. Start at 0.5, look at the report, move it.
  • A sharp subject on a soft background. It's handled - tile-based p90, faces measured when found. Don't "fix" it by raising min_score.
  • Skin-level detail work. Culling doesn't care, but if you're deciding between two near-identical frames where the difference is a pore, cull on a downscale is the wrong microscope. Pick by eye.

One framing thought: this is the deterministic, model-free end of the automation spectrum - no diffusion, no seed lottery. The detection-and-detailing loop that dominates this corner of ComfyUI is about re-rendering regions; this is about deciding, with plain arithmetic, which regions are worth re-rendering at all.

The pack itself is Apache-2.0, maintained by JH (@jetthuangai), version 1.10.0, 64 nodes, with a huge test suite and bilingual docs. There's no Reddit chatter about it at all - my search for the pack name turned up exactly one unrelated browser extension - which for a QA utility node is almost appropriate: nobody blogs about the bouncer.

CategoryComfyUI-JH-PixelPro/qa

Inputs (4)

NameTypeDefaultDescription
imagesIMAGEA batch of photos of the same size (e.g. Load Image Batch).
top_kINT00–1000Keep at most this many; 0 = every photo that passes.
min_scoreFLOAT0.500–1Drop photos scoring below this (0..1).
check_eyesBOOLEANtruePenalise closed eyes (needs MediaPipe; skipped if missing).

Outputs (3)

NameTypeDescription
selectedIMAGEThe kept photos, best first (at least one).
reportSTRINGJSON: one entry per input photo with score, sharpness, exposure, eyes_closed, faces and duplicate_of.
keptINTHow many photos were kept.