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:

  1. Point Cloud Conversion Converts an input PointCloud2 message into a NumPy array of shape (N, 4) containing [x, y, z, intensity].

  2. 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).

  3. 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\).

  4. 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_percentage and planar_percentage.

  5. 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:

LOAM Feature Extractor Parameters

Parameter

Value (from params.yaml)

Meaning

num_rings

16

Number of LiDAR laser beams (for example, VLP-16 has 16 rings).

neighbor_size

5

Number of neighboring points used to compute curvature. The total neighborhood size is 2 × m.

edge_percentage

2.0

Percentage of points per ring classified as edge features.

planar_percentage

2.0

Percentage of points per ring classified as planar features.

min_range

1.0

Minimum valid range for LiDAR points (in meters).

max_range

100.0

Maximum valid range for LiDAR points (in meters).

input_topic

"preprocessing/downsampled_cloud"

Input topic providing the filtered and downsampled point cloud.

edge_topic

"features/edge_cloud"

Output topic for detected edge features.

planar_topic

"features/planar_cloud"

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 the voxel_downsampler node.

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:

  1. `kitti_publisher` — publishes KITTI dataset LiDAR and pose data.

  2. `voxel_downsampler` — applies voxel-grid downsampling and optional ground filtering.

  3. `loam_feature_extractor` — extracts edge and planar features from the preprocessed cloud.

loam_feature_extraction/launch/feature_extraction.launch.py
  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

  1. Open Foxglove Studio.

  2. Go to Open ConnectionFoxglove WebSocket.

  3. Enter ws://localhost:8765 and click Open.

  4. Load the pre-configured layout from loam_feature_extraction/config/feature_extraction_layout.json to 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

Module