cv2.estimateAffine3D (1/2)
Registering two 3D point clouds, outliers included
- src
- dst
- retval
- out
- inliers
Two 3D point sets, matched point-for-point, and you want the transform that puts one on top of the other. That's estimateAffine3D - the 3D counterpart of estimateAffine2D, and the workhorse for registration between clouds that came from the same kind of source. This is the (1/2) variant, the RANSAC one: it rejects outliers and tells you which points it kept. Which is usually the version you want, because point clouds are noisy by nature.
The mechanism
It takes N x 3 correspondences and solves for the 3×4 [R|t] matrix that best maps src onto dst, sampling minimal subsets under RANSAC and keeping the consensus within ransacThreshold. Twelve unknowns, three point pairs minimum, tens of points for anything you'd trust. The inliers mask is the useful half: real correspondences - even ones from clean feature matching - include mismatches, and depth-derived points are worse, because stereo or monocular depth error grows with distance and bends the geometry rather than just jittering it.
Inputs and outputs
src (the cloud to be moved) and dst (the cloud to move onto) are both NPARRAY only, N×3, same length, paired in order. Since these are geometry, src and dst are usually what you get out of CV Depth to 3D Points, CV Sample Array At Points, CV Mesh From 3D Model, or a triangulation step - this pack's points/features modules are where those live.
ransacThreshold defaults to 3.0 and is in your cloud's own units. That default is inherited from OpenCV and is meaningless in the abstract: if your cloud is in metres, 3.0 means "within three metres", which accepts almost everything and defeats the point of RANSAC. The curated node in this pack defaults to 0.3 for exactly that reason - a few tenths of a metre is the realistic noise floor for stereo depth. confidence (0.99) is the probability that the returned fit came from an all-inlier sample; leave it.
Outputs: retval is a BOOLEAN success flag, out is the 3×4 matrix, inliers is the N×1 mask. Note that dst is documented as an InputOutputArray in the C++ API and OpenCV would write into it - the wrapper passes a private copy, so your input array is never modified. That's worth knowing if you've been burned by in-place cv2 calls before. out feeds CV Transform Points 3D, and if you're going to render or export afterwards, remember the axis-convention mess: CV Convert Axis Convention (3D) exists in this pack because OpenCV (Y down, Z into the scene) and glTF/Three.js (Y up) disagree, and negating one axis alone is a reflection that nothing downstream will catch.
Should you use this node, or the curated one?
This pack also ships CV Register Point Clouds (3D), which is estimateAffine3D with the plumbing done and some real thought baked in: a method dropdown that offers RANSAC-then-rigid-refit (the right default for stereo clouds, since affine can absorb depth error as shear), a ransac_threshold in metric units, inlier_count, the recovered uniform scale, and a found flag. Critically, it is failure-tolerant: fewer than three correspondences, mismatched counts or a degenerate configuration returns the identity matrix with found=false instead of raising, so a failed registration merges your clouds unaligned rather than killing the run.
Rigid refit plus graceful failure plus a scale readout is what you want 95% of the time. Reach for this raw wrapper when you specifically need the general affine fit or the raw inlier mask, or when you're reproducing a paper.
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 comfyui_cv via Manager, then restart. Python ≥ 3.12 and a recent V3-API ComfyUI. Node path: image/CV/low-level/cv2 E.
Traps
The registry for this pack is generated from the type stubs of a newer OpenCV than the version its behaviour is curated against, so an entry can exist whose implementation your build lacks - if this node exists in the menu but raises on execution, check CV Build Information before assuming you did something wrong. Beyond that: all-collinear or all-coplanar-perfectly-symmetric point sets are degenerate and give a numerically valid but arbitrary rotation. Feed it correspondences with actual 3D spread, and look at the reported inlier count before believing the matrix.
Inputs (4)
| 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. | |
| ransacThresholdopt | FLOAT | 3.0000-1e+38–1e+38 | Maximum reprojection error in the RANSAC algorithm to consider a point as an inlier. Preset to the OpenCV default (3.0). |
| confidenceopt | FLOAT | 0.9900-1e+38–1e+38 | Confidence level, between 0 and 1, for the estimated transformation. Anything between 0.95 and 0.99 is usually good enough. Values too close to 1 can slow down the estimation significantly. Values lower than 0.8-0.9 can result in an incorrectly estimated transformation. Preset to the OpenCV default (0.99). |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| retval | BOOLEAN | — |
| out | NPARRAY | — |
| inliers | NPARRAY | — |