ROS 2 Voxel Grid Downsampler Node¶
Overview¶
This script implements a ROS 2 node, voxel_downsampler, that subscribes to a sensor_msgs/msg/PointCloud2 topic, applies a voxel-grid downsampling filter, and publishes the resulting, smaller point cloud.
The node is designed for robustness and simplicity, using only NumPy for its computations. It can handle arbitrary input point cloud field layouts and ensures the output is always a sanitized, consistent PointCloud2 message with the fields [x, y, z, intensity] (all FLOAT32).
Core Functionality¶
The node’s processing pipeline, triggered on each incoming message, is as follows:
Robust Parsing: The input
sensor_msgs/msg/PointCloud2message is converted into an (N, 4) NumPy array.It uses the
sensor_msgs_py.point_cloud2.read_pointsutility to correctly handle different field names, data types, and memory offsets in the input cloud.It explicitly looks for
x,y,z, andintensity.If an
intensityfield is not found, it is synthesized with a default value of1.0to maintain a consistent [x, y, z, intensity] structure.
Sanitization: All points (rows) containing
NaNorInfvalues are removed.Ground Filtering (Optional): If the
filter_groundparameter is set toTrue, any point with azcoordinate less than or equal to theground_thresholdparameter is discarded.Voxel Downsampling: The core filtering logic is applied.
Points are discretized into integer voxel indices based on the
voxel_sizeparameter.The node computes the centroid (mean) of all point attributes [x, y, z, intensity] within each unique voxel.
Optionally discards voxels that contain fewer than
min_points_per_voxelpoints.
Publishing: The downsampled NumPy array (now shape (M, 4), where M <= N) is converted back into a new
sensor_msgs/msg/PointCloud2message.This output message always has the fixed, 16-byte layout:
[x(float32), y(float32), z(float32), intensity(float32)].The
header.frame_idandheader.stampare copied from the input message to maintain spatial and temporal consistency.If the resulting cloud is empty after filtering, nothing is published.
ROS 2 Interface¶
Parameters:
Parameter |
Value (from params.yaml) |
Meaning |
|---|---|---|
|
|
The edge length (in meters) of a single voxel cube. |
|
|
The minimum number of points a voxel must contain to be included in the output. |
|
|
If True, enables the simple |
|
|
The |
|
|
The ROS 2 topic to subscribe to for incoming |
|
|
The ROS 2 topic to publish the filtered and downsampled |
|
|
The QoS reliability policy for the publisher. This parameter was not specified in |
Subscriptions:
<input_topic>(sensor_msgs/msg/PointCloud2):Subscribes using
qos_profile_sensor_data(typically BEST_EFFORT).
Publications:
<output_topic>(sensor_msgs/msg/PointCloud2):Publishes using a configurable QoS (defaults to RELIABLE for easier debugging with tools like
ros2 topic echo).The output format is always 16-byte points: [x, y, z, intensity], all
FLOAT32.
Launch Files:
preprocessing.launch.py¶
File: src/lidar_preprocessing/launch/preprocessing.launch.py
This launch file starts the voxel downsampling node.
1"""
2Launch file for the LiDAR preprocessing pipeline and KITTI data loader.
3
4This launch file starts:
5 - `kitti_publisher`: Publishes raw KITTI LiDAR and pose data.
6 - `voxel_downsampler`: Subscribes to raw point clouds, performs voxel-grid
7 downsampling, and publishes the result.
8
9Parameters Loaded:
10 - `preprocessing_params.yaml`: Settings for voxel size, topics, and filtering.
11 - `kitti_data_loader/config/params.yaml`: KITTI dataset and topic settings.
12"""
13from launch import LaunchDescription
14from launch.substitutions import PathJoinSubstitution
15from launch.actions import IncludeLaunchDescription
16from launch.launch_description_sources import PythonLaunchDescriptionSource
17from launch_ros.actions import Node
18from launch_ros.substitutions import FindPackageShare
19
20
21def generate_launch_description():
22 """
23 Generate the launch description for preprocessing and KITTI data loading.
24
25 - Launches the KITTI publisher node (from `kitti_data_loader.launch.py`)
26 - Launches the voxel downsampler node (from `lidar_preprocessing`)
27 """
28 # Paths to configuration files
29 preprocessing_config = PathJoinSubstitution(
30 [FindPackageShare("lidar_preprocessing"), "config", "params.yaml"]
31 )
32
33 # Include the KITTI data loader launch file
34 kitti_loader_launch = IncludeLaunchDescription(
35 PythonLaunchDescriptionSource(
36 PathJoinSubstitution(
37 [FindPackageShare("kitti_data_loader"), "launch", "kitti_data_loader.launch.py"]
38 )
39 )
40 )
41
42 # Define the voxel downsampler node
43 downsampler_node = Node(
44 package="lidar_preprocessing",
45 executable="voxel_downsampler",
46 name="voxel_downsampler",
47 parameters=[preprocessing_config],
48 output="screen",
49 )
50
51 # Launch both the KITTI loader and the preprocessing node
52 return LaunchDescription([kitti_loader_launch, downsampler_node])
Compile and Run¶
1. Build the Workspace
Build the workspace (if not built already):
colcon build --symlink-install
source install/setup.bash
2. Run and Visualize
You will need two sourced ROS 2 terminals and the Foxglove Studio application.
Terminal 1: Start Foxglove Bridge Launch the websocket bridge that Foxglove Studio uses to communicate with ROS 2.
ros2 launch foxglove_bridge foxglove_bridge_launch.xml port:=8765
Foxglove Studio GUI
Open Foxglove Studio.
Navigate to Open Connection -> Foxglove WebSocket.
Ensure the URL is
ws://localhost:8765and click Open.Load the pre-configured layout: Layouts -> Personal -> Add, and navigate to your workspace to select
lidar_preprocessing/config/lidar_preprocessing_layout.json.
Terminal 2: Launch the KITTI Publisher
Once the bridge and Foxglove are running and connected, launch both the kitti_data_loader` and the ``lidar_preprocessing nodes:
ros2 launch lidar_preprocessing preprocessing.launch.py