LiDAR Only
Overview
The LiDAR Only perception system detects cones using 3D point cloud data from a Robosense LiDAR sensor. This approach relies purely on geometric features and clustering algorithms to identify cone-like objects without requiring visual information or color classification.
Key Concept: Point clouds are filtered to remove ground points, then clustered using GPU-accelerated DBSCAN. Clusters matching cone-like dimensions are identified and their positions estimated.
System Architecture
The LiDAR-only system provides:
Ground Plane Filtering: Removes ground points using fitted plane equation
GPU-Accelerated Clustering: Uses RAPIDS cuML DBSCAN for fast clustering on NVIDIA GPUs
Geometric Filtering: Identifies cone clusters based on bounding box dimensions
Position Estimation: Computes cone centroids from clustered points
Advantages:
Works in all lighting conditions (day/night)
Provides accurate 3D measurements
No camera calibration required
Simpler sensor setup
Limitations:
No color classification (all cones marked as unknown)
Sensitive to LiDAR point density
May struggle with distant/small cones
Requires GPU for real-time performance
LiDAR Cones Node
The LiDARCones
class processes LiDAR point clouds to detect cone positions.
Key Features
Ground Plane Removal: Vectorized filtering using plane equation
GPU Acceleration: cuML DBSCAN clustering on NVIDIA GPUs with CUDA
Geometric Validation: Bounding box filtering to identify cone-shaped clusters
Performance Monitoring: Detailed timing breakdowns for each processing stage
Visualization Support: Optional filtered point cloud publishing
ROS Topics
Subscribed Topics:
Topic |
Type |
Description |
---|---|---|
|
sensor_msgs/PointCloud2 |
Raw LiDAR point cloud with XYZ and intensity |
Published Topics:
Topic |
Type |
Description |
---|---|---|
|
ConesCartesian |
Detected cone positions (x, y, color=-1) |
|
sensor_msgs/PointCloud |
Visualization of detected cone positions |
|
sensor_msgs/PointCloud2 |
Ground-filtered point cloud (optional) |
Processing Pipeline
The LiDAR processing pipeline consists of the following stages:
Point Cloud Extraction
Convert ROS PointCloud2 message to NumPy arrays:
points_xyz = ros2_numpy.point_cloud2.point_cloud2_to_array(pointcloud)['xyz'] intensities = ros2_numpy.point_cloud2.point_cloud2_to_array(pointcloud)['intensity'] points = np.hstack([points_xyz, intensities]) points = points[~np.isnan(points).any(axis=1)] # Remove NaN values
Ground Plane Filtering
Remove ground points using the calibrated ground plane equation:
filtered_points = filter_points_by_plane_and_distance( points, threshold=ground_filter_threshold, top_threshold=ground_filter_top_threshold, max_distance=max_distance )
The filter uses the plane equation:
\[ax + by + cz + d = 0\]Points are kept if:
Distance from plane:
threshold < distance < top\_threshold
Distance from origin:
distance ≤ max\_distance
This vectorized operation efficiently removes thousands of ground points:
distance_to_plane = (a*x + b*y + c*z + d) / sqrt(a**2 + b**2 + c**2) distance_from_origin = sqrt(x**2 + y**2 + z**2) mask = (threshold < distance_to_plane) & (distance_to_plane < top_threshold) & (distance_from_origin <= max_distance)
GPU-Accelerated DBSCAN Clustering
Transfer data to GPU and perform density-based clustering:
import cupy as cp from cuml.cluster import DBSCAN as cuDBSCAN # Transfer to GPU filtered_points_gpu = cp.asarray(filtered_points[:, :3]) # Cluster using cuML DBSCAN clusterer = cuDBSCAN(eps=eps, min_samples=min_samples) clusterer.fit(filtered_points_gpu) labels = clusterer.labels_
DBSCAN Parameters:
eps
: Maximum distance between points in a cluster (neighborhood radius)min_samples
: Minimum points required to form a dense cluster
Points not belonging to any cluster are labeled as -1 (noise).
Cone Cluster Filtering
Filter clusters to identify cone-like objects based on bounding box dimensions:
for label in unique_labels: cluster = points[labels == label] min_values = cluster.min(axis=0) max_values = cluster.max(axis=0) x_box = max_values[0] - min_values[0] y_box = max_values[1] - min_values[1] z_box = max_values[2] - min_values[2] # Check if dimensions match cone size if (x_size[0] < x_box < x_size[1] and y_size[0] < y_box < y_size[1] and z_size[0] < z_box < z_size[1]): cones.append(label)
This geometric filtering rejects clusters that are too large (vehicles, walls) or too small (noise).
Cone Position Estimation
Compute the representative position for each cone cluster:
def find_cone_position(pointcloud, method='median'): if method == 'median': cone_positions = np.median(pointcloud, axis=0) elif method == 'mean': cone_positions = np.mean(pointcloud, axis=0) return cone_positions[0:2] # Return (x, y)
Median vs Mean:
Median (default): Robust to outliers, preferred for noisy data
Mean: Faster computation, sensitive to outlier points
Publishing
Publish detected cones:
cone_msg = ConesCartesian() cone_msg.x.append(cone_pose[0]) cone_msg.y.append(cone_pose[1]) cone_msg.color.append(-1) # Unknown color cone_msg.header.stamp = self.get_clock().now().to_msg() self.perception_pub.publish(cone_msg)
Note: Color is always -1 since LiDAR cannot distinguish cone colors.
Configuration Parameters
From LiDAROnlySettings
:
Parameter |
Description |
---|---|
|
Tuple (a, b, c, d) defining ground plane equation |
|
Minimum distance from ground plane (meters) |
|
Maximum distance from ground plane (meters) |
|
Maximum range for point filtering (meters) |
|
DBSCAN epsilon parameter (cluster radius in meters) |
|
DBSCAN minimum points per cluster |
|
Tuple (min, max) for cone bounding box X dimension (meters) |
|
Tuple (min, max) for cone bounding box Y dimension (meters) |
|
Tuple (min, max) for cone bounding box Z dimension (meters) |
|
Enable publishing of filtered point cloud for debugging |
Ground Plane Calibration
The ground plane must be calibrated to match the vehicle’s LiDAR mounting position. The plane equation coefficients (a, b, c, d) are typically obtained through:
Recording a static point cloud with the vehicle on flat ground
Fitting a plane to the ground points using RANSAC or least squares
Storing the resulting coefficients in settings
The module includes helper utilities for ground plane calibration:
find_ground_gui.py
: Interactive GUI for visualizing and fitting ground planesfind_ground_plane.py
: Automated plane fitting
Bounding Box Filtering
Cone dimensions are validated using 3D bounding boxes. Typical Formula Student cone dimensions:
Height: ~0.3 m
Base diameter: ~0.23 m
With LiDAR point cloud scatter, appropriate size ranges might be:
x_size: (0.05, 0.35) meters
y_size: (0.05, 0.25) meters
z_size: (0.10, 0.40) meters
These ranges should be tuned based on:
LiDAR vertical/horizontal resolution
Typical detection range
Cone visibility and occlusion patterns
Visualization and Debugging
Filtered Point Cloud
Enable publishing of ground-filtered points:
publish_ground_filtered_pointcloud: True
View in RViz on /filtered_pointcloud
topic to verify ground removal.
3D Cluster Visualization
The plot_clusters_3d()
function creates interactive matplotlib plots:
plot_clusters_3d(filtered_points[:, :3], labels)
Shows all clusters with unique colors, useful for tuning DBSCAN parameters.
Console Logging
The node prints detailed timing information:
Original: 52341 pts, Filtered: 1523 pts
Extracting Pointcloud: 0.008 s
Filtering Points: 0.012 s
Clustering Points: 0.015 s
Finding Cones From Clusters: 0.003 s
Total Time: 0.038 s
Usage Example
import rclpy
from lidar_perception.main import LiDARCones
rclpy.init()
node = LiDARCones()
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()
Launch with ROS2:
ros2 run lidar_perception main
API Reference
- class perception.lidar_perception.lidar_perception.main.LiDARCones[source]
Bases:
rclpy.node.Node
ROS2 node for detecting cones from LiDAR point clouds using DBSCAN clustering.
- lidar_callback(pointcloud)[source]
Process LiDAR point cloud to detect cones.
Filters ground points, clusters using GPU-accelerated DBSCAN, and validates cone-like clusters.
- Parameters
pointcloud (PointCloud2) – Raw LiDAR point cloud message
- filter_points_by_plane_and_distance(points, threshold=0.05, top_threshold=0.5, max_distance=10.0)[source]
Filter points using ground plane equation and distance constraints.
- Parameters
points (np.ndarray) – Nx4 array (x, y, z, intensity)
threshold (float) – Minimum distance from ground plane (m)
top_threshold (float) – Maximum distance from ground plane (m)
max_distance (float) – Maximum range from origin (m)
- Returns
Filtered points above ground within range
- Return type
np.ndarray
- filter_cluster_for_cones(points, labels)[source]
Filter DBSCAN clusters to identify cone-like objects by bounding box dimensions.
- Parameters
points (np.ndarray) – Filtered point cloud
labels (np.ndarray) – DBSCAN cluster labels
- Returns
Cluster labels that match cone dimensions
- Return type
list
- filter_cluster_for_cones_cuML(points, labels)[source]
Filter cuML DBSCAN clusters for cone-like objects using configurable bounding box constraints.
- Parameters
points (np.ndarray) – Filtered point cloud
labels (cupy.ndarray) – cuML DBSCAN cluster labels (GPU array)
- Returns
Cluster labels matching cone size criteria from settings
- Return type
list
- perception.lidar_perception.lidar_perception.visual_debugging.publish_filtered_pointcloud(self, points, header)[source]
Publish ground-filtered point cloud for visualization in RViz.
- Parameters
points (np.ndarray) – Nx4 array of filtered points (x, y, z, intensity)
header – ROS message header with frame_id and timestamp
- perception.lidar_perception.lidar_perception.visual_debugging.plot_clusters_3d(self, points, labels)[source]
Plot clustered 3D points using matplotlib for DBSCAN parameter tuning.
- Parameters
points (np.ndarray) – Nx3 array of 3D points
labels (np.ndarray) – Cluster labels for each point (-1 for noise)