Nodes/ComfyUI CV/CV Triangulate Points (Two-View)
ComfyUI Node

CV Triangulate Points (Two-View)

Turning matched pixels in two photos into actual 3D

By bmad4ever·Created 3 months ago·Updated 14 days ago· 1
CV Triangulate Points (Two-View)
  • points_a
  • points_b
  • camera_matrix
  • rotation
  • translation
  • points_3d
  • valid
  • reproj_error
  • count

Two photographs of the same scene, taken from two places, contain depth. Not inferred depth from a learned model - measured depth, from the tiny difference in where each point lands in each photo. Turning that difference into 3D coordinates is triangulation, and that's exactly what this node does.

CV Triangulate Points (Two-View) is the sparse-structure half of two-view structure-from-motion. Sparse is the operative word: you're getting hundreds of confident points, not a dense depth map. For that, go to CV Depth to 3D Points and a stereo pair with a proper disparity step.

How it works

It builds the two projection matrices from what you hand it - P1 = K[I|0] for the first view, P2 = K[R|t] for the second - calls cv2.triangulatePoints on each correspondence, and divides out the homogeneous coordinate. That last division is where the actual 3D coordinate comes from, and it's also where points sneak off to infinity when the correspondence is bad.

Then it does the check that separates a usable output from a tidy-looking lie: a cheirality test. A point only counts as valid if it has positive depth in both cameras. A point that triangulates behind one of the cameras is geometrically impossible for a real scene, and marking those as invalid catches most of what RANSAC left behind.

The inputs

  • points_a / points_b - matched Nx1x2 arrays in the same order. Straight out of CV Track Features (KLT), or from any matcher, as long as the pairing survives.
  • camera_matrix - the 3x3 intrinsic K, shared by both views. It doesn't have to be perfectly calibrated from a checkerboard - you can synthesize a plausible one with CV Camera Matrix - but a wrong K skews every 3D point and there's no downstream step that fixes it.
  • rotation - the 3x3 relative rotation R, and translation - the 3x1 relative translation t, both from CV Recover Pose (Essential Matrix).

The outputs

  • points_3d - Nx3 float64 in view-1 camera coordinates. Invalid points are zeroed.
  • valid - Nx1 uint8, 1 where the point is finite and in front of both cameras.
  • reproj_error - mean reprojection error in pixels over the valid points. Under about 1-2 px means a clean reconstruction; much larger means bad matches, a bad K, or a bad pose.
  • count - how many points survived.

Preview points_3d with Inspect CV Data, or send it into CV Write PLY (Point Cloud) and actually look at it in Preview Point Cloud.

Two caveats printed right there in the semantics: the points are in view-1 camera coordinates, not world or model space, and they come out only up to the global scale implied by a unit translation. Monocular two-view geometry gives you shape, never size. If you need real distances, you need a known baseline or a reference measurement, and that's the scale_reference input on CV Visual Odometry (Sequence).

Installing it

Manager → ComfyUI CV, or:

cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv

Restart. Python ≥ 3.12, a recent ComfyUI on the V3 node API, and opencv-contrib-python-headless~=5.0.0.93. No models. GPL-3.0; the pack is a fork of opencv-comfyui with a fair amount of LLM-written code, and the author says to review it before production use. Triangulation itself is a two-call piece of maths, so the risk here is mostly about conventions rather than implementation.

Where people get burned

Misordered points. Everything is positional. points_a[i] must correspond to points_b[i]. Shuffle either array and you'll still get Nx3 output - it'll just be wrong, and the valid mask will quietly be mostly zeros, which is your hint that something failed upstream.

Zero valid points is a valid answer. Too few matches, no real parallax (you moved the camera two centimetres), or a degenerate pose all give you empty arrays and count = 0 rather than an exception. Pure rotation is the classic case: no baseline, no triangulation, mathematically.

Reading points_3d without valid. Invalid rows are zeroed, so they sit at the origin. Send the raw array to a PLY writer and you get a dense ball of points at (0,0,0) that can dominate the view and hide the good geometry. Filter on valid first.

Trusting a low reprojection error on a bad pose. If the same wrong pose is used to both triangulate and evaluate, the error can look small while the geometry is nonsense. Sanity-check the reconstruction against something you know: depth ordering, a known object size, the shape of a wall.

Categoryimage/CV/features

Inputs (5)

NameTypeDefaultDescription
points_aNPARRAYPoints in view 1, Nx1x2 (matched, same order as points_b).
points_bNPARRAYCorresponding points in view 2, Nx1x2.
camera_matrixNPARRAY3x3 intrinsic matrix K shared by both views.
rotationNPARRAY3x3 relative rotation R (from 'CV Recover Pose').
translationNPARRAY3x1 relative translation t (from 'CV Recover Pose').

Outputs (4)

NameTypeDescription
points_3dNPARRAYNx3 float64 points in view-1 camera coordinates (invalid points are zeroed - see 'valid').
validNPARRAYNx1 uint8: 1 = finite and in front of both cameras.
reproj_errorFLOATMean reprojection error in pixels over valid points (0 if none are valid).
countINTNumber of valid 3D points.