4. 激光雷达数据处理:点云数据格式、噪声滤除与降采样

激光雷达扫出来的原始数据,说白了就是一堆点。每个点带着距离、角度、强度等信息。但你要是直接把这一堆点扔给导航算法,那基本跑不动——数据量太大,噪声太多,还有一堆没用的遮挡点。

我刚开始做机器人导航那会儿,就吃过这个亏。雷达一转,几万个点涌进来,程序直接卡死。后来才明白,数据处理不是可选项,是必选项。这一章,我就把点云数据怎么读、怎么滤、怎么降采样,掰开揉碎了讲清楚。

核心要点:原始点云 → 格式解析 → 噪声滤除 → 降采样 → 干净点云。每一步都影响后续建图和定位的精度。

激光雷达数据处理流程 原始点云 格式解析 噪声滤除 降采样 离群点滤除 遮挡点处理 体素滤波 均匀采样 说明: 原始点云经过格式解析后,先滤除 离群点和遮挡点,再通过体素滤波 或均匀采样降低数据量,最终得到 干净、轻量的点云用于后续处理。

4.1 点云数据格式:sensor_msgs/LaserScan

ROS2 里激光雷达的标准消息类型是 sensor_msgs/LaserScan。你想想看,一个旋转的激光雷达,每转一圈就发出一系列激光束,每条光束返回一个距离值。这些数据怎么组织?

我直接给你看结构:

# sensor_msgs/LaserScan 消息结构
std_msgs/Header header          # 时间戳和坐标系
  uint32 seq                    # 序列号
  time stamp                    # 时间戳
  string frame_id               # 坐标系名称,如 "laser_frame"

float32 angle_min               # 起始角度(弧度)
float32 angle_max               # 终止角度(弧度)
float32 angle_increment         # 角度分辨率(弧度/步)

float32 time_increment          # 每步时间间隔(秒)
float32 scan_time               # 整圈扫描时间(秒)

float32 range_min               # 最小有效距离(米)
float32 range_max               # 最大有效距离(米)

float32[] ranges                # 距离数据数组
float32[] intensities           # 强度数据数组(可选)

这里有几个关键点,我重点说一下:

  • angle_min 和 angle_max:决定了雷达的视野范围。比如 0 到 6.28 就是 360 度全覆盖。
  • angle_increment:角度步长。如果雷达是 0.5 度分辨率,那这个值就是 0.0087 弧度。
  • ranges 数组:长度 = (angle_max - angle_min) / angle_increment。每个元素对应一个角度上的距离值。
  • intensities:反射强度。不同材质反射率不同,可以用来区分地面和障碍物。

我的经验:拿到 LaserScan 数据后,第一件事不是处理,而是检查 frame_id。我遇到过好几次,雷达数据坐标系设错了,导致建图全歪。养成习惯:先打印 header.frame_id 确认坐标系。

4.2 噪声滤除:离群点与遮挡点

原始点云里什么妖魔鬼怪都有。灰尘、飞虫、玻璃反射、边缘跳变……这些噪声不滤掉,建图质量直接崩。

4.2.1 离群点滤除

离群点就是那些明显偏离正常范围的点。比如你站在 5 米外,突然冒出一个 50 米的点,那肯定是噪声。

常用的方法有两种:

方法 原理 适用场景
半径滤波 以每个点为中心,统计半径内的邻居数量。少于阈值则剔除。 稀疏离群点,比如飞虫、灰尘
统计滤波 计算每个点到邻居的平均距离,超过均值+若干标准差则剔除。 均匀分布的噪声,比如雷达自身抖动

我个人习惯用半径滤波,参数好调。举个例子:

// C++ 示例:PCL 半径滤波
pcl::RadiusOutlierRemoval<pcl::PointXYZ> radius_filter;
radius_filter.setInputCloud(cloud);
radius_filter.setRadiusSearch(0.1);      // 搜索半径 0.1 米
radius_filter.setMinNeighborsInRadius(3); // 至少 3 个邻居
radius_filter.filter(*cloud_filtered);

避坑指南:我曾经把半径设得太小(0.02 米),结果把墙上的点都滤掉了,因为墙上的点间距本来就大。后来我根据雷达分辨率来算:半径 = 角度分辨率 × 距离 × 2,这样比较合理。

4.2.2 遮挡点处理

遮挡点是什么?就是激光打到物体边缘时,一部分光束打到前景,一部分打到背景,返回一个中间值。这种点位置不准,必须处理。

我常用的方法是相邻点跳变检测:

  1. 遍历 ranges 数组,计算相邻点的距离差。
  2. 如果差值超过阈值(比如 0.3 米),则标记为遮挡点。
  3. 剔除这些点,或者用插值填补。
// Python 示例:遮挡点检测
def remove_occlusion_points(ranges, threshold=0.3):
    clean_ranges = ranges.copy()
    for i in range(1, len(ranges) - 1):
        diff = abs(ranges[i] - ranges[i-1])
        if diff > threshold:
            clean_ranges[i] = 0.0  # 标记为无效
    return clean_ranges

你想想看,如果两个相邻角度上的距离突然从 2 米跳到 10 米,那中间肯定有遮挡。这种点留着,建图时就会出现“穿墙”现象。

4.3 数据降采样

高分辨率雷达一秒钟能扫几十万个点。导航算法根本吃不下。降采样就是在保持几何特征的前提下,减少点的数量。

4.3.1 体素滤波(Voxel Grid Filter)

这是最常用的方法。把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心)。

体素大小是关键参数:

体素大小 效果 适用场景
0.01 米 几乎不降采样,保留细节 高精度建图
0.05 米 适中降采样,保留主要特征 实时导航
0.1 米 大幅降采样,丢失细节 快速定位
// C++ 示例:PCL 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.05f, 0.05f, 0.05f); // 5cm 体素
voxel_filter.filter(*cloud_downsampled);

我的经验:体素大小不要一刀切。室内场景用 0.03-0.05 米,室外开阔场景可以放宽到 0.1 米。我做过一个项目,在走廊里用 0.1 米体素,结果门框特征全丢了,定位直接飘。后来改成 0.03 米才稳住。

4.3.2 均匀采样

另一种方法,每隔 N 个点取一个。简单粗暴,但可能丢失重要特征。

// Python 示例:均匀采样
def uniform_sampling(ranges, step=5):
    return ranges[::step]

均匀采样适合点云密度均匀的场景。如果雷达近处点密、远处点稀,那均匀采样会导致远处特征丢失更严重。

4.3.3 两种方法对比

方法 优点 缺点
体素滤波 保持几何特征,密度均匀 计算量稍大
均匀采样 速度快,实现简单 可能丢失特征,密度不均

我个人更推荐体素滤波。虽然计算量稍微大一点,但效果稳定。你想想看,导航算法对点云质量很敏感,多花几毫秒处理,换来更可靠的定位,值。

4.4 实战:完整的数据处理管线

好了,理论讲完了。我直接给你一个完整的处理流程,你在 ROS2 里可以直接用:

# 伪代码:激光雷达数据处理管线
def process_laser_scan(raw_scan):
    # 1. 格式解析
    ranges = raw_scan.ranges
    angle_min = raw_scan.angle_min
    angle_increment = raw_scan.angle_increment
    
    # 2. 遮挡点滤除
    ranges = remove_occlusion_points(ranges, threshold=0.3)
    
    # 3. 离群点滤除(半径滤波)
    cloud = ranges_to_pointcloud(ranges, angle_min, angle_increment)
    cloud = radius_filter(cloud, radius=0.1, min_neighbors=3)
    
    # 4. 体素滤波降采样
    cloud = voxel_filter(cloud, leaf_size=0.05)
    
    return cloud

关键参数速查表:

  • 遮挡点阈值:0.2 - 0.5 米(根据场景调整)
  • 半径滤波半径:0.05 - 0.15 米
  • 半径滤波邻居数:2 - 5 个
  • 体素大小:0.03 - 0.1 米

嗯,这一章的内容就这些。数据处理看起来琐碎,但每一步都影响最终效果。我见过太多人跳过这一步,直接拿原始点云建图,结果地图全是噪点,定位也跑偏。记住:好数据是好结果的前提。