CV Downsample Point Cloud
Cut the cloud before the maths gets quadratic
- points
- points
- count
- found
Why you'd reach for this
Back-project a 640×480 depth map and you get roughly 38,000 points. That's a small number until you hand it to point-pair-feature registration, whose training cost is quadratic in the point count, or to a normals node that fits a plane around every single point. Then 38k becomes minutes of work for a pose you could have had in seconds.
This node thins the cloud. It's the pack's only decimator, and it's the step that decides whether the geometry half of this pack is usable on a laptop or is a coffee break.
How it works
cv2.ppf_match_3d.samplePCByQuantization - the node feeds it the cloud's per-axis min/max and a grid cell size expressed as a fraction of the bounding-box diagonal, then keeps one averaged representative per occupied cell. So relative_sampling_step = 0.05 leaves points about 5% of the diagonal apart, on average.
Note the word average: this sets density, it is not a hard minimum distance. Two points either side of a cell boundary can still end up close together. It's spacing-based, not stride-based, and that's the entire point - a cloud from CV Depth to 3D Points arrives in raster order, so taking every Nth point samples the image evenly and the surface unevenly. Grid quantization samples the geometry.
Two implementation details you'd only notice when they bite. The cloud is cast to float32 first, because float64 gives OpenCV "Unknown C++ exception" rather than a helpful message. And if the cloud has normal columns (Nx6), the averaged normals are re-normalized here, because cv2 averages them without doing so and PPF compares normal angles - a shortened normal vector bends those angles. Rows where the normals cancelled out (two opposing surfaces in one cell) come back with a zero normal, and the node logs how many.
Inputs and outputs that matter
- points - Nx3 or Nx6. The rule of thumb from the tooltip: downsample first, compute normals after, if the cloud is thin or noisy. Normals-first is fine and cheaper when the cloud came from a mesh, where the normals are exact.
- relative_sampling_step - default 0.05, range 0.005–0.5. Match it to the same input on
CV PPF Pose Estimation, which quantizes internally at the same scale. A cloud finer than the consumer's own step buys you nothing and costs the plane fits. - weighting -
uniformaverages the cell.weight by distance to centrepulls the representative toward the points furthest from the cloud's origin, biasing toward the silhouette. Leave it uniform; the weighted form is for deliberately sampling a rim. - count - points that survived. Watch it against your input size, because that's the number the next node's cost is quadratic in. The tooltip's own figure: 38k in, 1.4k out, same pose.
- found - false when the cloud was empty or cv2 refused it. Empty in, empty out, no exception.
Robustness is deliberate throughout: an empty or malformed cloud returns empty with found=false and a log line, so a graph that found no depth doesn't die three nodes later.
Install
cd ComfyUI/custom_nodes
git clone https://github.com/bmad4ever/comfyui_cv
pip install "opencv-contrib-python-headless~=5.0.0.93"
Python ≥ 3.12 and a current ComfyUI (this pack is written against the V3 node API), or install ComfyUI CV from ComfyUI Manager. ppf_match_3d is contrib, so the contrib wheel really is required here - a plain opencv-python won't have it at all.
Common issues
- "Unknown C++ exception". Historically the float64 input case; the node now casts, but a malformed cloud - all-NaN rows from a failed depth estimate - can still trip cv2. Filter or clean the cloud upstream and it goes away.
- Zero normals in the output. A cell spanned two opposing surfaces (a thin wall, a sign) and the averaged normal cancelled. Downsample first, compute normals on the thinned cloud.
- PPF pose gets worse, not faster. You set a step finer than PPF's own. Match them.
- Where the cloud should come from. Depth-to-3D in this pack, or an external reconstruction; the KB's depth-estimation notes are clear that on multi-view work the modern models (Depth Anything 3 and friends) are valued for giving you a fused cloud with consistent poses, and that point clouds are usually an intermediate rather than the deliverable. Whatever produced yours, the raster-order argument for grid sampling holds.
Inputs (3)
| Name | Type | Default | Description |
|---|---|---|---|
| points | NPARRAY | Nx3, or Nx6 (x,y,z,nx,ny,nz). The kept point is the AVERAGE of its cell, normals included (re-normalized here, since cv2 does not) - so a cell that spans two opposing surfaces, the two faces of a thin wall or a sign-arbitrary plane fit, averages to nothing and that row comes back with a ZERO normal. Downsample FIRST and compute normals after when the cloud is thin or noisy; normals-first is fine (and cheaper) for a mesh, where they are exact. | |
| relative_sampling_step | FLOAT | 0.0500.005–0.5 | Grid cell size, as a fraction of the bounding-box diagonal: 0.05 leaves points ~5% of the diagonal apart on average. Match it to the relative_sampling_step of 'CV PPF Pose Estimation' - that node quantizes internally at the same scale, so a finer cloud than its own step buys nothing and costs the plane fits. |
| weightingopt | COMBO | uniform | How the representative of a cell is chosen. 'uniform' averages the cell's points; the weighted form pulls it toward the points furthest from the cloud's origin, which biases the sample toward the silhouette. Leave it uniform unless you are deliberately sampling a rim. |
Outputs (3)
| Name | Type | Description |
|---|---|---|
| points | NPARRAY | The thinned cloud, float32, with the same number of columns as the input. |
| count | INT | Points that survived. Watch it against the input size - this is the number PPF's cost is quadratic in. |
| found | BOOLEAN | False when the cloud was empty or cv2 refused it. |