Nodes/Portrait Dataset Tools/Image Chroma Check (B&W / sepia detector)
ComfyUI Node

Image Chroma Check (B&W / sepia detector)

Find the B&W and sepia shots before you waste a colourise pass

By BillyBGit·Created 22 days ago·Updated 22 days ago· 1
Image Chroma Check (B&W / sepia detector)
  • image
  • is_monochrome
  • chroma
  • hue_spread
  • suffix
  • report
◄max_chroma5.0►
◄sepia_max_chroma8.0►
◄sepia_hue_spread0.45►
◄suffix_if_monochrome_col►
◄suffix_in►

Why you'd reach for it

Family-photo datasets are a mix. Some of it is colour, some is a scan of a print that was already monochrome, and a chunk is sepia or hand-toned. Colourising a photo that already has colour does nothing useful, and running a colourisation model on 300 images when 40 of them needed it is 260 wasted passes.

Image Chroma Check is a two-line measurement that sorts them: monochrome or not, and a ready-made filename suffix so the colourised files stay identifiable on disk. That suffix matters more than it sounds - three months later you want to know which images in your training set were invented by a model rather than photographed.

How it works

The mechanism is in the pack's shared portrait_quality.py, and it's more careful than a saturation threshold. It converts to CIELAB, computes mean chroma as sqrt(a² + b²) over pixels above a small chroma floor, and computes a circular hue spread - the resultant vector length of the hue angles gives you sqrt(-2·ln r), which is small when every coloured pixel shares one hue and large when they don't.

Then the rule:

is_monochrome = chroma < max_chroma
             or (chroma < sepia_max_chroma and hue_spread < sepia_hue_spread)

So: low chroma means black and white, but modest chroma with one dominant hue means a toned print. That's what lets you call a sepia scan monochrome without also calling every warm-toned colour photo monochrome. The defaults weren't guessed - they were calibrated on 176 vintage and modern portraits.

Inputs and outputs

Required: image, max_chroma (5.0 - B&W scans sit under 3, the first genuinely coloured ones show up around 7), sepia_max_chroma (8.0), sepia_hue_spread (0.45), and suffix_if_monochrome (_col). Optional suffix_in is prepended to the suffix output, so an upstream _rev tag survives the trip - that's the pattern across this pack, and it's why the suffixes compose into things like _col_sup_rev.

Setting max_chroma to 0 disables detection entirely, which is the escape hatch if you only care about the numbers.

Outputs: is_monochrome, chroma, hue_spread, suffix and report. Wire is_monochrome into a LazyImageSelect condition so the colourisation branch runs only on the images that need it, and suffix into your save node. The chroma and hue_spread floats are worth pushing into a log line - a histogram of them tells you what's actually in an archive.

Install

ComfyUI Manager: search Portrait Dataset Tools. Manual:

cd ComfyUI/custom_nodes
git clone https://github.com/BillyBGit/ComfyUI-PortraitDatasetTools
pip install -r ComfyUI-PortraitDatasetTools/requirements.txt

insightface, opencv-python, numpy, Pillow, onnxruntime, plus a one-time buffalo_l download (~300 MB) into ~/.insightface/models/ on first launch. This node itself is pure OpenCV and needs no model - but the pack's shared InsightFace app is what the rest of the nodes in the same workflow use.

Where people get burned

It measures the whole frame. That's the design, and it means context matters: put it on the head crop after Face Align Crop, not on the original scan. A black-and-white portrait against a coloured wall backdrop reads as colour, because the wall has chroma.

Sepia is a judgement call, and the node makes it for you. A toned monochrome photo is monochrome, so it will get sent down the colourise branch and come back as invented colour, while the original print's tone is gone. For a restoration project that's arguably the wrong answer. If you keep sepia prints as they are, tighten sepia_hue_spread toward 0 (or sepia_max_chroma to 0) so only genuinely B&W images trip the gate.

Old scans drift. Heavy grain and yellowed paper push mean chroma around, so an archive of cheap scans can sit right on the 5.0 line and flicker between the two verdicts image to image. Run it across a set with the log visible, look at the distribution of chroma, and put the threshold in a valley rather than on a peak.

Categoryimage/analysis

Inputs (6)

NameTypeDefaultDescription
imageIMAGE—
max_chromaFLOAT5.00–50Below this mean chroma = monochrome. B&W scans <3, first real colour ~7. 0 disables detection.
sepia_max_chromaFLOAT8.00–50Up to this chroma still counts as monochrome IF the hue is uniform (sepia / toned prints).
sepia_hue_spreadFLOAT0.450–3Circular hue spread below this = one dominant tint.
suffix_if_monochromeSTRING_col—
suffix_inoptSTRINGPrepended to the suffix output, so upstream tags (e.g. '_rev') survive.

Outputs (5)

NameTypeDescription
is_monochromeBOOLEAN—
chromaFLOAT—
hue_spreadFLOAT—
suffixSTRING—
reportSTRING—