Skin Texture Check (gate a restoration branch)
Did that restoration pass give the skin pores, or wax?
- image
- is_low
- texture
- suffix
- report
The measurement, and why it's the one that matters
Too-smooth skin is the most common tell in generated and upscaled portraits - the beauty-filter look that reads as uncanny the moment you zoom in. And on an upscaled face it's worse than a cosmetic flaw: if a super-res pass invents flat, plausible skin where there was none, your LoRA learns that flatness as the truth about this person's face.
Skin Texture Check measures texture on a finished head crop and answers one question: is this lacking skin texture? Wire the answer into a lazy branch and the restoration model only runs on the images that need it, which on a real dataset is a minority.
How it works
It uses the same Immerkaer noise estimate as PortraitQualityScore - a small Laplacian-style kernel where the mean absolute response, scaled, gives a noise measure that's largely blind to edges. It's sampled on flat skin patches (cheeks and forehead, located from InsightFace's 5-point landmarks) at a normalised face scale, so the number is comparable across a 600px crop and a 4000px one. Then it compares to min_texture.
The intended use is the interesting part: use it twice. First pass with min_texture at 0.85 and suffix_if_low set to _sup - is_low drives your restoration branch. Second pass, after restoration, with min_texture at 0.70 and suffix_if_low at _rev - anything still low gets flagged for a human. The suffix chain then reads cleanly on disk:
- no tag → the crop was fine to begin with
_sup→ restoration ran and fixed it_sup_rev→ restoration ran and it's still marginal, go look
Reference points on a 1024 crop with a face around 800px: wax and illustration below 0.55, a SUPIR pass lifts a soft crop into roughly 0.7–1.1, real photographic pores sit at 1.2–2.5.
Inputs and outputs
Required: image, min_texture (0.85 - set it to 0 to disable and always report OK), suffix_if_low (_sup). Optional suffix_in is prepended to the suffix output so upstream tags survive, and label (default texture) just labels the log line - useful when you're running the node twice and want to tell the two lines apart.
Outputs: is_low (boolean), texture (the float), suffix, report. Wire is_low into a LazyImageSelect condition so the SUPIR branch only executes when it's needed; wire suffix into your save node; push texture into a log or CSV and you've got a distribution you can actually set a threshold from.
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, and a one-time ~300 MB buffalo_l download into ~/.insightface/models/ - the landmarks come from that face model, so this node isn't dependency-free even though the maths is pure OpenCV.
Where people get burned
It cannot tell pores from dither. The author says this outright and it's the honest limitation: three separate failure modes - a VAE spraying uniform dither, an edit model laying a regular dot lattice, a base model keeping the input's chroma noise - all scored higher than real skin while looking obviously wrong at 1:1. Treat the number as a gate, not a verdict, and open a few files before you loosen it.
The label is your only way to tell two calls apart. Run it twice with the same label and the console gives you two identical-looking texture=0.x lines from different stages of the pipeline.
Thresholds are calibrated for a finished 1024 crop, so measuring 512px thumbnails or full frames with a tiny face will give you numbers that don't mean what the reference table says. Crop first - FaceAlignCrop in this pack, or anything that puts the head at roughly dataset scale.
Restoration that "fixes" texture can still be the wrong answer. SUPIR's whole reputation is that it restores degraded sources beautifully and invents plausible faces in the process. If the texture gate is passing everything and your set looks uncanny, run the output through FaceIdentityGuard too and see whether the person survived the pass.
And the standing caveat for anything in this stack: the pack's code is MIT, but the InsightFace models you're using for landmarks are non-commercial. Personal datasets, no issue. Commercial work, read the terms.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | — | |
| min_texture | FLOAT | 0.850–5 | Below this counts as lacking texture. 0 disables (always reports OK). |
| suffix_if_low | STRING | _sup | — |
| suffix_inopt | STRING | Prepended to the suffix output so upstream tags survive. | |
| labelopt | STRING | texture | Shown in the log line. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| is_low | BOOLEAN | — |
| texture | FLOAT | — |
| suffix | STRING | — |
| report | STRING | — |