Nodes/ComfyUI CV/CV Merge Point Clouds
ComfyUI Node

CV Merge Point Clouds

Two scans in, one mesh out (if they're in the same frame)

By bmad4ever·Created 4 months ago·Updated 14 days ago· 1
CV Merge Point Clouds
  • points_a
  • points_b
  • colors_a
  • colors_b
  • points
  • colors
  • count_a
  • count_b

Multi-view reconstruction produces one point cloud per view, and at some point you want one file. This node concatenates two Nx3 clouds - plus their per-vertex colours, kept in lockstep - so a register-then-merge pipeline can hand a single cloud to CV Write PLY.

It's the simplest node in the pack's 3-D section, and the constraint it enforces is the important part: both clouds must already be in the same coordinate frame. This node does not align anything. It concatenates. If you merge before registering, you get a PLY with two half-objects sitting next to each other, and nothing will tell you that's wrong.

The pipeline it sits in

The order that actually works, using this pack:

  1. Get clouds with a known relationship - stereo reconstruct, CV Depth to 3D Points, or two captures of the same object.
  2. Find the transform that puts B into A's frame: CV Sample Array At Points to establish correspondences (or a feature matcher), then cv2.estimateAffine3D, or if you have real correspondences CV Register Point Clouds (3D) / CV ICP Register for a refinement.
  3. Apply it: CV Transform Points 3D.
  4. Then merge here.
  5. CV Write PLY (Point Cloud) for a binary PLY, or feed onward to normals / downsampling.

Inputs and outputs

points_a is the only required input. Everything else is optional, and the optionality is load-bearing:

  • points_b - the second Nx3 cloud, already transformed into A's frame. Leave it unconnected and the result is cloud A alone with count_b = 0. That's how a failed registration degrades gracefully: you keep what you had instead of getting an error. It's also what lets a subgraph expose an optional cloud at its boundary, because prompt validation is static - one required socket downstream sinks the whole prompt, so every inner socket has to be optional for a boundary input to be unfed.
  • colors_a, colors_b - Nx3 colours. A missing colour input becomes white for that cloud, so you never get a length mismatch between points and colours.
  • Outputs: points ((Na+Nb)×3 float32, cloud A first), colors (same order, uint8), count_a, count_b.

Colours are the reason to use this rather than a generic concat: CV Write PLY writes per-vertex colour, and a cloud that lost its colours on the way through is a grey blob in every viewer.

Install

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

Manager → ComfyUI CV. Restart. Python ≥ 3.12, V3-API ComfyUI, opencv-contrib-python-headless~=5.0.0.93, numpy, torch. No model downloads - the 3-D side of this pack is geometry, not learned inference. That's worth noting given how much of the 3-D space in ComfyUI is model-based (Hunyuan3D, TRELLIS, TripoSR): this is the classical CV counterpoint, cheap and deterministic, and it works on reconstructions you already have rather than generating new ones.

Common issues

A doubled object in your PLY. Merged before registering. Look at the output in a viewer before you spend time downstream - it's obvious in three seconds with a mesh viewer and invisible in a node graph.

Units. Registering a depth-map-derived cloud against a mesh in centimetres gives you a merged cloud whose two halves are scaled differently. scale on CV Mesh From 3D Model and the transform you apply in CV Transform Points 3D are where that gets fixed.

Normals don't come along. This node merges points and colours, not normals. If the merged cloud feeds PPF or ICP, run CV Mesh Vertex Normals (from a mesh) or CV Point Cloud Normals on the result - and prefer the mesh-winding version when you have geometry, because a plane fit can't tell inside from outside and a closed model matched against a single view lands ~180° out.

Categoryimage/CV/points

Inputs (4)

NameTypeDefaultDescription
points_aNPARRAYFirst Nx3 cloud.
points_boptNPARRAYSecond Nx3 cloud, already transformed into the same frame as points_a. Leave it unconnected and the result is cloud A alone, with count_b = 0 - which is also what lets a SUBGRAPH expose an OPTIONAL cloud: a boundary input may be left unfed only when every inner socket it reaches is optional (prompt validation is static, so one required socket downstream sinks the whole prompt).
colors_aoptNPARRAYNx3 colours for cloud A (white if unconnected).
colors_boptNPARRAYNx3 colours for cloud B (white if unconnected).

Outputs (4)

NameTypeDescription
pointsNPARRAY(Na + Nb) x 3 float32 points, cloud A first.
colorsNPARRAY(Na + Nb) x 3 uint8 colours in the same order.
count_aINT—
count_bINT—