4. 图像预处理与增强的工程化封装
在工业视觉项目里,预处理往往占据整条流水线一半以上的代码量,却最容易被写成“一次性脚本”:换一个相机、换一种光照,就要把灰度变换、滤波、形态学、ROI 全部重写一遍。工程化封装的目标,是把这些步骤从“散落在主流程里的算子调用”变成“有明确输入输出契约、可参数化、可复用、可单独验证”的预处理算子。本节围绕 HALCON 的实际算子,讲清楚四类预处理各自该封装什么,以及如何把它们组织成一个可复用的预处理模块。
一、四类预处理在工程中的职责边界
灰度变换解决的是“对比度与亮度是否落在可用区间”。HALCON 中常用的有 scale_image(线性缩放与偏移)、scale_image_max(按极值拉伸到 0~255)、emphasize(高频增强)、equ_histo_image(直方图均衡)。工程封装时不应只暴露一个“增强”按钮,而应把 Mult、Add 两个参数显式暴露,因为线性变换的物理含义清晰:输出 = 输入 × Mult + Add,便于在不同光照批次间做标定式调整。
滤波解决的是“噪声与细节的取舍”。gauss_filter 适合高斯平滑,median_image 对椒盐噪声更稳健,mean_image 计算快但会模糊边缘,sobel_amp、edges_image 用于提取边缘。封装的关键是固定滤波类型与核尺寸的对应关系,避免调用方随意传入不匹配的 MaskType 与 Radius。
形态学解决的是“形状层面的修正”。opening_circle、closing_circle、opening_rectangle1、closing_rectangle1 用于去毛刺、填小孔;erosion_circle、dilation_circle 用于分离粘连或加粗目标。封装时应把结构元形状、半径、迭代次数作为参数,并明确“先开后闭”还是“先闭后开”的顺序,因为顺序不同结果差异明显。
ROI 管理解决的是“只处理该处理的地方”。HALCON 中 ROI 可以是矩形 gen_rectangle1、圆 gen_circle、任意区域 gen_region_polygon,也可以是 reduce_domain 得到的图像域。工程封装的核心不是“画 ROI”,而是把 ROI 的生成、裁剪、坐标还原三件事绑定在一起:在 ROI 内处理得到的区域,必须能通过 union 或坐标偏移还原到原图坐标系,否则后续测量会整体偏移。
二、把预处理封装成可复用算子的具体做法
HALCON 本身支持用 HDevelop 过程(procedure)或外部过程库来封装。一个可复用的预处理算子,接口应尽量窄:输入一张图像、一个参数元组,输出处理后的图像和可选区域。下面给出一个可直接参考的过程骨架,参数用元组传递,避免为每个参数单独开形参导致接口膨胀。
* preprocess_image.hdvp
* 输入: Image, Params
* Params = [Mult, Add, FilterType, Radius, MorphOrder, MorphRadius]
* FilterType: 'gauss' | 'median' | 'none'
* MorphOrder: 'open_close' | 'close_open' | 'none'
procedure preprocess_image (Image, Params : ImageOut : : )
Mult := Params[0]
Add := Params[1]
FilterType := Params[2]
Radius := Params[3]
MorphOrder := Params[4]
MorphRadius := Params[5]
* 1. 灰度变换
scale_image (Image, ImageScaled, Mult, Add)
* 2. 滤波
if (FilterType == 'gauss')
gauss_filter (ImageScaled, ImageFiltered, Radius)
elseif (FilterType == 'median')
median_image (ImageScaled, ImageFiltered, 'circle', Radius, 'mirrored')
else
copy_image (ImageScaled, ImageFiltered)
endif
* 3. 形态学(在图像域上做,也可改为在区域域上做)
if (MorphOrder == 'open_close')
opening_circle (ImageFiltered, ImageOpen, MorphRadius)
closing_circle (ImageOpen, ImageOut, MorphRadius)
elseif (MorphOrder == 'close_open')
closing_circle (ImageFiltered, ImageClose, MorphRadius)
opening_circle (ImageClose, ImageOut, MorphRadius)
else
copy_image (ImageFiltered, ImageOut)
endif
endprocedure
这段骨架体现了三个工程要点。第一,参数集中在一个元组里,调用方只需维护一份参数配置,便于把不同产品的参数存成配置文件或数据库记录。第二,滤波与形态学都做了“类型分支 + 默认直通”,当某一步不需要时不会报错,也不会引入额外计算。第三,所有中间图像都显式命名,便于在调试时单独 dev_display 观察每一步的效果,而不是只看到最终结果。
三、ROI 管理的封装:从“画框”到“可复用区域模板”
ROI 在工程中通常有两种用法。一种是固定 ROI:相机与工件相对位置稳定,ROI 直接写成常量矩形,封装时把 Row1, Col1, Row2, Col2 作为参数传入,内部用 gen_rectangle1 生成区域,再用 reduce_domain 裁剪。另一种是动态 ROI:先通过模板匹配或 Blob 找到工件,再根据匹配位姿生成跟随的 ROI,这时封装的重点是“位姿 → ROI 区域”的转换,通常用 affine_trans_region 把标准 ROI 变换到当前位姿。
无论哪种用法,封装时都要保证 ROI 的坐标参照系一致。如果 ROI 是在原图上定义的,那么 ROI 内处理得到的区域必须通过 union1 或坐标偏移还原到原图坐标系,否则后续 measure_pos、fit_circle_contour_xld 得到的尺寸会整体偏移一个 ROI 左上角坐标。建议在封装内部统一约定:所有输出区域一律使用原图坐标系,ROI 偏移只在内部临时使用。
Debug 布尔参数控制是否返回中间图像),这样在产线换型时能快速定位是灰度变换、滤波还是形态学导致的效果变化,而不必重新跑整条流程。四、参数默认值与校验:让算子“开箱可用”
可复用算子的默认值应来自典型工业场景的经验区间,而不是随意填 0。例如 Mult 默认 1.0、Add 默认 0,表示不做灰度变换;FilterType 默认 'gauss'、Radius 默认 1.5,对应轻微平滑;MorphOrder 默认 'open_close'、MorphRadius 默认 2.0,对应先去除小毛刺再填补小孔。这些默认值应写进算子文档,并在参数校验中检查 Radius > 0、MorphRadius >= 1、ROI 是否落在图像范围内。
参数校验不是形式主义。工业现场最常见的故障之一,就是调用方传入了一个超出图像范围的 ROI,导致 reduce_domain 返回空区域,后续所有算子都在空区域上运行,最终结果为空却不报错。封装时应在 ROI 生成后立即用 area_center 检查区域面积是否大于 0,若为 0 则抛出明确异常,把问题挡在预处理阶段。
五、一个可参考的预处理参数配置表
下表给出一个示例配置,用于某类金属件表面检测场景:光照偏暗、存在细小划痕与少量反光点。表中参数为教学示例,实际项目需根据图像直方图和试跑结果调整。
| 参数 | 示例值 | 作用 | 调整依据 |
|---|---|---|---|
| Mult | 1.6 | 提升对比度 | 直方图集中在 60~140,需拉伸 |
| Add | -20 | 压低背景亮度 | 背景偏亮,压暗后目标更突出 |
| FilterType | 'median' | 抑制反光点 | 存在孤立亮点,中值更稳健 |
| Radius | 2.0 | 滤波核半径 | 划痕宽度约 3~5 像素,不宜过大 |
| MorphOrder | 'open_close' | 去毛刺后填孔 | 先去除小凸起,再闭合小缺口 |
| MorphRadius | 2.0 | 结构元半径 | 与划痕尺度匹配,避免抹掉缺陷 |
封装完成后,验证方式不是“看起来正常”,而是用同一组参数在至少三张不同光照条件下的图像上运行,检查输出图像的灰度均值、目标区域面积、边缘位置是否稳定。若某张图的目标面积波动超过 10%,说明灰度变换或滤波参数对光照过于敏感,应回到参数标定环节,而不是在后续测量环节打补丁。预处理封装的价值,正在于把这种波动尽早暴露在流水线前端。