尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Kalibr多板同框内参标定崩溃?原图复制与遮挡方案详解

Kalibr多板同框内参标定崩溃?原图复制与遮挡方案详解 先交代一个前提我这里说的“一张图里有9块标定板”不是那种故意摆拍的艺术图而是多相机系统标定现场的真实数据。比如你在地上贴了一整排Aprilgrid或者在车间里布了一圈标定图案某个相机的原始图像里同时出现了好几块完整的板子。这种数据直接丢给Kalibr跑内参标定十有八九会出妖——检测线程会把所有板子的角点全部提取出来当作同一块板处理最后优化出来的焦距和畸变系数完全是错的。我经历过不止一次这种坑。最开始的第一反应是去改Kalibr源码让它支持多板检测。研究了两天发现改源码牵扯到Kalibr自带的角点聚类逻辑、ID编码识别机制、以及后续优化前的数据筛选工作量不小而且每次换了标定现场板子的布局一变改过的代码又得跟着调。更麻烦的是Kalibr是rosbuild和catkin混着的老工程改完重新编译依赖环境稍有变动就各种报错。后来我索性换了个思路不改Kalibr而是把图像数据在喂进去之前就处理成Kalibr期望的样子。这就是这篇文章要讲的原图复制方案和遮挡方案。这套处理方法适合所有用Kalibr做单目或双目内参标定、且现场无法避免多板同框的工程师。下面我把思路、代码、坑位一次讲清楚。1. 为什么多块板子会让Kalibr直接“崩溃”要理解后面的处理方案先得搞清楚Kalibr在多板场景下到底哪里翻车。这不是Kalibr本身质量不行而是它的设计前提就是“单帧图像中只有一块完整的标定板”。1.1 Kalibr检测机制的内建限制Kalibr的标定板检测核心依赖两个东西角点聚类和Tag ID编码。Aprilgrid每一行每一列的角点都不是孤立的它们通过黑色方块的边沿关系构成一个网格结构。Kalibr在检测时会先提取图像中所有的四边形候选区域然后通过Tag ID来判断每个四边形在板子上的具体坐标。这个机制在单板场景下非常稳健图像中只有一个四边形簇所有角点天然属于同一个网格Tag ID连续且唯一。但一旦画面中出现多块板子问题就来了——不同板子上的角点会被聚类到同一个候选集合里Kalibr根本不知道怎么把这些角点拆成多个独立的网格。更棘手的是如果地面上的多块板子是直接用同一个Aprilgrid.pdf打印出来的它们的Tag ID编码是重复的Kalibr会分不清检测到的某个ID到底是属于哪块板。结果就是两种典型失败模式检测报错提示找不到完整板子或者检测“成功”了但那是因为Kalibr把多块板的角点硬拼成了一个畸形的网格后续优化时重投影误差大得离谱内参输出完全不可用。1.2 内参优化对“单一刚体”的依赖这里多说一句原理层面的东西。Kalibr的相机内参标定本质上是求解一个重投影误差最小化问题把标定板上每个特征点的三维坐标通过相机外参投影到图像平面然后和检测到的像素坐标做差。整个优化过程依赖一个关键假设——所有特征点在同一个刚体坐标系下。单板场景下这个假设自然成立。但多块板子同时出现每块板都有自己的局部坐标系如果Kalibr错误地将不同板子的角点当作同一个刚体上的点优化器就会试图用一个外参去解释多个不同的坐标系变换。这种情况下内参估计必然被拉偏。焦距可能偏大或偏小畸变系数会出现不合理的“鱼眼”形状甚至优化过程直接不收敛。明白了这个原理后面的处理思路就清晰了不管用什么方法核心目标只有一个——让Kalibr在一帧图像里只看到一块完整的板子。2. 原图复制方案详解让多余板子“消失”在画面里所谓“原图复制”不是简单地把图片裁剪成只剩一块板而是基于原始图像把非目标板区域处理成不干扰Kalibr检测的背景。核心思路是保留目标板的原始纹理和光照信息其余区域用图像填充技术“涂掉”。2.1 为什么不用纯裁剪很多人第一反应是直接用OpenCV把目标板区域裁出来然后单独用这个小图去标定不就行了这个思路方向对但有个坑——裁剪会导致图像分辨率降低而Kalibr的内参标定对图像分辨率是很敏感的。你把一张1920x1080的图裁成600x600标定出来的焦距参数物理含义就变了除非你后续做严格的缩放换算否则很容易出错。另一个问题是裁剪后的图像中目标板如果占据了太大的画幅Kalibr检测角点时可能会丢失板子边缘的角点。Aprilgrid的检测要求整块板完整出现在画面里边缘角点一旦出了图像边界Tag ID连续性就断了检测同样会失败。2.2 图像填充的技术选型与实现路径既然不能裁剪那就只能在原图尺寸下做填充。所谓的“原图复制”其实包含两个层次的操作复制目标板区域的图像纹理以及用复制的纹理去覆盖非目标板区域。听起来像文字游戏但实际做起来是有讲究的。我推荐的做法分三步第一步先用Kalibr自带的检测工具或者OpenCV手动框选确定要保留的目标板位置。如果图像里有多块板建议通过角点ID来选定一块最完整、最清晰的板子作为目标。第二步生成一张掩膜图目标板区域设为白色其余区域设为黑色。第三步用图像修补算法将非目标板区域填充成与周围环境兼容的纹理。填充这一步有很多种实现方式。最简单的可以用OpenCV的cv2.inpaint函数做修复也可以手动从目标板周围取一块干净的背景区域复制平铺到其他板子所在的位置。我实测下来cv2.inpaint在均匀背景下效果很好但如果现场光照不均匀、地面纹理复杂修复出来的区域有时会有轻微的人工痕迹。这时候我会退一步用复制平铺高斯模糊过度的方式让填充区域和原始背景更好地融合。实操中生成掩膜这步是最容易出错的。建议先用Kalibr的kalibr_camera_find工具跑一遍原始图拿到目标板角点的像素坐标然后往外扩一定边距生成一个矩形掩膜区域。注意保留目标板四周的白色留边这个留边对于Aprilgrid的检测非常重要后面我会详细讲。2.3 一个可以直接用的Python处理脚本示例下面这个脚本就是我在实际项目中用过的版本做了简化。它读入一张多板图通过手动点击目标板的四个角点生成掩膜然后用cv2.inpaint把其他板子区域填充掉。import cv2 import numpy as np def select_target_board_points(image): # 手动点击目标板的四个角点左上、右上、右下、左下 points [] def on_mouse(event, x, y, flags, param): if event cv2.EVENT_LBUTTONDOWN: points.append((x, y)) cv2.circle(image_display, (x, y), 5, (0, 0, 255), -1) cv2.imshow(select board corners, image_display) image_display image.copy() cv2.imshow(select board corners, image_display) cv2.setMouseCallback(select board corners, on_mouse) cv2.waitKey(0) cv2.destroyAllWindows() return np.array(points, dtypenp.float32) def generate_mask(image, corners, margin20): mask np.zeros(image.shape[:2], dtypenp.uint8) # 将四个角点往外扩 margin 像素保留白色留边 x_min max(0, int(corners[:, 0].min()) - margin) x_max min(image.shape[1], int(corners[:, 0].max()) margin) y_min max(0, int(corners[:, 1].min()) - margin) y_max min(image.shape[0], int(corners[:, 1].max()) margin) mask[y_min:y_max, x_min:x_max] 255 return mask def remove_other_boards(image): corners select_target_board_points(image) mask generate_mask(image, corners) # 用 TELEA 算法修补非目标板区域 result cv2.inpaint(image, cv2.bitwise_not(mask), inpaintRadius3, flagscv2.INPAINT_TELEA) return result if __name__ __main__: img cv2.imread(multi_board.png) cleaned remove_other_boards(img) cv2.imwrite(single_board_cleaned.png, cleaned)这个脚本的适用前提是图像中有一块板子完整且清晰。如果目标板本身拍摄角度太斜或者有反光建议先换一块板子再处理。2.4 为什么保留“白色留边”是关键细节Kalibr检测Aprilgrid时不是只检测黑色方块它还需要确认板子的整体边界。Aprilgrid最外圈的黑色方块与白色留边之间有一个明确的梯度变化这个梯度帮助检测算法确定板的实际范围。如果处理时把白色留边也一起填充掉了Kalibr很可能把板子识别成一个较大的黑白混合区域检测精度会下降。经验值是留边宽度至少保持1.5倍的tagSize。比如标定板参数中tagSize是0.025m那么留边最好不要少于0.0375m对应的图像像素宽度。如果空间紧张也要尽量保证板子外边框和画面中其他内容之间没有大面积同色干扰。3. 遮挡方案详解比图像处理更省心的替代路径原图复制方案解决的是“数据已经采集完只能后期补救”的情况。如果你还在采集阶段或者有条件重新采集一批数据那遮挡方案会是更省心的选择。3.1 物理遮挡与数字遮挡的使用场景物理遮挡很好理解采数据的时候用不透光的板子把非目标板盖住或者把多余的板子撤走。这个方法最粗暴也最有效但存在几个现实问题。第一标定现场如果是在产线或者车间环境不是你完全说了算多余的板子可能贴在地上无法撕掉。第二对于多相机系统几台相机从不同角度拍摄同一个标定区域时一块遮挡板可能挡住了A相机视角下的多余板却正好挡住了B相机视角下的目标板。数字遮挡则是用软件手段实现的思路在处理图像序列时对图像中非目标板的区域进行掩膜处理。这与原图复制方案的主要区别在于它不追求填充后的图像看起来多自然而是用显式的方式告诉检测算法“这里不用看”。实践中最常用的是把非目标板区域涂成统一的灰度值。比如填成纯白色像素值255或纯黑色像素值0。3.2 掩膜填白还是填黑一个值得纠结的问题多数人直觉会选择填黑因为黑色在视觉上更像“背景遮挡物”。但Kalibr的检测线程对黑白区域的响应方式不一样。Aprilgrid本身就是一个白底黑格的图案如果非目标板区域被填成黑色检测线程有可能会把黑色遮罩区域误判为一个巨大的黑色方块从而干扰四边形检测。我实测下来填白比填黑更稳妥。理由很简单Aprilgrid的黑色方块是分散的小尺寸四边形白色背景均匀连续检测算法不会把一个完整的大白色区域识别成候选四边形。如果你把多余板子区域填成纯黑反而等于在图像里放了一个巨大的“黑色四边形”有些版本的Kalibr会把这个黑色区域纳入候选导致聚类结果异常。当然如果图像中目标板周围本身有墙面阴影或不均匀光照填白后目标板边缘会出现明显的灰度断层也会影响角点检测。这种情况下建议填成与目标板周围背景亮度接近的中性灰比如像素值128。要判断填什么最直接的方法是看目标板本身的亮度分布取板子周边背景的平均亮度作为填充值。3.3 数字遮挡的脚本实现细节数字遮挡的脚本比原图复制简单得多本质上就是生成掩膜赋值像素。import cv2 import numpy as np def digital_occlusion(image, board_corners, fill_value255, margin30): mask np.zeros(image.shape[:2], dtypenp.uint8) x_min max(0, int(board_corners[:, 0].min()) - margin) x_max min(image.shape[1], int(board_corners[:, 0].max()) margin) y_min max(0, int(board_corners[:, 1].min()) - margin) y_max min(image.shape[0], int(board_corners[:, 1].max()) margin) mask[y_min:y_max, x_min:x_max] 255 result image.copy() # 非目标板区域填成指定灰度值 result[mask 0] fill_value return result这里有个注意事项脚本处理的是图像序列不是单张图。如果整个序列中相机在移动目标板在不同帧中的位置会变那么每一帧都需要重新检测目标板坐标并更新掩膜。简单场景相机不动、只有板子被移动下可以只对第一帧生成掩膜后面帧沿用同一掩膜但相机一旦运动就必须逐帧更新。逐帧更新时手动点击四个角点的工作量会很大我一般先用kalibr_camera_find自动检测输出角点坐标再脚本批量处理。4. 两种方案怎么选对比与完整实操流程两种方案各有对应的使用场景。先给个对比表格后面再讲完整流程。对比维度原图复制方案遮挡方案适用阶段数据已采集完成只能后期处理数据采集阶段或允许重新采集操作成本需要逐帧生成掩膜并做图像修复掩膜生成更简单填充逻辑更直接处理效果图像自然度高Kalibr检测稳定图像可能有人工痕迹但检测也不差适用相机单目、双目均可单目、双目均可主要风险inpaint可能引入人工纹理干扰检测填充颜色选择不当会干扰四边形检测实施难度中等较低从我的实践来看如果是已经拿到手的bag数据我优先选原图复制方案如果是现场还能重新采一波数据我会直接选遮挡方案把多余的板子物理遮盖掉省得后期折腾。4.1 数据准备与标定板参数检查不管哪种方案数据准备阶段的检查都很重要。Kalibr标定前需要准备一个yaml文件描述标定板的物理参数。最常见的Aprilgrid配置如下target_type: aprilgrid tagCols: 6 tagRows: 6 tagSize: 0.025 tagSpacing: 0.3这里tagCols和tagRows代表每个Aprilgrid的标签行列数tagSize是单个标签的边长单位米tagSpacing是标签间距相对于标签边长的比例。如果一张图里有9块板子这些板子的物理参数必须完全一致混用不同尺寸的板子会导致Kalibr在检测时出现ID冲突。改完图像数据后重新生成bag时图像的topic名要和yaml中一致时间戳要保持原始采集顺序。Kalibr对图像时间戳的连续性要求很高处理脚本如果改变了图像的读取顺序标定结果就废了。4.2 完整流程从多板原图到内参出炉我将整个流程拆成下面几步照着走基本不会出错。第一步配置Kalibr环境。我用的环境是Ubuntu 18.04 ROS Melodic Kalibr源码编译。如果只是做图像预处理不涉及Kalibr本身的编译环境要求可以松一些但最终跑标定命令还是建议用官方推荐的ROS版本环境。第二步从bag中抽取图像序列。用rosbag play配合订阅topic抽取关键帧或者直接用以下命令rosrun image_transport republish compressed in:/cam0/image_raw raw out:/cam0/image_raw_clean第三步对所有抽出来的图像帧做原图复制处理或遮挡处理。这一步是整个方案的核心建议先随机抽5帧手动处理并跑通Kalibr检测确认检测结果稳定了再批量跑全序列。第四步将处理后的图像重新打包成bagrosbag record -O cleaned.bag /cam0/image_raw_clean第五步运行Kalibr标定命令kalibr_calibrate_cameras --target aprilgrid_6x6.yaml --models pinhole-radtan --topics /cam0/image_raw_clean --bagfile cleaned.bag --show-extraction --plot如果命令行加了--show-extraction会弹出每帧图像的角点检测结果方便你确认处理后的图像序列中每帧都只检测到目标板。第六步查看标定结果。输出文件中会包含相机内参矩阵和畸变系数。重点检查重投影误差是否在0.1像素量级如果误差明显大于这个值说明某些帧的角点检测还是有干扰需要回看检测结果图找到问题帧重新处理。4.3 多相机场景的额外注意事项如果是双目相机或多目相机系统还需要考虑不同相机之间的协同。举个例子你有左目和右目两个相机图像中的9块板子在两台相机中的位置不同。处理时不能只对左目图像做掩膜右目图像不做。更严谨的做法是对每台相机的图像序列分别做同样的预处理并保证左右目图像中保留的目标板是物理空间中的同一块板。如何保证左右目看到同一块板建议在处理之前对左右目图像做一次双目匹配找到同时在两幅图像中都完整出现、且没有大角度遮挡的那块板然后以它为各自的目标板。否则左目标定保留了A板右目保留了B板双目外参标定结果也会出错。5. 实操中常见的坑与排查方法这部分内容都是我自己在踩过坑之后总结出来的应该能帮你省下不少时间。5.1 标定结果发散或重投影误差偏大处理完图像跑完Kalibr后如果发现内参结果明显不合理比如焦距值比标称值差了一倍或者重投影误差超过0.5像素首先要怀疑的还是图像序列中没有被彻底清理掉的板子。我排查过一个案例当时地面有两块板子离得很近我处理时只掩膜了较大的一块较小的那块在画面边缘露出了一小块Kalibr把这个残缺的角点簇也纳入检测了结果整个内参优化直接发散。排查方法很简单跑标定命令时加上--show-extraction逐帧查看提取的角点图。如果发现某帧图像在目标板之外还有其他角点被提取出来就说明清理不彻底需要回头重新处理这一帧。5.2 掩膜区域边缘检测异常有时掩膜处理后的图像板子本身没问题但掩膜边缘会出现很多杂散角点。这通常是因为掩膜的边缘太“硬”在图像中形成了明显的灰度跳变被Kalibr误认为四边形的边。对策有两个要么用OpenCV的cv2.GaussianBlur对掩膜边缘做平滑过渡让灰度变化不那么剧烈要么把填充值设成和目标板周围背景更接近的值从源头上减少边缘跳变。# 对掩膜做高斯模糊让边缘更柔和 mask cv2.GaussianBlur(mask, (0, 0), sigmaX10)模糊之后填充区域与原始图像的接缝会变得比较自然Kalibr误检的概率会明显下降。5.3 处理后的图像能检测到板子但优化后内参精度仍不理想这种情况最让人头疼因为所有错误提示都没有内参看起来也差不多但精度就是上不去。我遇到过一次最后发现问题是出在图像压缩上。当时为了省存储空间采集时用了压缩图像topic解压后直接做处理处理完又用有损编码重新压缩生成bag。来回两次压缩导致图像中Aprilgrid的黑色方块边缘出现了明显的压缩伪影Kalibr提取角点时的亚像素精度大幅下降。建议整个处理链路中全程使用无损格式。如果原始bag里是压缩图像先用image_transport解压成raw格式处理完再存成raw格式的bag不要用jpeg或png的二次压缩。5.4 九点标定与Kalibr内参标定不要混淆写到最后提一个关联热词里经常被问到的点。很多人搜索“标定板标定和九点标定的区别”这里顺带说清楚九点标定通常是指相机与机械臂之间的手眼标定通过移动机械臂末端到九个已知点求解相机坐标系和机械臂坐标系的变换关系。它不涉及相机内参焦距和畸变的计算。而Kalibr做的是相机内参标定和相机-IMU外参标定。两者虽然都涉及标定板但解决的问题完全不一样。如果现场有人把两者混为一谈多板图像处理时也可能用错流程值得留意。6. 实用心得这类问题值得多花时间在预处理上最后分享一点我的个人体会。Kalibr这个工具虽然老但稳定性确实好前提是你喂给它的数据质量过关。很多人一遇到标定不合格就想着换标定算法、改源码其实问题往往出在数据本身。多板同框这种场景与其纠结Kalibr为什么不能多板检测不如从数据侧把问题提前解决掉。原图复制方案和遮挡方案本质都是“把Kalibr的世界恢复成它熟悉的样子”。这种思路不只适用于Kalibr其他需要固定标定板布局的视觉工具包也通用。我自己后来又用这套方法处理过几个不同项目的多相机标定数据只要预处理这步做好了后续标定都很顺利再没在检测阶段翻过车。最后再补一个小技巧不管用哪种方案处理完的图像序列建议随机抽20帧左右手动目视检查一遍再打包跑Kalibr。这一步虽然费一点时间但绝对比标定到一半发现数据有问题、回头重新处理要快得多。
返回列表