做机器视觉测量项目,我最怕听到一句话:先跑起来再说。相机装上去,像素坐标能读到,但你要把这些坐标换算成毫米,内部参数就是那道绕不过去的坎。Halcon里有个自带示例camera_calibration_internal.hdev,专门解决相机内部参数标定这件事,焦距、主点、像元尺寸、畸变系数,一套流程走完,相机与镜头之间的几何关系才算是彻底透明。这个示例适合刚接触标定的人,也适合已经在项目里被标定结果坑过的人。看完你不仅能跑通示例,还能知道为什么标定板要那么拍、误差报告怎么看、换镜头之后为什么要重新标定。
1. 为什么内部参数标定是视觉测量系统的“出厂校验”
1.1 像素坐标和物理坐标之间,藏着一层畸变
先看一个很朴素的现实:相机输出的图像只有行列坐标,也就是像素位置,像素本身不是标准长度单位。假设你的视野是一个200毫米宽的视场,相机分辨率是2000像素,那么单个像素大约对应0.1毫米,这个数字只有在镜头光轴完全垂直于被测平面、镜头没有任何畸变时才勉强成立。真正的工业镜头在小畸变镜头里通常有0.1%到0.5%的畸变,换算到刚才的场景就是0.2到1个毫米的系统误差。你如果直接把像素当成毫米用,测出来的尺寸就会忽大忽小,尤其在画面边缘位置,误差更加明显。
内部参数标定做的事情,就是把你手上这台相机和镜头的几何关系“反过来算清楚”。它要确定的内部参数包括等效焦距、主点位置、像元尺寸,以及镜头的畸变系数。有了这套参数,Halcon才能把图像坐标先校正成理想针孔模型下的坐标,再结合标定板的物理尺寸,把每个像素都映射到真实的物理坐标系里。没有内部参数,后面的测量、定位、抓取全都是建立在沙子上的。
1.2 内部参数与外部参数的分工,千万别混在一起
很多工程师第一次做标定,会把“相机标定”理解成一件事。实际上Halcon把标定拆成两个层次:内部参数描述相机自己的成像属性,外部参数描述相机在世界坐标系里的位置和姿态。camera_calibration_internal.hdev这个示例名字里写得很清楚,它只处理内部参数的标定,也就是建立一个“从像素到相机坐标”的数学关系。
外部参数则是另一个量,它回答的是“相机坐标怎么转到机器人坐标、传送带坐标或者产品坐标”。举个例子,同一台相机,如果固定支架松了一下重新锁紧,相机的空间位置变了,但镜头焦距、靶面、畸变这些内部物理属性并没有变。所以内部参数可以继续沿用旧标定结果,只需要重新做外部参数标定。反过来,如果你把镜头换掉,焦距、畸变都会变,内部参数就必须重新标定。把这个分工想清楚,现场遇到标定问题才不会到处瞎试。
1.3 什么时候必须老老实实做,什么时候可以简化
做内部标定确实是额外工作量,所以总有人问:能不能不用标定板,直接用画面上已知尺寸的工件反推像素当量?我的建议是,如果项目只做有无判断、缺陷检测、粗略定位,允许3%到5%的误差,那确实可以不做完整内参标定,用一个像素当量关系就够用了。但一旦涉及尺寸测量、高精度定位、机器人引导抓取,或者要求对画面不同区域都保持较高精度,就必须老老实实按internal标定流程走完。
还有一个容易被忽略的场景是广角镜头。镜头视角越大,边缘畸变越严重,那种“中间准、边缘飘”的测量结果,几乎都是因为没标定或标定不充分。我自己遇到过一台设备,画面中心测量精度能到0.05毫米,但移到视场边缘直接偏了0.4毫米,最后重新做内部参数标定才把整个视场拉回到0.08毫米以内。所以判断标准其实很简单:被测对象会不会出现在画面边缘?会,就别省这一步。
2. camera_calibration_internal.hdev示例替你铺好了一条路
2.1 示例的骨架:从标定板描述文件到相机模型
打开camera_calibration_internal.hdev,你会发现它不是一个几十行的简易Demo,而是一个相对完整的工程流程:先生成或读取标定板描述文件,再创建标定数据模型,然后逐张读入标定板图像、查找标记点、执行标定、输出误差和内部参数。这个流程结构本身就是一套可复用的方法论,你把它理解透了,自己写自动化标定脚本也只是改改图像来源而已。
先说标定板描述文件。Halcon里最常用的标定板是黑色实心圆点阵列,圆点之间间距已知,描述文件里保存了这些圆点的物理坐标和排列规则。示例通常使用一个现成的descr文件,比如caltab_30mm.descr,表示标定板相邻标记中心间距30毫米。你也可以用gen_caltab算子自己生成,生成时需要注意两个关键值:一个是标记间距,一个是标定板厚度,0.003到0.005米之间的玻璃板厚度是常见取值。
标定数据模型负责把所有信息串起来。你要告诉Halcon这套标定用的是哪种相机模型,常见的是针孔模型的division畸变模型,对应工厂里的普通工业相机;如果是远心镜头,则要选择远心模型。示例里的初始相机参数可以给得很粗略,因为标定算法会在后续迭代中修正,但初值如果偏离太多,比如焦距方向给反了,迭代就可能发散。
2.2 核心算子链路:从暗到明的一条主线
我习惯把这个示例的算子链拆成五个关键节点:
- create_calib_data:创建标定数据容器,确定相机数量和标定板数量。
- set_calib_data_cam_param:设置相机类型和初始内部参数。
- set_calib_data_calib_object:告诉容器使用哪个标定板描述文件。
- find_calib_object:在图像中自动查找标定板、提取标记点并估算标定板位姿。
- calibrate_cameras:用所有图像的标记点数据联合求解内部参数和每张图的标定板位姿。
这五个节点里,find_calib_object是现场最容易出问题的一步。它内部做了图像增强、阈值分割、圆点识别和排序,如果照明不好或者标定板拍得不完整,它会报找不到标记或者提取数量不对。很多初学者把这句算子当成“万能识别”,以为只要往里面丢一张图就能拿到结果,其实它非常依赖输入图像质量。后面第3章我会专门讲采图,这在示例代码上看不到,但恰恰是整个标定成败的关键。
calibrate_cameras执行完后,示例会输出一个Errors元组,里面包含了标定过程中每个标记点的重投影误差统计。这个误差通常以像素为单位。如果你看到的值是0.1像素以内,说明整体模型拟合得很好;如果到了1像素以上,先不要急着怀疑算法,优先检查图像有没有模糊、标定板有没有弯曲、相机模型是不是选错了。
2.3 为什么示例里反复强调“多张图像”,而不是一张定终身
很多初学者第一次跑通示例,会图省事只拍三五张图,结果标定结果每次都不一样。想让内部参数被稳定地解算出来,必须给算法提供足够丰富的约束条件。单张标定板图像只能确定一组标定板位姿和相机内参之间的耦合关系,拍摄的标定板姿态越多,不同约束互相交叉验证,焦距和畸变系数才不会被“猜”到某个局部最优里。
你可以这样理解:镜头的畸变参数好比一条曲线的形状,你只有一张图上几个点的信息,根本拟合不出真实曲线;当你让标定板出现在画面的左上、右上、中间、下方,并且每张图都有不同的倾斜角度时,算法才能从各个方向观测到畸变曲线。camera_calibration_internal.hdev的示例代码里,通常会用元组保存一批图像文件路径,然后在循环里逐一find_calib_object,这个设计不是冗余,而是标定稳定的基本前提。
我自己项目里通常一个相机拍15到20张,覆盖画面中心、四角、四条边,同时配合偏转和俯仰角度。这个数量不算多,但能明显减少标定结果的随机抖动。如果你追求更高重复精度,可以在此基础上增加绕光轴旋转的标定板图像,让圆点阵列在不同方向上都充分参与计算。
3. 采图环节的现场经验:角度、光照、板面一个都不能少
3.1 15到20张的分布策略,背后是几何约束在起作用
有人觉得拍20张就是把标定板放在不同位置按快门,这不够。关键指标是标定板姿态的丰富度。标定板永远正对着相机、只在画面里平移,等价于只用了一个固定的俯仰角,约束类型高度重复,畸变参数仍然容易不稳定。
合理分布可以按这个思路来:先正对相机,分别在画面的左上、右上、中心、左下、右下放一遍;然后让标定板绕水平方向稍微前倾后仰各来几张;再绕竖直方向左右偏转来几张;最后可以让标定板绕光轴旋转45度和90度,各拍一两次。这样下来,标记点在不同畸变区域的分布就非常均匀了。
这里有一个现场比较容易忽略的点:标定板表面要基本平整,推荐贴在玻璃或铝板上,但不要用手捏着板边弯着拍。标定算法假设标定板上所有圆点共面,板子一旦弯曲变形,圆点的物理坐标就不再是descr文件里描述的坐标,标定误差立刻上涨。我以前见过一个项目,怎么提高图像质量误差都下不来,最后发现问题出在标定板是一张薄亚克力板,拿在手里已经弯成了弧面,换成玻璃基板后第二次就过了。
3.2 反光、曝光、玻璃板:现场最常栽的三个跟头
工业现场的光照条件不会像实验室那么温柔,常见光源是环形LED、条光或者自然窗光。标定板表面通常有反光涂层或玻璃保护层,一旦角度不合适,某个圆点区域会出现高光,圆点中心被白光盖掉,find_calib_object提取到的中心就会偏移,相当于给标定结果注入了一个系统性偏差。
处理高反光,我常用的办法有三选一:把标定板换成哑光白色陶瓷基板;调整光源角度,让入射光不要垂直反射进镜头;给镜头加偏振片,并在光源前也加偏振片,消除镜面反光。最省事的其实是第一种,哑光板在柔光环境下几乎不用调参。曝光方面,标定图像里的圆点必须边缘锐利、黑白对比明显,不要让白色背景过曝成一大片,也不要把黑色圆点压成全黑没梯度。可以先用自动曝光拍一张,然后手动把曝光稍微降一点,保证白色区域灰度在220到250之间,黑色圆点灰度在30以下,这种对比度下提取最稳定。
玻璃标定板还有个特殊问题:玻璃厚度会让圆点图像产生双影或折射偏移,尤其是斜着拍的时候。如果你用的是带玻璃保护层的标定板,拍摄倾角不要太大,尽量控制在30度以内,角度过大时折射造成的圆点虚拟位置偏移会超出算法容忍范围。
3.3 标定板在画面里的占比:别把它当角落装饰
标定板在画面里占多大比例,直接决定像素点上的标记间距,也就决定了畸变参数能被识别到多细。如果标定板只占画面的五分之一,标记点之间的像素距离很短,镜头边缘畸变几乎没有被采样,拟合出来的畸变模型自然对边缘区域没有约束力。
我建议标定板至少覆盖画面宽度的三分之一到二分之一,实际操作中可以让标记点阵列尽量覆盖整个视场的不同区域,最理想的状态是标定板的有效区域能触及画面的四角和边缘。如果你的视野很大,比如500毫米宽,但手头只有一块小标定板,那就不要追求单张图覆盖全画面,而是靠多张图把标定板分别移到画面不同位置,让采样点最终铺满整个视场。
还有一种情况常见于大靶面相机:镜头中心区域成像清晰,边缘却模糊。这种模糊不是畸变,但它会影响圆点边缘提取精度。遇到这种情况,可以把标定图像的光圈稍稍收小一点,增加景深,同时保证整块板都在清晰范围内。光圈不要收到最小,否则衍射会让圆点边缘反而发虚,一般收到F5.6到F8之间是最稳的。
4. 标定结果不是跑完就完事:误差与物理合理性审查
4.1 误差数字怎么读,Errors到底在告诉你什么
执行calibrate_cameras后返回的Errors,是优化过程结束后所有观测点重投影误差的统计值。Halcon会把误差项组织成一个元组,里面通常包含平均误差、最大误差,以及每个标定板图像各自的误差分量。不要把注意力只放在平均误差上,最大误差更能反映问题。平均误差0.05像素但最大误差2像素,说明大部分点拟合很好,但存在个别异常点,这些点很可能来自某张反光干扰或者标定板边缘漏检。
我看到很多人对“0.03像素”特别兴奋,觉得这是世界最高精度。实际上,像素误差只是图像平面的拟合残差,最终物理测量精度还取决于工作距离、视场宽度、像元尺寸和算法稳定度。你可以在完成标定后做一个换算:视场宽度除以图像宽度,得到实际像元分辨率,再用它乘以0.1像素误差,就能大致换算成物理误差。比如视场100毫米、图像5000像素,单个像素对应0.02毫米,0.1像素误差就是0.002毫米量级,这是一种理想估算,但至少能帮你建立“图像误差”与“空间误差”的比例感。
如果误差稳定在0.1像素以上,优先检查以下几项:标定板是否有污渍或划痕;图像是否轻微Motion Blur;标定板描述文件和实际板子的间距是否一致;相机模型是否选错。在我的经验里,模型选错的概率比大家想象得高,比如把普通镜头当成远心镜头,或者反过来,误差会直接跳到几个像素。
4.2 内部参数的物理意义与常见“怪值”排查
内部参数标定完成后,不要把结果存进文件就完事。你要看一遍这些参数是否符合物理常识。以普通针孔相机为例,等效焦距应该大致等于镜头焦距除以像元尺寸再乘以放大倍率,如果算出来是负数或者小到离谱,标定过程八成出了问题。主点位置一般靠近图像中心,如果在画面边缘甚至跑到图像外面,那可能是你给的初始主点偏差太大,算法落进了局部最优。
畸变系数在这里尤其值得注意。工业镜头的畸变系数通常在几十到几千之间,具体取决于镜头类型和参数表达方式。如果你标出一个超乎正常范围几个数量级的畸变值,要优先怀疑标定板图像数量太少,或者标定板没有覆盖画面边缘。边缘畸变信息不足,算法就会用外推方式瞎猜一个畸变值,这种结果哪怕重投影误差看着不错,换一批图像再标一次也会得到完全不一样的参数。
判断内部参数是否可信,我习惯用一个“复标法”:用同一台相机、同样设定,分别采集两批各15张图像,单独执行两次标定,然后比较两次得到的焦距、主点、畸变系数差异。如果焦距波动在1%以内,主点波动在5个像素以内,畸变系数波动在20%以内,我可以放心直接用;如果差得很多,说明标定过程里某些约束不够,继续补图再试。
4.3 连锁验证:重投影和留图抽查
除了看Halcon给的标准差,我自己一定会做一步“留图验证”:从20张标定图像中抽取两到三张不参与标定,只在最后用标定出的内参去计算这些图的标定板位姿,看提取到的标记点重投影误差是否也在合理范围。如果参与标定的图像误差很小,但没参与标定的图像误差很大,说明标定结果过拟合了,或者这几张留图的拍摄姿态超出了之前覆盖的范围,这时需要补充边界姿态的图。
Halcon中可以通过get_calib_data拿到标定板位姿,再通过project_3d_point把标定板上的物理点投影回图像平面,和实际提取点比较。这一步能直观看到残差方向:如果某个局部区域的误差呈现规律性排列,比如都朝外偏,通常意味着畸变模型没有完全拟合镜头特性;如果误差分布非常随机,更多是标记点提取噪声,改善打光能压下来一部分。
重投影验证还有一个额外好处,就是可以提前发现“标定板放在画面角落、只露出一半”这类问题。有些图在find_calib_object阶段能通过,但边缘标记点实际已经超出标定板有效范围,参与标定反而会拖累整体精度。通过逐张查看每幅图的误差贡献,把误差特别大的图像剔除后重新标定,效果往往立竿见影。
5. 内参标定之后的那些事:换镜头、重标定与应用延伸
5.1 锁紧、温度、光圈:任何机械变动都要重标
内部参数不像是手机号一样终身不变。实际操作中,镜头焦距环如果松了、光圈动了、镜头重新拧紧、相机摔过一下,内部参数都可能变化。尤其是C接口镜头,锁紧螺丝不稳固时轻微振动就能让镜头相对相机靶面产生微小移动,主点位置跟着偏移,表现出来就是原本精准的测量结果开始有规律地偏一边。
我见过一个典型案例:一套设备出厂时标定误差很好,到现场跑了三个月后尺寸测量偏了0.1毫米,检查半天发现是镜头锁紧环松了,用手轻轻一拧,镜头居然能转出细微的间隙。所以内部参数标定文件里,一定要额外记录镜头品牌、焦距设定、光圈F数、工作距离、机械锁紧状态,甚至AI反馈都要带上。现场如果只是拆装过相机,我会直接重新标定内部参数,而不是只做外部标定,因为拆装镜头本身就可能让整个光路产生无法预判的变化。
温度也是一个隐变量。长波红外相机或者工作环境温差很大的车间,镜头材料热胀冷缩会影响焦距和畸变。如果是精度要求极高的测量设备,建议每次换季或者环境温度变化超过10摄氏度后,重新跑一遍camera_calibration_internal.hdev的流程,代价不大,但能避免系统性漂移。
5.2 从内部参数到外部参数、手眼标定
内部参数标定完成后,很多项目紧接着要做外部参数标定,也就是确定相机坐标系和世界坐标系之间的位姿关系。两件事通常是串联的:先用camera_calibration_internal.hdev把内参固化,然后用halcon里的标定板位姿或者机器人手眼标定算子求解外参。内参不准,外参标定做得再精细也是白搭,所以我在项目里会先反复确认内参的重复精度,再进入外参阶段。
对于固定安装的测量系统,外参通常通过观察一个基准标定板来建立;对于装在机械臂末端的相机,还需要做手眼标定,求解相机坐标系与机械臂末端坐标系之间的关系。手眼标定中,相机在每个机械臂位姿下拍摄同一个标定板,如果内部参数不准,算法会把畸变误差“分摊”到手眼矩阵里,导致机器人引导时不同区域出现不同的平移误差。经验是,手眼标定前的内参重投影误差要尽量控制在0.05像素以内,否则现场标定时好时坏,你很难定位是手眼的问题还是相机本身的问题。
5.3 把标定流程固化成可复用的“内部标准”
每次做标定都打开camera_calibration_internal.hdev手点一遍,效率低而且容易漏步骤。当你在一个项目里验证过采图策略和误差标准后,完全可以把这个示例改造成一个自动化标定脚本:批量读取指定文件夹里的标定图像,自动跑find_calib_object,筛选掉误差过大的图像,标定后输出一份内部参数文件和标定日志,日志里记录每张图的误差和最终参数。
我个人的习惯是,为同一个相机型号做一份固定的标定模板,里面预设初始参数、标定板描述路径、采图张数阈值、最大允许误差,现场工程师只需要按模板拍图、放文件夹、跑脚本,就能拿到一个格式统一的标定结果文件。这个文件再交给上位机程序,启动时加载即可。这样不仅减少人为操作误差,还能在设备出问题时快速回溯:到底用的哪批标定板、哪个版本的参数、哪个时间的误差值。
最后再分享一个小技巧:标定完成后,把相机对着同一块标定板,分别在不同天各拍5张,算一次位姿,观察位姿结果是否稳定。所有参数文件都要带上时间戳和操作人标识,机器视觉项目最怕的不是标定难,而是不知道当前设备用的参数是哪个版本。固定好流程,标定这件事就可以从“靠经验”变成“靠制度”。