3. 点云与图像预处理:让传感器“看清”世界
大家好,我是你们的老朋友。今天我们来聊聊预处理。说实话,很多刚入行的朋友容易忽略这一步,觉得“算法牛逼就行”。但我在实际项目中吃过太多亏了——预处理没做好,后面模型再强也白搭。
你想想看,激光雷达和摄像头采集的原始数据,就像没洗过的菜。直接下锅?那味道肯定不对。预处理就是洗菜、切菜的过程。做好了,后面的“烹饪”才能出彩。
核心观点:预处理的质量,直接决定了融合感知的上限。我见过太多项目,花90%精力调模型,最后发现是数据对齐出了问题。嗯,这很常见。
3.1 点云滤波:去噪是第一步
激光雷达点云,说白了就是一堆三维坐标点。但这里面有大量噪声——比如地面扬起的灰尘、远处的飞鸟、甚至传感器自身的电子噪声。滤波就是把这些“脏东西”去掉。
3.1.1 体素滤波:降采样与去噪兼顾
体素滤波是我个人最常用的方法。它的思路很简单:把空间划分成一个个小立方体(体素),每个体素内只保留一个点(通常是重心点)。
这样做有两个好处:一是大幅降低点云数量(比如从10万点降到1万点),二是顺便把体素内的噪声点给平均掉了。
// 体素滤波核心参数
// leaf_size: 体素边长(单位:米)
// 我一般设0.1m,场景大可以设0.2m
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f);
voxel_filter.setInputCloud(cloud_in);
voxel_filter.filter(*cloud_out);
我的经验:体素大小别设太小(比如0.05m),否则降采样效果不明显;也别太大(比如0.5m),会丢失细节。0.1m是个不错的起点。
3.1.2 统计滤波:干掉离群点
统计滤波,说白了就是“看邻居”。如果一个点周围邻居太少,那它很可能是噪声。我曾在高速场景遇到过——远处一个孤立的点,统计滤波直接把它干掉了,后来发现那其实是只飞鸟。
算法原理:计算每个点到其k个近邻的平均距离。如果这个距离超过全局均值+若干倍标准差,就判定为离群点。
// 统计滤波参数
// mean_k: 近邻数量(我常用50)
// std_dev: 标准差倍数(我常用1.0)
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setMeanK(50);
sor.setStddevMulThresh(1.0);
sor.setInputCloud(cloud_in);
sor.filter(*cloud_out);
注意:标准差倍数设太小(比如0.5),会把正常点也干掉;设太大(比如3.0),噪声滤不干净。我曾经在隧道场景踩过坑——隧道内点云稀疏,统计滤波把墙上的点都误删了。后来我把mean_k调小到20才解决。
3.1.3 半径滤波:简单粗暴但有效
半径滤波更直接:在指定半径内,如果邻居点数量少于阈值,就干掉。我一般在预处理流水线里把它放在统计滤波之后,作为二次过滤。
// 半径滤波参数
// radius: 搜索半径(0.1m)
// min_pts: 最少邻居数(3个)
pcl::RadiusOutlierRemoval<pcl::PointXYZ> ror;
ror.setRadiusSearch(0.1);
ror.setMinNeighborsInRadius(3);
ror.setInputCloud(cloud_in);
ror.filter(*cloud_out);
3.2 图像去畸变与增强
摄像头图像的问题,说白了就是“镜头不完美”。广角镜头尤其明显——画面边缘的直线会变弯。这就是畸变。
3.2.1 去畸变:让直线变直
畸变分两种:径向畸变(桶形/枕形)和切向畸变(镜头安装歪了)。去畸变需要相机内参和畸变系数,这些通常在标定阶段获得。
// OpenCV去畸变示例
cv::Mat camera_matrix, dist_coeffs;
// 从标定文件读取内参和畸变系数
cv::Mat undistorted;
cv::undistort(image, undistorted, camera_matrix, dist_coeffs);
我的习惯:去畸变后一定要检查图像边缘。如果边缘出现黑色区域,说明畸变系数不准,或者标定板没拍好。我曾经因为标定板没贴平,折腾了两天。
3.2.2 图像增强:让特征更明显
去畸变只是第一步。实际场景中,光照变化、阴影、夜间低照度都会影响图像质量。我常用的增强手段:
- 直方图均衡化:提升对比度,适合逆光场景
- CLAHE:限制对比度自适应直方图均衡,比普通均衡更自然
- 伽马校正:调整亮度,夜间场景常用
// CLAHE增强示例
cv::Ptr<cv::CLAHE> clahe = cv::createCLAHE(2.0, cv::Size(8, 8));
cv::Mat enhanced;
clahe->apply(gray_image, enhanced);
3.3 时间戳同步与空间对齐
这是融合感知里最头疼的问题。你想想看,激光雷达和摄像头采集数据的时间不一样,空间位置也不一样。怎么把它们对应起来?
3.3.1 时间戳同步:硬触发 vs 软触发
时间同步说白了就是让两个传感器“同时”采集数据。有两种方案:
| 方案 | 原理 | 精度 | 成本 | 我的评价 |
|---|---|---|---|---|
| 硬触发 | 硬件信号线连接,激光雷达触发摄像头 | 微秒级 | 高(需要硬件支持) | 最可靠,但调试麻烦 |
| 软触发 | 软件打时间戳,后对齐 | 毫秒级 | 低 | 够用,但要注意延迟 |
硬触发方案:我曾在量产项目里用过。激光雷达每转一圈,输出一个脉冲信号,触发摄像头曝光。这样时间差能控制在10微秒以内。但硬件布线、信号干扰都是坑。
软触发方案:更灵活。每个传感器数据都带上时间戳,然后找最近的时间对。我一般用线性插值来补偿时间差。
// 软触发时间对齐伪代码
// 找激光雷达帧和图像帧的时间差最小的配对
for each lidar_frame:
find image_frame with min(|timestamp_lidar - timestamp_image|)
if time_diff < 10ms: // 阈值可调
align(lidar_frame, image_frame)
我曾经踩过的坑:软触发方案里,如果系统负载高,时间戳可能延迟。有一次我发现图像和点云总是对不上,查了半天才发现是CPU过载导致时间戳打晚了。后来我加了时间戳校验和重传机制。
3.3.2 空间对齐:把点云投影到图像上
空间对齐,说白了就是知道激光雷达的一个点,对应图像上的哪个像素。这需要联合标定得到的旋转矩阵R和平移向量t。
// 点云投影到图像
// 输入:3D点 (X, Y, Z),相机内参 K,外参 R, t
// 输出:像素坐标 (u, v)
// 1. 将激光雷达坐标系下的点转换到相机坐标系
P_cam = R * P_lidar + t
// 2. 投影到图像平面
u = fx * P_cam.x / P_cam.z + cx
v = fy * P_cam.y / P_cam.z + cy
我的建议:投影后一定要做可视化检查。把点云投影到图像上,看看边缘是否对齐。如果出现系统性偏差(比如所有点都偏左),说明外参标定有问题。
3.4 本章知识体系
下面这张图,是我自己总结的预处理流程。你一看就明白了:
你看,整个流程其实很清晰。原始点云和图像各自经过滤波/增强后,再通过时间同步和空间对齐,最终得到一一对应的数据对。这就是后续融合感知的基础。
最后说一句:预处理看起来简单,但细节决定成败。我见过太多项目,算法模型调得飞起,最后发现是时间戳没对齐。嗯,别犯这种低级错误。