Blog

How to Process Point Clouds in ROS 2: A Practical Guide for Mobile Robots

Adrian Krzemiński,

How to Process Point Clouds in ROS 2

TL;DR: Point cloud processing in ROS 2 boils down to four reproducible steps: subscribe to a sensor_msgs/PointCloud2 topic, downsample with a voxel grid, crop the region of interest with a passthrough filter, and segment the ground plane before passing the result to mapping or obstacle avoidance. Using PCL 1.12 with ROS 2 Humble on a Leo Rover equipped with a Livox Mid-360, you can cut a roughly 20,000-25,000-point frame down to approximately 1,000-3,000 processed points and keep processing latency around 20-30 ms on a Raspberry Pi 4. A 2D RPLIDAR requires LaserScan handling or projection to PointCloud2. This guide walks you through the pipeline with working C++ and Python code, plus before/after numbers from a test run.

Why Point Cloud Processing in ROS 2 Matters for Mobile Robots

A raw LiDAR stream is rarely usable as-is. A Velodyne VLP-16 produces about 300,000 points per second; a Livox Mid-360 reaches 200,000 points per second with a non-repetitive scan pattern. Feeding that volume directly into SLAM or path planning can saturate the CPU on an embedded computer and introduce delays that break closed-loop control.

Point cloud processing in ROS 2 solves three concrete problems for a UGV: it reduces data volume, it isolates the geometry relevant to the task (obstacles, terrain, targets), and it normalizes the data so downstream nodes such as nav2, slam_toolbox, or custom inspection logic receive consistent input. Whether you run a Leo Rover for field research or a heavier platform for industrial inspection, the same pipeline applies.

What you need before starting

The examples below assume a working ROS 2 Humble installation, PCL 1.12 or newer, and the pcl_ros bridge. Install them on Ubuntu 22.04 with:

sudo apt install ros-humble-pcl-ros ros-humble-pcl-conversions libpcl-dev

For the hardware reference, the test setup uses a Leo Rover with a Raspberry Pi 4 (4 GB). The PointCloud2 pipeline below was benchmarked with a Livox Mid-360; an Intel RealSense D435i can publish PointCloud2 via the RealSense pointcloud filter, while an RPLIDAR S2 publishes LaserScan and must be projected to PointCloud2 before using this exact pipeline. The same code runs unchanged with an Ouster OS0 after changing the topic name.

How to Subscribe to a PointCloud2 Topic in ROS 2

Most 3D LiDAR and depth camera drivers in ROS 2 publish sensor_msgs/msg/PointCloud2. The message stores points as a packed byte buffer with field descriptors (x, y, z, and optionally intensity, ring, time or timestamp). You convert it to a PCL container before applying filters.

Here is a minimal C++ subscriber that converts incoming clouds to pcl::PointCloud<pcl::PointXYZ>:

#include <rclcpp/rclcpp.hpp>
#include <sensor_msgs/msg/point_cloud2.hpp>
#include <pcl_conversions/pcl_conversions.h>
#include <pcl/point_cloud.h>
#include <pcl/point_types.h>

class CloudProcessor : public rclcpp::Node {
public:
  CloudProcessor() : Node("cloud_processor") {
    sub_ = create_subscription<sensor_msgs::msg::PointCloud2>(
      "/livox/lidar", rclcpp::SensorDataQoS(),
      std::bind(&CloudProcessor::cb, this, std::placeholders::_1));
  }
private:
  void cb(const sensor_msgs::msg::PointCloud2::SharedPtr msg) {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    pcl::fromROSMsg(*msg, *cloud);
    RCLCPP_INFO(get_logger(), "Received %zu points", cloud->size());
  }
  rclcpp::Subscription<sensor_msgs::msg::PointCloud2>::SharedPtr sub_;
};

int main(int argc, char ** argv) {
  rclcpp::init(argc, argv);
  rclcpp::spin(std::make_shared<CloudProcessor>());
  rclcpp::shutdown();
  return 0;
}

Use rclcpp::SensorDataQoS() for LiDAR topics. The default reliable QoS may not match many sensor drivers, which commonly use best-effort reliability, and on saturated links it can increase latency or block instead of delivering the latest sample. This is common on Wi-Fi links between a Leo Rover and a ground station.

How to Downsample with a Voxel Grid Filter

Voxel grid downsampling replaces all points inside a 3D cell with their centroid. It is the single most effective optimization for point cloud processing in ROS 2 because it caps the worst-case point count regardless of scene density.

The C++ implementation is three lines:

#include <pcl/filters/voxel_grid.h>

pcl::VoxelGrid<pcl::PointXYZ> voxel;
voxel.setInputCloud(cloud);
voxel.setLeafSize(0.05f, 0.05f, 0.05f);
voxel.filter(*cloud_filtered);

A 5 cm leaf size is a practical default for outdoor UGV navigation. In a test on a Leo Rover with the Livox Mid-360, a raw frame of approximately 24,000 points dropped to roughly 4,200 points after voxelization, with no measurable loss in obstacle detection at distances under 8 m.

The Python equivalent using open3d is similarly short. With Open3D:

import open3d as o3d
pcd = o3d.io.read_point_cloud("scan.pcd")
down = pcd.voxel_down_sample(voxel_size=0.05)

Choosing the leaf size

Leaf size is a trade-off between detail and speed. The guideline below has worked across field deployments:

  • 0.02 m for indoor inspection and small-object detection.
  • 0.05 m for outdoor navigation and SLAM on a UGV.
  • 0.10 m for global mapping over hundreds of meters.

How to Crop the Region of Interest with a Passthrough Filter

A passthrough filter discards points outside a numeric range on a single axis. It removes the floor, the ceiling, or anything beyond the robot’s planning horizon, depending on the selected axis and limits. Cropping before any heavier algorithm is a standard practice in sensor and perception pipelines because the cost scales linearly with point count.

Example: keep only points between 0.1 m and 15 m in front of the robot and within +/- 0.5 m vertically.

#include <pcl/filters/passthrough.h>

pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud_filtered);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.1, 15.0);
pass.filter(*cloud_x);

pass.setInputCloud(cloud_x);
pass.setFilterFieldName("z");
pass.setFilterLimits(-0.5, 0.5);
pass.filter(*cloud_roi);

For a Leo Rover with a footprint of roughly 447 x 433 mm, cropping to a 15 m forward window typically removes 30-50% of the remaining points after voxelization, depending on the environment.

How to Remove the Ground Plane with RANSAC

Ground removal is usually required before clustering obstacles. The standard approach fits a plane model with RANSAC and removes inliers. PCL exposes this through SACSegmentation.

#include <pcl/segmentation/sac_segmentation.h>
#include <pcl/filters/extract_indices.h>

pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::ModelCoefficients::Ptr coeffs(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);

seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.05);
seg.setMaxIterations(100);
seg.setInputCloud(cloud_roi);
seg.segment(*inliers, *coeffs);

pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud_roi);
extract.setIndices(inliers);
extract.setNegative(true); // keep non-ground
extract.filter(*obstacles);

A 5 cm distance threshold works on smooth surfaces. For rough outdoor terrain, increase it to 0.10-0.15 m or switch to a more specialized algorithm such as linefit_ground_segmentation or patchwork++, both of which are available as ROS 2 packages or ports.

How the Full Pipeline Performs on a Leo Rover

The pipeline below was measured on a Leo Rover with a Raspberry Pi 4 (4 GB, Ubuntu 22.04, ROS 2 Humble) and a Livox Mid-360 publishing at 10 Hz. Numbers are averaged over a 60-second indoor run; treat them as approximate.

  • Raw cloud: 24,000 points, no processing.
  • After voxel grid (0.05 m): approximately 4,200 points, 6 ms per frame.
  • After passthrough (15 m forward, 1 m vertical band): approximately 2,800 points, +2 ms.
  • After RANSAC ground removal: approximately 900 obstacle points, +12 ms.
  • Total end-to-end: approximately 22 ms, well within the 100 ms budget at 10 Hz.

The CPU load on a single core stayed below 35%. Replacing PCL’s voxel grid with the approximated version (ApproximateVoxelGrid) reduces the first step to roughly 3 ms at the cost of slightly less uniform output.

Visualizing before and after

Use rviz2 with two PointCloud2 displays subscribed to the raw and processed topics. Set different colors per topic. For offline analysis, publish to a bag with ros2 bag record and inspect frame counts with ros2 bag info. For numerical comparison, log point counts and per-stage timestamps to a CSV file and plot them; a simple matplotlib line chart of “points per frame” before and after the pipeline is enough to validate parameters.

How to Integrate the Pipeline with Navigation and SLAM

Once you have a clean obstacle cloud, two integrations are standard. For autonomous navigation, publish the filtered cloud as an input to nav2‘s voxel layer or spatio_temporal_voxel_layer. For mapping, feed it to slam_toolbox after projection to 2D with pointcloud_to_laserscan, or to rtabmap_ros for 3D SLAM.

This pipeline is the same one used in mobile inspection deployments described under inspection applications and in academic research projects running on the Leo Rover platform.

Frequently Asked Questions

What is the difference between PointCloud2 and PointCloud in ROS 2?

sensor_msgs/msg/PointCloud is the older message format with separate arrays per channel. sensor_msgs/msg/PointCloud2 is the current standard for 3D cloud data: a single binary buffer with self-describing field offsets. Most modern 3D drivers and tools use PointCloud2. The older type still exists for backward compatibility but should not be used in new code.

Should you use PCL or Open3D in ROS 2?

PCL is the default for C++ nodes because of the pcl_conversions bridge and tight integration with PCL data types. Open3D is convenient for Python prototyping and has strong visualization tools, but you will write conversion code between PointCloud2 and Open3D tensors. For production nodes on a robot, PCL with C++ is the practical choice; for offline analysis or machine learning preprocessing, Open3D is often easier.

How do you handle point cloud data over Wi-Fi?

Avoid transmitting dense raw clouds over Wi-Fi. A 200,000-point cloud at 10 Hz is roughly 24 MB/s in PointCloud2 format if it contains only XYZ float fields, and more when intensity, ring, or timestamp fields are included. Run the filtering pipeline on the robot’s onboard computer and publish only the downsampled result, or use the draco or point_cloud_transport packages for compressed transport where available.

What QoS settings work best for LiDAR topics?

Use rclcpp::SensorDataQoS(), which sets best-effort reliability and a small history depth. Reliable QoS can cause the publisher to block or add latency when subscribers cannot keep up, which is a common source of “dropped LiDAR frames” complaints on embedded systems.

Can you run point cloud processing in ROS 2 on a Raspberry Pi?

Yes, within limits. A Raspberry Pi 4 handles a voxel-passthrough-RANSAC pipeline at 10 Hz on clouds of approximately 25,000 raw points. For denser sensors such as a 64-beam LiDAR or higher rates, move to a Jetson Orin Nano or an x86 onboard computer.

How do you debug a pipeline that produces empty clouds?

Check three things in order: that the input frame_id matches your TF tree, that the passthrough limits are not excluding all points (a common mistake when the sensor is mounted upside down and z becomes negative), and that the voxel leaf size is not larger than the scene extent. Use ros2 topic echo /processed_cloud --field width to see point counts at each stage for unorganized clouds.

Which LiDAR works best with the Leo Rover?

The Leo Rover’s user plate and 5 V/12 V power outputs support most compact LiDARs. Common choices are the RPLIDAR S2 for 2D mapping, the Livox Mid-360 for 3D SLAM, and the Unitree L1 or Ouster OS0 for research projects requiring dense vertical coverage. Sensor selection depends on the task; the processing pipeline in this guide works with 3D point cloud sources, and with a 2D RPLIDAR only after projecting LaserScan data to PointCloud2.

Build Your Custom Perception Stack with Fictionlab

If you need a mobile robot platform with a tested ROS 2 perception pipeline integrated out of the box, Fictionlab’s team designs and delivers custom configurations based on the Leo Rover and Raph Rover. Discuss your sensor stack and software requirements with the engineers at Fictionlab Custom Robotics.


Read more