ROS 2 LOAM Feature Extraction Node¶
Overview¶
This script implements a ROS 2 node, loam_feature_extractor, that subscribes to a sensor_msgs/msg/PointCloud2 topic (typically the output of a voxel downsampling node) and extracts geometric features — edge and planar points — following the method described in the LOAM (LiDAR Odometry and Mapping) algorithm by Zhang & Singh (2014).
The node provides feature-level information suitable for subsequent stages such as odometry, mapping, or registration. It has been adapted from ENPM818Z L2C lecture content and is designed for research and teaching applications within the NIST SLAM Front-End testbed.
Core Functionality¶
The node performs the following sequence of operations for each incoming LiDAR scan:
Point Cloud Conversion Converts an input
PointCloud2message into a NumPy array of shape (N, 4) containing[x, y, z, intensity].Ring Organization Groups points into scan rings (laser channels) based on vertical angles. This preserves LiDAR beam structure (for example, 16 rings for a VLP-16 sensor).
Curvature Computation For each point, computes local curvature using neighboring points within the same ring according to the LOAM formulation:
\[c_i = \frac{1}{|S| \, \lVert \mathbf{p}_i \rVert} \left\lVert \sum_{j \in S} (\mathbf{p}_i - \mathbf{p}_j) \right\rVert\]where \(S\) is the neighborhood containing \(m\) points on each side of \(\mathbf{p}_i\).
Feature Selection
Points with highest curvature values are classified as edge features.
Points with lowest curvature values are classified as planar features.
The percentages of each type are configurable via
edge_percentageandplanar_percentage.
Publishing The node publishes two new point clouds:
features/edge_cloud— points with high curvature (edges)features/planar_cloud— points with low curvature (planes)
Both outputs use the same format as the input cloud:
[x, y, z, intensity].
ROS 2 Interface¶
Parameters:
Parameter |
Value (from params.yaml) |
Meaning |
|---|---|---|
|
|
Number of LiDAR laser beams (for example, VLP-16 has 16 rings). |
|
|
Number of neighboring points used to compute curvature.
The total neighborhood size is |
|
|
Percentage of points per ring classified as edge features. |
|
|
Percentage of points per ring classified as planar features. |
|
|
Minimum valid range for LiDAR points (in meters). |
|
|
Maximum valid range for LiDAR points (in meters). |
|
|
Input topic providing the filtered and downsampled point cloud. |
|
|
Output topic for detected edge features. |
|
|
Output topic for detected planar features. |
Subscriptions:
<input_topic>(sensor_msgs/msg/PointCloud2): Subscribes to the preprocessed and downsampled LiDAR scan. Typically this comes from thevoxel_downsamplernode.
Publications:
<edge_topic>(sensor_msgs/msg/PointCloud2): Publishes extracted edge features.<planar_topic>(sensor_msgs/msg/PointCloud2): Publishes extracted planar features.
Launch Files:
File: src/loam_feature_extraction/launch/feature_extraction.launch.py
This launch file starts the complete LOAM feature extraction pipeline, including all dependent stages required for full functionality.
It launches the following sequence:
`kitti_publisher` — publishes KITTI dataset LiDAR and pose data.
`voxel_downsampler` — applies voxel-grid downsampling and optional ground filtering.
`loam_feature_extractor` — extracts edge and planar features from the preprocessed cloud.
1"""
2Launch File: feature_extraction.launch.py
3=========================================
4
5Description
6-----------
7This launch file orchestrates the full LOAM (LiDAR Odometry and Mapping)
8feature extraction pipeline. It sequentially launches three key components
9to form a modular, simulation-ready LiDAR processing chain:
10
111. **KITTI Data Loader (`kitti_data_loader.launch.py`)**
12 - Publishes raw KITTI LiDAR and ground-truth pose data as ROS 2 topics.
13 - Emulates sensor playback from the KITTI odometry dataset.
14
152. **LiDAR Preprocessing (`preprocessing.launch.py`)**
16 - Applies voxel-grid downsampling, optional ground filtering,
17 and other configurable preprocessing steps.
18 - Subscribes to `/kitti/pointcloud_raw` and publishes a reduced-resolution
19 cloud on `/preprocessing/downsampled_cloud`.
20
213. **Feature Extraction (`feature_extractor`)**
22 - Consumes the downsampled point cloud and extracts geometric features
23 (e.g., edge and planar points) for subsequent odometry and mapping stages.
24
25This configuration is intended to be used with the NIST SLAM front-end
26workspace, but it can be adapted for other LiDAR datasets and sensors by
27changing the parameter files.
28
29Execution
30---------
31To launch the entire pipeline:
32
33.. code-block:: bash
34
35 ros2 launch loam_feature_extraction feature_extraction.launch.py
36
37Upon execution, the following nodes are launched:
38
39- **`kitti_publisher`** — Publishes KITTI LiDAR scans and ground-truth poses.
40- **`voxel_downsampler`** — Performs voxel-grid downsampling on incoming point clouds.
41- **`loam_feature_extractor`** — Extracts edge and planar features from preprocessed clouds.
42
43Expected Topics
44---------------
45+--------------------------------------+--------------------------------------------+
46| **Input** | **Output** |
47+--------------------------------------+--------------------------------------------+
48| `/kitti/pointcloud_raw` | `/preprocessing/downsampled_cloud` |
49| `/preprocessing/downsampled_cloud` | `/features/edge_cloud`, `/features/planar_cloud` |
50+--------------------------------------+--------------------------------------------+
51
52Configuration Files
53-------------------
54- ``lidar_preprocessing/config/params.yaml`` — Parameters for preprocessing
55 (voxel size, filtering thresholds, topic names).
56- ``loam_feature_extraction/config/params.yaml`` — Parameters for
57 feature extraction (ring count, curvature thresholds, etc.).
58- ``kitti_data_loader/config/params.yaml`` — Dataset paths and topic mappings.
59
60Returns
61-------
62launch.LaunchDescription
63 A ROS 2 launch description object containing all nodes and their
64 configurations for the LOAM feature extraction pipeline.
65"""
66
67from launch import LaunchDescription
68from launch.actions import IncludeLaunchDescription
69from launch.launch_description_sources import PythonLaunchDescriptionSource
70from launch.substitutions import PathJoinSubstitution
71from launch_ros.substitutions import FindPackageShare
72from launch_ros.actions import Node
73
74
75def generate_launch_description():
76 """
77 Generate the ROS 2 launch description for the complete LOAM feature
78 extraction pipeline.
79
80 This function composes and returns a `LaunchDescription` object that
81 performs the following:
82 1. Includes the `preprocessing.launch.py` file from the
83 `lidar_preprocessing` package. This, in turn, launches both
84 the `kitti_publisher` and `voxel_downsampler` nodes.
85 2. Launches the `loam_feature_extractor` node configured with
86 parameters defined in `params.yaml`.
87
88 The resulting pipeline provides an end-to-end LiDAR data processing
89 workflow — from dataset playback to feature extraction — suitable for
90 real-time visualization in Foxglove Studio or RViz.
91
92 Returns
93 -------
94 launch.LaunchDescription
95 The composed launch description containing all dependent nodes.
96 """
97 # -------------------------------------------------------
98 # Include LiDAR preprocessing (which also includes KITTI loader)
99 # -------------------------------------------------------
100 preprocessing_launch = IncludeLaunchDescription(
101 PythonLaunchDescriptionSource(
102 PathJoinSubstitution([
103 FindPackageShare("lidar_preprocessing"),
104 "launch",
105 "preprocessing.launch.py",
106 ])
107 )
108 )
109
110 # -------------------------------------------------------
111 # Feature extraction configuration and node
112 # -------------------------------------------------------
113 feature_config = PathJoinSubstitution([
114 FindPackageShare("loam_feature_extraction"),
115 "config",
116 "params.yaml",
117 ])
118
119 feature_extractor_node = Node(
120 package="loam_feature_extraction",
121 executable="feature_extractor",
122 name="loam_feature_extractor",
123 parameters=[feature_config],
124 output="screen",
125 )
126
127 # -------------------------------------------------------
128 # Launch both preprocessing (with KITTI loader) and feature extractor
129 # -------------------------------------------------------
130 return LaunchDescription([
131 preprocessing_launch,
132 feature_extractor_node,
133 ])
Compile and Run¶
1. Build the Workspace
Build the workspace (if not already built):
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.
Go to Open Connection → Foxglove WebSocket.
Enter
ws://localhost:8765and click Open.Load the pre-configured layout from
loam_feature_extraction/config/feature_extraction_layout.jsonto visualize the following topics:/kitti/pointcloud_raw/preprocessing/downsampled_cloud/features/edge_cloud/features/planar_cloud
Terminal 2 - Launch the Feature Extraction Pipeline
Once Foxglove Studio is connected, launch all nodes:
ros2 launch loam_feature_extraction feature_extraction.launch.py