4、激光雷达基础与点云处理

各位同学,欢迎来到第四章。这一章我们聊聊激光雷达和点云处理。说实话,激光雷达在自动驾驶里是个“硬骨头”——数据量大、处理复杂,但又是感知系统里不可或缺的一环。我个人习惯把激光雷达比作“眼睛里的尺子”,它不关心颜色,只关心距离和形状。

4.1 激光雷达工作原理

激光雷达,英文叫LiDAR,全称是Light Detection And Ranging。说白了,就是通过发射激光束,测量目标距离。原理其实不复杂:发射一束激光,打到物体上反射回来,接收器捕捉到回波,根据时间差算出距离。

公式很简单:距离 = 光速 × 飞行时间 / 2。为什么除以2?因为激光走了个来回嘛。

我在项目中遇到过一个问题:雨天激光雷达的噪声特别大。为什么?因为雨滴也会反射激光,产生大量“假点”。嗯,这里要注意,激光雷达对天气敏感,这是它的天然短板。

小提示: 激光雷达按扫描方式分为机械式、固态式和混合固态式。机械式旋转扫描,视野360度,但寿命有限;固态式没有运动部件,可靠性高,但视场角小。目前主流方案是混合固态,比如禾赛AT128、速腾M1。

4.2 点云数据结构

点云,就是一堆三维点的集合。每个点通常包含:

  • xyz坐标:点在空间中的位置
  • 强度值(intensity):反射回来的激光强度,可以用来区分不同材质
  • 时间戳:记录点被采集的时刻
  • 线束号:对于多线激光雷达,每个点属于哪条扫描线

你想想看,一帧64线激光雷达的数据,大约有12万个点。如果每个点存6个字段(x,y,z,intensity,time,ring),那就是72万个浮点数。处理起来,内存和CPU都是压力。

点云数据的存储格式,常见的有:

格式 特点 适用场景
PCD PCL原生格式,支持多种字段 开发调试、数据交换
PLY 支持顶点和面片,可带颜色 三维重建、可视化
LAS/LAZ 行业标准,压缩率高 测绘、地理信息
BIN 二进制裸数据,解析快 实时系统、嵌入式

我个人习惯在开发阶段用PCD格式,方便调试和可视化。但部署到实车上,一定转成BIN格式,省空间、速度快。

4.3 PCL基础

PCL,全称Point Cloud Library,是点云处理的事实标准。它提供了从滤波、分割到配准、识别的一整套工具。

安装PCL很简单,Ubuntu下一条命令:

sudo apt install libpcl-dev pcl-tools

写一个最简单的点云读取程序:

#include <pcl/io/pcd_io.h>
#include <pcl/point_types.h>

int main() {
    pcl::PointCloud<pcl::PointXYZ>::Ptr cloud(new pcl::PointCloud<pcl::PointXYZ>);
    
    if (pcl::io::loadPCDFile<pcl::PointXYZ>("data.pcd", *cloud) == -1) {
        PCL_ERROR("Couldn't read file.\n");
        return -1;
    }
    
    std::cout << "Loaded " << cloud->points.size() << " points." << std::endl;
    return 0;
}

这段代码我闭着眼睛都能写出来。为什么?因为每个点云项目的第一步都是加载数据,检查数据对不对。我曾经有一次加载完发现点数为0,查了半天才发现文件路径写错了……嗯,这种低级错误,谁还没犯过呢?

核心概念: PCL中所有算法都基于模板类,比如pcl::PointCloud<T>。T可以是PointXYZ、PointXYZI、PointXYZRGB等。选择正确的点类型,能省去很多类型转换的麻烦。

4.4 体素滤波

体素滤波,英文叫Voxel Grid Filter。它的作用是什么?说白了,就是降采样。

一帧激光雷达数据可能有十几万个点,但很多点其实是冗余的。比如一面墙,你不需要几万个点来描述它,几百个点就够了。体素滤波的做法是:把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点)。

代码实现:

pcl::VoxelGrid<pcl::PointXYZ> sor;
sor.setInputCloud(cloud);
sor.setLeafSize(0.1f, 0.1f, 0.1f);  // 体素大小10cm
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
sor.filter(*cloud_filtered);

体素大小怎么选?我一般这样设置:

  • 近距离(0-30米):0.05m(5cm),保留细节
  • 中距离(30-80米):0.1m(10cm),平衡精度和效率
  • 远距离(80米以上):0.2m(20cm),反正远处点本来就稀疏

避坑指南:体素滤波会改变点的精确位置。如果你需要做高精度定位,比如激光SLAM,体素大小别超过0.05m,否则定位精度会下降。

4.5 直通滤波

直通滤波,PassThrough Filter。它的逻辑很简单:只保留指定范围内的点。

比如,我们只关心前方50米内的障碍物,那就把x方向范围设为0到50米。再比如,激光雷达安装在车顶,地面以下和车顶以上的点都是噪声,那就用z方向过滤掉。

代码示例:

pcl::PassThrough<pcl::PointXYZ> pass;
pass.setInputCloud(cloud);
pass.setFilterFieldName("z");
pass.setFilterLimits(-1.5, 2.0);  // 保留z在-1.5到2.0米之间的点
pcl::PointCloud<pcl::PointXYZ>::Ptr cloud_filtered(new pcl::PointCloud<pcl::PointXYZ>);
pass.filter(*cloud_filtered);

我在项目中遇到过一个问题:直通滤波后,点云数量没减少多少。后来发现是坐标系的锅——激光雷达坐标系和车体坐标系没对齐,z轴方向搞反了。所以,做滤波之前,一定要确认坐标系。

注意: 直通滤波的setFilterLimits函数,默认是保留范围内的点。如果想保留范围外的点,可以调用setFilterLimitsNegative(true)。这个参数很容易被忽略,我曾经因为这个bug排查了一下午。

4.6 RANSAC地面分割

地面分割,是点云处理里最基础也最重要的任务之一。为什么?因为自动驾驶需要区分“可行驶区域”(地面)和“障碍物”(非地面)。

RANSAC,全称Random Sample Consensus,随机采样一致性。它的核心思想是:从点云中随机选三个点,拟合一个平面,然后统计有多少点在这个平面附近。重复这个过程,找到包含点最多的那个平面,就是地面。

代码实现:

pcl::SACSegmentation<pcl::PointXYZ> seg;
pcl::ModelCoefficients::Ptr coefficients(new pcl::ModelCoefficients);
pcl::PointIndices::Ptr inliers(new pcl::PointIndices);

seg.setOptimizeCoefficients(true);
seg.setModelType(pcl::SACMODEL_PLANE);
seg.setMethodType(pcl::SAC_RANSAC);
seg.setMaxIterations(100);        // 最大迭代次数
seg.setDistanceThreshold(0.15);   // 距离阈值,单位米
seg.setInputCloud(cloud);
seg.segment(*inliers, *coefficients);

这里有几个关键参数:

  • setMaxIterations:迭代次数。次数越多,找到最优平面的概率越大,但耗时也越长。我一般设100-200次。
  • setDistanceThreshold:距离阈值。点离拟合平面多远才算“内点”?这个值很关键。设大了,会把低矮障碍物误判为地面;设小了,地面点会被漏掉。我一般设0.1-0.2米。

分割完成后,coefficients里存的是平面方程参数(a,b,c,d),满足ax+by+cz+d=0。inliers里存的是属于地面的点的索引。

提取地面和非地面点:

pcl::ExtractIndices<pcl::PointXYZ> extract;
extract.setInputCloud(cloud);
extract.setIndices(inliers);
extract.setNegative(false);  // 提取地面
pcl::PointCloud<pcl::PointXYZ>::Ptr ground(new pcl::PointCloud<pcl::PointXYZ>);
extract.filter(*ground);

extract.setNegative(true);   // 提取非地面(障碍物)
pcl::PointCloud<pcl::PointXYZ>::Ptr obstacles(new pcl::PointCloud<pcl::PointXYZ>);
extract.filter(*obstacles);

我曾经在高速场景下用RANSAC分割地面,发现效果很差。为什么?因为高速路面平坦,但远处的地面点因为激光入射角太大,点云稀疏,RANSAC很难拟合出准确的平面。后来我改用“先降采样,再分割”的策略,效果好了很多。

经验之谈: RANSAC地面分割的局限性在于,它假设地面是一个平面。但实际道路有坡度、有起伏。对于非平坦路面,可以考虑使用“区域生长”或“基于图割”的地面分割方法。不过,对于大多数城市道路场景,RANSAC已经够用了。
激光雷达点云处理流程 原始点云 直通滤波 体素滤波 RANSAC分割 地面点(可行驶区域) 障碍物点(非地面) 车道线检测、可行驶区域 障碍物检测、跟踪

这张图展示了点云处理的完整流程。从原始点云开始,先做直通滤波去掉无关区域,再做体素滤波降低数据量,最后用RANSAC分割出地面和障碍物。每一步都有它的意义,缺一不可。

好了,这一章的内容就到这里。点云处理是感知融合的基础,打好这个基础,后面做车道线融合、目标检测才会得心应手。

可可精品资料库