4、传感器数据预处理:激光雷达点云滤波、相机图像去畸变、多传感器时间同步

各位同学,大家好。今天我们来聊聊传感器数据预处理。说实话,这部分工作看起来不起眼,但往往是整个感知系统的基石。我见过太多团队,算法模型调得再好,预处理没做好,结果就是各种奇葩的误检和漏检。说白了,你喂给模型的是垃圾,它吐出来的也只能是垃圾。

这节课,我们聚焦三个核心环节:激光雷达点云滤波、相机图像去畸变、多传感器时间同步。这三个环节,我每个都踩过坑,今天一并分享给你们。

4.1 激光雷达点云滤波

激光雷达点云,说白了就是一堆三维坐标点。但原始数据里,噪声点、离群点、地面点、甚至飞鸟和塑料袋,什么都有。我们得先把这些“脏东西”过滤掉。

4.1.1 为什么要滤波?

你想想看,一个点云里有几万甚至几十万个点。如果直接扔给检测算法,计算量巨大,而且容易被噪声干扰。我刚开始做感知时,就遇到过因为一个飞行的塑料袋,导致车辆急刹的尴尬情况。嗯,从那以后,我特别重视滤波。

4.1.2 常用滤波方法

我个人习惯,把滤波分为两类:基于统计的和基于几何的。

  • 直通滤波(PassThrough):最简单粗暴。设定一个范围,比如只保留前方50米、左右20米、高度-1米到3米内的点。这能快速剔除远处的无用点。
  • 体素滤波(VoxelGrid):下采样用的。把空间划分成小立方体(体素),每个体素内只保留一个重心点。这能大幅降低点云密度,同时保留形状特征。
  • 统计滤波(StatisticalOutlierRemoval):去除离群点。计算每个点周围K个邻居的平均距离,如果距离过大,就认为是噪声。我在项目中遇到过,雨天的点云噪声特别多,这个滤波器效果很好。
  • 半径滤波(RadiusOutlierRemoval):设定一个半径,如果某个点周围邻居数量少于阈值,就剔除。这能有效去除孤立的噪声点。

避坑指南:我曾经在高速场景下,把体素滤波的格子设得太大,结果把远处的行人点云给滤没了。后来我学乖了,近处用细格子,远处用粗格子,或者干脆分区域处理。

4.1.3 代码示例:PCL 点云滤波

这里给一个PCL库的简单示例,展示体素滤波和统计滤波的用法。

// 体素滤波
pcl::VoxelGrid<pcl::PointXYZ> voxel_filter;
voxel_filter.setInputCloud(cloud);
voxel_filter.setLeafSize(0.1f, 0.1f, 0.1f); // 10cm格子
voxel_filter.filter(*cloud_filtered);

// 统计滤波
pcl::StatisticalOutlierRemoval<pcl::PointXYZ> sor;
sor.setInputCloud(cloud_filtered);
sor.setMeanK(50);          // 邻居数量
sor.setStddevMulThresh(1.0); // 标准差倍数
sor.filter(*cloud_clean);

我的经验:体素滤波的格子大小,我一般从0.05m开始试,根据实际场景调整。统计滤波的阈值,1.0倍标准差比较保守,如果噪声大可以放宽到2.0。

4.2 相机图像去畸变

相机镜头,尤其是广角镜头,拍出来的图像会有畸变。直线变弯了,物体变形了。如果不矫正,后续的视觉检测、车道线识别都会出问题。

4.2.1 畸变的来源

畸变主要分两种:径向畸变和切向畸变。径向畸变是镜头形状导致的,表现为“桶形”或“枕形”。切向畸变是镜头和成像平面不平行导致的,表现为“倾斜”。

4.2.2 去畸变的原理

说白了,就是通过标定得到畸变参数,然后对每个像素进行重新映射。我们通常用张正友标定法,得到内参矩阵和畸变系数(k1, k2, p1, p2, k3)。

去畸变的公式其实不复杂,但我不建议你手写。OpenCV已经封装好了。

4.2.3 代码示例:OpenCV 去畸变

// 读取标定参数
cv::Mat camera_matrix = (cv::Mat_<double>(3,3) << 
     fx, 0, cx,
     0, fy, cy,
     0, 0, 1);
cv::Mat dist_coeffs = (cv::Mat_<double>(1,5) << k1, k2, p1, p2, k3);

// 去畸变
cv::Mat undistorted;
cv::undistort(image, undistorted, camera_matrix, dist_coeffs);

注意:去畸变后,图像边缘会被裁剪掉一部分。如果你需要保留完整的视野,可以用 cv::getOptimalNewCameraMatrix 来调整内参,让图像缩放后保留更多边缘信息。

我记得有一次,在停车场场景下,因为畸变矫正参数没更新(换了镜头没重新标定),导致视觉检测到的车位角点位置偏差了十几个像素。嗯,从那以后,我每次换镜头或调整焦距,都会重新标定一次。

4.3 多传感器时间同步

这是最容易被忽视,但也是最容易出问题的一环。激光雷达、相机、IMU,每个传感器都有自己的时钟。如果时间不同步,你拿到的点云和图像,其实对应的是不同时刻的场景。车辆在高速运动时,哪怕10毫秒的偏差,都会导致严重的空间错位。

4.3.1 同步策略

我常用的同步策略有两种:硬件同步和软件同步。

  • 硬件同步:通过GPS的PPS(秒脉冲)信号,给所有传感器一个统一的时钟基准。这是最可靠的方式,但需要硬件支持。
  • 软件同步:通过时间戳对齐。每个传感器数据都打上时间戳,然后在软件层面找到最接近的时间对。

4.3.2 时间戳对齐方法

我个人习惯用最近邻插值。比如,激光雷达是10Hz,相机是20Hz。每来一帧点云,就去找时间戳最接近的那帧图像。如果时间差超过阈值(比如5ms),就丢弃或插值。

避坑指南:我曾经在项目中,发现点云和图像总是对不上。查了半天,发现是相机的时间戳是系统启动时间,而激光雷达的时间戳是GPS时间。两个时钟差了整整1秒。所以,统一时间基准是第一位的。

4.3.3 代码示例:时间同步逻辑

// 伪代码:最近邻时间同步
struct SensorData {
    double timestamp;
    // ... 数据内容
};

SensorData find_nearest_image(
    const std::vector<SensorData>& images, 
    double lidar_ts, 
    double max_diff = 0.005) {
    
    SensorData best;
    double min_diff = std::numeric_limits<double>::max();
    
    for (const auto& img : images) {
        double diff = std::abs(img.timestamp - lidar_ts);
        if (diff < min_diff) {
            min_diff = diff;
            best = img;
        }
    }
    
    if (min_diff > max_diff) {
        // 时间差太大,返回空或做插值
        return SensorData();
    }
    return best;
}

我的建议:如果条件允许,尽量用硬件同步。软件同步虽然灵活,但总会有延迟和抖动。另外,记得给每个传感器数据打上UTC时间戳,而不是本地时间。

4.4 本章知识体系

为了让大家更直观地理解这三个预处理环节的关系,我画了一张流程图。

传感器数据预处理流程 原始传感器数据 激光雷达点云 相机图像 IMU/GPS时间基准 点云滤波 图像去畸变 时间同步 对齐后的点云 + 图像数据

从这张图可以看得很清楚:原始数据先分流到激光雷达和相机各自处理,同时IMU/GPS提供统一的时间基准。最后,经过滤波和去畸变的数据,再通过时间同步对齐,输出给后续的感知算法。

好了,这节课的内容就到这里。预处理虽然繁琐,但值得你花时间打磨。记住,好的预处理,能让你的感知算法事半功倍。

可可精品资料库