ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Kalibr多标定板失效怎么办?预处理技巧实现高效相机内参标定

Kalibr多标定板失效怎么办?预处理技巧实现高效相机内参标定 1. 为什么一张图里出现多块标定板Kalibr 就不干了1.1 Kalibr 内参标定到底在做什么先捋一下 Kalibr 内参标定的基本逻辑。kalibr_calibrate_cameras接收一段 rosbagbag 里是一段连续拍摄标定板的图像序列。它先对每一帧图像提取标定板角点然后通过多帧图像中同一个三维标定板在不同姿态下的投影关系联合估计相机内参焦距、主点、畸变系数和每一帧的相机外参。整个优化过程建立在“同一帧图像里只有一个确定标定板”这个假设上。这个假设平时不会暴露问题因为大家标定时都是把一块棋盘格或者 Aprilgrid 放在视野里转一转、移一移录个几十秒的 bag。Kalibr 检测角点时先在整个图像范围内搜索四边形网格再根据标定板的行列数做亚像素优化。它内部用的检测链非常依赖“整幅图中只能有一个完整标定板”的先验。一旦画面里出现多块板子检测器往往会陷入两种状态要么把不同板子的角点混在一起组合出一个完全错误的“棋盘”要么根本无法收敛直接报提取失败。我一开始接到这个需求时也觉得奇怪正常人标定内参一块板子就够了谁会搞出一张图里 9 块标定板后来细问才知道对方想一次性标定多个相机的公共视野或者在一个大场景里布置多块板子以提高角点覆盖范围、加快数据采集速度。还有一种是做仿真数据渲染器直接在一张高分辨率图里摆放了 9 个棋盘格。不管哪种来源问题都一样Kalibr 默认不支持多目标共存。1.2 多标定板为何会干扰检测与优化很多人以为 Kalibr 检测“失败”只是找不到角点其实更深层的问题是优化阶段的退化。内参标定本质上是求解一个非线性最小二乘问题误差项是重投影误差。如果输入图像里同时出现 9 块板子Kalibr 在提取阶段就晕了但即便你人工把 9 块板的角点都提取出来塞给它它在优化时也会因为不知道“当前帧到底对应哪块板”而把外参求错。更隐蔽的问题是内参可观测性。单目内参标定需要标定板在图像中覆盖足够多的位置和姿态。一张图里有 9 块板子听起来覆盖范围很大但如果这 9 块板子固定在同一块刚体平面上那么同一帧内的 9 块板只有同一个相机位姿它们之间是强约束关系不是 9 组独立观测。Kalibr 优化的过程中如果无法区分这 9 块板各自的位姿就会把内参和外参一起带偏。结果就是要么检测阶段直接失败要么勉强跑完输出一个看起来合理但实际误差很大的内参。还有一点很多人忽略Kalibr 对输入图像的尺寸非常敏感。内参标定结果中的主点坐标和焦距都是以像素为单位描述的图像分辨率一旦改变主点位置就必须跟着变。如果你试图通过“裁剪 ROI”的方式把一张多板大图切成 9 张单板图裁剪后的图像尺寸变了主点不再位于裁剪后图像的中心附近这个信息 Kalibr 并不知道标出来的内参就直接废了。这就是为什么不能简单裁剪必须保持原始分辨率。1.3 认清约束图像分辨率与主点位置不能随便动这条约束是整个方案的核心前提。保持原始分辨率意味着我们不能靠裁图来“去掉”多余的标定板。那怎么办只能做图像编辑把不需要的板子从画面里“抹掉”抹掉之后图像尺寸不变主点位置自然不变Kalibr 拿到的还是正常的内参标定输入。“抹掉”说起来简单实际操作里有讲究。直接用纯色填充遮挡区域虽然也能骗过检测器但纯色块的边缘会产生高梯度反而可能被 Kalibr 的角点检测器误判为棋盘格的边界尤其在遮挡区域紧贴目标板的时候。更好的办法是从原图里找一块平滑背景区域复制过来盖住干扰板这就是“原图复制”的由来。复制源必须是平滑纹理不能有强边缘、不能有文字、更不能有另一个棋盘格的残影。想清楚这些约束之后整个方案就顺了不改 Kalibr 一行代码只改输入数据。把一张多板图复制成多份每份只保留一块板子其余 8 块用原图背景遮挡掉输出成新的图像序列或 bag然后正常跑 Kalibr。下面我把每一步拆开讲。2. 整体方案设计只改数据不动源码2.1 “原图复制 遮挡”背后的思路这个方案的本质是把“一张图有 9 块板子”这个不满足 Kalibr 假设的问题转化成“9 张图各有 1 块板子”这个完全常规的问题。具体操作是假设你有一段视频或一组图像每帧都包含 9 块标定板。对每一帧你复制出 9 份新图像第 i 份只保留第 i 块标定板其他 8 块全部遮挡掉。这样原来的一段多板视频就变成了 9 段单板视频。为什么强调“复制”因为只有复制才能保证每一份新图像都保留完整的原始分辨率和原始相机姿态信息。同一帧的不同板子虽然在三维空间里位置不同但它们共享同一个相机位姿。如果这 9 块板子之间有足够的空间分布那么这一帧就能提供 9 个覆盖不同图像区域的单板样本比单独拍 9 次效率高得多。当然这里有一个关键前提多板图必须来自不同的相机角度。如果你只是把相机固定原地拍一张包含 9 块板子的照片然后复制出 9 份这 9 份样本本质上只对应一个相机位姿内参标定的可观测性不足。正确做法是让相机或标定板动起来拍多帧不同姿态的多板图然后对每一帧做“复制 遮挡”得到一个样本量充足的单板序列。2.2 为什么不是直接把 ROI 裁出来这是新手最容易踩的坑。看到一张图有 9 块板子第一反应肯定是把每块板子周围的区域裁出来不就有 9 张单板图了吗逻辑上确实如此但问题在于裁剪会改变图像尺寸。内参标定里焦距和主点都是以像素为单位的图像尺寸一变主点位置必然改变。Kalibr 的内参模型默认主点接近图像中心你裁出来的小图中心并不是原始大图的中心标定结果必然出现系统性偏差。有人可能说那我裁剪后把主点设为裁剪区域的中心不就行了吗理论上可以但实际操作很麻烦因为你得对每一块板子都记录裁剪偏移量还要重新折算主点坐标稍有疏忽就会引入额外误差。更重要的是Kalibr 的畸变模型是和整幅图像的分辨率强绑定的你裁剪之后原始镜头的畸变分布被截断了边缘畸变信息直接丢失这对广角镜头来说是致命的。所以最稳妥的做法就是不裁剪保持图像尺寸不变用遮挡的方式把干扰板“藏起来”。这样内参的像素坐标系、主点位置、畸变分布全都和原始相机一致Kalibr 标出来的结果就是整个相机的真实内参。2.3 遮挡填充的三种做法与选型遮挡区域填什么直接决定 Kalibr 检测是否顺滑。我实践下来有三种做法各有适用场景方法具体操作优点缺点适用场景纯色填充把遮挡区域填充为黑色或白色实现简单速度最快填充边缘高梯度可能干扰角点检测干扰板距离目标板较远边缘不会接触原图复制从原图中选一块平滑背景区域复制到遮挡区域自然无痕几乎不引入虚假梯度需要手动或自动选择背景源区域遮挡区域附近有足够的平滑背景跨帧背景替换用其他帧同一区域没有标定板时的像素来填充最自然完全消除遮挡痕迹需要相机运动或额外采集背景帧相机运动幅度较小或可拍背景帧我优先推荐“原图复制”。原因很简单它既不像纯色填充那样会制造强边缘又不像跨帧替换那样需要额外的采集条件。只要原图里有平滑区域——比如一面白墙、一块纯色桌面、一块均匀的幕布——就能直接复制过来用。复制的时候稍微做一点羽化让填充边界过渡柔和检测器基本感知不到遮挡痕迹标定过程就非常顺。3. 实操全流程从多板原图到可标定的单板序列3.1 准备标定板与 Kalibr 的 target 配置开始之前先确认你的标定板格式。Kalibr 支持 checkerboard 和 aprilgrid配置文件是一个 yaml。棋盘格配置长这样target_type: checkerboard targetCols: 11 targetRows: 8 rowSpacingMeters: 0.03 colSpacingMeters: 0.03Aprilgrid 配置长这样target_type: aprilgrid tagCols: 6 tagRows: 6 tagSize: 0.088 tagSpacing: 0.3这里有一个非常重要的细节棋盘格配置里的targetCols和targetRows定义的是角点数不是格子数。比如一个 11x8 的棋盘格内部角点是 10x7 还是 11x8取决于你用的 OpenCV 检测接口。Kalibr 里写的是内部角点数搞错一个数字检测永远失败。建议先在单张图上用cv2.findChessboardCorners验证一下确保角点数目和 yaml 一致。如果你用的是 Aprilgrid强烈建议用 6x6 或更大尺寸因为 Aprilgrid 的每个 tag 都有独立 IDKalibr 可以通过 ID 唯一识别每一块板子。这对后续自动定位板子区域非常有帮助。我后面会细讲。3.2 定位 9 块标定板区域这一步是预处理的核心。要把第 i 块板保留、其余遮挡第一步必须先知道每块板在图像中的准确位置。提供两种方法方法一手工框选。适用于图像尺寸固定、标定板位置变化不大、或者你只需要处理少量帧的场景。用 Python 脚本读入第一帧用鼠标依次点击每块板的四个角点把 9 组四边形坐标保存成 JSON。后续帧如果相机基本静止可以直接复用第一帧的坐标。如果相机在移动这个方法就不太合适了。方法二借助 Aprilgrid 的 ID 自动定位。如果你用的是 Aprilgrid每块板由多个独立的 tag 组成每个 tag 有唯一 ID。Kalibr 底层使用 apriltag 检测库OpenCV 里也集成了cv2.aruco可以直接检测出所有 tag 的角点和 ID。把 ID 落在同一块板范围内的 tag 聚起来就能得到该板的准确四边形区域。下面是一段基于 OpenCV Aruco 的定位示例import cv2 import numpy as np aruco_dict cv2.aruco.getPredefinedDictionary(cv2.aruco.DICT_APRILTAG_36h11) detector cv2.aruco.ArucoDetector(aruco_dict) img cv2.imread(multi_board_frame.png) corners, ids, _ detector.detectMarkers(cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)) # 假设每块板由 4 个 tag 组成按 ID 范围分组成 9 块板 board_corners {} for corner, tag_id in zip(corners, ids.flatten()): board_idx tag_id // 4 # 按你的板子 ID 分布调整 board_corners.setdefault(board_idx, []).append(corner.reshape(4, 2))需要注意的是OpenCV 的 Aruco 检测器需要你确认 tag 字典和实际打印的 tag 一致。DICT_APRILTAG_36h11 是 Kalibr 在 Aprilgrid 中常用的字典但如果你用的是自定义生成的 Aprilgridtag ID 分布可能不同。最好的办法是先打印一小块跑一下检测确认 ID 和位置对不对。还有一种更省事的思路直接用 Kalibr 自带的检测器去提取角点。Kalibr 源码里的detect_apriltags.py可以输出每个 tag 的 ID 和角点你只要调用它不需要改源码把结果拿来用就行。这不违反“不改 Kalibr”的前提因为我们只是调用不改代码。3.3 生成掩码与填充遮挡区域定位到 9 块板的四边形区域后生成掩码就很直接了。对第 i 份图像生成一个和原图等大的掩码目标板区域设为 0其他 8 块板区域设为 255。为了保险遮挡区域最好比板子的四边形稍大一点比如向外扩展 15 到 20 个像素避免板子边缘的高梯度残留在画面里。import cv2 import numpy as np def dilate_polygon(points, img_shape, dilate_px15): mask np.zeros(img_shape, dtypenp.uint8) cv2.fillPoly(mask, [np.array(points, dtypenp.int32)], 255) kernel np.ones((dilate_px, dilate_px), np.uint8) mask cv2.dilate(mask, kernel, iterations1) return mask # 假设 board_polys 是 9 块板的四边形坐标 for i, poly in enumerate(board_polys): # 第 i 块板保留其他 8 块生成遮挡掩码 mask np.zeros(img.shape[:2], dtypenp.uint8) for j, other_poly in enumerate(board_polys): if j i: continue cv2.fillPoly(mask, [np.array(other_poly, dtypenp.int32)], 255) # 外扩遮挡区域 kernel np.ones((15, 15), np.uint8) mask cv2.dilate(mask, kernel, iterations1)掩码生成之后填充逻辑如下先选一块背景源区域可以用一块远离所有板子的平滑区域像素来填也可以用图像边缘附近的均匀区域。复制过来后最好做一下高斯模糊来消除复制接缝或者在边界处做 alpha 混合。我自己用的做法是在遮挡区域边缘做 10 像素的渐变融合def fill_with_patch(img, mask, patch): result img.copy() # 将 patch 贴到 mask 区域 result[mask 0] patch[mask 0] # 边缘羽化减少接缝 edges cv2.GaussianBlur(mask, (31, 31), 0) edges (edges 0).astype(np.uint8) * 255 blend_zone cv2.dilate(edges, np.ones((11, 11), np.uint8)) ~edges blend_mask cv2.GaussianBlur(blend_zone, (11, 11), 0) / 255.0 blend_mask cv2.merge([blend_mask] * 3) result (result * (1 - blend_mask) img * blend_mask).astype(np.uint8) return result这段代码的核心思想是遮挡区内部完全用 patch 填充遮挡区外保持原图边界处做一个渐变过渡。这样做出来的图Kalibr 基本看不出这里被动过手脚。3.4 批量处理并送入 Kalibr如果只有一张图处理完就是 9 张单板图。如果是一段视频或一组多板图那就写个循环批量处理。注意保持输出分辨率不变文件名最好带上帧号和板子编号比如frame_0010_board_3.png。Kalibr 本身输入是 rosbag你需要把处理好的图像序列打包成 bag。常见的做法是写一个 ROS 节点订阅原始图像话题对每一帧做“复制 遮挡”处理然后发布到多个新话题再用rosbag record录制rosbag record -O calibrated_sequence.bag /cam0/board0 /cam0/board1 ... /cam0/board8也可以先把图像序列封装成 bag。早期 Kalibr 提供了kalibr_bagcreater工具但新版本不一定带更通用的做法是用 Python 写一个简单节点发布图像import rospy import rosbag from sensor_msgs.msg import Image from cv_bridge import CvBridge import cv2 import os bag rosbag.Bag(multi_board_sequence.bag, w) bridge CvBridge() rate rospy.Rate(10) for frame_file in sorted(os.listdir(processed_frames)): if not frame_file.endswith(.png): continue img cv2.imread(os.path.join(processed_frames, frame_file)) msg bridge.cv2_to_imgmsg(img, encodingbgr8) msg.header.stamp rospy.Time.now() # 这里按文件名的 board 编号发布到不同话题 board_id frame_file.split(_)[-1].split(.)[0] bag.write(f/cam0/board{board_id}, msg) rate.sleep() bag.close()bag 准备好之后就可以运行 Kalibr 内参标定了。以单目、pinhole 模型 radtan 畸变为例kalibr_calibrate_cameras \ --bag multi_board_sequence.bag \ --topics /cam0/board0 \ --models pinhole-radtan \ --target april_6x6.yaml \ --show-extraction \ --verbose如果你想同时标定 9 块板子生成的 9 个话题可以在一个命令里传入多个 topics 和 models。但实际经验是分 9 次单独标定每次只用一个 board 话题然后把 9 次结果对比一下更容易发现问题。因为不同位置的板子对畸变参数的敏感度不同对比结果能帮你判断哪些区域的角点对优化贡献大哪些板子的位姿覆盖不好。3.5 运行 kalibr_calibrate_cameras 并解读结果Kalibr 跑完之后会输出一个camchain-xxx.yaml里面是标定出的内参以及重投影误差的统计信息。我通常先看重投影误差的 RMS一般来说小于 0.5 像素就算合格小于 0.2 像素算非常好。如果 RMS 超过 1 像素说明输入数据有问题可能是角点提取不准、图像有运动模糊、或者标定板 yaml 的尺寸参数写错了。还要检查焦距和主点是否合理。以常见的畸变模型 pinhole-radtan 为例标定结果里cam0: camera_model: pinhole distortion_coeffs: [0.01, -0.02, 0.001, 0.002] intrinsics: [640.5, 641.2, 640.0, 360.0] resolution: [1280, 720]如果图像分辨率是 1280x720主点应该在 (640, 360) 附近。如果标出来主点跑到 (500, 400)说明图像被裁剪过或者输入数据有误。这一步能有效验证你的预处理是否成功。4. 常见问题与排查技巧实录4.1 Kalibr 检测不到角点或角点不全这是我遇到最多的问题尤其是在遮挡区域离目标板很近的时候。具体表现是Kalibr 的--show-extraction可视化窗口里目标板周围的角点时而完整、时而缺一条边。排查思路第一检查遮挡掩码是否外扩得足够大。板子边缘即使露出 2 到 3 个像素的残影都可能干扰角点检测。我把外扩像素从 10 调到 20效果立竿见影。第二检查填充纹理是否引入了虚假边缘。如果原图复制选中的背景区域有纹理或者光照渐变Kalibr 可能把这个区域误认为棋盘格的延伸。对策是选择更平滑的背景源或者对填充区域多做几次高斯模糊。第三检查标定板 yaml 的角点数量。棋盘格的内角点数、Aprilgrid 的 tag 行列数任何一个数字错了检测都会失败。建议先用单板原图验证一下。4.2 处理后仍检测到多个板子如果预处理后 Kalibr 还是检测到多块板子绝大多数情况是掩码没生效。我遇到过两种低级错误一是掩码存成了三通道而不是单通道导致img[mask 0]的索引维度错乱二是填充时用了img[mask 0] patch[mask 0]把目标板内部也覆盖了一部分结果目标板角点缺失。另一种可能是遮挡区域外扩不够Kalibr 检测到了板子的部分角点虽然不完整但检测算法在贪心搜索时把两块板的角点混在一起拼出了一个伪棋盘。这时候把遮挡外扩加大同时检查一下填充区域是否已经完全覆盖了干扰板的最外侧角点。4.3 内参标定结果异常怎么排查标定结果异常一般分三种第一种是重投影误差整体偏高。这种情况通常不是预处理的问题而是数据本身的质量问题比如图像运动模糊、光照反光、标定板不平整。排查方法是先用肉眼逐帧回放挑掉质量差的帧。第二种是焦距和主点和预期偏差很大。如果预处理保持了原始分辨率一般不太会出现这种情况。但要注意如果你在录制 bag 时用了图像压缩或缩放Kalibr 拿到的图像尺寸就不是原始尺寸了内参自然不对。检查 bag 里图像消息的分辨率务必和原始图像一致。第三种是不同 board 话题标出的内参互相矛盾。9 块板子分布在画面不同位置理论上每块板子单独标定出的内参应该接近。如果某块板子标出来的焦距和其他板子差很多说明这块板子的图像区域畸变覆盖不足或者检测时反复出错。这时候可以优先相信覆盖画面中心附近的板子标定结果再用多块板子的联合数据整体标定一次。4.4 更适合多板场景的其他思路最后说说“不改 Kalibr”之外的几个替代方案供大家参考。第一个是继续用多板图但引入额外的标定流程比如先用 OpenCV 的calibrateCamera跑一遍得到粗内参再用多板序列做精化。OpenCV 的findChessboardCorners虽然也是单棋盘检测但对多棋盘的处理比 Kalibr 稍微宽容一些你可以通过掩码把检测结果限制在指定区域等于用 OpenCV 完成了“遮挡”这一步。第二个是换 MultiCam 标定工具或一些更新的标定库它们天然支持多目标板。但这样你就需要改代码、迁移流程和我标题里的前提“不改 Kalibr”就不太一样了。第三个是使用 ChArUco 板。ChArUco 是 Aruco 和棋盘格的结合体每个角点都有编码信息OpenCV 的cv2.aruco支持直接检测 ChArUco 板角点。如果你愿意换标定板类型ChArUco 在多板场景下的鲁棒性明显优于普通棋盘格这在工业标定里非常常见。我个人在实际项目里的习惯是优先保证数据质量再考虑工具链。多板标定的问题看似是 Kalibr 不支持本质上是我们给 Kalibr 的输入数据不符合它的预期。预处理方案把这个预期补上了问题就自然解决了。最后再分享一个小技巧如果你打算长期反复标定类似的多板场景不妨写一个批处理脚本把“定位板子—生成掩码—填充遮挡—打包 bag”这几步固化下来第一次调试可能需要半天后面就只需要换数据路径十分钟出结果。
返回列表