laser_line_extraction线段提取开源功能包解读和使用例程

laser_line_extraction 功能包介绍

laser_line_extraction 是一个功能明确的 ROS 软件包,专门用于从二维激光雷达数据中实时提取直线线段。


一、包的核心功能与处理流程

该包订阅激光扫描话题(通常为 /scan),对每一帧点云执行两个主要步骤:

1. 点云分割

首先剔除距离超出设定范围(min_rangemax_range)或孤立的噪点(outlier_dist)。然后采用分割合并算法,根据相邻点之间的欧氏距离是否超过阈值 max_line_gap 将点云划分为不同的候选点簇,每个点簇可能对应环境中的一条直线结构。

2. 线段拟合

对每个候选点簇,使用加权最小二乘法迭代拟合最优直线。该方法来源于 Pfister 等人的论文,能够同时输出线段在极坐标下的参数(距离 radius 和角度 angle)以及这些参数的协方差矩阵,从而反映拟合的不确定性。拟合结果再经处理得到线段端点的笛卡尔坐标。


二、消息结构

包内定义了两种自定义消息类型:

  • LineSegment.msg

    :描述单条线段,包含以下字段

    • float32 radius

      :极坐标系下原点到线段所在直线的距离

    • float32 angle

      :线段法向量的角度(范围通常为 [-π/2, π/2))

    • float32[2] start

      float32[2] end:线段端点的笛卡尔坐标

    • float32[4] covariance

      radiusangle 的协方差矩阵

  • LineSegmentList.msg

    :表示一帧激光数据中提取的所有线段,内含 LineSegment[] 数组和时间戳。


三、安装与使用

1. 获取源码

bash

复制代码
cd ~/catkin_ws/srcgit clone https://github.com/kam3k/laser_line_extraction.gitcd ~/catkin_wscatkin_make

2. 参数配置

基础配置

包的所有参数通过 ROS 参数服务器加载,官方推荐将参数写入 launch 文件进行管理。关键参数及其作用如下:

参数名 默认值 说明
scan_topic "scan" 订阅的激光话题名
frame_id "laser" 发布线段的参考坐标系
publish_markers false 是否发布可视化标记(用于 RViz 调试)
算法相关的参数配置
参数名 默认值 说明
min_line_length 0.5 m 线段最小长度,短于此值的线段被丢弃
max_line_gap 0.4 m 点云分割时允许的最大间隔
outlier_dist 0.05 m 孤立点判定阈值
min_line_points 9 构成一条线段所需的最少点数
bearing_var - 方位方差
range_var - 距离方差
least_sq_angle_thresh - 改变角度(rad)阈值以停止迭代最小二乘法(还必须满足 least_sq_radius_thresh
least_sq_radius_thresh - 改变半径(m)阈值,以停止迭代最小二乘法(还必须满足 least_sq_angle_thresh
max_line_length - 大于此长度的行未发布(m)
min_range - 比这更近的点将被忽略(m)
max_range - 超过此值的点将被忽略(m)
min_split_dist - 当执行拆分和合并的"拆分"步骤时,两个点相距至少这么远(m)会导致拆分
outlier_dist - 与所有邻居相距至少该距离的点被视为异常值(m)

3. 运行节点

创建 launch 文件后,执行:

bash

复制代码
roslaunch laser_line_extraction your_config.launch

节点将发布两个话题:

  • /line_segments

    laser_line_extraction/LineSegmentList):检测到的线段列表

  • /line_markers

    visualization_msgs/MarkerArray):用于 RViz 显示的线段标记(若 publish_markerstrue

4. 运行的效果

绿色的雷达原始的点云,红色提取出的线段。

四、关键代码解读

1. 最小二乘线拟合方法

cpp

复制代码
void Line::leastSqFit(){  // 第一步:计算点的协方差矩阵  calcPointCovariances();  // 第二步:初始化前一次迭代的参数值  // 用于判断收敛条件(当前参数与上一次参数的差值)  double prev_radius = 0.0;  double prev_angle = 0.0;  // 第三步:迭代优化循环  // 当半径或角度的变化超过阈值时继续迭代  while (fabs(radius_ - prev_radius) > params_.least_sq_radius_thresh ||         fabs(angle_ - prev_angle) > params_.least_sq_angle_thresh)   {    prev_radius = radius_;    prev_angle = angle_;    // 计算点的标量协方差    calcPointScalarCovariances();    // 基于最小二乘原理重新计算半径    // 通过优化方法得到更精确的半径值    radiusFromLeastSq();    // 基于最小二乘原理重新计算角度    // 通过优化方法得到更精确的角度值    angleFromLeastSq();  }  // 第四步:计算线段的协方差矩阵  // 反映拟合线段参数的不确定性  calcCovariance();  // 第五步:投影端点  // 将线段的端点投影到拟合的直线上,确保端点在线上  projectEndpoints();}

2. 主要的识别线段函数

cpp

复制代码
void LineExtraction::extractLines(std::vector<Line>& lines) {  filtered_indices_ = c_data_.indices;  lines_.clear();
  // 过滤近距离和远距离点  filterCloseAndFarPoints();  // 过滤异常点  filterOutlierPoints();
  // 如果剩余点不够,则不返回任何线段  if (filtered_indices_.size() <= std::max(params_.min_line_points, static_cast<unsigned int>(3)))  {    return;  }
  // 将索引拆分为线段并过滤掉短行和稀疏线段  split(filtered_indices_);  // 根据线段的最大最小长度,过滤线段  filterLines();
  // 使用最小二乘法拟合每条直线并合并共线段  for (std::vector<Line>::iterator it = lines_.begin(); it != lines_.end(); ++it)  {    it->leastSqFit();  }  // 如果有多线段,检查是否应根据合并条件合并线段  if (lines_.size() > 1)  {    mergeLines();  }  lines = lines_;}

3. 使用贝叶斯估计框架合并线段

cpp

复制代码
void LineExtraction::mergeLines(){  std::vector<Line> merged_lines;
  for (std::size_t i = 1; i < lines_.size(); ++i)  {    // Get L, P_1, P_2 of consecutive lines    Eigen::Vector2d L_1(lines_[i-1].getRadius(), lines_[i-1].getAngle());    Eigen::Vector2d L_2(lines_[i].getRadius(), lines_[i].getAngle());    Eigen::Matrix2d P_1;    P_1 << lines_[i-1].getCovariance()[0], lines_[i-1].getCovariance()[1],           lines_[i-1].getCovariance()[2], lines_[i-1].getCovariance()[3];    Eigen::Matrix2d P_2;    P_2 << lines_[i].getCovariance()[0], lines_[i].getCovariance()[1],           lines_[i].getCovariance()[2], lines_[i].getCovariance()[3];
    // Merge lines if chi-squared distance is less than 3    if (chiSquared(L_1 - L_2, P_1, P_2) < 3)    {      // Get merged angle, radius, and covariance      Eigen::Matrix2d P_m = (P_1.inverse() + P_2.inverse()).inverse();      Eigen::Vector2d L_m = P_m * (P_1.inverse() * L_1 + P_2.inverse() * L_2);      // Populate new line with these merged parameters      boost::array<double, 4> cov;      cov[0] = P_m(0,0);      cov[1] = P_m(0,1);      cov[2] = P_m(1,0);      cov[3] = P_m(1,1);      std::vector<unsigned int> indices;      const std::vector<unsigned int> &ind_1 = lines_[i-1].getIndices();      const std::vector<unsigned int> &ind_2 = lines_[i].getIndices();      indices.resize(ind_1.size() + ind_2.size());      indices.insert(indices.end(), ind_1.begin(), ind_1.end());      indices.insert(indices.end(), ind_2.begin(), ind_2.end());      Line merged_line(L_m[1], L_m[0], cov, lines_[i-1].getStart(), lines_[i].getEnd(), indices);      // Project the new endpoints      merged_line.projectEndpoints();      lines_[i] = merged_line;    }    else    {      merged_lines.push_back(lines_[i-1]);    }
    if (i == lines_.size() - 1)    {      merged_lines.push_back(lines_[i]);    }  }  lines_ = merged_lines;}
相关推荐
沫儿笙12 小时前
库卡弧焊机器人混合气焊接节气装置
人工智能·机器人
刀法如飞12 小时前
JavaScript 数组去重的 20 种实现方式,学会用不同思路解决问题
前端·javascript·算法
卡卡大怪兽12 小时前
MCP 和 CLI
人工智能
Black蜡笔小新12 小时前
企业私有化AI训练推理一体工作站DLTM:筑牢企业数据安全,打造企业专属AI能力底座
人工智能
code_pgf12 小时前
知识蒸馏在 sVLM 中的作用及实现方式
人工智能·深度学习·机器学习
美狐美颜SDK开放平台12 小时前
美颜SDK接入流程详解:Android、iOS、鸿蒙兼容方案解析
android·人工智能·ios·华为·harmonyos·美颜sdk·视频美颜sdk
Land032912 小时前
指纹浏览器自动化集成方案|多浏览器RPA适配实战记录
运维·人工智能·爬虫·python·selenium·自动化·rpa
洛水水12 小时前
【力扣100题】46.单词拆分
算法·leetcode·职场和发展
小江的记录本12 小时前
【AI大模型选型指南】《2026年5月(最新版)国内外主流AI大模型选型指南》(个人版)
前端·人工智能·后端·ai·aigc·ai编程·ai写作
Oflycomm13 小时前
无人机远距离图传新选择:国产私有SDR模组O9201UDH技术解析与选型指南
人工智能·科技·物联网·无人机·模组·qogrisys