cv2.estimateAffine3D (2/2)
The no-RANSAC overload, and the reflection switch
- src
- dst
- out
- retval
Same function name, second parameter list. This is the other overload of cv2.estimateAffine3D, and the difference is not cosmetic: the RANSAC knobs are gone (no threshold, no confidence, no inlier mask) and you get one boolean instead, force_rotation. The pack exposes both overloads as two separate nodes - (1/2) is the robust one, this (2/2) is the plain fit. If you need to understand the family first, read the sibling article; this one is about when this variant is the better choice.
What you're choosing between
(1/2) gives you outlier rejection and an inliers mask at the cost of two parameters you have to think about. (2/2) uses every point you give it, equally, and returns the transform. That's the whole trade. On clean correspondences - hand-typed, synthetic, or already filtered by an inlier pass - fitting all the points is better, and it's faster. On anything derived from real detection, feeding all points into a least-squares fit means the worst matches get the loudest vote in the result, which is how you get a registration that's subtly rotated and no error message to explain it.
So: use this variant when you have already vetted the correspondences, or when you don't care about per-point noise because the sets are small and geometric.
The one parameter, and why it exists
force_rotation defaults to True, and the tooltip is the author's own blunt version of OpenCV's: if true, the returned rotation will never be a reflection. That wording is the interesting part - "This might be unwanted, e.g. when optimizing a transform between a right- and a left-handed coordinate system."
That's not a hypothetical. Flip-handedness is the recurring bug in 3D work: OpenCV is Y-down, Z-into-the-scene; glTF and Three.js are Y-up; some scan formats are Z-up. A mirror image and a 180-degree turn about X look identical in a matrix until you check the determinant. With force_rotation=True you are telling the solver "the answer must be a proper rotation, not a mirror", which is right when both clouds genuinely live in the same handedness and the fits are just noisy. Set it to False when you know the two sets are in opposite conventions, because forcing a proper rotation there will quietly produce a transform that's a reflection plus whatever rotation best compensates - and then nothing downstream catches it. The pack ships CV Convert Axis Convention (3D) for the cloud-level version of this problem, and its author notes the same trap: negating one axis alone is a reflection, you negate Y and Z.
Inputs and outputs
src and dst are NPARRAY only, N x 3, same length, paired one-to-one. The dst tooltip repeats the note from the sibling node: the low-level cv2 call writes into that array in place, but this wrapper hands cv2 a private copy, so your input is never mutated.
On the way out, the brief swaps the order you might expect: out is the NPARRAY 3×4 matrix, and retval is a scalar - a status/quality value OpenCV returns alongside the matrix in this overload. The matrix is the output that matters: it drops into CV Transform Points 3D, CV Matrix To Pose (which will decompose a 3×4 into rvec/tvec and warn you if it had to divide out a scale), or a mesh-transform step. There is no inlier output at all on this variant, so if your pipeline branches on registration quality, you're branching on retval out of a curve fit rather than a consensus count - a weaker signal, and worth knowing before you build a threshold around it.
Installing it
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Or install comfyui_cv from ComfyUI Manager and restart. Python ≥ 3.12 and a recent V3-API ComfyUI - this pack is built on the V3 node API and will not load on an older ComfyUI.
Traps
Because the registry is generated from newer type stubs than the OpenCV version the pack is curated against, this overload is a plausible candidate for the "node exists but the installed build lacks the implementation" case the README warns about - if execution throws, check CV Build Information before rewriting your graph.
Then the practical ones. Fewer than three correspondences, or three collinear ones, and you get a matrix with no meaningful rotation. And if force_rotation=False suddenly "fixes" a registration that looked slightly off, that is a signal about handedness, not a tuning success - go find which conversion introduced the mirror. For the cloudy pipeline this node usually sits in, depth-estimation.md and 3d-generation.md cover where the points come from and how forgiving they aren't.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| src | NPARRAY | First input 3D point set containing $(X,Y,Z)$. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| dst | NPARRAY | Second input 3D point set containing $(x,y,z)$. The low-level cv2 function writes its result into this array in place, but this wrapper passes cv2 a private copy, so your input array is never modified. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| force_rotationopt | BOOLEAN | true | If true, the returned rotation will never be a reflection. This might be unwanted, e.g. when optimizing a transform between a right- and a left-handed coordinate system. Preset to the OpenCV default (True). |
Outputs (2)
| Name | Type | Description |
|---|---|---|
| out | NPARRAY | — |
| retval | FLOAT | — |