1. 基于LiDAR点云的障碍物检测与聚类
任务目标
本案例基于Autoware的激光雷达驱动,完成从原始点云到障碍物检测结果的完整闭环:对Velodyne VLP-16采集的点云进行体素降采样与直通滤波,剔除地面点后执行欧几里得聚类,为每个障碍物簇生成轴对齐包围框,并输出可视化结果。
环境与依赖
- Ubuntu 20.04 + ROS 2 Foxy(或对应Autoware版本)
- Autoware Universe(已安装
autoware_auto_perception相关组件)
- PCL 1.10+、Open3D(仅用于离线验证,非运行时依赖)
- 测试数据:从
autoware_data下载示例rosbag(含/points_raw话题)
核心实现
以下节点订阅原始点云话题,依次执行:体素降采样(leaf size 0.1m)→ 直通滤波(保留前方20m、左右10m、高度-1~2m范围)→ 基于RANSAC的地面分割 → 欧几里得聚类(距离阈值0.5m,最小簇50点)→ 生成包围框并发布。代码为最小可复现实现,不包含工程化封装。
```cpp
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
class ObstacleClusterNode : public rclcpp::Node
{
public:
ObstacleClusterNode() : Node("obstacle_cluster_node")
{
sub_ = create_subscription(
"/points_raw", 10,
[this](sensor_msgs::msg::PointCloud2::SharedPtr msg) { processCloud(msg); });
pub_clusters_ = create_publisher("/clusters", 10);
pub_boxes_ = create_publisher("/bboxes", 10);
}
private:
void processCloud(const sensor_msgs::msg::PointCloud2::SharedPtr msg)
{
pcl::PointCloud::Ptr raw(new pcl::PointCloud());
pcl::fromROSMsg(*msg, *raw);
// 1. 体素降采样
pcl::VoxelGrid voxel;
voxel.setInputCloud(raw);
voxel.setLeafSize(0.1f, 0.1f, 0.1f);
pcl::PointCloud::Ptr downsampled(new pcl::PointCloud());
voxel.filter(*downsampled);
// 2. 直通滤波(前方20m,左右10m,高度-1~2m)
pcl::PassThrough pass;
pass.setInputCloud(downsampled);
pass.setFilterFieldName("x");
pass.setFilterLimits(0.0, 20.0);
pcl::PointCloud::Ptr x_filtered(new pcl::PointCloud());
pass.filter(*x_filtered);
pass.setInputCloud(x_filtered);
pass.setFilterFieldName("y");
pass.setFilterLimits(-10.0, 10.0);
pcl::PointCloud::Ptr xy_filtered(new pcl::PointCloud());
pass.filter(*xy_filtered);
pass.setInputCloud(xy_filtered);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.0, 2.0);
pcl::PointCloud::Ptr xyz_filtered(new pcl::PointCloud());
pass.filter(*xyz_filtered);
// 3. RANSAC地面分割
pcl::SACSegmentation seg;
seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setDistanceThreshold(0.2);
seg.setMaxIterations(100);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices());
pcl::ModelCoefficients::Ptr coeffs(new pcl::ModelCoefficients());
seg.setInputCloud(xyz_filtered);
seg.segment(*inliers, *coeffs);
pcl::ExtractIndices extract;
extract.setInputCloud(xyz_filtered);
extract.setIndices(inliers);
extract.setNegative(true);
pcl::PointCloud::Ptr obstacles(new pcl::PointCloud());
extract.filter(*obstacles);
// 4. 欧几里得聚类
pcl::search::KdTree::Ptr tree(new pcl::search::KdTree());
tree->setInputCloud(obstacles);
std::vector cluster_indices;
pcl::EuclideanClusterExtraction ec;
ec.setClusterTolerance(0.5);
ec.setMinClusterSize(50);
ec.setMaxClusterSize(100000);
ec.setSearchMethod(tree);
ec.setInputCloud(obstacles);
ec.extract(cluster_indices);
// 5. 合并所有簇点云并生成包围框
pcl::PointCloud::Ptr all_clusters(new pcl::PointCloud());
visualization_msgs::msg::MarkerArray boxes;
int cluster_id = 0;
for (const auto& indices : cluster_indices) {
pcl::PointCloud::Ptr cluster(new pcl::PointCloud());
for (int idx : indices.indices)
cluster->points.push_back(obstacles->points[idx]);
cluster->width = cluster->points.size();
cluster->height = 1;
cluster->is_dense = true;
*all_clusters += *cluster;
// 计算AABB包围框
Eigen::Vector4f min_pt, max_pt;
pcl::getMinMax3D(*cluster, min_pt, max_pt);
visualization_msgs::msg::Marker box;
box.header = msg->header;
box.ns = "obstacles";
box.id = cluster_id++;
box.type = visualization_msgs::msg::Marker::CUBE;
box.action = visualization_msgs::msg::Marker::ADD;
box.pose.position.x = (min_pt[0] + max_pt[0]) / 2.0;
box.pose.position.y = (min_pt[1] + max_pt[1]) / 2.0;
box.pose.position.z = (min_pt[2] + max_pt[2]) / 2.0;
box.scale.x = max_pt[0] - min_pt[0];
box.scale.y = max_pt[1] - min_pt[1];
box.scale.z = max_pt[2] - min_pt[2];
box.color.r = 1.0f; box.color.g = 0.0f; box.color.b = 0.0f; box.color.a = 0.6f;
boxes.markers.push_back(box);
}
// 发布结果
sensor_msgs::msg::PointCloud2 out_msg;
pcl::toROSMsg(*all_clusters, out_msg);
out_msg.header = msg->header;
pub_clusters_->publish(out_msg);
pub_boxes_->publish(boxes);
}
rclcpp::Subscription::SharedPtr sub_;
rclcpp::Publisher::SharedPtr pub_clusters_;
rclcpp::Publisher::SharedPtr pub_boxes_;
};
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared());
rclcpp::shutdown();
return 0;
}
```
编译与运行
- 将上述代码保存为
obstacle_cluster_node.cpp,放入Autoware感知包的src/目录,并在CMakeLists.txt中添加:
```cmake
find_package(PCL REQUIRED)
find_package(visualization_msgs REQUIRED)
add_executable(obstacle_cluster_node src/obstacle_cluster_node.cpp)
ament_target_dependencies(obstacle_cluster_node rclcpp sensor_msgs visualization_msgs)
target_link_libraries(obstacle_cluster_node ${PCL_LIBRARIES})
install(TARGETS obstacle_cluster_node DESTINATION lib/${PROJECT_NAME})
```
- 编译并运行:
```bash
colcon build --packages-select your_package
source install/setup.bash
ros2 run your_package obstacle_cluster_node
```
验证方法
在另一个终端播放测试数据并观察结果:
```bash
ros2 bag play your_data.db3
rviz2 # 添加PointCloud2话题/clusters和MarkerArray话题/bboxes
```
通过条件:
- RViz中
/clusters显示为去除地面后的障碍物点云簇,颜色与原始点云可区分;
/bboxes在车辆前方障碍物(行人、车辆、锥桶)周围显示红色半透明立方体,每个障碍物一个框,框体紧密贴合点云簇边界;
- 终端无报错,节点持续输出且帧率与输入点云话题一致(约10Hz)。
常见问题
- 无聚类输出:检查
setMinClusterSize(50)是否过大——若场景中障碍物较小,可降至20;确认直通滤波范围未将目标区域完全滤除。
- 地面未完全去除:RANSAC距离阈值0.2m在颠簸路面可能残留地面点,可降至0.15m或改用
SACMODEL_NORMAL_PLANE。
- 包围框抖动:体素leaf size增大至0.2m可减少噪声,但会降低小障碍物检测精度,需根据场景权衡。