2023年2月重要补充

这个代码我个人觉得不好用且坑太多,所以后来换了一个。推荐大家用新的代码。
详见更新的一篇博客总结:【学习总结】激光雷达与相机外参标定:代码(cam_lidar_calibration)


这一周多学习并调试了激光雷达和相机外参标定的代码,踩了一堆坑,特此记录。

0. 参考资料:

代码来源:https://github.com/ankitdhall/lidar_camera_calibration
参考论文:LiDAR-Camera Calibration using 3D-3D Point correspondences
修改后的代码:https://github.com/LarryDong/lidar_camera_calibration

之前找了几个开源的标定代码,在github上看就这个代码具有最多的star,觉得比较靠谱。结果这个代码写的真的一言难尽:逻辑混乱,需要注意的细节极其多,以及存在不少坑。这一个多星期,就是踩坑和调试。今天终于能够正常运行出一个正确(但并不准确)的结果,在此记录。

代码混乱的原因:

  1. 大量变量定义未使用,以及定义与实际使用的距离太远,让人误解;
  2. 需要修改多个参数文件,参数管理混乱;参数由不同文件读取,同时一些临时变量也存储到文件中再在不同函数中读取调用,以及全局变量的使用;
  3. 官方使用文档一些细节没有强调。

1. 基本原理

计算雷达和相机的外参,基本原理较为简单:只需要找雷达系下的点与相机系下对应点的坐标关系,然后计算外参即可。之前一篇博客总结了几种常用的方法:【学习记录】激光雷达与相机标定

问题核心是,如何确定对应关系,即correspondence?

所采用的代码的基本原理:用一块矩形纸板,用激光雷达检测纸板的边界线,之后确定纸板的角点,获取纸板角点在lidar系下的坐标;而相机本身角点无法提供深度信息,但我们可以通过aruco提供带深度的marker的角点,再计算纸板四个角的坐标。

2. 代码总览


首先需要按照原始代码链接中的wiki进行安装,之后需要修改一些参数配置文件。如果运气好,搞清楚怎么用,或许就能一次成果。但许多细节在原始的代码readme中没有讲清楚,因此本博客详细整理。

整个代码的逻辑如下:

首先这个代码的核心是 lidar_camera_calibration下的find_transform.launch 启动的标定程序。这个程序接收数据包括:

  • 激光雷达的原始扫描数据
  • aruco marker 在 相机系下的位姿
  • 相应参数配置文件

其中激光雷达的数据有激光雷达启动节点直接提供,aruco marker的消息由aruco_mapping节点提供,相应参数配置文件为 “lidar_camera_calibration/conf/config_file.txt” 提供。

核心代码在收到上述数据后,对雷达数据进行纸板的边界线和角点检测,最后与aruco_mapping提供的位姿进行迭代求解。下面按先后顺序,介绍程序与节点。

3. 代码详细解释

3.1 数据流

首先从数据流的角度看。

图像数据流:相机驱动->aruco_mapping->”lidar_camera_calibration/conf/transform.txt"文件->find_transform节点。其中相机驱动传输完整图像给aruco_mappingaruco_mapping从其配置文件 “aruco_mapping/data/xxx.ini"文件中获取相机的内参(包括分辨率、K、畸变参数、以及投影参数等),还从"aruco_mapping/launch/aruco_mapping.launch"文件中获取marker的参数。最后aruco_mapping保存位姿数据在"lidar_camera_calibration/conf/transform.txt"文件,再由find_transform节点读取,再根据从"lidar_camera_calibration/config_file.txt"中获取的marker和纸板(以下记作"board”)的参数
用于纸板的四角在相机下参数计算。

激光雷达数据流:激光雷达驱动->find_transform,相对简单。激光雷达将原始点云数据直接发给find_transform节点,节点根据扫描情况以及"lidar_camera_calibration/conf/config_file.txt"中的配置,将点云中的边界点投影到camera的图像下,用于可视化与人工标注。

另外还有一些中间数据,例如find_transform节点将激光雷达检测出的board角点、与计算出的角点在camera系下的3D坐标,临时存储在"lidar_camera_calibration/conf/points.txt"中,用于后续的读取。

3.2 aruco_mapping节点

上述数据流已说明,aruco_mapping节点负责将原始图像获取后输出图像中marker在相机系下的坐标。它所接受的图像topic名称、marker的尺寸在aruco_mapping.launch中给出,同时还需要相机的参数,相机的参数需要提前标定并按照指定格式修改"/data/xxx.ini"配置文件。
输出6DoF位姿到"lidar_camera_calibration/conf/transform.txt"文件。这部分代码还是比较完善的,详细可参考aruco_mapping官方文档。

要点:需修改接收topic名称、marker参数文件、相机参数.ini文件

3.3 激光雷达节点

激光雷达节点较为简单,即激光雷达的ros下驱动,启动后发布激光雷达数据即可。后由find_transform接收。

3.4 find_transform节点(核心)

这个节点是算法的核心,按照先后顺序以此展开。

3.4.1 消息接收与回调函数

从main函数入手,首届先接收了上述节点发送的topic。其中有一处选择,是采用"callback"还是"callback_noCam",二者的区别是:相机的内参是从topic接收,还是从文件读取。本质上是一样的,我们采用从文件读取,这样就不需要aruco_mapping节点或相机驱动节点再发送topic数据。读取的文件还是上面说的"conf/config_file.txt"。

这里在回调时用了ROS下message_filters的消息同步机制,保证camera的数据和lidar的数据是同步的。但由于场景可以认为静止,这部同步起始没有太大意义。

3.4.2 点云数据接收

进入回调函数,第一件事就是接收lidar驱动节点发出的数据。这里我遇到了第一个坑:【fromRosMsg()报错 Failed to find match for field “intensity”】。出现这个原因是因为我采用的镭神激光雷达的intensity字段定义的是奇葩的uint类型,而RosMsg下的intensity字段是float,因此无法直接转化。为此需要重新定义自定义点数据类型与拷贝函数。详见上述的学习总结。(其实intensity并没有用)

点云数据接收后,需要转成统一的点云格式,作者采用的是自定义的PointXYZRID,其中有用的字段是"I"表示intensity和"R"表示ring,即第几条线扫描到的数据。一般雷达都提供了扫描的线束信息,在雷达转化时加上即可。如果并没有提供线束信息,可以通过类似A-LOAM源码中的计算俯仰角确定线束。

紧接着遇到了第二个坑:无效数据点即NaN的问题。由于后续需要用到intensity进行边界点检测,而如果激光雷达发出了NaN数据,在后续归一化时会出现错误,所以需要提出NaN数据。但不幸的是,PCL自带的去除NaN数据函数removeNaNFromPointCloud不支持自定义的"PointXYZRID"数据类型,因此不能直接调用。只能自己手写一个判断每个点是不是NaN。

3.4.3 点云变换

拿到点云后,首先将Lidar系下的点云,通过transform函数大致变换到相机坐标系。这一步是为了后续点云能够在相机中有个大致的位置,方便相机画图显示。此时,大致的变换参数是我们估计的,采用"conf/config_file.txt"下给出的初始旋转和平移进行。这里的平移和旋转,是“相机系经过该平移旋转到雷达系”,即“雷达系下的坐标点通过旋转平移后成为相机系下的坐标”。而参数文件中第12行的三个角度,是ZYX欧拉角形式,eularAngle(2,1,0),单位是弧度。可以通过计算得到的旋转矩阵,判断给的旋转角度对不对。

这里补充一句,一定要注意激光雷达的坐标系定义,和相机的坐标系定义。这里相机的坐标系定义为:z轴沿镜头向外,y轴向下,x轴由右手系定义。

3.4.4 点云边界点检测

边界点检测是intensityByRangeDiff()函数提供,得到场景中所有的边界点。这个函数通过判断同一个ring上一定范围内符合intensity的变化强度,确定边界点。然而这里的intensity具体就是采用的range,即距离。具体如下:

从每一圈第2个点开始,判断与前一个以及后一个点的range的差值,当作强度。之后调用normalizeIntensity函数进行归一化(注:这里要求所有的距离是有效的,如果测到极端距离,例如很远点或者NaN点,则归一化效果不佳)。归一化后,保留强度大于一定阈值且在xyz给定的范围内的点,阈值和xyz的范围从"conf/config_file.txt"第2-5行给出。这里吐槽一句,在前面我intensity转化了半天,后来根本没用到;另外明明是根据range进行检测,这里非叫做intensity,造成里不小的理解歧义,可能是为了方便使用PointXYZI格式吧。

提取完边界后,返回场景中所有的边界点。下一步就需要进行检测角点。

3.4.5 点云中纸板角点检测

角点检测是getCorners()函数。吐槽一下这个函数内部,opencv的代码写的贼烂,各种不明含义的mask和来回赋值。

第一步将整个场景中的边界点投影到相机图像中,函数为project,即“相机图像能够看到哪些激光雷达的边界点”。这里用到了相机的投影矩阵,来自从"conf/config_file.txt"第8-10行读取的config参数。

第二步手动标注纸板的边界点。由于场景中有许多边界点,需要知道哪些是纸板哪条边的边界点。因此采用手动标注的方式。运行到这一步后,会出现cloud窗口和polygon窗口,前者显示在图像视角下看到的边界点。我们在cloud窗口中按顺序点4个点,形成一个多边形(polygon),框住纸板的一条边界,每次单击后按任意键存储坐标。点完4个点后,polygon窗口会画出所绘制的多边形,以及这个多边形框住的边界点。这样完成了第一条边界的框选,然后按顺序框选其他3条边界。
图:左侧为边界点检测,右侧为框选出的第一条边界

注意1:如果没有激光雷达扫描的边界点没有按照预期投影到相机中,请检查初始给的相机和雷达估计外参(主要是旋转)是否合理。
注意2:这里框选一条边时按照顺时针或逆时针顺序形成多边形,不能较差,否则采用opencv判断某个点是否落入多边形时会出现问题;
注意3:框选4条边时要按照顺时针顺序,这个顺序应该和aruco_mapping检测的marker的旋转一致。如果我们按照官方代码给出的图片中marker的摆放方式,即x轴朝向“左上”,z轴“右上”,如下图,那么在标注雷达点的边界时首先标左上这条边,再标右上这条边。因为要求4个角点要按顺序依次对应,而代码后续寻找雷达的角点时存储的顺序依次是前后两次标注的边界的交点,后续find_transformation()函数中相机系下角点的顺序是右上-右下-左下-左上的顺序。

第三步,注意这里有一个迭代。这个迭代是最外侧callback_noCam()中迭代的,即 3.4.1~3.4.6迭代,而非在这个函数中迭代,迭代次数是MAX_ITERATION次,默认为100。迭代时,我们第一次标注了4条边界的4个polygon,后续就自动提取这4个多边形中的边界点,和marker的边界点,进行计算。

第四步,计算纸板角点。由于上一步提取出了每个边界,那么计算每个边界3维直线参数,采用的是PCL的SampleConsensusModelLine方法进行直线拟合,之后采用PCL的lineToLineSegment计算相邻两条直线的中垂线,并取中点作为角点。

至此,完成了一次迭代中纸板角点的提取。第五步,将提取的3D角点存储到临时文件"/conf/points.txt"中,有n个marker就存储n*4行角点。

3.4.6 相机中纸板角点的检测与外参计算

相机系下纸板角点检测和外参计算都在函数find_transformation()中。这个函数需要接收aruco_mapping节点发出的marker的消息lidar_camera_calibration_rt,并从上述的"/conf/points.txt"文件中读取lidar下角点,进行配准。

第一步,计算角点在相机中的三维坐标。直接读取aruco的参数,之后根据参数以及board和marker的尺寸(由“conf/marker_coordinates.txt"下几行数据定义)计算出角点,就是从这里我们能够判断出几个角点在相机下的坐标。计算方式很简单,由于marker定义了xz平面,所以同一平面的纸板的四个角的y坐标都是0,xz坐标可以通过markder的margin和纸板的尺寸确定。确定后,所有点数据又以追写的方式存在了临时文件"/conf/points.txt"当中。吐槽一下,真的是瞎传参。

第二步,由readArray()函数读取"/conf/points.txt"中所有角点在lidar和camera下的三维坐标。

第三步,由calc_RT()函数迭代计算外参。这个函数的计算,就是典型的ICP算法,首先计算平移,然后SVD分解计算R。这里的Rt是camera系变化到lidar系,即lidar系下坐标转移到camera系的Rt。

正如3.4.5中所说,共进行了100次迭代,每次迭代时都经过了3.4.6这几个函数,计算出一次camera系到lidar系的Rt,然后100次后结束迭代,求解平均的Rt。这里需要注意,程序输出了好几个Rt和T,计算的平均R起始只是在初始给定的旋转下的增量,并不是实际的R,而完整的T中所包含的左上角3x3矩阵才是真正的外参R,右上角3x1的是外参t。注意,这里的迭代100次,用的是全局变量,即在3.4.2中首次标注多边形时iteration_counter就是这个全局变量。这里又在用全局变量传参。


最终结果:红框内为真实的Rt


激光雷达在相机的y轴方向5cm处,基本认为标定正确,只是还不准。后续再提升。

【学习总结】激光雷达与相机外参标定:原理与代码1相关推荐

  1. mlcc激光雷达与相机外参标定初体验

    论文阅读模块将分享点云处理,SLAM,三维视觉,高精地图相关的文章.公众号致力于理解三维视觉领域相关内容的干货分享,欢迎各位加入我,我们一起每天一篇文章阅读,开启分享之旅,有兴趣的可联系微信diany ...

  2. Lidar-camera calibration单线激光雷达与相机外参标定

    课题中涉及多传感器融合定位的部分,需要对camera.IMU.2d-Lidar进行外参标定.camera-IMU标定可以使用Kalibr,但是单线激光雷达和相机的的标定目前能找到的只有贺老师开源的这个 ...

  3. 激光雷达与相机外参标定(附open3d python代码)

    现在的激光雷达与相机的标定程序基本都是Ubuntu框架下面的,并且都是C++代码,需要安装的依赖也比较复杂,于是自己写了一个python版本的标定程序,依赖非常简单,Windows系统也可以运行.并且 ...

  4. 基于先验时间一致性车道线的IPM相机外参标定

    文章:Online Extrinsic Camera Calibration for Temporally Consistent IPM Using Lane Boundary Observation ...

  5. 【自动驾驶】31.【相机外参标定】、【相机障碍物后处理】【地面的2D点反投影到3D】的过程对比

    相机的平移向量一般标定到imu坐标系或者车身坐标系,欧拉角 yaw.pitch.roll\color{red}yaw.pitch.rollyaw.pitch.roll是相对于前向相机坐标系的位姿: 前 ...

  6. 激光雷达相机外参标定

    首要参考Matlab官方提供的方法: https://ww2.mathworks.cn/help/lidar/ug/lidar-and-camera-calibration.html 在Matlab2 ...

  7. 单目相机外参标定及标定结果验证

    运行前需要获得point3s.point2s对应的数值,明确坐标系(这里,前X右Y) import cv2 import numpy as np import math ############### ...

  8. 视觉SLAM——英特尔D435i双目相机外参标定

    标定Camera number:0 step1:使用左右相机同时采集标定板图像各4张,命名为left0104和right0104 附左右相机同时采集标定板图像的代码: #include <ios ...

  9. cam_lidar_calibration标定速腾激光雷达和单目相机外参

    目录 一.资源链接 二.代码测试 2.1安装依赖 2.2代码下载和修改 2.2.1 optimiser.h文件 2.2.2 feature_extractor.h文件 2.3编译代码 2.4测试数据集 ...

最新文章

  1. Jmeter Md5加密操作之-------BeanShell PreProcessor
  2. android wms,Android解析WindowManagerService(一)WMS的诞生
  3. Bootstrap 表格的可选样式
  4. 力扣203,移除链表元素(JavaScript)
  5. CountDownLatch1.8源码
  6. oracle 制定定时任务
  7. 几种经典的hash算法
  8. 12通过作用域链实现闭包
  9. python 计算相关系数和决定系数
  10. GIS常用数据平台网站
  11. uncode,utf8编码的文章
  12. python opencv将图片转为灰度图
  13. 有效年利率和年化百分比利率
  14. Linux上安装NetCat
  15. 电源电力专业术语整理
  16. 【华为机试真题 Python】窗口滑动和最大值
  17. Mac中如何通过terminal命令行进入“iCloud Driver”目录
  18. python 导入第三方包_python 导入第三方包---
  19. 【松岩L2点金圈】历劫方知股海深,掀天浪起百舟倾。
  20. 贪心算法——活动安排、会场安排

热门文章

  1. GAL汉化界的一点实话。。。。。
  2. UEFI无法进入WIN10的系统?BIOS找不到ssd硬盘?reboot and select proper boot device的另一种解决思路
  3. 死亡结束生命,却不会结束一段关系
  4. python 蓝桥杯--数的读法
  5. 新的开始——1月英语
  6. 基于Android的记账本的设计与实现
  7. AcWing周赛 72 场 LeetCode单周赛 314 场 总结
  8. Games101 计算机图形学课程笔记: Lecture 08 Shading 2 (Shading, Pipeline and Texture Mapping)
  9. 内存不能为“read”或written的解决
  10. 求解器:助力智能决策的利器