3、单目标定流程:从图像采集到内参输出,完整走一遍 calibrate_cameras 算子
单目标定,说白了就是让相机知道自己的“眼睛”长什么样——焦距、畸变、主点位置。很多新手一上来就调算子参数,结果标定出来偏差很大。我个人的习惯是:先把流程走通,再谈优化。
这一章,我们就用 calibrate_cameras 算子,把单目标定的完整流程跑一遍。从采集标定板图像开始,到最终输出相机内参,每一步我都会讲清楚“为什么这么做”以及“我踩过的坑”。
3.1 标定前的准备工作
别急着写代码。先确认三件事:
- 标定板规格:你用的是棋盘格还是圆点阵?我建议初学者用棋盘格,角点检测更稳定。
- 图像数量:至少15-20张。太少的话,畸变参数容易跑偏。
- 覆盖范围:图像要覆盖视场的四个角和中心区域。我曾经只拍了中心区域,结果边缘畸变完全没校正过来。
我的习惯:每次标定前,我都会用
disp_caltab 先看一眼标定板在图像中的姿态。如果某个角度明显缺失,就补拍一张。
3.2 图像采集:别让“坏图”混进来
采集图像时,最容易犯的错误是:标定板没完全在视场内。哪怕缺了一个角,角点检测都会失败。
我一般这样操作:
- 固定相机,移动标定板。
- 每拍一张,快速检查角点是否完整。
- 剔除模糊、过曝、反光的图像。
嗯,这里要注意:标定板不要平行于像平面。如果所有图像都是正面拍摄,那焦距和主点会耦合在一起,解不出来。你想想看,一个平面在另一个平面上的投影,信息量是不够的。
避坑指南:我曾经有一批标定图像,全部是标定板正对相机拍的。结果标定出来的焦距偏差了5%以上。后来补拍了倾斜角度,才恢复正常。
3.3 核心流程:从图像到内参
下面这张图,是我自己总结的单目标定流程。每次做项目,我都会对照着走一遍。
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)
嗯,到这里,单目标定的完整流程就走完了。从图像采集到内参输出,每一步我都尽量讲清楚了我自己的做法和踩过的坑。你可以在自己的项目里直接复用这段代码,只要改一下标定板参数和图像路径就行。