4. 畸变校正评估:如何通过校正后的图像直线度来评估畸变校正效果
标定做完,内参外参都算出来了,接下来最要紧的一件事——验证畸变校正到底行不行。
我见过不少项目,标定误差看着挺小,0.1 个像素都不到,结果一校正图像,直线全弯了。为什么?因为标定板上的点拟合得好,不代表图像边缘的畸变被完美消除了。说白了,标定误差是数值指标,直线度才是视觉指标。
这一章,我就带你走一遍用直线度评估畸变校正效果的方法,顺便把 map_image 这个关键算子讲透。
4.1 为什么直线度是畸变校正的“照妖镜”
畸变校正的本质,是把图像里弯曲的直线拉直。所以最直接的验证方式,就是找一张校正后的图像,在上面取几条本该是直线的边缘,量一量它们到底有多直。
你想想看,如果校正参数有偏差,图像中心区域可能还好,但边缘区域——尤其是四个角附近——直线会变成 S 形或者弧形。肉眼有时候看不出来,但用算法一量,偏差就现原形了。
我在一个 PCB 检测项目里就吃过这个亏。当时标定误差 0.08 像素,觉得稳了。结果用校正后的图像去测焊盘间距,边缘的测量值总是跳。后来一查,是畸变校正没到位,图像四角的直线偏了将近 2 个像素。从那以后,直线度验证就成了我标定流程里的固定环节。
4.2 核心工具:map_image 到底怎么用
在 HALCON 里做畸变校正,绕不开 map_image。它的作用是把原始图像按照一张“映射表”重新采样,生成校正后的图像。
基本用法很简单:
* 读取标定结果
read_cam_par('calib_data/camera_params.dat', CamParam)
read_calib_data('calib_data/calib_result.dat', CalibData)
* 生成映射表
gen_radial_distortion_map(Map, CamParam, 'calibrated', 'bilinear')
* 执行校正
map_image(Image, Map, ImageRectified)
这里有个关键点——gen_radial_distortion_map 的第三个参数。用 'calibrated' 表示输出校正后的图像,用 'undistorted' 表示输出无畸变图像。两者区别在于:
| 参数值 | 输出效果 | 适用场景 |
|---|---|---|
'calibrated' |
保持原始焦距,只消除畸变 | 测量、定位等需要保持尺度一致的任务 |
'undistorted' |
同时消除畸变并归一化焦距 | 多视角拼接、三维重建 |
我个人习惯在标定验证阶段用 'calibrated',因为这样能直接对比校正前后的图像,看直线度的改善情况。
'bilinear' 改成 'bicubic'。虽然慢一点,但边缘质量好很多。
4.3 直线度评估的完整流程
好,现在图像校正完了,怎么量化直线度?我一般分四步走:
- 选线:在图像中找一条理论上应该是直线的边缘(比如标定板的边缘、工件的直边、或者特意放置的直线标靶)。
- 提取亚像素轮廓:用
edges_sub_pix提取边缘,得到一系列点坐标。 - 拟合直线:用
fit_line_contour_xld拟合一条理想直线。 - 计算偏差:计算每个边缘点到拟合直线的距离,取最大值或均方根误差作为直线度指标。
代码实现大概是这样的:
* 提取边缘
edges_sub_pix(ImageRectified, Edges, 'canny', 1.5, 20, 40)
* 选择一条直线边缘(假设是第一条)
select_obj(Edges, LineEdge, 1)
* 拟合直线
fit_line_contour_xld(LineEdge, 'tukey', -1, 0, 5, 2, RowBegin, ColBegin, RowEnd, ColEnd, Nr, Nc, Dist)
* 计算每个点到直线的距离
distance_contours_xld(LineEdge, LineEdge, 'point_to_line', Distances)
* 获取最大偏差和均方根误差
tuple_max(Distances, MaxDev)
tuple_mean(Distances, MeanDev)
tuple_deviation(Distances, StdDev)
得到 MaxDev 和 StdDev 之后,我一般这样判断:
- MaxDev < 0.3 像素:校正效果优秀,可以用于高精度测量。
- MaxDev 0.3 ~ 0.8 像素:校正效果良好,大多数视觉应用都能接受。
- MaxDev > 0.8 像素:需要检查标定数据,或者重新采集标定图像。
4.4 知识结构:畸变校正评估的核心逻辑
下面这张图把整个评估流程串起来了,你可以对照着看:
4.5 避坑指南:我踩过的几个坑
直线度评估看起来简单,但实际操作中容易翻车。我把自己踩过的坑列出来,你遇到了可以少走弯路:
- 别用标定板上的圆点来评估直线度。圆点中心拟合出来的直线,精度受圆点提取算法影响,不能真实反映畸变校正效果。我建议用标定板的物理边缘,或者单独放一把直尺在视野里。
- 注意图像边缘的插值伪影。校正后的图像边缘区域,因为重采样,可能会出现 0.1~0.2 像素的锯齿。这不是畸变校正的问题,是插值算法的固有限制。评估时可以把边缘区域排除掉,或者用
'bicubic'插值减轻这个问题。 - 多测几条线,别只测一条。我一般会在图像的上、下、左、右、四个角各取一条直线,共 8 条线,取它们的最大偏差作为最终指标。这样能覆盖整个视场。
- 曾经有一次,我测出来的直线度一直很好,但实际测量精度就是上不去。后来发现,我选的直线都在图像中心区域,边缘的畸变根本没测到。从那以后,我强制自己每次评估都选边缘的线。
4.6 进阶:自动化直线度评估脚本
如果你需要批量验证多组标定结果,可以写一个自动化脚本。核心思路是:
- 在图像中预设几个 ROI,覆盖不同区域。
- 在每个 ROI 内自动提取最长的直线边缘。
- 计算每条线的直线度,输出统计报告。
代码骨架如下:
* 定义ROI区域(假设图像尺寸为 2448x2048)
ROIs := [gen_rectangle1(0, 0, 200, 200), * 左上角
gen_rectangle1(0, 2248, 200, 2448), * 右上角
gen_rectangle1(1848, 0, 2048, 200), * 左下角
gen_rectangle1(1848, 2248, 2048, 2448)] * 右下角
* 遍历每个ROI
for i := 1 to 4 by 1
reduce_domain(ImageRectified, ROIs[i], ImageROI)
edges_sub_pix(ImageROI, Edges, 'canny', 1.5, 20, 40)
* 取最长的边缘
length_xld(Edges, Lengths)
tuple_sort_index(Lengths, Indices)
select_obj(Edges, LongestEdge, Indices[|Indices|-1])
* 拟合直线并计算偏差
fit_line_contour_xld(LongestEdge, 'tukey', -1, 0, 5, 2, _, _, _, _, _, _, _)
distance_contours_xld(LongestEdge, LongestEdge, 'point_to_line', Distances)
tuple_max(Distances, MaxDev)
* 记录结果
Results[i-1] := MaxDev
endfor
这样跑一遍,你就能得到整个视场的直线度分布。如果某个区域的偏差明显偏大,说明那个区域的畸变校正没做好,可能需要重新采集该区域的标定图像。
map_image 校正图像,用 edges_sub_pix + fit_line_contour_xld 量化直线度,用边缘区域的直线作为评估样本。记住,直线度才是畸变校正效果的最终裁判。