CV Thin Plate Spline Warp
When a Homography Isn't Enough, Bend the Image Through Your Points
- image
- points_from
- points_to
- extra_points
- image
- warped_points
- residual
A homography moves a plane rigidly: eight parameters, straight lines stay straight, and it can't express a bend. A thin plate spline is the opposite tradeoff - every control point you give it lands exactly where you said, and the surface in between bends as little as it can get away with. The physical picture is a thin metal sheet pinned at your points.
That's the right tool for face and object morphing, warping one shape onto another after feature matching, and correcting whatever non-linear distortion your camera's lens model doesn't cover - barrel roll that no k1/k2 fix quite nails, or a printed page photographed with a fold in it.
How it works
points_from are Nx2 control points in the input image - matched keypoints, landmarks, a grid, whatever. points_to are Nx2 targets, row-aligned: point i goes to position i. Minimum 3 points; the node raises loudly if the counts don't match, which is the error you actually want.
Under the hood it fits the spline and warps the image with a backward mapping - the pixels need an inverse transform while your points need the forward one, so internally it's two fits. CV Affine Shape Warp is the sibling node for the same job with a rigid or affine model, and it's the one to try first when your correspondences are noisy.
extra_points (optional) is an Nx2 set pushed through the same warp without influencing the fit - a bounding box or contour you want to follow the image. Those come back as warped_points.
residual is the honest number: where points_from actually ended up minus where they should have. At the default regularization it's ~0 - the spline hit every point - and it grows as you smooth, which is exactly how much accuracy you traded away. Print it or preview it; it's the difference between knowing and hoping.
The regularization trap
regularization is 0 by default, which interpolates the control points exactly - including your noisy, slightly-wrong matched keypoints. That's the setting that produces a beautiful fit and a folded, rippling image, because the spline is faithfully chasing outliers.
Here's the part that gets everyone: the value is image-scale dependent in a big way. OpenCV adds it to the diagonal of a kernel matrix whose other entries are d²·log(d²) in pixels squared. On a few-hundred-pixel image, 100 buys you about 0.1 px of give, and ~1e4 is the first setting you can actually see. Scale it with the square of your image size - the value that works at 512 px is not the value that works at 1024 px, it's roughly four times larger.
So: raise regularization until the fold disappears, then check residual to see what it cost you. If the residual is large at a regularization that looks good, your correspondences are bad and no spline setting will save them.
The image format follows the input - an NPARRAY stays an NPARRAY, a ComfyUI IMAGE comes back as an IMAGE at the same size.
Install
ComfyUI Manager → search ComfyUI CV (publisher bmad4ever), or:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Restart after. Python ≥ 3.12 and a recent ComfyUI on the V3 node API. Keep the contrib wheel - a plain opencv-python install shares site-packages/cv2 and silently empties the contrib submodules, taking other nodes with it. tools/repair_opencv_contrib.py --check / --apply.
46_affine_shape_warp.json and 32_shape_match_warp.json show the shape-warp family; the README gallery has a dedicated thin-plate GIF. Sample images arrive via 01_install_example_inputs.json (run it, then reload the page).
Where it bites
One implementation detail from the source is worth repeating because it's a classic OpenCV gotcha: the shape transformer has to come from the createThinPlateSplineShapeTransformer factory. Constructing the class directly gives you an abstract object whose every method throws. The pack handles this - you can't hit it from the graph - but it's why this exists as a curated node instead of a wrapper.
The rest is the pack's usual framing, in the author's own words: personal project, heavy LLM assistance, curated against one OpenCV build, no support planned, workflows not production-grade. This node does two fits and reports its own residual, which is more honesty than most warp nodes offer.
Inputs (5)
| Name | Type | Default | Description |
|---|---|---|---|
| image | COMFY_MATCHTYPE_V3 | Image to bend; it is warped so that 'points_from' land on 'points_to'. 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. | |
| regularization | FLOAT | 00–1000000 | Smoothing of the spline. 0 interpolates the control points EXACTLY (and follows noisy correspondences faithfully); raising it trades exactness for a gentler, more rigid warp - the knob to turn when the result folds or ripples. IMAGE-SCALE DEPENDENT: cv2 adds this to the diagonal of a kernel matrix whose other entries are d^2*log(d^2) in PIXELS SQUARED, so useful values are large - on a few-hundred-pixel image 100 buys 0.1 px of give and ~1e4 is the first setting you can see. Scale it with the SQUARE of the image size. |
| extra_pointsopt | NPARRAY | Optional extra Nx2 points to push through the same warp (e.g. a bounding box or contour that must follow the image). Not used to fit the spline. |
Outputs (3)
| 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 spline (empty when none were connected). |
| residual | NPARRAY | Nx2 where 'points_from' actually landed minus where they should have - all ~0 at regularization 0, growing as you smooth. The honest measure of how much the spline gave up. |