相机模型:面阵相机 + 透视镜头,畸变模型选用
area_scan_polynomial(含径向 K1/K2/K3 与偏心 P1/P2)涉及算子:change_radial_distortion_cam_par、gen_radial_distortion_map、map_image、change_radial_distortion_image、change_radial_distortion_points、zoom_image_factor涉及过程库:procedures/general/camera_parameters.hdpl------gen_cam_par_*、get_cam_par_data、set_cam_par_data、get_cam_par_names都是 MVTec 标准过程(procedure)
- 算子参考:
doc/html/reference/operators/*.htmlexamples/solution_guide/3d_vision/radial_distortion.hdev------ 去畸变全流程 + 四种Mode的官方示范examples/hdevelop/Calibration/Rectification/change_radial_distortion_points.hdev------ polynomial 模型的真实系数examples/hdevelop/Calibration/Rectification/lens_distortions.hdev------ division 模型的真实系数 说明:文中算子签名均按上述本地 26.05 文档核对过。粘贴到 HDevelop 后请先核对本机图库路径(HALCONROOT/examples/images)再运行。
一、为什么必须校正畸变
理想的小孔成像模型(针孔模型)假设光线笔直穿过焦点落到感光面上,于是「像素坐标」和「物理尺寸」之间是严格的线性关系:
- 直线在图像上还是直线;
- 图像中心就是光轴;
- 同一个物理平面上的正方形,拍出来还是正方形。
真实镜头做不到这一点。镜筒为了控制体积和成本,前后镜片组不可能完美共轴,中间又有一堆厚度不均的玻璃,结果就是不同视场位置的光线偏折角度不一样。这带来两个直接后果:
- 几何变形:直线变弯,画面边缘被拉长或压缩;
- 测距错误:同一个物理点,在画面中央和画面角落算出来的距离不一样------这不是镜头模糊,是模型本身错了。
畸变模型描述的是「光学系统本身的行为」,所以:
- 标定阶段必须启用畸变。不把畸变算进去,标定出来的内参会用一堆残差去补偿畸变,标定板在画面边缘的误差明显偏大;
- 测量 / 定位阶段必须校正畸变。拍照点算出来的物理坐标是错的,抓取就会偏;
- 纯外观任务(只看有没有料、有无明显缺陷,且位置不敏感)可以不校正,性能省一大截。
一句话:畸变是镜头的固有属性,标定负责把它测出来,畸变校正负责把它去掉。
二、畸变模型长什么样
HALCON 把畸变系数分成两组,官方定义(引自 doc/html/reference/operators/toc_calibration.html):
- K1, K2, K3 ------ "Distortion coefficients to model the radial distortions"(径向,3 个)
- P1, P2 ------ "Distortion coefficients to model the decentering distortions"(偏心,2 个)
术语提醒 :HALCON 官方用词是 decentering(偏心 / 离心) ,国内资料习惯叫「切向畸变」。指同一类误差(镜片组不共轴导致的非对称偏移),但在手册里搜 tangential 搜不到 ,要搜 decentering。
径向部分就是经典的多项式------K1 乘 r²、K2 乘 r⁴、K3 乘 r⁶:
x_d = x_u · (1 + K1·r² + K2·r⁴ + K3·r⁶) + (P1/P2 偏心项)
y_d = y_u · (1 + K1·r² + K2·r⁴ + K3·r⁶) + (P1/P2 偏心项)
其中 x_u, y_u 是无畸变的理想坐标,x_d, y_d 是有畸变坐标,r² = x_u² + y_u² 是到光轴的归一化距离平方。
偏心项(P1/P2)的确切交叉项本文不复述:HALCON 手册里这组公式是以**公式图片(SVG)**渲染的,本机 HTML 文档抽不出文字,逐字照抄容易记错。要用精确形式时以手册原文为准。工程上记住三点就够:
- 手册里这组公式的方向是「有畸变 → 无畸变」(distorted 到 undistorted),与本文下文「无畸变 → 有畸变」的表格互为反函数;
- 径向项只与半径
r有关,所以畸变是对称的; - 偏心项与
x、y各自的值有关,所以是非对称的。
各类系数的真实取值(取自官方示例,见下表下方的说明):
| 系数 | 官方名称 | 数量 | 官方示例里的真实取值 | 主要成因 |
|---|---|---|---|---|
| K1 | radial(径向) | 1 | 5142.64 | 主项,镜片曲率失配 |
| K2 | radial(径向) | 1 | −1.98485e+08 | 高阶球差 |
| K3 | radial(径向) | 1 | −4.26734e+12 | 更高阶,广角 / 鱼眼才明显 |
| P1 | decentering(偏心) | 1 | 0.0462524 | 镜片组不完全共轴(光轴歪了) |
| P2 | decentering(偏心) | 1 | 0.114147 | 同上 |
| Kappa | radial(径向,仅 division 模型) | 1 | −78129.2 | division 模型唯一的畸变系数 |
量级一定要小心,不要凭印象 :上表数字来自官方示例------polynomial 系取自 change_radial_distortion_points.hdev,division 系取自 lens_distortions.hdev。可以看到 K1 = 5142.64、K2 = −1.98e+08、K3 = −4.27e+12、Kappa = −78129.2,全是大数 ,不是 1e-2 / 1e-3 这种小数。原因在于 HALCON 内部用的是自己的归一化坐标约定,所以这些系数与 OpenCV / MATLAB 里的 k1、k2 数值不能通用 ,也不能「凭经验先填个 −0.05 试试」。真实项目里只有一个正确来源:calibrate_cameras 的返回值 / get_calib_data 读出的标定结果。
径向畸变让直线保持对称但弯曲 ,视觉上就是桶形(barrel)或枕形(pincushion)。偏心畸变没有对称性,表现为画面边缘被斜向拉扯,这是现场「标定板明明是正方形、检测出来却是平行四边形」的常见原因之一。
不要凭系数的正负号推断是桶形还是枕形------不同模型、不同归一化约定下符号含义会变。要确认方向就实测:用第七 / 第八节的规则网格图走一遍,弯曲方向一眼可见。
三、HALCON 的相机参数模型怎么选
HALCON 不用「pinhole」这种泛称,而是用相机类型字符串区分模型。畸变校正的第一步就是选对类型。
| 相机类型 | 参数序列(下标 1 起) | 畸变项个数 |
|---|---|---|
| area_scan_division | Focus, Kappa, Sx, Sy*, Cx, Cy, ImageWidth, ImageHeight | 1(kappa) |
| area_scan_polynomial | Focus, K1, K2, K3, P1, P2, Sx, Sy*, Cx, Cy, ImageWidth, ImageHeight | 5 |
| area_scan_telecentric_division | Magnification, Kappa, Sx, Sy*, Cx, Cy, ImageWidth, ImageHeight | 1(kappa) |
| area_scan_telecentric_polynomial | Magnification, K1, K2, K3, P1, P2, Sx, Sy*, Cx, Cy, ImageWidth, ImageHeight | 5 |
| line_scan_polynomial | Focus, K1, K2, K3, P1, P2, Sx*, Sy*, Cx, Cy, ImageWidth, ImageHeight, Vx, Vy, Vz | 5 |
带
*的参数是固定值、不参与估计 :Sy*表示当 Sx 与 Sy 相等时 HALCON 只估 Sx;线阵的 Sx*、Sy* 都固定。上表序列引自toc_calibration.html的 "Camera Model" 一节,注意官方参数名是ImageWidth/ImageHeight,不是 W / H。
26.05 的相机类型远不止这 5 种。 除了上面的透视 / 远心,还有倾斜镜头(area_scan_tilt_*)、双侧远心 / 物方远心 / 像方远心(area_scan_tilt_bilateral_telecentric_* 等)、超心镜头(area_scan_hypercentric_*)、以及配套的线阵系列。选型时先把镜头对号入座 :倾斜镜头要用带 tilt 的类型(多出 ImagePlaneDist / Tilt / Rot 三个参数),超心镜头要用 hypercentric,线阵用 line_scan_*。完整列表见手册 "Camera Model" 一节。
选型经验:
area_scan_division:只有一个径向系数 κ,模型极简单,反畸变解析可算,不用迭代,速度快。但只能拟合对称的径向畸变,拟合不出偏心分量。area_scan_polynomial:含 K1/K2/K3 + P1/P2 共 5 个畸变系数,能描述径向 + 偏心,精度更高,是现在的主流选择。代价是反畸变必须迭代求解,慢一些,且不能外推------标定板没覆盖到的画面边缘区域,畸变建模不可靠。- 远心镜头(
*_telecentric_*) :第 1 个参数不是焦距Focus而是放大倍率Magnification,远心镜头焦距物理上不存在,不能填焦距。
最常见的选型错误 :现场拿远心镜头却按 area_scan_polynomial 填了焦距。症状是标定能收敛但重投影误差怎么都降不下来。这时应改用 area_scan_telecentric_polynomial 并把第 1 个参数填放大倍率。
四、相机参数 tuple 的结构(最容易踩坑的地方)
相机参数在 HALCON 里是一个 tuple,下标 0 是相机类型字符串,不是焦距。这是从 HALCON 13 起的约定,官方文档明确写了「the information about the camera type is contained in the first element of CameraParam」。
| 下标 | 含义(area_scan_polynomial) | 按名取值 |
|---|---|---|
| 0 | 相机类型字符串 | camera_type |
| 1 | Focus(焦距,远心模型为 Magnification) | focus |
| 2 | K1(径向) | k1 |
| 3 | K2(径向) | k2 |
| 4 | K3(径向) | k3 |
| 5 | P1(偏心 decentering) | p1 |
| 6 | P2(偏心 decentering) | p2 |
| 7 | Sx(像元宽) | sx |
| 8 | Sy(像元高) | sy |
| 9 | Cx(主点列坐标) | cx |
| 10 | Cy(主点行坐标) | cy |
| 11 | ImageWidth(图像宽) | image_width |
| 12 | ImageHeight(图像高) | image_height |
即 area_scan_polynomial 的完整 tuple 共 13 个元素 :1 个类型字符串 + 12 个数。下标别只记到 cx 就停,后面还有 cy / image_width / image_height ------ gen_radial_distortion_map 的输出图尺寸要靠它们来算。
所以这段代码是错的:
*错:Focus 不是下标 0,下标 0 是 'area_scan_polynomial' 这个字符串
Focus := CamParam[0]
正确做法有两条路。
路线 A:用下标(快,但要自己记住模型对应关系)
*对 area_scan_polynomial:下标 1 = Focus
Focus := CamParam[1]
K1:= CamParam[2]
K2 := CamParam[3]
路线 B:用库函数按名字读写(推荐,模型无关)
HALCON 把这些按名读写的过程 放在过程库里(本机 26.05 是 procedures\general\camera_parameters.hdpl;.hdpl 是过程库、.hdvp 是单过程文件,如 list_image_files.hdvp),换模型也不用改代码:
*按名取值:ParamName 既可以是单个名字,也可以是名字 tuple,返回值同样是 tuple
get_cam_par_data (CamParam, ['image_width', 'image_height'], ImageSize)
get_cam_par_data (CamParam, 'focus', Focus)
*division 模型只有 kappa;polynomial 模型才有 k1/k2/k3/p1/p2 ------ 名字与模型不匹配会直接抛异常
get_cam_par_data (CamParam, 'kappa', Kappa)
get_cam_par_data (CamParam, 'camera_type', CameraType)
这段代码做了什么:
get_cam_par_data按参数名而不是下标取值;- 传名字 tuple 就一次取多个值,返回值也是 tuple(
ImageSize=[ImageWidth, ImageHeight]); focus取焦距,camera_type取模型字符串(等于 tuple 下标 0);kappa取 division 模型的一阶径向系数。
合法参数名只有这些 (引自过程自带的说明):camera_type、focus、magnification、kappa、k1、k2、k3、p1、p2、image_plane_dist、tilt、rot、sx、sy、cx、cy、image_width、image_height、vx、vy、vz。说明里同时写明:「If the camera type does not support the requested value, an exception is raised.」 所以 get_cam_par_data (CamParam, 'k1', K1) 与 get_cam_par_data (CamParam, 'kappa', Kappa) 不能对同一份 CamParam 同时调用------本文初稿把这两句并排写在一段代码里,是错的。
为什么推荐路线 B :area_scan_division 的畸变段只有 1 个系数(kappa),area_scan_polynomial 有 5 个(k1/k2/k3/p1/p2),area_scan_telecentric_polynomial 第 1 个还从 Focus 变成了 Magnification。用下标写死的代码一旦换模型就全错,用名字就完全免疫。
路线 C:用 gen_cam_par_* 构造 tuple(「创建」相机参数时用这条)
官方示例里构造相机参数用的都是过程 gen_cam_par_area_scan_polynomial(division 模型则用 gen_cam_par_area_scan_division),参数顺序与 tuple 顺序一一对应:
*官方示例 change_radial_distortion_points.hdev 里的真实例子
gen_cam_par_area_scan_polynomial (0.00506526, 5142.64, -1.98485e+08, -4.26734e+12, \
0.0462524, 0.114147, 9.30055e-06, 9.3e-06, \
276.713, 266.078, 640, 512, CamParam)
比手写 ['area_scan_polynomial', ...] 安全的地方在于:手写 tuple 少写一个元素或顺序记错,构造阶段不会报错,要等到后面算子算出离谱结果才发现。
get_cam_par_data / set_cam_par_data / get_cam_par_names 三个都是过程而不是算子 ,在算子参考手册(doc/html/reference/operators)里查不到。HDevelop 启动时默认已挂载 MVTec 标准过程,所以官方示例可以直接调用(radial_distortion.hdev 就是这么用的);但在 HDevEngine、或导出成 C++ / C# 的场景,必须把 camera_parameters 这个过程库显式带上。
五、change_radial_distortion_cam_par ------ 换一套畸变系数
这是畸变校正的第一棒:根据「想要什么样的畸变」,算出一份新的相机参数。
注意参数顺序:官方签名是
change_radial_distortion_cam_par( : : Mode, CamParamIn, DistortionCoeffs : CamParamOut)
即 Mode** 在第 1 位,CamParamIn 在第 2 位,输出 CamParamOut 在最后**。这个算子没有「输出参数在第一位」的写法,凭记忆写很容易写成 change_radial_distortion_cam_par(NewCamParam, ...) 然后报参数个数错误。
*把 area_scan_polynomial 相机参数的第 1 组畸变系数改成全 0
change_radial_distortion_cam_par ('adaptive', CamParam, [0,0,0,0,0], CamParamNoDist)
| 参数 | 类型 | 含义 | 默认值 |
|---|---|---|---|
| Mode | input, string | 模式,取值见下表 | adaptive |
| CamParamIn | input, campar | 原相机参数 | --- |
| DistortionCoeffs | input, real(-array) | 期望的畸变系数,元素个数必须为 1 或 5 | 0.0 |
| CamParamOut | output, campar | 修改畸变后的相机参数(输出在最后) | --- |
这段代码做了什么:取 CamParam 的第 0 组相机参数,把它的 5 个畸变系数(k1/k2/k3/p1/p2)全置 0,输出一份新的 CamParamNoDist。语义就是「假设这块玻璃是理想的、无畸变」,后面拿它当 CamParamOut 就能把畸变图掰直。
Mode 决定「掰直之后画面多大」,四种取值的取舍:
| Mode | 改了哪些参数 | 可见区域 | 未定义像素 |
|---|---|---|---|
| fixed | 只改畸变系数,其他内参不动 | 会变(手册原文:leads to a change of the visible part) | 手册未提及 |
| fullsize | 改缩放因子 Sx/Sy + 图像中心点 Cx/Cy(线阵远心还会改运动分量 Vy) | 原图全部可见点仍可见 | 「In general, this leads to undefined pixels」 |
| adaptive | 与 fullsize 改同样这些参数 |
视场略微缩小 | 「to prevent undefined pixels」(避免产生) |
| preserve_resolution | 同 fullsize,并额外增大图像尺寸 |
原图全部可见点仍可见 | 同 fullsize |
上表最后一列的措辞直接引自算子手册,不要自行脑补:
'fixed'的手册描述里没有 关于未定义像素的保证,只有'fullsize'明确写了「一般会产生未定义像素」。
这是实际工程里最该纠结的一个参数:
- 想要校正后不丢视野 (原图四角的内容都能看到)→ 用
'fullsize',代价是校正后图像四周会出现未定义像素(黑边); - 想要不要黑边 → 用
'adaptive',视场略微缩小一点,边缘用有效像素填满; - 想要不丢视野 + 不降分辨率 → 用
'preserve_resolution',输出图像会变大。
一个容易忽略的硬约束 :DistortionCoeffs 的元素个数必须是 1 或 5 。area_scan_division 模型只吃 1 个(就是 kappa),area_scan_polynomial 吃 5 个。给 3 个或者给 4 个会直接抛异常。
1 系数模型在官方示例里是直接给标量 ,不是 [0]:
*lens_distortions.hdev:division 模型,只给 1 个系数
change_radial_distortion_cam_par ('adaptive', CamParIn, 0, CamParOut)
适用范围 :数组(面阵)相机无论什么镜头都支持,带远心镜头的线阵相机也支持 ;透视镜头的线阵相机不支持 。这与 gen_radial_distortion_map(只支持面阵)是两回事,别记混。
官方示例 radial_distortion.hdev 在同一份相机参数上把四种 Mode 依次跑了一遍,还顺带演示了不改系数的等价写法:
*radial_distortion.hdev:用 set_cam_par_data 直接改 kappa,与 change_radial_distortion_cam_par('fixed', ...) 等价
CamParVirtualFixed := CamParOriginal
set_cam_par_data (CamParVirtualFixed, 'kappa', 0, CamParVirtualFixed)
change_radial_distortion_cam_par ('fixed', CamParOriginal, 0, CamParVirtualFixed)
六、gen_radial_distortion_map ------ 生成映射图(本文最关键的一节)
这个算子的签名是全文最容易搞错的地方,官方定义是:
gen_radial_distortion_map( : Map : CamParamIn, CamParamOut, MapType : )
参数表(上一节末尾那张错位的表格本来就是它的,这里归位):
| 参数 | 类型 | 含义 | 默认值 |
|---|---|---|---|
| Map | output, image | 映射数据(int4 / int8 / uint2 / vector_field_absolute) | --- |
| CamParamIn | input, campar | 决定输入图尺寸的那份相机参数 | --- |
| CamParamOut | input, campar | 决定输出图尺寸的那份相机参数 | --- |
| MapType | input, string | 映射类型:nearest_neighbor / bilinear / coord_map_sub_pix | bilinear |
它要两份相机参数,不是一份。 语义是:
输出图的每个像素,先用
CamParamOut变换到像平面,再投影到CamParamIn图像上的一个亚像素位置。
由此得到两条必须记住的结论:
| 目的 | CamParamIn |
CamParamOut |
输出图尺寸由谁决定 |
|---|---|---|---|
| 去畸变(校正) | 带畸变(原图) | 零畸变(CamParamNoDist) |
CamParamOut,通常与原图同尺寸 |
| 造畸变(模拟) | 零畸变(理想图) | 带畸变(CamParam) |
CamParamOut |
方向是由「位置」决定的,不是由「参数本身」决定的。 一个常见误解是「传带畸变参数就是造畸变」------错。正确说法是:CamParamIn 是原图、CamParamOut 是目标图;原图有畸变、目标图无畸变,才叫校正。
两个配套的尺寸规则也要记住:
- 输出图尺寸由
CamParamOut决定; - 输入图尺寸由
CamParamIn决定。
所以去畸变时输出图与原图尺寸是否相同,取决于所选的 Mode:'preserve_resolution' 会让 CamParamOut 的宽高变大,输出图跟着变大;'adaptive' 也会改缩放因子,所以拿不准就用 get_cam_par_data (CamParamOut, 'image_width', W) 直接读。
三种 MapType 的取舍:
| MapType | Map 通道数 | map_image 行为 | 适用场景 |
|---|---|---|---|
| nearest_neighbor | 1 通道(int4) | 不插值,取最近邻灰值,最快 | 实时性优先、畸变小 |
| bilinear | 5 通道(1×int4 + 4×uint2) | 双线性插值,边缘更平滑 | 常规首选(默认) |
| coord_map_sub_pix | vector_field_absolute | 存亚像素坐标 | 唯一支持 OpenCL 设备 |
通道数决定了 map_image 会不会插值 ,这是 MapType 最实际的影响:
'nearest_neighbor'→Map是 1 通道 ,每个输出像素直接取输入图最近邻的灰值,不插值,速度快;'bilinear'→Map是 5 通道 :第 1 通道是左上角参考点的线性化坐标,第 2~5 通道是四个邻居的插值权重,做双线性插值,边缘更平滑;'coord_map_sub_pix'→Map是vector_field_absolute类型的矢量场图,存亚像素坐标,也是唯一能在 OpenCL 计算设备上跑的 Map 类型。
注意这个算子只能用于面阵相机(area scan) 。线阵相机(line scan)用不了,除了远心线阵相机可以在配套场景下用 change_radial_distortion_cam_par。
性能提示 :官方文档明确指出,如果多张图要用同一套相机参数做映射,gen_radial_distortion_map + map_image 的组合远优于 change_radial_distortion_image,因为变换只算一次 。这就是「生成映射图 + 复用」这套流程存在的意义,不要写成「每张图调一次 change_radial_distortion_image」。
如果要跨程序复用映射图,可以用 write_image 存成 TIFF (多通道 tiff)保存,读回来直接给 map_image 用。
⚠️ map_image 不做越界检查(会直接崩) :手册在 Attention 一节里明确写了------「For runtime reasons during the mapping process, it is not checked whether the linearized coordinates lie inside the input image. Thus, it must be ensured by the user that this constraint is fulfilled... Otherwise, the program may crash!」。由 gen_radial_distortion_map 生成的 Map 通常已经保证坐标落在原图内;但自己拼的 Map (手改坐标、或别的途径生成的映射)一旦越界,map_image 是崩程序 而不是给你一张带黑边的图。用 'bilinear' 时还要额外保证参考点右侧、下方、右下方的邻居像素也都在图内。
顺带记住这一家的三个兄弟算子 (都吃 (..., CamParamIn, CamParamOut) 两份相机参数):
| 算子 | 处理对象 | 何时用 |
|---|---|---|
change_radial_distortion_image |
图像(多一个 Region 入参) |
只校正一两张图 |
change_radial_distortion_contours_xld |
XLD 轮廓 | 轮廓直接校正,不重采样整幅图 |
change_radial_distortion_points |
亚像素点坐标 | 已提取的点(标定板标记点、量测点) |
change_radial_distortion_image 的第 2 个入参类型是 Region (不是图像),它决定校正哪块区域------这是最容易写错的地方。官方示例里图省事直接传了整幅图像,因为图像的 domain 会被当作 region 用:
*radial_distortion.hdev:按目标相机参数算出输出图尺寸,造覆盖整幅的 ROI
get_cam_par_data (CamParVirtualFullsize, 'image_width', WidthVirtualFullsize)
get_cam_par_data (CamParVirtualFullsize, 'image_height', HeightVirtualFullsize)
gen_rectangle1 (ROI, 0, 0, HeightVirtualFullsize - 1, WidthVirtualFullsize - 1)
change_radial_distortion_image (GrayImage, ROI, ImageRectifiedFullsize, CamParOriginal, CamParVirtualFullsize)
注意那两个相机参数的顺序与 gen_radial_distortion_map 一致 :(输入图用哪份, 输出图用哪份),即原图 CamParamOriginal 在前、目标 CamParamVirtual* 在后。
这同时就是「'fullsize' 产生黑边后怎么裁」的官方做法:先按目标相机参数算出输出图宽高,再用覆盖整幅的 ROI 去校正。
七、去畸变:三步走
三步分别是「换参数 → 生成映射 → 按映射重采样」。
*===============================================================
* 第一步:算出一份零畸变的相机参数
*===============================================================
change_radial_distortion_cam_par ('adaptive', CamParam, [0,0,0,0,0], CamParamNoDist)
*===============================================================
* 第二步:生成校正映射图
* CamParamIn = 带畸变(原图) CamParamOut = 零畸变(目标)
* ===============================================================
gen_radial_distortion_map (Map, CamParam, CamParamNoDist, 'bilinear')
*===============================================================
* 第三步:按映射图重采样,得到校正后的图
* ===============================================================
map_image (Image, Map, ImageCorrected)
分步讲解:
- 第一步 用
'adaptive'模式换一份零畸变参数。选'adaptive'而不是'fullsize',是为了避免校正后四周出现未定义像素(手册原文:visible part 略微缩小以 prevent undefined pixels)。注意'adaptive'与'fullsize'改的是同一批参数(缩放因子 Sx/Sy、图像中心 Cx/Cy) ,所以不要假设输出图尺寸一定没变------需要尺寸时用get_cam_par_data (CamParamNoDist, 'image_width', W)去读。 - 第二步 把「带畸变的
CamParam」放在CamParamIn位、「零畸变的CamParamNoDist」放在CamParamOut位------顺序不能反 。MapType选'bilinear',Map会是 5 通道。 - 第三步
map_image只有 3 个参数 :Image(原图)、Map(映射图)、ImageMapped(输出)。它没有Interpolation/Padding/Clamping这几个参数------是否插值完全由Map的通道数决定(1 通道不插值,5 通道双线性插值)。
配图说明:校正后画面边缘的网格线应重新变直,原来笔直的标定板轮廓恢复为矩形。
八、造畸变:模拟一块带畸变的镜头
理解去畸变的最好办法是先亲手造一次。同一个算子,把两份参数换个位置:
*CamParamIn = 零畸变(理想图) CamParamOut = 带畸变(目标)
gen_radial_distortion_map (MapSim, CamParamNoDist, CamParam, 'bilinear')
map_image (ImageGrid, MapSim, ImageDistorted)
ImageGrid 用官方 gen_grid_region 生成规则网格,网格图是真值,弯曲多少一眼可判:
*生成无畸变的规则网格图,作为畸变验证的真值
gen_image_const (ImageGrid, 'byte', ImageWidth, ImageHeight)
gen_grid_region (GridRegion, 20, 20, 'lines', ImageWidth, ImageHeight)
paint_region (ImageGrid, GridRegion, ImageGrid, 255, 'fill')
这段代码做了什么:
gen_image_const生成一张全 0 的byte图作底;gen_grid_region按 20 像素步长生成线条 网格(Type='lines');想只要交点就传'points';paint_region把网格画成白色(255),得到一张无畸变的网格真值图。
配图说明 :过一遍 gen_radial_distortion_map (MapSim, CamParamNoDist, CamParam, ...) + map_image 后,网格线应明显变弯:桶形(barrel)= 画面四周被压缩、直线向外弓;枕形(pincushion)= 画面四周被拉伸、直线向内弓 。至于你手上这组系数到底出的是哪一种,实测一眼可判,不要去推算符号(不同模型、不同归一化下符号含义会变)。
顺带验证一件事 :把这两步的参数换回第七节的顺序(CamParam 在前、CamParamNoDist 在后),跑出来的就是「把刚才造出的畸变再掰直」,应当回到规则网格。这一正一反跑通,说明「位置决定方向」这条规律真的吃透了。
九、补视场:zoom_image_factor
Mode='fullsize' 或 'preserve_resolution' 时,输出图四周会有未定义像素。要把有效区域填满,可以按比例放大一次。
*按比例放大输出,把有效区域填满
zoom_image_factor (ImageCorrected, ImageZoom, 1.15, 1.15, 'constant')
| 参数 | 类型 | 含义 | 默认值 |
|---|---|---|---|
| Image | input, image | 输入图像 | --- |
| ImageZoomed | output, image | 缩放后图像 | --- |
| ScaleWidth | input, real | 宽度缩放比例 | 0.5 |
| ScaleHeight | input, real | 高度缩放比例 | 0.5 |
| Interpolation | input, string | bicubic / bilinear / constant / nearest_neighbor / weighted | constant |
**算子名是 zoom_image_factor ,不是 **image_zoom------HALCON 里没有 image_zoom 这个算子。
这段代码做了什么:把 640×512 的校正图按 1.15 倍放大成 736×589,填掉外圈的空白区域。缩放系数不要拍脑袋 :正确做法是回到 CamParamOut 去读它实际的 image_width / image_height('fullsize' / 'preserve_resolution' 会改写这两个值),按「目标尺寸 ÷ 原尺寸」算出比例;或者直接目视------刚好消掉黑边即可。放太多等于白白损失有效视野和分辨率。
更省事的官方替代方案 :干脆不放大,改用 ROI 把有效区域裁出来。官方示例 radial_distortion.hdev 就是这么办的------先按目标相机参数的 image_width / image_height 造一个覆盖整幅的 Region,再交给 change_radial_distortion_image(见第六节末尾的代码)。
容易混的两个算子:
zoom_image_factor(Image, ImageZoomed, ScaleWidth, ScaleHeight, Interpolation)------ 按比例缩放,适合「按比例补视场」;zoom_image_size(Image, ImageZoom, Width, Height, Interpolation)------ 按目标尺寸缩放,适合「统一成固定分辨率送后端算法」。
需求不同,别混用。两者输出位置的名字都不一样(ImageZoomed vs ImageZoom),这是官方文档里就有的不一致,容易看漏。
两个算子的 Interpolation 可选值都是 'bicubic' / 'bilinear' / 'constant' / 'nearest_neighbor' / 'weighted',默认 'constant'。
十、完整示例:畸变校正全流程
把前面几步串起来跑一遍,注意输入和输出的对比展示:
*===============================================================
* 镜头畸变校正
* 运行环境:Halcon 26.05 Progress(示例图取自官方 images 图库)
*===============================================================
dev_update_off ()
*
* 1. 读取图像并取真实尺寸
* 官方图库里带头畸变的标定图就叫 calib_distorted_01.png;
* read_image 支持相对路径,HALCON 会在 HALCONROOT/examples/images 下找
*
read_image (Image, 'calib/calib_distorted_01.png')
get_image_size (Image, ImageWidth, ImageHeight)
*
* 2. 准备相机参数
* 下面这组系数直接取自官方示例 change_radial_distortion_points.hdev,
* 是一台真实带桶形畸变的 640x512 相机。注意系数是 5142.64 / -1.98e+08
* 这种大数,不是 1e-2 这种小数。真实项目里应改成
* get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam) 读标定结果
*
* area_scan_polynomial tuple 顺序:
* [类型, Focus, K1, K2, K3, P1, P2, Sx, Sy, Cx, Cy, ImageWidth, ImageHeight]
*
gen_cam_par_area_scan_polynomial (0.00506526, 5142.64, -1.98485e+08, -4.26734e+12, \
0.0462524, 0.114147, 9.30055e-06, 9.3e-06, \
276.713, 266.078, ImageWidth, ImageHeight, CamParam)
*
* 3. 算出一份零畸变参数
*
change_radial_distortion_cam_par ('adaptive', CamParam, [0,0,0,0,0], CamParamNoDist)
*
* 4. 生成校正映射图并校正
* 注意:CamParamIn = 带畸变,CamParamOut = 零畸变
*
gen_radial_distortion_map (Map, CamParam, CamParamNoDist, 'bilinear')
map_image (Image, Map, ImageCorrected)
*
* 5. 补视场
* 本流程用 'adaptive',输出图本身没有未定义像素,所以这一步不需要。
* 若第三节把 'adaptive' 换成 'fullsize' / 'preserve_resolution',
* 输出图四周会出现黑边,这里再补一次按比例放大:
* zoom_image_factor (ImageCorrected, ImageZoom, 1.15, 1.15, 'constant')
*
* 6. 显示对比
*
dev_open_window (0, 0, ImageWidth, ImageHeight, 'black', WindowHandle1)
dev_set_color ('white')
dev_display (Image)
dev_disp_text ('Original (distorted)', 'window', 12, 12, 'white', [], [])
dev_open_window (ImageWidth, 0, ImageWidth, ImageHeight, 'black', WindowHandle2)
dev_set_color ('white')
dev_disp_text ('Corrected (undistorted)', 'window', 12, 12, 'white', [], [])
dev_display (ImageCorrected)

分步讲解:
- 读图 + 取尺寸 :
get_image_size把真实宽高取出来,CamParam里的ImageWidth/ImageHeight/Cx/Cy都要用;不要硬写 640×480,不同相机分辨率不一样。Cx/Cy初值取图像宽高的一半即可。 - 准备相机参数 :用过程
gen_cam_par_area_scan_polynomial构造(参数顺序与 tuple 一一对应,比拼手写 tuple 安全)。这组系数取自官方示例,K1 = 5142.64是真实量级 ------polynomial 模型的系数本来就很大,写成-0.05这种小数反而不对。真实项目这一步是get_calib_data (CalibDataID, 'camera', 0, 'params', CamParam)读标定结果。 - 换零畸变参数 :
change_radial_distortion_cam_par的参数顺序是('adaptive', CamParam, [0,0,0,0,0], CamParamNoDist)------Mode 第一、输出最后 。5 个系数对应 polynomial 模型(division 模型这里只给一个标量0)。 - 生成映射 + 校正 :
gen_radial_distortion_map这里传的是两份 相机参数,'bilinear'让Map成为 5 通道;map_image只有 3 个参数,插不插值由通道数决定。 - 补视场 :本例
'adaptive'无黑边,故跳过;换'fullsize'时才需要zoom_image_factor。 - 双窗对比 :左原图右校正图,畸变和校正效果一目了然。注意
dev_disp_text的调用格式是(WindowHandle, Text, CoordSystem, Row, Column, Color, GenParamName, GenParamValue),窗口句柄在第 1 位 ;黑底窗口里文字颜色要写'white',写'black'会看不见。
十一、怎么验证校正是否到位
不要只看「图好像变直了」,用下面三个量化手段:
11.1 用规则网格图做客观判断
第八节已经给出了做法:用 gen_grid_region + paint_region 生成一张无畸变的规则网格真值图 ,然后正着(造畸变)、反着(去畸变)各走一遍,观察网格线是否恢复笔直。因为网格图是真值,弯曲多少一眼可判。
说明:这是工程上通用的判断手法(本文按此编写),并非 Halcon 官方畸变示例里的固定套路------官方那几个示例(
radial_distortion.hdev、change_radial_distortion_points.hdev)用的是标定板图 + 标记点/轮廓来对比校正前后的位置。
11.2 用重投影误差看整体水平
如果这套参数来自 calibrate_cameras,就利用它返回的重投影误差:关键是看误差的空间分布------把每个标定点的残差按「到画面中心的距离」排序,如果边缘点的残差明显大于中心点,说明畸变没被充分建模(例如该用 polynomial 却用了 division,或标定板没覆盖到边缘)。
注意 polynomial 模型不能外推:如果标定板没覆盖画面四角,那四角的畸变建模本身就不可靠,重投影误差再低也不能代表全画面可信。
11.3 边缘 vs 中心的空间一致性
最贴近现场判据:在图像中心和四角各放同一个尺寸的标准块,分别量测。如果中心量出来是 100 mm、四角量出来是 97 mm,那就是畸变没校正干净------同一个物理尺寸在不同视场位置量出不同结果,是畸变残留最典型的信号。
十二、踩坑清单
12.1 相机参数 tuple 下标 0 不是焦距
从 HALCON 13 起,CamParam 的第 0 个元素是相机类型字符串 ('area_scan_polynomial' 等),下标 1 才是 Focus。写 Focus := CamParam[0] 拿到的是一串字符串而不是数字。
优先用过程 get_cam_par_data / set_cam_par_data 按名字读写 (它们在 camera_parameters.hdpl 过程库里,不是算子 )。但要注意:参数名必须与模型匹配 ------division 模型只有 kappa,polynomial 模型才有 k1/k2/k3/p1/p2。对同一份 CamParam 既取 'kappa' 又取 'k1',必然有一个会抛异常(过程说明原文:"If the camera type does not support the requested value, an exception is raised.")。
12.2 拿远心镜头按透视镜头填焦距
远心镜头的相机类型带 _telecentric_ 字样,第 1 个参数是 Magnification(放大倍率)而不是 Focus。填错了会出现「标定能收敛但误差降不下来」。症状:重投影误差怎么调都在 0.3 px 以上,降不动。
12.3 DistortionCoeffs 元素个数只能是 1 或 5
area_scan_division 只吃 1 个系数(kappa,传标量 即可------官方示例 lens_distortions.hdev 写的就是 change_radial_distortion_cam_par ('adaptive', CamParIn, 0, CamParOut)),area_scan_polynomial 吃 5 个(k1/k2/k3/p1/p2,传 5 元 tuple)。给 3 个或 4 个直接抛异常。
12.4 change_radial_distortion_cam_par 参数顺序记错
Mode 在第 1 位,输出 CamParamOut 在最后。凭印象写成「输出参数在第一位」会报参数个数错误。
12.5 gen_radial_distortion_map 需要两份相机参数
它要 CamParamIn 和 CamParamOut 两份。去畸变 = (带畸变, 零畸变) ,造畸变 = (零畸变, 带畸变),位置决定方向。 顺序传反了效果正好相反------想校正结果变成加倍畸变。
12.6 Mode 选 'fullsize' 后出现黑边
'fullsize' 保证原图所有可见点在校正后仍可见,代价是输出图里会有未定义像素 。这是预期行为,不是 bug。要消除黑边就改用 'adaptive'(视场略缩)或事后放大补齐。
12.7 map_image 没有 Interpolation / Padding 参数
map_image(Image, Map, ImageMapped) 只有 3 个参数。是否插值由 Map 的通道数决定 :1 通道(MapType='nearest_neighbor')不插值,5 通道('bilinear')双线性插值。写 map_image(Image, Map, Out, 'bilinear', 'true', 'true') 必然报错。
12.8 没有 image_zoom 这个算子
按比例放大用 zoom_image_factor,按尺寸放大用 zoom_image_size。两个算子的输出变量名还不一致(ImageZoomed vs ImageZoom),容易写错。
12.9 只在标定阶段校正,测量阶段忘了校正
最常见的现场事故。标定阶段模型带了畸变,从图上取出的像素坐标本身就含畸变;到了拍照测量环节,却直接拿这些像素坐标去 image_points_to_world_plane 换算物理尺寸,中间没做畸变校正 → 越靠边偏得越多。链路上下游的畸变状态必须一致,写代码时在注释里明确标出「当前图像是否已去畸变」。
12.10 每张图都调 change_radial_distortion_image
官方文档明确说了:多图同参数时, gen_radial_distortion_map** + map_image 远优于逐张调 **change_radial_distortion_image,因为变换只算一次。逐张调是性能陷阱。
12.11 手写畸变系数(把 4842.64 写成 0.04 这种)
畸变系数是大数 (K1 = 5142.64、K2 = −1.98e+08、Kappa = −78129.2),不存在「典型量级 1e-2」这种说法------那是 OpenCV 的约定,和 HALCON 不通用。永远不要手填畸变系数 :要么用 gen_cam_par_area_scan_polynomial 按官方示例的真实数值构造(仅用于试验),要么从 calibrate_cameras / get_calib_data 读。
12.12 map_image 越界会崩,不是给黑边
map_image 出于性能考虑不检查坐标是否落在输入图内 ,越界会直接崩程序(手册 Attention 原文:"Otherwise, the program may crash!")。用 gen_radial_distortion_map 生成的 Map 一般安全;自己拼的 Map 一定要先保证坐标在范围内。
十三、小结
- 畸变是镜头的固有属性,标定负责测出畸变系数,畸变校正负责去掉畸变。
- HALCON 把畸变分成 K1/K2/K3(径向 radial) 与 P1/P2(偏心 decentering,手册里不叫 tangential) 两组。
- HALCON 用相机类型字符串选模型:
area_scan_division(快,只对称径向)/area_scan_polynomial(准,含偏心,主流)。26.05 还有tilt_*、hypercentric_*、line_scan_*等一大批,选型先对镜头。 CamParam下标 0 是类型字符串 ,下标 1 才是Focus;polynomial 模型共 13 个元素 (末尾还有cy、image_width、image_height)。get_cam_par_data/set_cam_par_data/get_cam_par_names是过程库 (camera_parameters.hdpl)里的过程,不是算子;按名取值时名字必须与模型匹配。- 畸变系数是大数(K1=5142.64、Kappa=−78129.2),不要拿 OpenCV 的量级来填。
gen_radial_distortion_map要两份相机参数 :去畸变(带畸变, 零畸变),造畸变(零畸变, 带畸变),位置决定方向。Mode决定校正后画面多大:'fullsize'不丢视野但有黑边,'adaptive'略缩视野无黑边,'preserve_resolution'不丢视野且不降分辨率。map_image只有 3 个参数,是否插值由Map通道数决定;越界会崩程序,不会给黑边。- 按比例放大用
zoom_image_factor,不是不存在的image_zoom。 gen_radial_distortion_map只能用于面阵相机;MapType='coord_map_sub_pix'才能上 OpenCL 设备。- 多图同参数务必复用映射图,别逐张调
change_radial_distortion_image(注意它第 2 个入参是Region)。 - 验证靠规则网格图、重投影误差、边缘 / 中心一致性三条,不要只靠肉眼。