Nodes/ComfyUI CV/CV Point Cloud Normals
ComfyUI Node

CV Point Cloud Normals

The node that stops PPF lying to you

By bmad4ever·Created 4 months ago·Updated 15 days ago· 1
CV Point Cloud Normals
  • points
  • cloud
  • normals
  • found
◄num_neighbors12►
◄orientationas computed (sign arbitrary)►
◄viewpoint_x0.0►
◄viewpoint_y0.0►
◄viewpoint_z0.0►

If you are doing surface matching in ComfyUI - finding a known 3D object inside a scanned or stereo-reconstructed scene - this is the node that decides whether the rest of the pipeline works or quietly returns nonsense. It takes a bare Nx3 point cloud and gives every point a surface normal.

Why it exists

Two nodes in this pack match surfaces: CV PPF Pose Estimation and CV ICP Register. Both want an oriented cloud, Nx6 - x, y, z, nx, ny, nz. Hand either of them a plain Nx3 cloud and they do not complain. They just get 31x to 100x slower and answer with a wrong pose. That is the whole reason this node is on the front of the pipeline, and it is worth internalising before you spend an afternoon debugging a "matching" problem that is really a normals problem.

How the normals are made

For each point it fits a local plane to that point's k nearest neighbours (cv2.ppf_match_3d.computeNormalsPC3d), and the plane's normal becomes the point's normal. num_neighbors defaults to 12; 6–20 is the useful band. Low values track detail but are noisy on real scans; high values smooth the cloud and smear normal orientation across sharp edges of the object. The output cloud is Nx6 float32, ready to wire into the matchers.

The catch is the sign. A plane fit genuinely cannot tell inside from outside - the normal it returns may point into the object. That is what orientation is for. Left on as computed (sign arbitrary) you get whatever the fit decided. Switch to flip toward viewpoint and every normal is aimed at the viewpoint you give it in viewpoint_x/viewpoint_y/viewpoint_z. For a cloud reconstructed from one camera - which is what CV Stereo Pair To Cloud gives you, camera origin at 0,0,0 - that is exactly right. For a closed object scanned from all sides it is wrong and will flip half the cloud.

If your cloud came from a mesh instead, use CV Mesh Vertex Normals rather than this node: triangle winding carries a real sign, and a plane fit on a closed model tends to come out inverted (about 180 degrees), which inverts the PPF pose with it.

Inputs and outputs

The two things you actually touch: num_neighbors (that smoothing/noise dial) and orientation plus the viewpoint triplet if you are flipping. Everything else is wiring. points accepts an Nx6 cloud too, but only to recompute its normals.

Out of it you get cloud - the Nx6 array the surface matchers want, not the Nx3 you fed in - a normals output with just the Nx3 unit normals in input order, useful for drawing or filtering, and a found boolean that is false when there was nothing to compute (fewer than 3 points, or a cv2 error). Gate the downstream branch on found with an if/else node rather than letting an identity pose through.

Installing

Same as the rest of the pack. ComfyUI Manager, search "ComfyUI CV", or by hand:

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

The pack needs Python ≥ 3.12 and a recent ComfyUI built on the V3 node API. Its one real dependency is opencv-contrib-python-headless~=5.0.0.93, pinned because the pack's behaviour is curated against that build. numpy and torch come along with it.

Where people get burned

The contrib wheel. All four OpenCV distributions share one site-packages/cv2, so if something later installs plain opencv-python or opencv-python-headless over the contrib one, the contrib submodules get emptied and ppf_match_3d - along with every contrib-backed node here - disappears without an error at install time. If nodes you saw yesterday are missing from the menu, run the pack's diagnostic:

python tools/repair_opencv_contrib.py --check
# then, to fix it:
python tools/repair_opencv_contrib.py --apply

Feeding Nx3 into PPF. Covered above, but it's the single most common mistake, because nothing fails loudly.

Trusting found less than you should. Fewer than 3 points, or a cloud that is really just a plane, returns found=false with empty outputs rather than raising. If your workflow ignores that boolean, the next node happily processes empty arrays.

Categoryimage/CV/points

Inputs (6)

NameTypeDefaultDescription
pointsNPARRAYNx3 point cloud (an Nx6 cloud is accepted - its normals are recomputed).
num_neighborsINT123–200Neighbours per local plane fit. Higher = smoother normals and more smearing across edges; lower = noisier. 6-20 is the usual range.
orientationCOMBOas computed (sign arbitrary)A plane fit cannot tell inside from outside. 'flip toward viewpoint' points every normal at the viewpoint below - correct for a cloud seen from one camera, wrong for a closed object sampled all round.
viewpoint_xoptFLOAT0.0Viewpoint the normals are flipped towards (only read when orientation is 'flip toward viewpoint').
viewpoint_yoptFLOAT0.0Viewpoint Y. The camera origin is (0,0,0) for a cloud straight out of 'CV Stereo Pair To Cloud'.
viewpoint_zoptFLOAT0.0Viewpoint Z.

Outputs (3)

NameTypeDescription
cloudNPARRAYNx6 float32 (x,y,z,nx,ny,nz) - feed the surface matching nodes with this, not with the Nx3 input.
normalsNPARRAYNx3 float32 unit normals only, in the input order (for drawing or filtering).
foundBOOLEANFalse when the normals could not be computed (fewer than 3 points, cv2 error).