3、传感器数据接入:Camera、Lidar、Radar数据格式与预处理流水线

好,咱们进入第三章。传感器数据接入,这步要是没做好,后面模型推理再牛也白搭。我见过太多项目,算法调得漂漂亮亮,一上实车数据就崩,十有八九是预处理流水线出了岔子。

说白了,BEV模型吃的不是原始数据,而是经过精心“料理”的特征。Camera、Lidar、Radar这三兄弟,脾气秉性完全不同,你得摸透它们各自的数据格式和预处理门道。

3.1 Camera:图像数据的“标准化”之路

Camera数据最直观,但也最“娇气”。原始图像受光照、畸变、噪声影响极大。我个人习惯,流水线里至少包含这三步:

  1. 去畸变:鱼眼、广角镜头都有畸变。用OpenCV的cv::fisheye::undistortImagecv::initUndistortRectifyMap生成映射表,然后重映射。注意,映射表要提前算好,运行时只查表,别在推理循环里算畸变系数,太慢了。
  2. 归一化:像素值从[0,255]映射到[0,1]或[-1,1]。BEV模型通常用mean=[0.485, 0.456, 0.406]std=[0.229, 0.224, 0.225]这套ImageNet标准。嗯,这里要注意,不同模型预训练权重对应的归一化参数可能不同,别搞混。
  3. Resize与Padding:模型输入尺寸固定,比如640x640。直接resize会改变宽高比,导致目标变形。我建议用letterbox方式,保持宽高比,四周填充灰边。

避坑指南:我曾经在项目里直接用cv::resize把1920x1080硬拉到640x640,结果远处行人全变成了“矮胖子”,模型召回率直接掉了5个点。后来改成letterbox填充,问题解决。

代码示例(C++,嵌入式优化版):

// 提前计算映射表
cv::Mat map1, map2;
cv::initUndistortRectifyMap(K, D, cv::Mat(), K_new, 
                            cv::Size(640, 640), CV_32FC1, map1, map2);

// 运行时只查表
cv::remap(raw_img, undistorted_img, map1, map2, cv::INTER_LINEAR);

// letterbox填充
int w = undistorted_img.cols, h = undistorted_img.rows;
float scale = min(640.0f / w, 640.0f / h);
int new_w = w * scale, new_h = h * scale;
cv::resize(undistorted_img, resized_img, cv::Size(new_w, new_h));
cv::Mat padded_img = cv::Mat::zeros(640, 640, CV_8UC3);
resized_img.copyTo(padded_img(cv::Rect(0, 0, new_w, new_h)));

3.2 Lidar:点云数据的“降维打击”

Lidar数据是稀疏的3D点云,每帧可能有十几万到上百万个点。BEV模型通常把它投影到鸟瞰图网格上。怎么做?

  • 坐标变换:Lidar坐标系(前x左y上z)转车辆坐标系。注意外参标定精度,差个几厘米,远处目标就偏了好几米。
  • 体素化(Voxelization):把点云划分到BEV网格中。每个网格单元统计点云特征,比如高度、强度、密度。我常用pillar方式(PointPillar),只保留非空网格,节省内存。
  • 特征编码:每个pillar内的点用(x, y, z, intensity, x_c, y_c, z_c)编码,其中x_c, y_c, z_c是相对于pillar中心的偏移。这样模型能学到局部结构。

个人经验:点云预处理最耗时的其实是“点云裁剪”。我建议先根据BEV感知范围(比如前80m、左右40m)做快速过滤,只保留感兴趣区域内的点。这样能减少80%以上的计算量。

伪代码示意:

// 定义BEV网格参数
float x_range[2] = {-40.0, 40.0};  // 左右
float y_range[2] = {0.0, 80.0};    // 前向
float z_range[2] = {-3.0, 5.0};    // 高度
float voxel_size = 0.2;            // 网格分辨率

// 快速裁剪
for (auto &pt : point_cloud) {
    if (pt.x < x_range[0] || pt.x > x_range[1]) continue;
    if (pt.y < y_range[0] || pt.y > y_range[1]) continue;
    if (pt.z < z_range[0] || pt.z > z_range[1]) continue;
    
    // 计算网格索引
    int ix = (pt.x - x_range[0]) / voxel_size;
    int iy = (pt.y - y_range[0]) / voxel_size;
    
    // 填充pillar特征
    pillars[ix][iy].points.push_back(pt);
}

3.3 Radar:稀疏但“倔强”的测距数据

Radar数据最容易被忽视,但它对测距和速度估计至关重要。Radar点云比Lidar稀疏得多,而且有大量多径反射和杂波。

预处理要点:

  • 多普勒速度解算:Radar直接输出径向速度,需要结合车辆自身速度解算出目标真实速度。公式:v_target = v_radial + v_ego * cos(θ)
  • 杂波过滤:静止目标(如护栏、地面)会产生大量无效点。我习惯用CFAR(恒虚警率)算法做初步过滤,或者简单点,根据RCS(雷达散射截面)阈值过滤。
  • 时间戳对齐:Radar数据更新频率通常较低(10-20Hz),需要与Camera、Lidar做时间同步。我建议用最近邻插值,别用线性插值,Radar数据跳变太大。

注意:Radar的方位角分辨率很低(通常1°-3°),远处目标的位置误差可能达到米级。千万别把Radar的x,y坐标直接当Lidar精度用。我一般只取Radar的距离和速度信息,位置信息用Lidar或视觉融合。

3.4 多传感器时间同步与触发

三路数据必须对齐到同一时间戳。嵌入式系统里,我推荐两种方案:

方案 原理 适用场景
硬件触发 GPS的PPS信号同步各传感器 高精度要求,有硬件支持
软件时间戳 各传感器打上系统时间戳,后处理对齐 成本敏感,精度要求不高

我个人更倾向硬件触发。你想想看,Camera曝光时刻、Lidar旋转角度、Radar发射脉冲,如果能用同一个PPS信号同步,时间误差能控制在微秒级。软件方案嘛,我曾经遇到过Camera和Lidar时间戳差了20ms,高速场景下目标位置偏差超过1米,直接导致AEB误触发。

3.5 预处理流水线的性能优化

嵌入式平台资源有限,预处理不能拖后腿。我总结了几条铁律:

  • 零拷贝:用DMA或共享内存传递数据,别用memcpy。我见过有人把Camera图像从ISP拷贝到CPU再拷贝到NPU,白白浪费5ms。
  • 流水线并行:Camera、Lidar、Radar的预处理用三个独立线程,通过环形缓冲区传递数据。别串行执行,否则帧率上不去。
  • 定点化计算:归一化、坐标变换等操作,能用整数运算就别用浮点。ARM Neon或RISC-V Vector指令集对定点数优化得很好。

核心观点:预处理流水线的延迟,必须控制在传感器帧间隔的30%以内。比如Camera 30fps(33ms一帧),预处理不能超过10ms。否则整个系统的端到端延迟会爆炸。

好了,传感器数据接入这块,核心就是“理解数据特性,设计高效流水线”。Camera要标准化,Lidar要体素化,Radar要过滤杂波。三者时间同步是基础,性能优化是保障。下一章咱们聊聊BEV特征提取网络的设计,那才是真正考验架构能力的地方。