4. 激光雷达基础与点云处理
各位同学,欢迎来到第四章。这一章我们聊聊激光雷达和点云处理。说实话,激光雷达在自动驾驶里是个“硬骨头”——数据量大、处理复杂,但又是感知系统里不可或缺的一环。我个人习惯把激光雷达比作“眼睛里的尺子”,它不关心颜色,只关心距离和形状。
4.1 激光雷达工作原理
激光雷达,英文叫LiDAR,全称是Light Detection And Ranging。说白了,就是通过发射激光束,测量目标距离。原理其实不复杂:发射一束激光,打到物体上反射回来,接收器捕捉到回波,根据时间差算出距离。
公式很简单:距离 = 光速 × 飞行时间 / 2。为什么除以2?因为激光走了个来回嘛。
我在项目中遇到过一个问题:雨天激光雷达的噪声特别大。为什么?因为雨滴也会反射激光,产生大量“假点”。嗯,这里要注意,激光雷达对天气敏感,这是它的天然短板。
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,查了半天才发现文件路径写错了……嗯,这种低级错误,谁还没犯过呢?
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轴方向搞反了。所以,做滤波之前,一定要确认坐标系。
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分割出地面和障碍物。每一步都有它的意义,缺一不可。
好了,这一章的内容就到这里。点云处理是感知融合的基础,打好这个基础,后面做车道线融合、目标检测才会得心应手。