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 本章知识体系

下面这张图,是我自己总结的预处理流程。你一看就明白了:

点云与图像预处理流程 原始点云 原始图像 点云滤波 体素滤波 | 统计滤波 | 半径滤波 降采样 + 去噪 图像预处理 去畸变 | 直方图均衡 | CLAHE 校正 + 增强 时间戳同步 硬触发 / 软触发 空间对齐(投影) 对齐后的点云 + 图像 预处理是融合感知的基石,马虎不得

你看,整个流程其实很清晰。原始点云和图像各自经过滤波/增强后,再通过时间同步和空间对齐,最终得到一一对应的数据对。这就是后续融合感知的基础。

最后说一句:预处理看起来简单,但细节决定成败。我见过太多项目,算法模型调得飞起,最后发现是时间戳没对齐。嗯,别犯这种低级错误。

专注资料整理