3、传感器数据接入:Camera、Lidar、Radar数据格式与预处理流水线
好,咱们进入第三章。传感器数据接入,这步要是没做好,后面模型推理再牛也白搭。我见过太多项目,算法调得漂漂亮亮,一上实车数据就崩,十有八九是预处理流水线出了岔子。
说白了,BEV模型吃的不是原始数据,而是经过精心“料理”的特征。Camera、Lidar、Radar这三兄弟,脾气秉性完全不同,你得摸透它们各自的数据格式和预处理门道。
3.1 Camera:图像数据的“标准化”之路
Camera数据最直观,但也最“娇气”。原始图像受光照、畸变、噪声影响极大。我个人习惯,流水线里至少包含这三步:
- 去畸变:鱼眼、广角镜头都有畸变。用OpenCV的
cv::fisheye::undistortImage或cv::initUndistortRectifyMap生成映射表,然后重映射。注意,映射表要提前算好,运行时只查表,别在推理循环里算畸变系数,太慢了。 - 归一化:像素值从[0,255]映射到[0,1]或[-1,1]。BEV模型通常用
mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225]这套ImageNet标准。嗯,这里要注意,不同模型预训练权重对应的归一化参数可能不同,别搞混。 - 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特征提取网络的设计,那才是真正考验架构能力的地方。