cv2.undistortPoints
Fix lens distortion when you only have points, not pixels
- src
- cameraMatrix
- distCoeffs
- nparray
cv2.undistortPoints is the point-shaped sibling of cv2.undistort. One takes a whole image and resamples it; this one takes a handful of detected coordinates out of a distorted photo and hands back where those same points should have been if your lens were perfect. If you're doing anything geometric with the pack - camera pose, epipolar geometry, triangulation, calibration checks - this is the node in the middle of it, and it's the one where beginners get numbers that look wrong because of a default they didn't know about.
Why you'd use it instead of undistorting the image
Because resampling is destructive. Warping a frame through a distortion model resamples every pixel, softens the image, and changes the geometry you were about to measure. If all you need is where corner 47 actually is, then undistorting 47 numbers is exact, free, and reversible in spirit - nothing is thrown away. In the wider ComfyUI world, distortion correction mostly shows up when you're feeding a pose network or aligning two views; for the image side of that the pack has curated nodes (CV Fisheye Undistort, CV Omnidir Undistort) that do the whole-frame job with the right model. This node is the sparse-data half.
How it works
For each (u, v) it normalises by the intrinsics, runs OpenCV's iterative undistortion against the distortion coefficients (k1, k2, p1, p2[, k3 …]), and optionally applies a rotation and a new projection. The important bit is the tail of that chain: if the new camera matrix P is omitted, the output is normalised coordinates, not pixels. The OpenCV doc says it plainly: "If matrix P is identity or omitted, dst will contain normalized point coordinates." This wrapper exposes src, cameraMatrix, distCoeffs and a termination-criteria triplet - but no P. So yes: your "undistorted pixel coordinates" come back as numbers roughly in the range ±1. That's not a bug, and it's the number one confusion with this node. To get pixels back you scale by fx, fy and add cx, cy yourself - cv2.perspectiveTransform with a 3×3 K will do it in one shot - or measure in normalised space, which is what solvePnP and triangulatePoints want anyway.
The inputs that matter
src- the observed points, in pixel coordinates. It accepts anNPARRAY, or anIMAGE/MASKlink (frame 0of a batch). The array needs to beNx1x2/1xNx2two-channel floats, so feed it the raw point array from a corner or feature detector, not a picture.cameraMatrixanddistCoeffs- NPARRAY only, no image link. These are data, not pictures. The clean way in isCV Load Camera Params (JSON), which emits exactly this pair (camera_matrix,dist_coeffs);CV Camera Matrixbuilds a K from fx/fy/cx/cy if you're typing intrinsics by hand, andCV Numbers To Arrayturns a list of coefficients into the vector this input wants.- The optional triplet
criteria_type,criteria_max_count(30),criteria_epsilon(0.001) is the iteration limit for that iterative undistortion. It's OpenCV's default unless you touch it, which is fine for normal lenses and worth tightening only when distortion is steep enough that you can see convergence matter.
Outputs
One NPARRAY of undistorted (normalised, remember) points. From here it's the geometry half of a workflow: cv2.solvePnP / CV Solve PnP (Pose) to get a camera pose, cv2.findFundamentalMat or cv2.triangulatePoints for multi-view work, cv2.perspectiveTransform to get back into pixels. If you just want to see what came out, wire it into Inspect CV Data - it reports shape, dtype and value statistics of any array, which beats guessing from a preview.
Installing it
The whole wrapper family ships in one pack. In ComfyUI Manager, search ComfyUI CV; manually:
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
# restart ComfyUI
Needs Python ≥3.12 and a recent ComfyUI (the pack is built on the V3 node API). It also brings ~470 other cv2.* wrappers, generated from whatever your OpenCV build exposes - so a node that's missing from the menu means your build lacks that function.
What goes wrong
The usual story is the normalised-coordinates thing above: people wire the output into a node that expects pixels and get a tiny blob near the origin. The second is the wrong lens model. This is the pinhole/Brown–Conrady path; a fisheye or catadioptric camera needs cv2.fisheye.undistortPoints or cv2.omnidir.undistortPoints. The pack's own docs point out that the curated CV Omnidir Undistort uses a closed-form CMei inversion "instead of the wrong undistortPoints" - if your 360 camera's undistorted points land somewhere silly, that's why.
And one environment trap that hits this pack generally: four OpenCV pip distributions share a single site-packages/cv2, and whichever installed last wins. Install opencv-python over the contrib wheel and the contrib submodules silently become empty stubs. If you ever need to check or fix that, the pack ships tools/repair_opencv_contrib.py --check / --apply.
Inputs (6)
| Name | Type | Default | Description |
|---|---|---|---|
| src | NPARRAY,IMAGE,MASK | Observed point coordinates in **pixel coordinates** of the distorted image, 2xN/Nx2 1-channel or 1xN/Nx1 2-channel (CV_32FC2 or CV_64FC2) (or vector\ ). 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. | |
| cameraMatrix | NPARRAY | Camera matrix $\vecthreethree{f_x}{0}{c_x}{0}{f_y}{c_y}{0}{0}{1}$ . A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| distCoeffs | NPARRAY | Input vector of distortion coefficients $(k_1, k_2, p_1, p_2[, k_3[, k_4, k_5, k_6[, s_1, s_2, s_3, s_4[, \tau_x, \tau_y]]]])$ of 4, 5, 8, 12 or 14 elements. If the vector is NULL/empty, the zero distortion coefficients are assumed. A data array (points / matrix), NOT an image - only an NPARRAY link is accepted here. | |
| criteria_typeopt | COMBO | max count or epsilon (whichever first) | When to stop iterating: after max_count iterations, when the change drops below epsilon, or whichever comes first. |
| criteria_max_countopt | INT | 301–2147483647 | Maximum iterations (ignored when 'epsilon only'). |
| criteria_epsilonopt | FLOAT | 0.000–1e+38 | Target accuracy / smallest change worth continuing for (ignored when 'max count only'). |
Outputs (1)
| Name | Type | Description |
|---|---|---|
| nparray | NPARRAY | — |