Geometry: Auto Align
Line Two Photos Up Without Nudging Anything by Hand
- image
- reference
- aligned
- valid_mask
- matrix_json
- inliers
Every image comparison breaks the same way: the two shots are two pixels apart, and your diff, your color match, your blend all read that miss as a real difference. Geometry: Auto Align (JHPixelProAutoAlign) fixes the boring half of that problem automatically. Feed it a photo and a reference, it finds the same features in both, works out the transform, and warps one onto the other. No markers, no eyeballed offsets.
This is the setup step, not the result. It's what you run before the interesting deterministic work in the post-processing layer - color matching, layer blends, before/after diffs. Statistics-based color transfer (the Reinhard-style mean/std thing that Color Match Image and the PixelPro Color Matcher both do) is only as good as the region it samples; if half your pixels are sky in one frame and wall in the other, it quietly measures the wrong thing. Align first and it measures the right thing.
How it works
Both images get turned into greyscale copies, shrunk so their long side is at most max_side. With method = sift you get classic SIFT keypoints with mutual nearest-neighbour matching - no download, runs anywhere. With method = loftr you get Kornia's LoFTR, a learned dense matcher that survives lighting changes and repeated texture where SIFT gives up; it pulls a ~45 MB checkpoint into the torch hub cache the first time you touch it.
Either way the matches then go through RANSAC (ransac_threshold, in pixels at matching size), which fits the model on random subsets of matches and keeps the fit the most matches agree with. Surviving inliers get a least-squares refit, the matrix is scaled back up to full resolution, and the warp is a bilinear warp_perspective. Matching happens small, the warp happens full-size - that's why max_side doesn't cost you sharpness.
What you actually set
image is the picture to move; reference is the target frame, and the output takes its size. Batch of 1, or matching the image batch.
model is the choice worth thinking about, and the tooltip is honest about it. homography handles perspective change - a crop and an original, a slight tilt. affine (rotate/scale/shear/shift only) is the steadier of the two when the scene is near-flat or you only have a handful of matches, because it has fewer degrees of freedom to fit noise with.
min_inliers (default 12) is your safety valve. Below it, the node does not error - it logs a warning, resizes the image to the reference size, and hands you an all-white valid_mask. That's a trap if you only look at the picture.
The outputs: aligned is the warped image, valid_mask is 1 wherever the warp has real pixels (the borders after a rotation are black - use this to restrict your stats or crop), matrix_json is a JSON list of 3×3 matrices in full-resolution pixels, and inliers is your quality signal.
One catch about inliers
inliers reports the first item of the batch only. For a batch, read matrix_json instead: a matrix that's pure scale means that frame hit the fallback. Looks fine, isn't aligned.
Install
ComfyUI Manager: search ComfyUI-JH-PixelPro, install, restart. Manually:
cd ComfyUI/custom_nodes
git clone https://github.com/jetthuangai/ComfyUI-JH-PixelPro.git
cd ComfyUI-JH-PixelPro
pip install -r requirements.txt # use the Python that runs ComfyUI
Restart, then find it at Add Node → ComfyUI-JH-PixelPro → geometry. The pack pulls kornia>=0.7, mediapipe>=0.10.33, opencv-python-headless>=4.8 and scipy>=1.10; PyTorch comes from ComfyUI itself. onnxruntime is optional and only matters for the depth nodes - this one doesn't need it.
Where people get burned
- Black borders after warping. Expected. Either crop with
valid_maskor feed the mask into whatever comes next. loftron a first offline run. The 45 MB checkpoint downloads on first use; on a machine with no outbound access that's a failure, not a slow start. Keepsiftif you're air-gapped.- Two shots of totally different scenes. It won't refuse; it'll find a handful of accidental matches if it can. Check
inliersagainst yourmin_inliersbefore trusting the output. - Big lighting shift, soft focus, wallpaper-like repeated texture. SIFT starves here - this is exactly the case
loftrexists for.
One aside: the pack itself is Apache-2.0, actively versioned (1.10.0, 64 nodes), with bilingual docs and a pile of automated tests - and, as of writing, essentially zero chatter on Reddit. If you go looking for tutorials you'll find nothing, which is why the node's own tooltips are worth reading instead of skimming.
For a hand-held exposure bracket, this is the aligner you want before HDR Merge can't help you - that node only does translation. For everything else, it's the quiet step that makes the next three steps work.
Inputs (7)
| Name | Type | Default | Description |
|---|---|---|---|
| image | IMAGE | Image to warp onto the reference. | |
| reference | IMAGE | Target frame. Output has this size. Batch 1 or same as image. | |
| method | COMBO | sift | sift: classic keypoints, no download. loftr: learned dense matcher for hard pairs; downloads ~45 MB on first use. |
| model | COMBO | homography | homography handles perspective change; affine (rotate/scale/shear/shift) is steadier for near-flat scenes or few matches. |
| max_side | INT | 1024256–2048 | Longest side used for matching. The warp uses full resolution. |
| ransac_threshold | FLOAT | 3.00.5–20 | Max error (pixels at matching size) for a match to count. |
| min_inliers | INT | 124–500 | Below this many agreeing matches the image is just resized to the reference size (warning in the log). |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| aligned | IMAGE | Image warped into the reference frame. |
| valid_mask | MASK | 1 where the warped image has real pixels. |
| matrix_json | STRING | JSON list of 3x3 matrices, one per batch item (image to reference pixels). |
| inliers | INT | Agreeing-match count of the first batch item only. |