ROB-330 · Lesson 8 of 12 · 34 min

Depth sensing and point clouds

Stereo cameras, structured-light sensors, time-of-flight cameras and lidar all produce the same kind of output in the end: measurements of where surfaces are in 3-D. ROB-220 explained how each sensor works; this lesson is about what to do with the data. Depth images and point clouds let a robot find the table top it must place a part on, the free floor it can drive across, the object sitting on a shelf and the gap in front of a gripper. The core operations are few and reusable: back-projecting a depth image into points, transforming points between frames, downsampling and filtering, estimating surface normals, fitting planes and other shapes robustly, clustering points into objects, and aligning point clouds with each other.

From depth image to point cloud

A depth image stores, at each pixel (u, v), the depth Z of the surface seen there, usually as 16-bit millimetres, with 0 meaning no measurement. Back-projecting each pixel with the camera intrinsics (lesson 2) gives a 3-D point in the camera frame:

\[X = \frac{(u - c_x)\,Z}{f_x}, \qquad Y = \frac{(v - c_y)\,Z}{f_y}, \qquad Z = Z\]

An organized point cloud keeps the image's grid structure, so neighbouring points in the array are neighbours on the surface, which makes normals and segmentation fast. An unorganized cloud is just a list of points, as produced by a spinning lidar or by merging several views. Point clouds are typically stored with optional colour, intensity and normals per point, and in ROS 2 travel as sensor_msgs/PointCloud2 messages; libraries such as PCL (the Point Cloud Library) and Open3D provide the standard algorithms.

Two practical details matter. Depth values are measured along the optical axis (Z) for most depth cameras, but some time-of-flight sensors report radial range along the ray, which must be converted before back-projection. And the depth and colour images of an RGB-D camera come from different sensors, so they must be registered (aligned) using their extrinsic calibration before colour can be attached to points.

Worked problem

Back-projecting a pixel

A depth camera has \( f_x = f_y = 600 \), \( c_x = 320 \), \( c_y = 240 \). Pixel (470, 180) reads 1250 mm. Where is the point?

Show solution
  1. \( Z = 1.250 \) m.
  2. \( X = (470 - 320) \times 1.250/600 = 0.3125 \) m.
  3. \( Y = (180 - 240) \times 1.250/600 = -0.125 \) m.

Answer: (0.313, −0.125, 1.250) m in the camera optical frame: right of centre, above the axis (negative y is up) and 1.25 m away.

Worked problem

How much data is a point cloud?

A depth camera produces 848 × 480 depth images at 30 Hz. Stored as three 32-bit floats per point plus a 32-bit colour, how much data per second is that?

Show solution
  1. Points per frame: \( 848 \times 480 = 407{,}040 \).
  2. Bytes per point: 16. Per frame: about 6.5 MB.
  3. Per second: \( 6.5 \times 30 = 195 \) MB/s.

Answer: About 195 MB/s, far more than most processing can use. That is why the first step in nearly every pipeline is downsampling and cropping.

Filtering and downsampling

Raw depth data are noisy and redundant. Standard clean-up steps include the following.

  • Crop to a region of interest, such as a box above the work table, which discards most points cheaply.
  • Remove invalid points (zero depth, not-a-number) and points beyond the sensor's reliable range.
  • Remove statistical outliers: for each point, compute the mean distance to its k nearest neighbours and discard points whose mean distance is unusually large. This removes the 'flying pixels' that depth cameras produce along object edges, where a pixel mixes foreground and background.
  • Downsample with a voxel grid: divide space into cubes of side a and replace all points in each occupied cube by their centroid. This evens out density (near surfaces are otherwise sampled far more densely than far ones) and cuts the point count dramatically.

Remember the noise model from ROB-220 and lesson 6: depth noise grows with distance, roughly with \( Z^2 \) for stereo and structured light. A voxel size suitable at 1 m may be too fine at 4 m, where noise exceeds it.

Worked problem

Choosing a voxel size

A robot needs to find boxes at least 100 mm across on a pallet 1.5 m away. The depth noise there is about 4 mm. What voxel size is sensible, and roughly how many voxels cover a 100 mm face?

Show solution
  1. The voxel should be larger than the noise (4 mm) but small relative to the smallest feature (100 mm). 10 mm is a reasonable choice.
  2. A 100 mm square face then covers about \( 10 \times 10 = 100 \) voxels.

Answer: About 10 mm, keeping roughly 100 points per smallest box face, enough for reliable plane fitting while reducing the cloud size by a large factor.

Surface normals

Many algorithms need the orientation of the surface at each point, its normal vector. The normal at a point is estimated from its neighbourhood: collect the k nearest neighbours (or, in an organized cloud, the adjacent pixels), compute their covariance matrix, and take the eigenvector with the smallest eigenvalue, the direction in which the neighbourhood is thinnest. The ratio of the smallest eigenvalue to the sum, sometimes called surface variation or curvature, is near zero on flat patches and larger at edges and corners. Normals are flipped if necessary to point towards the sensor. For three points the normal is simply the normalized cross product of two edge vectors.

Worked problem

Normal from three points

Three nearby points on a surface are A = (0, 0, 1.0), B = (0.1, 0, 1.0) and C = (0, 0.1, 0.95) m. Find the unit normal and its angle from the camera's optical axis.

Show solution
  1. Edges: \( B - A = (0.1, 0, 0) \), \( C - A = (0, 0.1, -0.05) \).
  2. Cross product: \( (0 \cdot (-0.05) - 0 \cdot 0.1,\; 0 \cdot 0 - 0.1(-0.05),\; 0.1 \cdot 0.1 - 0) = (0, 0.005, 0.01) \).
  3. Normalize: length \( 0.01118 \), so n = (0, 0.447, 0.894). Flip to face the camera: (0, −0.447, −0.894).
  4. Angle from the axis: \( \arccos(0.894) = 26.6^{\circ} \).

Answer: The surface is tilted about 27° from facing the camera squarely, leaning so that its upper part is farther away.

Fitting planes and shapes with RANSAC

Planes dominate indoor and industrial scenes: floors, tables, walls, shelves and box faces. RANSAC (lesson 5) finds them robustly. Sample three points, compute the plane \( \mathbf{n} \cdot \mathbf{p} + d = 0 \) through them, count the points within a distance threshold, repeat, and refit the best plane to its inliers by least squares (the normal is the smallest-eigenvector direction of the inliers' covariance). Removing the dominant plane, the table or floor, is usually the first step in tabletop object detection: what remains above it are the objects. The same approach fits cylinders (pipes, cans), spheres and lines, and can be constrained, for example to planes whose normal is near vertical when looking for the floor.

Back-projecting a synthetic depth image, voxel downsampling and RANSAC plane fitting to isolate an object on a table
import numpy as np
rng = np.random.default_rng(8)

fx = fy = 300.0; cx, cy = 160.0, 120.0
H, W = 240, 320
v, u = np.mgrid[0:H, 0:W]

# synthetic depth: a table plane tilted away from the camera, plus a box on top
Z = 0.9 + 0.0012 * (v - cy)                            # table depth rises toward the image bottom
box = (u > 130) & (u < 190) & (v > 90) & (v < 140)
Z = np.where(box, Z - 0.08, Z)                         # box top 8 cm closer to the camera
Z = Z + rng.normal(0, 0.002, Z.shape)                  # 2 mm noise
Z[rng.random(Z.shape) < 0.03] = 0.0                    # 3 % missing pixels

valid = Z > 0
X = (u - cx) * Z / fx; Y = (v - cy) * Z / fy
pts = np.column_stack([X[valid], Y[valid], Z[valid]])
lab = box[valid]
print("valid points:", len(pts))

# voxel downsample with 1 cm cubes
keys = np.floor(pts / 0.01).astype(int)
_, inv = np.unique(keys, axis=0, return_inverse=True)
cnt = np.bincount(inv.ravel())
vox = np.column_stack([np.bincount(inv.ravel(), pts[:, i]) / cnt for i in range(3)])
print("after 1 cm voxel grid:", len(vox))

# RANSAC plane
best_in = None
for _ in range(200):
    a, b, c = vox[rng.choice(len(vox), 3, replace=False)]
    n = np.cross(b - a, c - a)
    if np.linalg.norm(n) < 1e-9:
        continue
    n /= np.linalg.norm(n); d = -n @ a
    inl = np.abs(vox @ n + d) < 0.01
    if best_in is None or inl.sum() > best_in.sum():
        best_in = inl
P = vox[best_in]; centroid = P.mean(0)
n = np.linalg.svd(P - centroid)[2][-1]                 # least-squares refit
n = n if n[2] < 0 else -n                               # face the camera
d = -n @ centroid
print("table normal: (%.3f, %.3f, %.3f)" % tuple(n))
height = vox @ n + d                                    # signed height above the table
above = vox[height > 0.02]
print("voxels more than 2 cm above table:", len(above))
print("object height: %.3f m" % np.median(height[height > 0.02]))
Output
valid points: 74574
after 1 cm voxel grid: 12156
table normal: (0.000, 0.359, -0.933)
voxels more than 2 cm above table: 544
object height: 0.068 m

The fitted normal shows the table tilted about 21° from facing the camera, and the voxels above it are the box. Its measured height, about 7 cm, is less than the 8 cm step in depth because the step was along the optical axis while height is measured perpendicular to the tilted table (8 cm × 0.93 ≈ 7.5 cm), and because voxels along the box edges mix box and table points. Reasoning about which direction a quantity is measured in is a constant habit in 3-D perception.

A side view of a depth camera looking down at an angle onto a table carrying a box. Sample points are drawn as dots on the table surface and on the box top and front. The fitted table plane is drawn as a long line through the table points with its normal arrow pointing toward the camera. Points on the box are coloured differently and labelled more than 2 cm above the plane, with the box height marked perpendicular to the table.
Fitting the dominant plane with RANSAC: points well above the table plane belong to objects, and height is measured along the plane normal

Clustering into objects

After the supporting plane is removed, the remaining points must be grouped into separate objects. Euclidean cluster extraction grows clusters by repeatedly adding neighbours within a distance threshold, using a k-d tree for fast neighbour search; clusters below a minimum size are discarded as noise. DBSCAN does the same with a density criterion. In organized clouds, connected-component labelling on the depth image with a depth-difference threshold is faster still. Each cluster then yields an oriented bounding box (from the principal axes of its points) or a centroid and extent, often enough for a simple grasp, or it can be passed to a recognition stage. Clustering fails when objects touch, which is where learned instance segmentation (lesson 10) helps.

Aligning point clouds

To merge views, track an object, or compare a scan with a CAD model, point clouds must be aligned. The iterative closest point (ICP) algorithm, introduced in ROB-310, alternates between pairing each point with its closest point in the other cloud and solving for the rigid transform that best aligns the pairs, which has a closed-form solution via SVD (the Kabsch method). Point-to-plane ICP, which minimizes distances along the target's normals, converges much faster on flat surfaces. ICP needs a good initial guess, because it converges to the nearest local minimum; global registration methods match local 3-D descriptors such as FPFH between the clouds and use RANSAC to find a rough alignment first.

Worked problem

Does ICP need help?

A robot must align a scan of a part with its CAD model. The part may lie anywhere on a 0.5 m × 0.5 m tray in any rotation about the vertical axis. Should ICP be started from the identity transform?

Show solution
  1. ICP converges reliably only from roughly within a few degrees and a fraction of the part's size of the true pose.
  2. Here the initial error may be up to 0.35 m and 180°.

Answer: No. First obtain a coarse pose, for example by matching FPFH features with RANSAC, by a learned pose estimator, or by detecting the part in the colour image, then refine with ICP.

Occupancy and free space

Mobile robots often convert depth data into simpler representations for navigation. A 2-D occupancy grid marks floor cells as free, occupied or unknown, built by projecting obstacle points down and ray-casting free space from the sensor to each measurement (ROB-340 covers mapping in depth). A 3-D voxel map such as an OctoMap stores occupancy probability in a hierarchical tree that adapts its resolution. For manipulation, signed distance fields and their truncated version (TSDF) fuse many depth frames into a smooth surface that collision checkers and planners can query. Whatever the representation, unknown space should be treated as potentially occupied: a depth camera that sees nothing over a dark or glass surface is not seeing empty space.

Common mistakes

  • MistakeBack-projecting radial range values as if they were Z depths

    InsteadCheck what the sensor reports and convert range along the ray to Z first.

  • MistakeProcessing every raw point at full resolution

    InsteadCrop and voxel-downsample first, choosing a voxel larger than the noise at the working range.

  • MistakeLeaving flying pixels along object edges

    InsteadRemove statistical outliers before fitting and clustering.

  • MistakeStarting ICP from a poor initial pose

    InsteadGet a coarse alignment from features, detection or global registration, then refine.

  • MistakeTreating pixels with no depth as free space

    InsteadMark them unknown; dark, shiny and transparent surfaces often return nothing.

Summary and key terms

  • Depth pixels back-project to 3-D points with \( X = (u - c_x)Z/f_x \) and \( Y = (v - c_y)Z/f_y \).
  • Organized clouds keep the image grid; unorganized clouds are lists of points from lidar or merged views.
  • Cropping, outlier removal and voxel downsampling make clouds manageable and even out density.
  • Normals come from the smallest-eigenvalue direction of a neighbourhood's covariance.
  • RANSAC finds planes and other shapes robustly; removing the support plane exposes objects for clustering.
  • ICP refines alignment from a good initial guess; global registration with 3-D descriptors provides it.
  • Occupancy grids, OctoMaps and TSDFs turn depth into navigation and manipulation maps, with unknown space treated cautiously.
Depth image
An image whose pixels store distance to the surface, usually in millimetres.
Point cloud
A set of 3-D points, often with colour or normals.
Organized point cloud
A cloud stored on the sensor's pixel grid, preserving neighbour relations.
Voxel grid filter
Downsampling by replacing points in each cube by their centroid.
Flying pixels
Spurious depth points between foreground and background at object edges.
Surface normal
The unit vector perpendicular to a surface at a point.
Euclidean clustering
Grouping points into objects by connecting near neighbours.
ICP
Iterative closest point: alternating pairing and rigid alignment of two clouds.
FPFH
Fast point feature histogram, a local 3-D shape descriptor.
TSDF
Truncated signed distance field, a volumetric surface representation fused from depth frames.

Practice problems

Work each problem on paper first, then reveal the answer to check.

  1. f = 500, \( c_x = 320 \), u = 420, Z = 2 m: what is X?

    Show answer

    0.4 m.

  2. What does a depth value of 0 usually mean?

    Show answer

    No measurement at that pixel.

  3. Why downsample with a voxel grid?

    Show answer

    To reduce point count and even out density.

  4. How is a normal found from a neighbourhood's covariance?

    Show answer

    As the eigenvector with the smallest eigenvalue.

  5. How many points define a plane in RANSAC?

    Show answer

    Three.

  6. What is usually removed first in tabletop object detection?

    Show answer

    The dominant support plane.

  7. Which ICP variant converges faster on flat surfaces?

    Show answer

    Point-to-plane ICP.

  8. Name a global registration approach.

    Show answer

    Matching FPFH descriptors with RANSAC.

  9. Why should missing depth be treated as unknown?

    Show answer

    Dark, shiny or transparent surfaces may return nothing while still being solid.

  10. Name a hierarchical 3-D occupancy map.

    Show answer

    OctoMap.

Lesson quiz · 5 questions

Score 60% or more to complete this lesson.

Take the quiz