CV Affine Shape Warp
The rigid fit that tells you when rigid was the wrong answer
- image
- points_from
- points_to
- extra_points
- image
- warped_points
- residual
- matrix
Thin plate splines are the flashy warp: give them control points and they bend the image until every single point lands exactly where you said. That's wonderful with clean correspondences and a disaster with noisy ones, because a spline has no way to say "that match was probably wrong" - it just folds the picture to honour it. CV Affine Shape Warp is the opposite temperament. Six parameters for the entire image, everything solved in least squares, straight lines stay straight, and a residual output that tells you honestly how badly the rigid model fit your points.
Why you'd reach for it
- Noisy correspondences. Matched keypoints, auto-detected landmarks, a tracker that isn't sure. With a dozen mediocre pairs, affine gives you the average consistent motion; a spline gives you a funhouse mirror.
- When the true motion really is rigid - a scanned page, a straightened photo, an object that rotated slightly between frames, a subject pasted into a background at an angle.
- As a diagnostic. Fit affine and read
residual; if it's near zero, the motion was affine and you're done - you never needed the spline. If it's large, the deformation is genuinely non-linear and that is whenCV Thin Plate Spline Warpearns its keep. That's a two-node experiment that saves you from guessing.
It's a curated node because the underlying API is a class (cv2.createAffineTransformer, behind cv2.shape), which the pack's ~470 auto-generated function wrappers can't reach - the create* factories simply aren't functions.
How it works
The node fits warpfit's model of your choosing through the correspondences and applies it with the transformer's warpImage. Unlike the spline - whose warpImage goes backwards, so the pack has to fit twice, once for pixels and once for points - this warp is forward, one fit drives both. Areas pulled in from outside the frame come out black (BORDER_CONSTANT), and the transform is recovered as a 2×3 float64 matrix by pushing the origin and the two unit vectors through it, because the transformer itself won't hand you the matrix.
The inputs that matter
image- the picture to warp, same format back out. Takes anIMAGE/MASK/NPARRAY.points_from- Nx2 control points in the input image, at least 3. Fewer than 3 and the node raises rather than silently doing nothing;cv2itself would die insidesolvewith an unhelpful message, so this is a genuine courtesy.points_to- Nx2 targets, row-aligned withpoints_from. With more than 3 rows it's least squares, so the targets need not be reachable exactly.model- two options, both spelled out: full affine – 6 dof (rotation, scale, shear, translation) is the usual 6-dof least-squares affine; similarity – 4 dof throws away shear and non-uniform scale, keeping angles and shape. Similarity is stiffer, which resists bad correspondence better - but it cannot represent a shear, so its residual stays large no matter how clean your points are. If you're debugging a large residual withsimilarity, tryfull affinebefore blaming your points.extra_points(optional) - Nx2 points pushed through the same transform, not used to fit it. This is how a bounding box, a contour or a mask's ring follows the image you just warped.
Outputs: image (warped, same size), warped_points (Nx2, empty when extra_points wasn't connected), residual (Nx2: where the points actually landed minus where they should have), and matrix (2×3 float64, ready for cv2.warpAffine / cv2.transform or to compare against a known pose).
Where the points come from
Three routes, and the manual one is better than it sounds. CV Annotate Correspondences gives you a click-drag editor that writes the pairs as normalized JSON and emits exactly the two row-aligned pixel sets this node eats - and because both halves come from one widget, they can't fall out of alignment. The automatic route is CV Detect Features → CV Match Features, which is where noise actually comes from. The third is a synthetic grid pushed through a known matrix, which is how you'd sanity-check the whole chain.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
Restart. Or Manager → ComfyUI CV. Hard dependency: opencv-contrib-python-headless~=5.0.0.93, plus Python 3.12+ and a V3-API ComfyUI. Contrib matters because the rest of the pack uses the contrib submodules, and all four OpenCV wheel flavours install into one site-packages/cv2 - a careless pip install opencv-python empties them. tools/repair_opencv_contrib.py --check and --apply are the diagnosis and the fix.
Traps
residualis not an error message, it's a measurement. Near zero withsimilaritymeans the points really are a similarity; large withsimilaritymight just be shear. Always compare the two models before concluding the correspondences are bad.- Row alignment is on you. Mismatched counts raise; mismatched order silently produces a wrong warp with a plausible-looking residual. If you're building the arrays by hand, sort both sides the same way.
- Black borders are not a bug. Rotating an image inside a fixed frame always orphans corners. If you need to fill them, that's a crop policy or a paste-through-warp job, not something this node will do for you.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Image to warp; it is transformed so that 'points_from' move towards 'points_to'. Areas pulled in from outside the frame come out black (BORDER_CONSTANT). Accepts a ComfyUI IMAGE/MASK directly (frame 0 of a batch) or an NPARRAY. Arithmetic ops (add, multiply, etc.) process the full IMAGE batch when both inputs have the same batch size. | |
| points_from | NPARRAY | Nx2 control points in the INPUT image (matched keypoints, landmarks, a grid...). At least 3. | |
| points_to | NPARRAY | Nx2 target positions, row-aligned with points_from - where each control point should end up. With more than 3 rows the fit is least-squares, so they need not be reachable exactly. | |
| model | COMBO | How much freedom the fit gets. 'full affine' is the usual 6-dof least-squares affine (it can shear and scale the axes differently). 'similarity' drops shear and non-uniform scale, keeping angles and shape - a stiffer model that resists bad correspondences but cannot represent a shear at all (its residual stays large no matter how clean the points are). | |
| extra_pointsopt | NPARRAY | Optional extra Nx2 points to push through the same transform (e.g. a bounding box or contour that must follow the image). Not used to fit it. |
Outputs (4)
| Name | Type | Description |
|---|---|---|
| image | COMFY_MATCHTYPE_V3 | The warped image, same format and size as the input. |
| warped_points | NPARRAY | Nx2 'extra_points' mapped through the transform (empty when none were connected). |
| residual | NPARRAY | Nx2 where 'points_from' actually landed minus where they should have. ~0 only when the correspondences ARE an affine (a similarity, for the 4-dof model); otherwise it is the honest least-squares error - the measure of how badly the rigid model fits. |
| matrix | NPARRAY | The fitted transform as a 2x3 float64 matrix, read back by pushing the origin and the two unit vectors through it - ready for 'cv2.warpAffine' / 'cv2.transform' or for comparing against a known pose. |