简介:这份 Halcon 第六章 Calibration 英文参考手册直译中文版(无删减),面向计算机视觉、视觉检测和 Halcon 算法应用人员,重点解决相机标定概念理解、标定流程搭建与测量精度提升问题。资源包仅含 1 个 PDF 文件,约 813KB,便于集中查阅和随用随看。内容围绕标定对象描述与查找、三维点投影和反投影、角度与径向畸变补偿、相机内外参处理、自标定,以及单目、双目、多视图和手眼标定展开。已有 362 人学习下载,可作为 Halcon 标定主题的参考资料,适合需要系统学习相机标定的人员。章节还系统梳理六边形与矩形校准板的选择和拍摄要求、标定输入数据准备、find_calib_object 与 calibrate_cameras 的使用、结果检查及后投影均方根误差评估,并说明不同相机模型的限制,便于读者把概念、参数配置和项目排错串联起来。
1. 标定误差 0.1 像素不是「越准越好」:它其实是标记点提取的噪声底
很多人跑完一次相机标定,第一反应是看 RMSE 数字:0.08 就松一口气,0.35 就开始怀疑参数写错了。差别通常不在优化算法,而在手上那组图。RMSE 衡量的是「重投影回来的标定板标记点」和「图像里真实提取到的标记点」之间的平均像素距离,它的下限由标记点中心的提取精度决定,亚像素边缘拟合再稳,也有零点几个像素的抖动。0.1 像素附近意味着模型已经贴合观测噪声的底,再压没有意义;0.4 以上则基本说明数据本身有问题,不是模型不够复杂。
把这条链拆开看:3D 世界点 → 相机坐标 → 归一化像面 → 畸变补偿 → 像素坐标,每一环都有确定的参数和确定的不可辨识性。搞懂这些,标定才从「点一下跑一遍」变成可以诊断的工程步骤。
2. 相机模型的参数账本:为什么 Sy 永远带星号、Kappa 只能有一个
相机标定的产物是一组参数,不是一张校正图。这组参数分成内参和外参两本账:内参描述「这台相机加这颗镜头如何把光线折到传感器上」,外参描述「标定板此刻站在相机的哪个位置、朝哪个方向」。所有标定算子最终都在填这两本账,区别只在于账本里哪些格子允许被优化、哪些格子必须由人钉死。
2.1 带星号的参数不是省略,是不可辨识
打开区域扫描相机的参数元组,最常见的area_scan_division长这样:
* area_scan_division 的 8 个内参,顺序固定 * [CameraType, Focus, Kappa, Sx, Sy, Cx, Cy, ImageWidth, ImageHeight] ['area_scan_division', 0.008, 0.0, 2.2e-6, 2.2e-6, 320.0, 240.0, 640, 480]注意Sy后面那个星号。针孔模型的投影式里,水平方向真正起作用的是Focus / Sx这个比值,不是 Focus 和 Sx 各自的值——把焦距放大两倍、同时把像元尺寸放大两倍,投出来的像素坐标一模一样。三个未知量只有两个独立自由度,方程欠定,所以必须挑一个钉死。选 Sy 而不是 Sx,是因为多数工业相机的行方向采样是硬件同步的,像素间距由芯片规格直接给出、查手册就有,比让优化器去猜更可靠。
远心镜头是同一回事的另一副面孔:投影式里只出现Magnification / Sx,放大倍率和像元尺寸同样只能定比值,于是Sy继续带星号。像侧远心的倾斜镜头更极端,Focus、Sx、Sy、Tilt、Rot 五者不能同时确定,所以Sx和Sy一起被固定。线扫描相机则是另一套约束:Sx固定(焦距与横向尺度不可分),Sy固定,原因是标定方程里Sy只以pv = -Sy · Cy的乘积形式出现,两者不可分离。
提示:星号参数不是「算法懒得算」,而是数学上算不出来。手工改这些值不会让结果更准,只会整体缩放整条投影链。
2.2 畸变模型:一个参数和五个参数的职责划分
畸变这一层,HALCON 给了两套模型,参数个数差得很远:
| 模型 | 参数 | 建模对象 | 能否解析求逆 |
|---|---|---|---|
| division(分割模型) | Kappa | 仅径向畸变 | 可以 |
| polynomial(多项式模型) | K1, K2, K3, P1, P2 | 径向 + 离心畸变 | 不可以,需迭代 |
分割模型只用一个Kappa描述径向弯曲,形式上是把畸变像面坐标乘一个与半径平方相关的因子。它的关键优势在于这个式子可以代数反解——给定一个畸变坐标,能直接算出未畸变坐标,不需要迭代。多项式模型用三个高阶项逼近径向弯曲、两个参数描述镜头光轴与传感器不垂直造成的离心偏移,拟合能力更强,但方程没有闭式反解,反畸变只能数值迭代。
这两套模型的选择直接决定了运行时开销。做测量时,每一条从图像反推到空间射线都要走一次反畸变;如果模型不可解析反演,这条路径上每次调用都在跑迭代。常见的实现思路是:先用 division 跑通全流程,只有当标定残差明显高于检测噪声、并且确认是畸变建模不足导致的,才切到 polynomial,同时补拍覆盖更充分的图像。
2.3 九种相机类型不是配置项,是九套不同的方程
参数元组的第一项CameraType决定了解析器读哪一套公式。区域扫描相机按「投影方式 × 是否有倾斜镜头 × 畸变模型」组合出十几种,线扫描相机再叠一层运动矢量。选择错误会出现两种典型后果:一是参数位置整体错位,把Kappa填进了K1的槽位;二是公式和物理设置不匹配,比如拿area_scan_telecentric_division去描述一颗普通定焦镜头,优化器会拼命用放大倍率去补偿本应由透视除法承担的近大远小,最后 RMSE 看起来还行,但测量结果随工作距离漂移。
判断方法很朴素:镜头物体侧输出的是会聚光束还是平行光束?会聚就是透视投影,选area_scan_*;平行就是远心投影,选area_scan_telecentric_*。镜头是否相对传感器倾斜?看机械结构有没有俯仰调节或者 Scheimpflug 铰链。畸变是否需要用高阶项描述?先看 division 的残差够不够用。这三个问题答完,类型唯一确定。
3. 从世界点到像素的四步变换:一条投影链是怎么被拆开的
一个 3D 世界点pw = (xw, yw, zw)落到图像上变成(r, c),中间要过四道工序。把这四道工序拆开,才能理解为什么外参和内参可以分离优化,以及为什么某些相机类型的参数天然不可确定。
3.1 第一步:世界坐标到相机坐标,R 和 T 的六个自由度
这一步是纯粹的刚体变换:pc = R · pw + T。R 是 3×3 旋转矩阵,T 是平移向量,一共 6 个自由度,对应外参里的 TransX、TransY、TransZ、RotX、RotY、RotZ。
外参的位姿元组有 7 个元素而不是 6 个,多出来的那个是编码项,它把旋转的书写顺序(OrderOfTransform)、欧拉角顺序(OrderOfRotation)、观察方式(ViewOfTransform)打包成一个整数。原因很实在:三组欧拉角能拼出同一个旋转,不记录顺序就无法从 6 个数唯一还原矩阵。标定结果里丢掉这个编码,读出来的姿态就是错的。
使用标准标定板时,世界坐标系由板面定义:原点在板面中心或第一个取景图案的中心,z 轴垂直指向板子,x 轴向右,y 轴向下,视线沿 z 轴方向。这也是为什么板子必须放正、不能反着拍——坐标系的定义绑在板子的正面上。
3.2 第二步:相机坐标到像面,透视除法与远心缩放的差别
针孔模型下,投影是两次除法:
u = Focus · xc / zc v = Focus · yc / zc分母里的zc就是近大远小的来源。超中心镜头虽然在成像上很特殊,但它依然在物体侧做透视投影,所以仍归入针孔相机,公式形式一致。
远心模型的投影式里没有除法:
u = Magnification · xc v = Magnification · yczc消失了。这不是简化,而是物理事实:远心镜头只接收平行于光轴的光束,物体沿光轴移动不改变成像位置。代价是深度信息彻底丢失,所以远心相机的标定结果里,标定板沿 z 方向的平移会被强制设成 1 米——不是测出来的,是占位值。这一点在做手眼标定或者多相机拼接时是必须知道的:远心相机的外参 z 分量没有物理意义。
3.3 第三步:畸变补偿,为什么公式方向常常是反的
标定文档里给出的畸变公式,通常写的是「畸变坐标 → 未畸变坐标」,而不是反过来。原因在于优化目标:calibrate_cameras要算的是「把空间点投到图像上会落在哪」,也就是正向投影;而find_calib_object拿到的是图像里的真实像素,要去反推空间射线。两个方向都需要,但只有一个是闭式的。
分割模型两个方向都闭式,写起来对称;多项式模型只有「畸变 → 未畸变」是显式的,反方向必须解一个非线性方程、用迭代收敛。这就是为什么同样的图像、同样的标定板,切到多项式模型后单次project_3d_point的耗时明显上升。
畸变前坐标 --(多项式模型: 不可解析求逆)--> 畸变后坐标 畸变前坐标 <--(多项式模型: 显式公式)------ 畸变后坐标注意:多项式模型的反畸变迭代通常在图像边缘收敛更慢,因为高阶项在那里的贡献最大。如果测量区域贴着图像边界,务必检查那里的重投影残差是否仍然可控。
3.4 第四步:倾斜像面,3×3 射影变换和 2×2 线性变换的分界线
倾斜镜头(光轴相对传感器法线倾斜)在畸变之后还要多一层变换,把点从「虚拟的无倾斜像面」映射到真实的倾斜像面上。这一步用哪种矩阵,取决于倾斜发生在镜头的哪一侧:
- 图像侧执行透视投影的倾斜镜头(普通倾斜、物侧远心倾斜):用 3×3 齐次矩阵 H 做二维射影变换,点要转成齐次坐标再除回来,多出一个尺度分量。
- 像侧执行平行投影的倾斜镜头(像侧远心、双侧远心):用 2×2 线性变换,没有分母。
分界线就是「有没有透视收缩」。平行投影不产生近大远小,映射退化成仿射,自然不需要齐次坐标。
H 的两个来源参数是Tilt和Rot,再加一个ImagePlaneDist(镜头出瞳到像面的距离)。Rot描述倾斜轴在传感器平面内的方向,必须先应用;Tilt描述倾角本身,取值在 0° 到 90° 之间;Rot从 0° 到 360°,0° 时光轴垂直向下倾斜,90° 对应向左水平倾斜。这三个数通常能从机械结构上读个大概,作为初值喂给优化器。
3.5 线扫描的额外一维:运动矢量把一条传感器线扫成一张图
线扫描相机每次只曝一行,整幅图像是靠相机与物体相对运动「拼」出来的。所以要建模的不只是光学投影,还有运动。
HALCON 对运动做了三条假设:相机沿直线匀速移动、相机方向恒定、所有图像的运动相等。运动由矢量V = (Vx, Vy, Vz)描述,单位是「米/行」。有意思的是,这个矢量描述的是相机的运动(假设物体固定),等价于相机不动、物体沿-V移动。
相机坐标系的定义也随之调整:原点在投影中心(针孔)或畸变中心(远心),z 轴沿光轴且可见点 z 为正,y 轴垂直于传感器线并让运动矢量的 y 分量为正,x 轴补齐右手系。由于每一行都是从不同位置成像的,理论上每行一个位姿;为了简化,HALCON 只保留第一图像线的位姿,运动由V在投影时补偿。
V的初值质量直接决定标定能否收敛。原文说得很直白:初值不够精确,标定可能直接失败。常规做法是让相机光轴垂直于传送带、传感器线平行于运动方向,此时取Vx = Vz = 0,再用已知尺寸的标尺或标定板图像反推Vy。如果相机绕光轴转了 30°,或者绕 x 轴俯仰了 20°,初值就得按旋转关系重新算,不能照抄。
远心线扫描还有一个坑:Vz对成像位置没有影响,因此无法被确定,会保留初始值。所以远心线扫的Vz应当显式设成 0。
# 四步投影链的示意实现(区域扫描,针孔 + 分割畸变) def project(pw, R, T, focus, kappa, sx, sy, cx, cy): pc = R @ pw + T # 第一步:世界 -> 相机 u, v = focus * pc[0] / pc[2], focus * pc[1] / pc[2] # 第二步:透视除法 r2 = u * u + v * v k = 1.0 / (1.0 + kappa * r2) # 第三步:分割模型反畸变,可解析 u, v = u * k, v * k col = u / sx + cx # 第四步:像面 -> 像素 row = v / sy + cy return row, col这段伪代码对应的是「标定完成后,给一个已知世界点,预测它落在图像哪个像素」。注意sy虽然在校准中被固定,投影时仍然要参与计算——固定的是它不被优化,不是它不存在。cx、cy是主点,使用半图宽度和高度作为初值;如果图像做过降采样,这四个尺度量和ImageWidth、ImageHeight必须同步缩小,否则整条链的尺度会整体跑偏。
4. 标定数据模型与 calibrate_cameras:一次非线性优化如何被组织
标定不是一次函数调用,是「建模型 → 灌数据 → 跑优化 → 查结果」四段式。中间那个CalibDataID就是一个句柄,把相机、标定板、观测点、优化配置全部装在一个容器里,最后一次性丢给优化器。
4.1 create_calib_data 的三个入参,决定了后面所有调用的下标
* 1 台相机、1 块标定板、使用标定对象模型 create_calib_data ('calibration_object', 1, 1, CalibDataID)三个入参分别是模型类型、相机数量、标定对象数量。相机数量为 1 是单目,为 2 是双目,更多就是多视图。标定对象数量指的是「画面里可能同时出现几块不同的板」,多相机同时看不同板时才需要大于 1。
这两个数字不是摆设,它们决定了后续所有算子的下标索引:set_calib_data_cam_param (CalibDataID, CameraIdx, ...)里的CameraIdx从 0 到相机数减一;find_calib_object里的相机索引和标定对象索引同理。索引写越界不会崩溃,但数据会悄悄丢进一个不存在的槽位,最后表现为「明明拍了 20 张,优化只用了 3 张」。
4.2 观测数据的两个入口:自动提取和手动接管
find_calib_object是最省事的一条路:喂一张图,它自己找标定板、提取标记中心、估一个粗略位姿,把观测点写进数据模型。适合标准板、正常光照、板子完整可见的场景。
set_calib_data_observ_points是手动挡:你自己算好标记点的图像坐标和对应的世界坐标,再灌进去。什么时候需要它?标定板上的标记被遮挡了一部分、需要剔除个别坏点、或者根本不用标准板而是拿一个特征点已知的工装件做标定。原文提到「也可以使用任意对象进行校准,唯一的要求是物体具有可以在图像中鲁棒检测到的特征点,并且这些点的 3D 世界位置是高精度的」——这条路走的就是手动入口。
提示:自打印的标定板通常精度不够。打印机的定位误差和纸张的热胀冷缩会直接进入世界坐标,而世界坐标是标定的基准,基准歪了整个结果都歪。高精度场景用蚀刻或光刻工艺做的板子,附带描述文件,
find_calib_object能直接读。
4.3 calibrate_cameras 之后,三样输出各说明什么
优化跑完,calibrate_cameras (CalibDataID, Error)返回的Error就是重投影 RMSE,单位像素。它对应的是「反投影回来的标定板标记点」和「图像里提取到的标记点」之间的平均距离。
另外两样要用get_calib_data单独查:
camera:优化后的内参元组,包含所有带星号参数的最终值(星号参数保持初值不变)。calib_obj_pose:每张图像里标定板相对于相机的位姿,也就是外参。covariances:内参的标准差和协方差。这个容易被忽略,但它才是判断「参数是否被数据充分约束」的依据。标准差很大意味着该参数在优化中几乎没被约束住,值不可信,哪怕 RMSE 很小。
RMSE 小但某个参数标准差大,是典型的「过拟合式标定」:模型用别的参数补偿了这个自由方向。常见于所有标定图里板子姿态过于单一的情况。
4.4 排除参数:当你已经知道某个值不该被优化
如果某个参数有独立的高精度来源,就该把它从优化里踢出去,减少自由度、提高其余参数的约束强度。做法是用set_calib_data配合excluded_settings:
* 相机 0 的像元尺寸已知且可信,只优化其余内参 set_calib_data (CalibDataID, 'camera', 0, 'excluded_settings', ['sx', 'sy']) calibrate_cameras (CalibDataID, Error) get_calib_data (CalibDataID, 'camera', 0, 'params', CameraParam) get_calib_data (CalibDataID, 'calib_data', 0, 'error', Error)字符串'sx'、'sy'要和参数元组里的字段名对应。排除掉之后,这两个值原样保留初值,其余参数在「已知尺度」的约束下优化。这一步在产线设备上很实用:相机芯片的像元尺寸是规格书给的确定值,没必要让优化器去猜,猜错了反而拖累焦距和主点。
排除要有依据。把畸变系数也排除掉(比如直接设'kappa'),等于强行假设镜头无畸变,短焦或大视场下 RMSE 会立刻变难看。
5. 标定现场的踩坑排查:从 RMSE 异常到板子镜像的五个现象
标定失败很少报错,多数时候是「跑通了但数值不对」。下面这几条是按现象反查的经验记录,每条都对应一个具体的机制。
5.1 现象:RMSE 卡在 0.4 以上,图看着挺清楚
原因通常不是模糊,而是标记点的直径不够。标定要求每个标记在图像里的直径至少 20 像素,低于这个值,中心提取的量化误差会直接吃掉亚像素精度。另一个隐形杀手是对比度:明暗区域之间至少要 100 个灰度值(8 位图),如果照明不均导致亮部只有 60 灰阶差,边缘检测就会在噪声里摇摆。
解决办法是先量再拍:拿一张图数一下标记直径占几个像素,不够就加分辨率、缩短工作距离或者换大尺寸标定板。同时检查曝光,亮部灰度值不要超过 240,避免过曝把标记边缘「烧平」;板面照明的均匀性也有要求,亮部灰度值的跨图波动控制在 45 灰阶以内。
5.2 现象:优化收敛了,但焦距和外参的 z 平移数值离谱
原因是标定板的姿态太单一。如果所有图像里板子都基本平行于传感器平面(尤其是全部放在同一平面上),透视投影的方程会退化:焦距和板到相机的距离互相补偿,无法分离。原文明确写了这个限制——所有六个外参加焦距无法一起确定。
解法是补拍倾斜图。板子以 30° 到 45° 向不同方向倾斜,而且要保证视场的每个区域至少被板子覆盖过一次。六边形排列标记的板子至少 6 张图、其中至少 4 张倾斜;矩形排列标记的板子至少 15 张。数量是下限不是目标,姿态的多样性比张数更重要。
5.3 现象:结果整体镜像,或者 HDevelop 直接报错退出
原因是标定板被反着拍。玻璃基底的板子从背面拍,标记图案会左右翻转,世界坐标和图像坐标的对应关系整个反了。六边形排列的板子靠取景图案定位,图像里至少看到一个取景图案就能定板,但要判断是否镜像必须看到至少两个——只有一个的时候,镜像检测不出来,结果就是悄悄错。矩形排列的板子要求整块完全可见,因为取景图案是围着标记的那圈框架,框架不全就没法确认朝向。
解法是确认板子的正面朝上,并且遵守各自的可见性要求:六边形板至少两个取景图案入镜,矩形板整板入镜。
5.4 现象:换了标定板之后结果完全不对,或者算子直接不认
原因是算子与板型不匹配。六边形排列标记的板子不适用于camera_calibration这个老算子,要用calibrate_cameras走数据模型流程。反过来,如果已经在用calibrate_cameras,板子的描述文件必须通过set_calib_data_calib_object正确挂上去,文件里的标记世界坐标和实际板子对不上,优化结果必然跑偏。
还有一个容易混淆的点:矩形排列标记的板子,find_caltab加find_marks_and_pose两个算子组合起来,效果等价于find_calib_object。维护老代码时会看到这种写法,不必强行改写,但新流程建议统一用find_calib_object。
5.5 现象:测量结果差一个固定比例,或者远心相机的工作距离怎么都测不出来
比例问题的根源在单位。标定板尺寸以米为单位,所有相机参数也用米,焦距初值写8而不是0.008,整条链就整体放大 1000 倍。降采样也会带来同类问题:图像做了 2 倍降采样,Sx、Sy要乘 2,Cx、Cy、ImageWidth、ImageHeight要除以 2,漏改任何一个都会让尺度不一致。
远心相机测不出工作距离,不是配置问题,是物理限制。远心投影不含zc,板到相机的距离不可确定,外参的 z 分量被强制设成 1 米。同理,远心线扫描相机的Vz也确定不了,会保留初始值,正确做法是把它显式置 0。
6. 除法模型与多项式模型的取舍:可逆性、稳定性与覆盖度
两套畸变模型的差别,表面看是参数个数,实质是「你要为精度付出多少运行时代价和多少数据代价」。
| 维度 | 分割模型(division) | 多项式模型(polynomial) |
|---|---|---|
| 参数数量 | 1 个Kappa | 5 个(K1/K2/K3/P1/P2) |
| 能建模离心畸变 | 否 | 是 |
| 反畸变方式 | 解析求解 | 数值迭代 |
| 少量图像下的稳定性 | 较高 | 较低 |
| 对覆盖度的要求 | 一般 | 高,测量区域必须被板子覆盖 |
| 大畸变下的拟合精度 | 一般 | 较高 |
分割模型的隐性优势在「稳定性」这一行。参数少意味着它不容易把噪声拟合成畸变,样本量不足或者视场覆盖不全时,结果不会剧烈漂移。多项式模型的高阶项则有很强的外推能力——这是优点也是陷阱:校准板没有覆盖到的区域(图像边界、视场角落),它照样会给出一个数,但那个数可能完全脱离物理事实。原文对此说得很清楚,多项式模型在标定板覆盖区域之外可能不准确地模拟变形。
所以默认路线应该是:先用分割模型标,看 RMSE 和协方差。如果残差明显高于标记点提取的噪声水平,并且确认是畸变建模不足(而不是图像质量问题),再切多项式,同时补拍覆盖更充分的图,尤其是你后面真正要做测量的那块区域。切过去之后,务必把测量区域的重投影残差单独验一遍,不要只看全局 RMSE——全局平均值会把边界的糟糕拟合平均掉。
倾斜镜头的取舍是另一条线。Tilt、Rot、ImagePlaneDist这三个参数的可辨识性依赖镜头本身的畸变量:畸变越大,倾斜造成的不对称越明显,参数越能被约束住;畸变很小的镜头,Tilt基本确定不了,优化出来的值可能和机械上读到的标称值差很远。这种情况下不要慌着调参,先看误差——如果 RMSE 和协方差都正常,说明这组参数在校准体积内能一致地描述成像几何,用于测量是没问题的;参数值和标称值不一致,只是说明这组数据无法区分它们,不代表结果错。
我自己的习惯是每次标完先做三件事:看 RMSE 是否落在 0.1 像素这个量级附近,看covariances里有没有哪个参数的标准差异常大,再看一组留出图的重投影残差在图像四个角和中心是否一致。这三步走完,才敢把这组参数写进测量流程。早年吃过一次亏——RMSE 只有 0.09,看着漂亮,结果测量区域在图像右下角,那块恰好没有被任何一张标定图覆盖,多项式模型在那里外推出了一个不存在的弯曲,一整批工件的尺寸全部系统性偏大。从那以后我每次切多项式模型之前,都会先把标定板的覆盖范围画在图像上,确认测量区域被完整包住,才允许往下走。希望这些经验帮到你,少走一遍我走过的弯路。
本文还有配套的精品资源,点击获取