第四章 数据采集与标定:传感器时间同步、相机内参标定、相机与激光雷达联合标定
各位同学,欢迎来到第四章。这一章的内容,我敢说是整个感知联调里最「磨人」的一环。为什么这么说?因为数据不准,后面所有算法都是白搭。你想想看,传感器时间都对不上,相机拍的图和激光雷达打的点云根本不是同一时刻的场景,那还融合个啥?
我个人习惯把标定工作比作「给传感器做体检」。体检做不好,后面开再好的药方也没用。今天咱们就聊聊这三件事:时间同步、相机内参标定、以及相机和激光雷达的联合标定。
核心观点:标定不是一次性的工作,而是贯穿整个系统生命周期的持续任务。每次更换传感器、甚至环境温度剧烈变化后,都建议重新走一遍标定流程。
4.1 传感器时间同步——别让数据「穿越」
时间同步这事,说白了就是让所有传感器都「看」向同一个时刻。我在项目中遇到过最离谱的情况:相机帧率30Hz,激光雷达10Hz,IMU 100Hz。如果不做同步,你拿到的点云和图像可能差了100毫秒。100毫秒对于一辆时速60公里的车来说,意味着它已经跑了1.67米。你想想看,这融合出来的结果能准吗?
时间同步通常分两个层面:
- 硬件同步(硬同步):通过PPS(秒脉冲)或GPS授时,给所有传感器一个共同的时钟基准。这是最可靠的方式,延迟通常在微秒级。
- 软件同步(软同步):当硬件条件不具备时,靠时间戳对齐。比如激光雷达每帧有一个时间戳,相机也有,我们找最近邻的配对。
我的经验:能上硬同步就别用软同步。软同步在低速场景下勉强能用,但一旦遇到急加速或颠簸路面,误差会大到让你怀疑人生。我曾经在一个项目中偷懒只做了软同步,结果车辆过减速带时,点云和图像直接错位了半个车身。
软同步的常见做法是「插值」或「外推」。举个例子,激光雷达在t1时刻有一帧,相机在t2时刻有一帧,t2比t1晚了20ms。我们可以根据IMU数据,把激光雷达的点云「推」到t2时刻的位置。代码实现大致如下:
// 伪代码:基于IMU的激光点云时间补偿
void compensateLidarWithImu(LidarFrame &frame, ImuData &imu, double target_timestamp) {
double dt = target_timestamp - frame.timestamp;
if (fabs(dt) < 0.001) return; // 时间差小于1ms,不做补偿
for (auto &point : frame.points) {
// 利用IMU的角速度和线速度进行运动补偿
point.x += imu.vx * dt;
point.y += imu.vy * dt;
point.z += imu.vz * dt;
// 旋转补偿略...
}
frame.timestamp = target_timestamp;
}
4.2 相机内参标定——把镜头「掰直」
相机内参标定,说白了就是搞清楚两件事:焦距和光心在哪,以及镜头畸变有多严重。每个镜头都有畸变,尤其是广角镜头,边缘的弯曲程度简直像哈哈镜。
最经典的方法是张正友标定法。你只需要打印一张棋盘格,从不同角度拍个十几张照片,然后让算法去算。嗯,这里要注意:棋盘格一定要平整,不要皱巴巴的。我见过有人拿A4纸打印的棋盘格,纸都卷边了,标定出来的结果自然一塌糊涂。
标定结果通常是一个内参矩阵K和畸变系数:
K = [fx, 0, cx;
0, fy, cy;
0, 0, 1]
畸变系数: [k1, k2, p1, p2, k3]
// k1,k2,k3 是径向畸变,p1,p2 是切向畸变
避坑指南:我曾经在户外强光下标定,结果棋盘格反光严重,角点检测总是失败。后来我换成了哑光材质的棋盘格,并且在阴天或室内均匀光照下进行标定。记住,光照均匀比光照强度更重要。
标定完成后,一定要做验证。怎么验证?把标定好的参数拿去矫正一张图,看看棋盘格的边缘是不是直线。如果还有弯曲,说明标定不够好,需要重新来。
4.3 相机与激光雷达联合标定——让「眼睛」和「耳朵」对齐
联合标定的目标,是求出一个外参矩阵(R|t),把激光雷达坐标系下的点云,投影到相机图像上。说白了,就是让激光雷达「看到」的物体,和相机「看到」的物体在空间上重合。
具体做法有很多种,我比较常用的是手动标定和自动标定两种:
- 手动标定:找一个有明确边缘的标定物(比如棋盘格或三角架),在点云中手动选取几个点,再在图像中对应选取像素坐标,然后解算PnP问题。优点是直观,缺点是费时费力。
- 自动标定:利用标定物的几何特征,自动检测点云中的平面和图像中的角点,然后优化求解。现在很多开源工具如
lidar_camera_calibration都做得不错。
联合标定的结果,可以用一个投影公式来表示:
// 将激光雷达点云投影到图像平面
cv::Point2f projectLidarToCamera(const cv::Point3f &lidar_pt,
const cv::Mat &R,
const cv::Mat &t,
const cv::Mat &K) {
// 1. 将激光雷达坐标转换到相机坐标系
cv::Mat cam_pt = R * cv::Mat(lidar_pt) + t;
// 2. 投影到像素平面
float u = cam_pt.at<float>(0) * K.at<float>(0,0) / cam_pt.at<float>(2) + K.at<float>(0,2);
float v = cam_pt.at<float>(1) * K.at<float>(1,1) / cam_pt.at<float>(2) + K.at<float>(1,2);
return cv::Point2f(u, v);
}
验证方法:标定完成后,把点云投影到图像上,看看点云的边缘和图像的边缘是否对齐。比如,路沿在点云中是一条线,投影到图像上也应该和图像中的路沿重合。如果偏差超过3个像素,建议重新标定。
我记得有一次在园区里做联合标定,怎么调都差那么几个像素。后来发现是激光雷达的安装支架有轻微变形。你想想看,支架变形1毫米,在50米外就是几十厘米的误差。所以,标定前一定要检查机械安装的刚性。
4.4 标定流程总结
说了这么多,咱们把整个流程串一下。我个人习惯按这个顺序来:
- 硬件准备:固定传感器,确保机械结构稳定。连接PPS/GPS做硬同步。
- 相机内参标定:拍摄棋盘格,用OpenCV或MATLAB标定工具箱。保存内参K和畸变系数。
- 联合标定数据采集:在场景中放置标定物,同时录制激光雷达和相机数据。注意标定物要在两者视野内。
- 联合标定求解:用工具或手动方法求解外参R|t。
- 验证:投影点云到图像,检查对齐精度。如果不满意,回到第3步。
| 步骤 | 输入 | 输出 | 常见问题 |
|---|---|---|---|
| 时间同步 | 各传感器原始数据 | 时间对齐后的数据帧 | 时间戳精度不够,建议用硬件触发 |
| 相机内参标定 | 棋盘格图像 | 内参K + 畸变系数 | 棋盘格不平整、光照不均 |
| 联合标定 | 点云 + 图像 + 内参 | 外参R|t | 标定物不在共同视野、机械变形 |
一个小技巧:标定完成后,把外参和内参写到一个YAML文件里,方便后续读取。我习惯的格式是:
camera:
K: [fx, 0, cx, 0, fy, cy, 0, 0, 1]
dist: [k1, k2, p1, p2, k3]
lidar_to_camera:
R: [r11, r12, r13, r21, r22, r23, r31, r32, r33]
t: [tx, ty, tz]
好了,这一章的内容就到这里。标定工作虽然繁琐,但它是整个感知系统的基石。你花在标定上的每一分钟,都会在后面的融合和检测环节里加倍回报给你。记住,数据质量决定算法上限。
可可精品资料库