4. 视觉任务分解与模块边界
在 HALCON 视觉项目中,代码架构是否可复用,往往不取决于算子写得多熟练,而取决于任务分解是否清晰。一个常见的失败模式是:把所有逻辑塞进一个 main 函数,图像采集、预处理、模板匹配、测量、判定、结果输出混在一起。当客户要求“把定位算法换成另一种匹配方式”或“增加一个缺陷检测工位”时,改动会牵一发而动全身。要避免这种局面,第一步就是按视觉任务类型划分功能模块,并为每个模块定义明确的输入、输出与职责边界。
四类基本视觉任务及其本质差异
工业视觉任务通常可归为四类:定位(Localization)、测量(Measurement)、检测(Detection/Inspection)、识别(Identification)。它们的本质差异决定了模块边界。
- 定位:回答“目标在哪里、姿态如何”。输出通常是位姿(Row、Column、Angle,必要时含缩放或 3D 位姿)。典型算子如
find_shape_model、find_ncc_model、find_scaled_shape_model。定位是后续测量的坐标系基准。 - 测量:回答“尺寸是多少”。输入依赖定位结果建立局部坐标系,输出是长度、直径、角度、间距等带单位的量值。典型算子如
measure_pos、measure_pairs、fit_line_contour_xld。 - 检测:回答“有没有缺陷、是否合格”。输出通常是缺陷区域、缺陷数量、面积或布尔判定。典型算子如
threshold、dyn_threshold、connection、select_shape。 - 识别:回答“是什么”。输出是类别标签或字符/条码内容。典型算子如
read_ocr_class_mlp、find_bar_code、classify_image_class_mlp。
这四类任务的输入输出语义完全不同,因此不应混在同一个函数里。定位模块的输出是位姿,测量模块的输入是位姿加图像,检测模块的输入是图像加 ROI,识别模块的输入是图像加字符区域。边界一旦清晰,模块之间就只通过数据契约通信,而不是通过全局变量或共享句柄。
输入输出契约:模块之间只传数据,不传状态
模块边界清晰之后,下一步是定义每个模块的输入输出契约。契约的核心原则是:模块之间只通过参数传递数据,不通过全局变量或共享句柄传递状态。在 HALCON 中,图像对象(HObject)和句柄(HTuple)都可以作为参数传递,但句柄的生命周期必须由创建它的模块负责释放。
以定位模块为例,一个可复用的接口可以设计为:
procedure localize_part(Image : : ModelID, MinScore, NumMatches : Row, Col, Angle, Score)
{
find_shape_model(Image, ModelID, -0.39, 0.78, MinScore, NumMatches,
0.5, 'least_squares', 0, 0.9, Row, Col, Angle, Score)
return ()
}
这个接口的输入是图像、模型句柄、最小分数和匹配数量;输出是位姿和分数。它不关心图像从哪来,也不关心位姿拿去做什么。测量模块的接口则接收图像和位姿:
procedure measure_width(Image : : Row, Col, Angle, MeasureLen1, MeasureLen2, Sigma : Width)
{
gen_measure_rectangle2(Row, Col, Angle, MeasureLen1, MeasureLen2,
Width(Image), Height(Image), 'nearest_neighbor', MeasureHandle)
measure_pos(Image, MeasureHandle, Sigma, 30, 'all', 'all', RowEdge, ColEdge, Amplitude, Distance)
Width := Distance
close_measure(MeasureHandle)
return ()
}
注意这里 MeasureHandle 是局部变量,在函数内部创建并释放,不暴露给调用者。这就是职责边界的体现:测量模块自己管理测量句柄的生命周期,调用者只需要提供位姿和测量参数。
模块划分的粒度判断
模块划分不是越细越好。粒度过细会导致函数调用链过长,参数传递成本高;粒度过粗则失去复用价值。一个实用的判断标准是:如果一个功能在不同项目中会以不同组合方式出现,就应该独立成模块。例如,模板匹配定位几乎每个项目都要用,但匹配方式可能不同(形状匹配、NCC、缩放匹配),因此定位模块应该把“匹配算法”作为可替换的内部实现,而不是把不同匹配方式拆成不同模块。
另一个判断标准是变化频率。如果某个逻辑经常因为客户需求变化而修改,它就应该被隔离在独立模块中。例如,判定阈值经常调整,那么判定逻辑应该独立于测量和检测模块,单独放在一个 judge_result 函数中。这样修改阈值时不需要重新测试测量算法。
config 字典或参数表集中管理阈值、ROI 坐标、模型路径等易变参数,模块内部只读取参数,不硬编码数值。这样模块边界不会因为参数变化而被破坏。
一个完整的任务分解示例
假设一个连接器引脚检测项目,需要完成:定位连接器、测量引脚间距、检测引脚缺失、识别连接器上的字符。按照任务分解,可以划分为四个模块加一个判定模块:
| 模块 | 输入 | 输出 | 核心算子 |
|---|---|---|---|
| 定位模块 | Image, ModelID | Row, Col, Angle | find_shape_model |
| 测量模块 | Image, Pose, 测量参数 | PinPitch (mm) | measure_pos |
| 检测模块 | Image, Pose, ROI | DefectCount, DefectArea | threshold, connection |
| 识别模块 | Image, Pose, 字符ROI | Text | read_ocr_class_mlp |
| 判定模块 | PinPitch, DefectCount, Text | OK / NG | if / tuple 比较 |
这个划分中,定位模块的输出被测量、检测、识别三个模块共享,但每个模块只读取自己需要的部分。测量模块不关心缺陷,检测模块不关心字符,识别模块不关心尺寸。判定模块是唯一知道“合格标准”的地方,它接收所有模块的输出,按照业务规则给出最终结论。
measure_width 内部直接比较宽度是否超差并返回 OK/NG。这样测量模块就同时承担了测量和判定两个职责,当判定标准变化时,测量模块需要重新验证,复用性下降。
边界清晰带来的复用收益
当模块边界清晰后,复用变得自然。新项目如果也是连接器检测,可以直接复用定位模块和测量模块,只需要替换模型文件和测量参数。如果新项目是字符识别,可以复用识别模块,定位模块换成适合该产品的匹配方式即可。判定模块因为集中了业务规则,修改时只影响一个地方。
更重要的是,模块边界清晰后,调试和测试也变得可控。可以单独测试定位模块的匹配分数是否稳定,单独测试测量模块的重复精度,单独测试检测模块的缺陷召回率。每个模块的输入输出都是明确的,可以用固定图像和已知位姿做单元测试,而不需要跑完整流程。
总结来说,视觉任务分解的核心不是把代码拆成更多文件,而是按照定位、测量、检测、识别四类任务的本质差异划分职责,用明确的输入输出契约连接模块,把易变逻辑隔离在独立模块中。这样得到的 HALCON 项目架构,才具备真正的复用能力。