Nodes/Portrait Dataset Tools/Face Align Crop (derotate to eye level, square crop)
ComfyUI Node

Face Align Crop (derotate to eye level, square crop)

Rotate first, then crop, so the corners don't get clipped

By BillyBGit·Created 22 days ago·Updated 22 days ago· 1
Face Align Crop (derotate to eye level, square crop)
  • image
  • crops
  • labels
  • scores
◄target_sizenative►
◄padding_pct0.15►
◄save_path►
◄filename_prefixface_aligned►
◄clamp_before_upscale512►

What this is actually for

If you're building a face LoRA, the loose rule is 15–25 images of the person, and the thing that makes or breaks them is consistency of framing: close crops, upright heads, similar scale. Photos you scrape off the internet are not consistent. Half of them are tilted, the head runs to a corner, and the face you want to train on occupies 12% of a 4000px landscape.

Face Align Crop does one job: derotate the face to eye level, then take a square head crop. That order matters. Rotating after you crop clips the corners of the face; rotating the whole frame first and then mapping the face box through the same rotation means the crop follows the newly upright face and nothing gets cut. If you've ever eyeballed a dataset and found a run of images where the chin is missing, that's usually a crop-then-rotate pipeline.

How it works

It shares the same InsightFace buffalo_l detector as the rest of this pack - specifically the 5-point landmarks (left eye, right eye, nose, both mouth corners). The eye line gives a roll angle; the whole frame is rotated around the eye midpoint with cv2.warpAffine, and the rotation fills the gaps with reflected pixels rather than a black wedge. Then the original face box's four corners are pushed through the same matrix, expanded by padding_pct, forced square around that centre, and cropped.

Two things worth knowing. First, it corrects roll only. Yaw and pitch are real 3D pose - a 2D rotation can't stand a turned head upright, and no setting here pretends otherwise. Second, detection on tight crops is handled: if SCRFD finds nothing, it pads the image with black borders and retries, because a face that fills the frame defeats the detector's anchors.

If no face is found at all, the branch is blocked with an ExecutionBlocker rather than passing a wrong image downstream. You'll see [FaceAlignCrop] No face detected in the console and a dead branch in the graph.

Inputs and outputs that matter

Three required inputs. image, obviously. target_size - native (the default) returns the crop at original pixels, which is what you want before the upscale/resize stage; 512/768/1024 resize to a square right here. padding_pct (default 0.15) expands the box by that fraction of its width and height per side; 0.15 gives you some hair and neck, 0 gives you a tight, slightly odd-looking box.

The optional ones: save_path writes cropped JPEGs (quality 95) straight to a folder as face_aligned_0000.jpg - handy for a quick eyeball pass, not what you want for the real dataset. filename_prefix names them. clamp_before_upscale (default 512) only applies in native mode and downsamples any crop whose longest side exceeds it - set it to 0 if you intend to feed a genuinely 3000px source into a super-res pass and want all of it.

Outputs are crops, labels and scores. labels comes back as ["FACE_ALIGNED"] and scores as the detector confidence, both JSON-style strings - the node was built as a drop-in for the NudeNet detect+crop pair, so it keeps the same three-output shape and can feed the same upscale → resize → save chain. Wire crops into UpscaleIfSmaller or a resize node. scores is detector confidence, not an image quality score; don't gate anything on it.

Install

ComfyUI Manager: search Portrait Dataset Tools and install, then restart. Manual:

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

That pulls insightface, opencv-python, numpy, Pillow and onnxruntime. On the first run, InsightFace downloads its buffalo_l pack (~300 MB) to ~/.insightface/models/, so the first crop is slow and looks frozen.

Where people get burned

The InsightFace install is the notorious one - for years it needed a C++ toolchain on Windows, and reactor-style nodes still ship a dedicated .bat to get around it. InsightFace 1.0 (May 2026) finally dropped the C++ build requirement, so a plain pip install insightface should work on a current setup.

Then the quieter traps. clamp_before_upscale defaults to 512, so leaving target_size on native quietly hands your upscaler a 512px crop - not what "native" led you to expect. The reflected fill is a real visual artifact: on a face near the frame edge you'll get mirrored pixels in the padding margin, which beats a black wedge but isn't real data. And the licence split hasn't gone away - MIT code, non-commercial InsightFace weights. Fine for personal LoRA work, a problem if you're selling.

Categoryimage/face

Inputs (6)

NameTypeDefaultDescription
imageIMAGE—
target_sizeCOMBOnativenative = return crop at original pixel dimensions (recommended before upscaling). 512/768/1024 = resize to square.
padding_pctFLOAT0.150–0.5Expand the aligned face box by this fraction of width/height on each side.
save_pathoptSTRINGDirectory to save cropped JPEGs directly. Empty = no save here.
filename_prefixoptSTRINGface_aligned—
clamp_before_upscaleoptINT5120–2048Only applies in native mode. If the crop's longest side exceeds this, scale it down before returning. 0 = disabled.

Outputs (3)

NameTypeDescription
cropsIMAGE—
labelsSTRING—
scoresSTRING—