3D visionHow-to

Store point cloud data in a voxel grid for 3D inspection

How to turn raw x, y, z points into a voxel grid, choose the voxel size, index cells, handle empty voxels and read occupancy back for 3D inspection.

LumaSpec Vision9 min read Share on LinkedIn

On this page 11 sections
  1. 01What a voxel grid is, and why not a plain array
  2. 02Choose the voxel size from your sensor, not by eye
  3. 03Convert coordinates to voxel indices
  4. 04Numbered steps: build the grid
  5. 05Read occupancy and intensity back
    1. Empty voxels and dropout
  6. 06Worked example: memory and cell counts
  7. 07Checks before you commit to a grid size
  8. 08When a voxel grid is the wrong tool
  9. 09Quick checklist
  10. 10Next step
  11. 11Questions

A point cloud as delivered by a 3D sensor is just a list of x, y, z coordinates, which is awkward for inspection: you cannot ask "is there material at this position?" without searching the whole list. A voxel grid fixes that by binning every point into a fixed-size cube and letting you address space directly. This article walks through converting a raw coordinate list into a voxel grid, choosing the voxel size from your sensor's numbers, indexing cells efficiently, and reading occupancy and intensity back out. It covers the data-structure fundamentals rather than comparing two scans; for coverage-change work see the article on voxel grid comparison: align scans to identify new coverage.

What a voxel grid is, and why not a plain array

A voxel grid divides space into cubes of side length s and records which cubes contain points. Two storage schemes dominate. A dense 3D array with one bin per cube is simple to index but wasteful, because real surfaces are thin: a 1 m scene on a 1 mm grid is 10^9 cells, while a single scan touches well under one percent of them. A sparse hash table keyed on the integer voxel index stores only occupied cells, so memory follows surface area instead of volume.

For inspection of single parts on a line, either works. For robot cells, bins or anything larger than a few tens of centimetres across, use the hash table. The hash approach also matches how downstream libraries expect data, so export stays simple.

Choose the voxel size from your sensor, not by eye

The voxel size must respect the measurement, not the other way round. Two rules of thumb:

  • Never below the sensor's point spacing at the working distance, or you get a grid that is mostly empty.
  • Usually 1x to 3x the median nearest-neighbour distance in the cloud, which you can compute once from a sample of a few thousand points.

Laser profilers make this easy because the spacing is printed. The MIS-DP3850-01P is a laser profiler from the 3D sensors range, and models across the DP3000 and DP2000 series print the z resolution and point interval on their data sheets; the voxel size should be at least the point interval. Where profiles move on a line, combine the in-profile spacing with the travel per profile to get the true 3D spacing before fixing s.

index_x = floor((x - x_min) / s) index_y = floor((y - y_min) / s) index_z = floor((z - z_min) / s) voxel centre = (index + 0.5) * s + min_corner

Convert coordinates to voxel indices

The conversion is three lines of arithmetic per point. Take the eight-point example of a small cube with coordinates around 0.25 to 0.40 in each axis, and pick s = 0.02 with the minimum corner at (0.27, 0.25, 0.47):

  • Point (0.270598, 0.254439, 0.471311): indices floor(0.03), floor(0.22), floor(0.065) = (0, 0, 0).
  • Point (0.396951, 0.282268, 0.518814): indices floor(6.35), floor(1.61), floor(2.44) = (6, 1, 2).

Every point lands in exactly one cell; neighbouring points on the same face share a cell. Note that floor() must round toward negative infinity, not toward zero, or points below the origin wrap into the wrong bin. Subtract the bounding-box minimum first and this trap disappears.

Numbered steps: build the grid

  1. Read the coordinate file. Store points as a single N x 3 array of 32-bit floats; a cv::Mat with CV_32FC3 works the same way, one 3-channel point per element.
  2. Compute the bounding box (min and max per axis) and the median nearest-neighbour distance on a sample of points.
  3. Pick the voxel size s, then round it to a convenient value near the median spacing, for example 1 mm instead of 1.03 mm.
  4. For each point, compute the three indices with the formula above.
  5. Combine the indices into one integer key, for example key = (ix << 40) + (iy << 20) + iz, guarding against overflow, and insert into a hash map from key to voxel record.
  6. In each voxel record keep: point count, sum of x, y, z (for the centroid), and optional mean intensity or colour.
  7. After the pass, derive occupancy (count > threshold) and centroids (sum / count) per voxel.

On the vision-controller side, a plain model like the MV-VC3301H-128G66 from the vision controllers range handles this comfortably for clouds of a few million points; the hash pass is memory-bandwidth bound, not compute bound. For source and debug tooling on the same machine, see the notes on camera not detected in notebook: compare Python environments.

Read occupancy and intensity back

Reading is the inverse: compute the indices for the query position, look up the key, and act on what you find. A presence check is a single hash lookup, which is what makes voxel grids useful for go/no-go inspection. Per voxel you can read:

  • Count — how many points fell in the cell. Compare against the expected density to catch dropouts.
  • Centroid — sub-voxel position, recovering most of the quantization you introduced.
  • Mean intensity or colour — useful when surface appearance carries the defect; for getting appearance into the grid reliably, the depth-to-color registration article (raw versus rectified frames) explains which stream to sample.

Empty voxels and dropout

A voxel being empty does not always mean the surface is absent; laser and structured-light sensors drop out on dark, shiny or fuzzy surfaces. Distinguish true gaps from dropouts by checking neighbours: an empty cell surrounded by well-populated cells at the same height is suspicious, while a hole in the middle of a closed region may be real. The article on close range depth dropout on fuzzy surfaces and thin parts lists the surface types that cause it.

Worked example: memory and cell counts

Take a 200 mm x 150 mm x 80 mm part scanned at 0.1 mm point spacing, voxelized at s = 0.3 mm:

  • Grid dimensions: 667 x 500 x 267 = about 89 million cells. A dense 8-bit occupancy array would need about 89 MB; a 32-bit count array about 355 MB.
  • Occupied cells: the part surface is roughly 2 x (0.2 x 0.15) + 2 x (0.2 x 0.08) + 2 x (0.15 x 0.08) = 0.116 m^2, giving about 0.116 / 0.0003^2 = 1.29 million surface cells.
  • Hash storage at roughly 48 bytes per voxel record plus table overhead: around 90 MB, similar to the dense occupancy array here because the part fills its box; on a flat, wide or sparse scene the hash wins by a wide margin.

If your input spacing is coarser than the voxel size you chose, cells hold zero or one point and the count statistics become useless. Match them, as covered above, or merge profiles so each cell receives several points. Line-scan 2.5D systems such as the MV-HLVS-16K100-L60 sample at 120 kHz over a 600 mm emitting length, so the along-travel spacing per voxel follows directly from line rate and conveyor speed.

Checks before you commit to a grid size

  • Compute the median nearest-neighbour distance; set s between 1x and 3x of it.
  • Count occupied cells at that s on a real scan; if fewer than five percent of points share a cell with another point, s is too small for count-based decisions.
  • Verify the floor() behaviour with a negative-coordinate test point.
  • Round-trip one voxel: index it, print its centroid, and confirm it lies within s/2 of the original points.
  • Time the hash build; if it exceeds your cycle budget, batch points or shrink the cloud by range-cropping first.

For visual verification, project the voxelized result and inspect it as described in the article on stereo point cloud visualization: check missing XYZ points; holes and tilt show up immediately once cells replace raw points.

When a voxel grid is the wrong tool

  • Dimensional measurement to a tight tolerance: quantization adds up to half a voxel of uncertainty per axis. Fit planes or cylinders to the raw points instead, and use the grid only for region-of-interest selection.
  • Very thin features: a slot narrower than one voxel may never register a cell, even though points exist inside it. The calibration-related article on why a laser triangulation point cloud is tilted: calibration checks covers a related trap: alignment errors smear points across cells and inflate the surface thickness.
  • Dense volumetric data from a ToF camera: if your device already delivers a depth image, the pixel grid may be all you need; see the article on depth map change detection without speckle false alarms for the image-based alternative.

Quick checklist

  • [ ] Bounding box computed from the actual cloud, with a small margin.
  • [ ] Voxel size s set from measured point spacing, not guessed.
  • [ ] floor() verified for negative coordinates.
  • [ ] Hash key encoding checked for overflow at your grid extents.
  • [ ] Occupancy threshold set from expected points per cell.
  • [ ] Centroids stored when position accuracy matters.
  • [ ] Dropout rule agreed before empty cells are treated as "no material".

Next step

If you are building this around a profiler or stereo camera, start from the 3D sensors finder to match sensor resolution and spacing to the voxel size your inspection needs, or go straight to configuring a system with the camera as the first step. For processing hardware, the vision controllers category lists machines with the camera ports and memory for on-line voxelization, and our team can check a proposed sensor-plus-controller combination before you order through the contact page.

Questions

What voxel size should I use for a point cloud?

Start at or slightly above your sensor's point spacing: roughly the depth resolution times the ratio of working distance to baseline, or simply the median nearest-neighbour distance in the cloud. A voxel smaller than the point spacing leaves most cells empty; one several times larger averages away the detail you are inspecting. Try 1x to 3x the median spacing and compare inspection results.

Why use a hash table instead of a 3D array for voxels?

A dense array sized to the bounding box wastes memory because real clouds are sparse: a 1 mm grid over a 1 m scene needs a billion cells, of which a scan may fill under one percent. A hash table keyed on the integer voxel index stores only occupied cells, so memory scales with surface area rather than volume. Use a dense array only for small or tightly cropped regions.

Can I store colour or intensity per voxel?

Yes. Keep a small struct per voxel: point count, the last or mean intensity, and mean colour if your sensor delivers registered texture. When several points fall in one voxel, either average the values or keep the one closest to the voxel centre. Count-based averaging is the most robust against sensor noise.

How many points do I need per voxel for reliable inspection?

For occupancy checks, a single point can mark a cell occupied, but noise robustness improves with at least three points per occupied voxel. Compare the count against the expected point density for the surface; a cell with far fewer points than its neighbours is likely dropout or an edge artefact and should be flagged rather than trusted.

Does voxelizing a point cloud lose accuracy?

Quantization adds up to half a voxel of position uncertainty per axis by default. You can remove the systematic part by storing the centroid of the points that fell into each voxel alongside the index, so the grid acts as an organizing structure while the centroids keep the original measurement precision.