3、单目标定流程:从图像采集到内参输出,完整走一遍 calibrate_cameras 算子

单目标定,说白了就是让相机知道自己的“眼睛”长什么样——焦距、畸变、主点位置。很多新手一上来就调算子参数,结果标定出来偏差很大。我个人的习惯是:先把流程走通,再谈优化。

这一章,我们就用 calibrate_cameras 算子,把单目标定的完整流程跑一遍。从采集标定板图像开始,到最终输出相机内参,每一步我都会讲清楚“为什么这么做”以及“我踩过的坑”。

3.1 标定前的准备工作

别急着写代码。先确认三件事:

  • 标定板规格:你用的是棋盘格还是圆点阵?我建议初学者用棋盘格,角点检测更稳定。
  • 图像数量:至少15-20张。太少的话,畸变参数容易跑偏。
  • 覆盖范围:图像要覆盖视场的四个角和中心区域。我曾经只拍了中心区域,结果边缘畸变完全没校正过来。
我的习惯:每次标定前,我都会用 disp_caltab 先看一眼标定板在图像中的姿态。如果某个角度明显缺失,就补拍一张。

3.2 图像采集:别让“坏图”混进来

采集图像时,最容易犯的错误是:标定板没完全在视场内。哪怕缺了一个角,角点检测都会失败。

我一般这样操作:

  1. 固定相机,移动标定板。
  2. 每拍一张,快速检查角点是否完整。
  3. 剔除模糊、过曝、反光的图像。

嗯,这里要注意:标定板不要平行于像平面。如果所有图像都是正面拍摄,那焦距和主点会耦合在一起,解不出来。你想想看,一个平面在另一个平面上的投影,信息量是不够的。

避坑指南:我曾经有一批标定图像,全部是标定板正对相机拍的。结果标定出来的焦距偏差了5%以上。后来补拍了倾斜角度,才恢复正常。

3.3 核心流程:从图像到内参

下面这张图,是我自己总结的单目标定流程。每次做项目,我都会对照着走一遍。

单目标定核心流程 1. 读取标定板描述文件 2. 创建标定数据模型 3. 查找标定板角点 4. 收集所有图像数据 5. 执行标定 6. 获取内参并评估误差 关键输入 标定板描述文件 (.descr) 标定板物理尺寸 (mm) 相机初始内参猜测 关键输出 相机内参 (CameraParam) 标定误差 (平均像素误差) 每张图像的外参

3.4 代码实战:一步一步来

下面这段代码,是我从实际项目中抽出来的。注释里写了我当时踩的坑。

* 1. 读取标定板描述文件
* 注意:文件路径不要有中文,否则 HALCON 会报错
create_caltab (7, 7, 0.0125, 'caltab.descr')

* 2. 创建标定数据模型
* 我习惯用 'area_scan_division' 模型,畸变参数少,收敛快
create_calib_data ('calibration_object', 'area_scan_division', 1, 1, CalibDataID)

* 3. 设置标定板描述文件
set_calib_data_calib_object (CalibDataID, 0, 'caltab.descr')

* 4. 循环读取图像,查找角点
* 这里我用了 20 张图像,覆盖了视场的各个区域
for i := 1 to 20 by 1
    read_image (Image, 'calib_images/image_' + i$'02d')
    
    * 查找标定板角点
    * 如果找不到,HALCON 会返回一个空数组
    find_caltab (Image, CaltabRegion, 'caltab.descr', 0, 1, 0.5)
    find_marks_and_pose (Image, CaltabRegion, 'caltab.descr', 0, 0.5, 0.2, 0.1, 'true', RCoord, CCoord, StartPose)
    
    * 检查角点数量是否正确
    * 7x7 的棋盘格,应该有 49 个角点
    if (|RCoord| == 49)
        * 将角点坐标和初始位姿加入标定数据
        set_calib_data_observ_points (CalibDataID, 0, 0, i-1, RCoord, CCoord, 'all', StartPose)
    else
        * 如果角点数量不对,跳过这张图像
        * 我曾经遇到过标定板反光导致角点丢失的情况
        continue
    endif
endfor

* 5. 执行标定
* 这一步会迭代优化内参和外参
calibrate_cameras (CalibDataID, Errors)

* 6. 获取标定结果
get_calib_data (CalibDataID, 'camera', 0, 'params', CameraParam)
get_calib_data (CalibDataID, 'calib_obj_pose', [0, 0], 'pose', Pose)

* 输出内参
* CameraParam 的格式:[焦距, 畸变系数k1, 畸变系数k2, 主点x, 主点y, 宽高比]
disp_message (WindowHandle, 'Camera Parameters: ' + CameraParam, 'window', 12, 12, 'black', 'true')

* 输出平均误差
* 如果误差大于 0.5 像素,建议重新标定
disp_message (WindowHandle, 'Mean Error: ' + Errors[0] + ' pixels', 'window', 12, 40, 'black', 'true')

3.5 参数解读:内参到底长什么样?

标定完成后,CameraParam 是一个包含 6 个元素的数组。我整理了一个表格,方便你对照查看:

索引 参数名称 说明 典型值范围
0 焦距 (f) 单位:米。实际值 = 像素焦距 × 像元尺寸 0.002 ~ 0.050
1 畸变系数 k1 径向畸变的一阶系数 -0.5 ~ 0.5
2 畸变系数 k2 径向畸变的二阶系数 -0.5 ~ 0.5
3 主点 x (cx) 单位:像素。理想情况下是图像宽度的一半 图像宽度/2 ± 20
4 主点 y (cy) 单位:像素。理想情况下是图像高度的一半 图像高度/2 ± 20
5 宽高比 (sx) 通常为 1.0,如果像素是正方形 0.99 ~ 1.01
重点:主点 (cx, cy) 如果偏离图像中心超过 30 像素,说明标定板没有覆盖到视场边缘。我建议重新采集图像,确保四个角都有标定板出现。

3.6 误差评估:标定结果好不好?

calibrate_cameras 会返回一个 Errors 数组。第一个元素是所有图像的平均重投影误差,单位是像素。

我的经验是:

  • 误差 < 0.3 像素:标定质量很好,可以直接用于高精度测量。
  • 误差 0.3 ~ 0.5 像素:可以接受,但建议检查是否有图像质量不佳。
  • 误差 > 0.5 像素:需要重新标定。常见原因是标定板不平整、图像模糊、或者角点检测有偏差。
避坑指南:我曾经遇到过一次,误差只有 0.2 像素,但实际测量偏差很大。后来发现是标定板本身有翘曲。所以,标定板的物理质量也很重要。

3.7 快速复用:把标定流程封装成函数

如果你经常做标定,我建议把上面的代码封装成一个函数。这样换项目时,只需要改一下标定板参数和图像路径就行。

* 封装函数:calibrate_single_camera
* 输入:标定板描述文件路径、图像文件夹路径、图像数量
* 输出:相机内参、平均误差
procedure calibrate_single_camera (DescrFile, ImageDir, NumImages : CameraParam, MeanError)
    
    create_calib_data ('calibration_object', 'area_scan_division', 1, 1, CalibDataID)
    set_calib_data_calib_object (CalibDataID, 0, DescrFile)
    
    for i := 1 to NumImages by 1
        read_image (Image, ImageDir + '/image_' + i$'02d')
        find_caltab (Image, CaltabRegion, DescrFile, 0, 1, 0.5)
        find_marks_and_pose (Image, CaltabRegion, DescrFile, 0, 0.5, 0.2, 0.1, 'true', RCoord, CCoord, StartPose)
        
        if (|RCoord| == 49)
            set_calib_data_observ_points (CalibDataID, 0, 0, i-1, RCoord, CCoord, 'all', StartPose)
        endif
    endfor
    
    calibrate_cameras (CalibDataID, Errors)
    get_calib_data (CalibDataID, 'camera', 0, 'params', CameraParam)
    MeanError := Errors[0]
    
    return ()
endprocedure

这样,下次做项目时,一行代码就能完成标定:

calibrate_single_camera ('caltab.descr', 'calib_images', 20, CameraParam, MeanError)

嗯,到这里,单目标定的完整流程就走完了。从图像采集到内参输出,每一步我都尽量讲清楚了我自己的做法和踩过的坑。你可以在自己的项目里直接复用这段代码,只要改一下标定板参数和图像路径就行。