Nodes/QAlign/QAlign
ComfyUI Node

QAlign

The node that warps an edit model's drift back onto the original

By zuanfilm·Created 19 days ago·Updated 18 days ago· 7
QAlign
  • reference
  • source
  • subject_mask
  • aligned_image
  • overlay_preview
  • difference_preview
  • alignment_report
◄alignment_mode▾►
◄local_correctiontrue►
◄correction_strength100►
◄max_global_shift256►
◄max_local_flow16.0►
◄flow_smoothing81►
◄protect_backgroundfalse►
◄preserve_source_sizefalse►
◄diagnostic_overlaytrue►
◄pyramid_levels4►
◄ecc_iterations100►
◄max_scale_deviation0.12►
◄max_rotation_degrees12►
◄scale_modeAnisotropic X/Y►
◄max_scale_deviation_x0.080►
◄max_scale_deviation_y0.080►

QAlign is a one-trick node: you have two pictures of the same scene, one has drifted or stretched a little, and you want the geometry of one without the pixels of the other. It generates nothing, calls no API, ships no weights. It's OpenCV image registration wearing a ComfyUI hat.

Why you'd want this

Spend any time with an instruction editor and you know the failure. The model re-emits the whole frame rather than patching it, so unedited regions come back close but not identical, and the error lands as a geometric offset that distorts proportions - the complaint behind the most-upvoted "image offset problem of Qwen-image-edit" post of 2025. Faces drift, shirts wander.

The established fixes are upstream: keep the input resolution at a sensible multiple so the model stops hallucinating the offset, or crop to a mask, edit, and stitch. QAlign is the third option, and the after-the-fact one: measure the drift, then warp it out. Same category of fix as Klein's PixelDriftFix node, different mechanism.

The author's framing is the whole design: the reference defines where things belong; the source defines how they look. Your aligned output is transformed source pixels - the reference contributes geometry and nothing else, so lighting, texture, and color stay wrong-on-purpose.

How it works

Classical computer vision, no learned model anywhere. The pair becomes 8-bit RGB, and a blurred grayscale copy gets CLAHE'd so uneven lighting is less of a problem for matching.

Automatic mode runs feature registration: ORB keypoints (up to 10,000), a brute-force Hamming matcher, a Lowe-style ratio test to drop ambiguous matches, then a v0.5.0 addition that buckets the survivors into an 8×8 grid so one textured region can't dominate. RANSAC fits an affine transform, accepted only if it clears your translation/rotation/scale limits with at least eight inliers - otherwise it falls back to multi-scale ECC, and the report says which path won.

Hybrid, Affine, and Translation skip features: phase correlation supplies a translation seed, then multi-scale ECC refines it (Translation stops at the seed). Local correction then runs Farneback dense optical flow, smooths the field with a Gaussian, clips it to max_local_flow, scales it by correction_strength, and remaps. The final warp is Lanczos with reflected borders.

Inputs worth touching

Wire reference (correct geometry) and source (the appearance you're keeping) in that order; backwards makes the node faithfully move the wrong image. alignment_mode has a small trap: the README table lists Affine, but that's only the default when you call the node through the API - the UI gives you the first dropdown entry, Automatic, which is where you want to start anyway. max_global_shift is the first thing to raise if the output is massively offset. And the scale-deviation and rotation values are acceptance limits, not requests: max_scale_deviation_x = 0.08 permits 0.92–1.08, it doesn't apply an 8% correction.

For local correction, turning local_correction off first is the discipline - get the global transform right, check the overlay, then enable flow. correction_strength and max_local_flow multiply into your actual maximum displacement; flow_smoothing decides how broadly it spreads. If flow bends a face or smears an edge, those three are your dials, in that order. The optional subject_mask gates local flow to a region and keeps the globally aligned source outside it; it doesn't affect the global estimate.

Outputs

aligned_image is the one you wire onward. overlay_preview is a 50/50 blend of reference and result when diagnostic_overlay is on - this is how you actually judge alignment, on eye corners and shoe outlines. difference_preview is a normalized grayscale absolute difference; it stays bright wherever clothing or lighting changed, which is why it's a diagnostic and not a score. alignment_report is JSON: the transform, RANSAC inliers, which estimator supplied the matrix, and the flow statistics.

Install

Manager → search "Qalign", or:

cd ComfyUI/custom_nodes
git clone https://github.com/zuanfilm/Qalign

Restart and search the node menu for QAlign - no model files, no keys, Apache-2.0.

The one real dependency is OpenCV, and this pack's pyproject.toml declares no dependencies at all, so nothing installs cv2 for you - other packs usually bring it in, but a clean environment can miss it:

python -m pip install opencv-python

Where it goes wrong

Feature matching doesn't know what a subject is. A busy background, a repeated pattern, or a big outfit change can drive a transform that agrees with the wallpaper and not your character - high inlier ratio, wrong alignment - and the 8×8 balancing only partly mitigates that. Switch to Affine or Translation and compare. Also: only the first image in a batch is processed, so one pair per execution.

Two smaller traps. If dimensions differ, the source gets resized to the reference unless preserve_source_size is on, and a resize is itself a geometric operation - normalize both inputs first and let QAlign fix what's left. And a difference preview that never goes dark usually isn't an alignment failure; it's new clothes.

CategoryQAlign

Inputs (19)

NameTypeDefaultDescription
referenceIMAGE—
sourceIMAGE—
alignment_modeCOMBO5 options: Automatic, Hybrid, Translation, Affine, Local only
local_correctionBOOLEANtrue—
correction_strengthINT1000–100—
max_global_shiftINT2561–4096—
max_local_flowFLOAT16.00–256—
flow_smoothingINT813–301—
protect_backgroundBOOLEANfalse—
preserve_source_sizeBOOLEANfalse—
diagnostic_overlayBOOLEANtrue—
pyramid_levelsINT41–6—
ecc_iterationsINT10010–500—
max_scale_deviationFLOAT0.120.01–1—
max_rotation_degreesFLOAT121–90—
scale_modeCOMBOAnisotropic X/Y2 options: Full affine, Anisotropic X/Y
max_scale_deviation_xFLOAT0.0800.005–1—
max_scale_deviation_yFLOAT0.0800.005–1—
subject_maskoptMASK—

Outputs (4)

NameTypeDescription
aligned_imageIMAGE—
overlay_previewIMAGE—
difference_previewIMAGE—
alignment_reportSTRING—