
做机器视觉的朋友应该都遇到过“在线测量尺寸”这类需求。今天我把Halcon官方例程里一个很典型的项目拿出来拆一拆——check_fish_stick_dimension翻译过来就是测量生鱼条的大小。这个例程解决的是食品加工产线上的一个真实问题鱼条切出来之后长度和宽度是否满足包装规格。超长、超宽、断条、粘连都会直接影响后续装罐、称重和品控判定。别小看这个例程。它虽然只处理“一条鱼条”的测量但内部几乎覆盖了机器视觉测量的所有基础环节图像分割、连通域分析、形态学处理、亚像素边缘提取、矩形拟合、标定以及公差判断。你把这一套吃透很多二维尺寸测量项目都可以套用同样的套路去开发从金属冲压件、木地板条到药板、管材外径思路完全一致。这个例程适合谁来读如果你是刚接触视觉测量、想找一份能落地参考代码的新手这个例程是一个很理想的起步项目。如果你想在现有产线里快速验证“视觉能不能替代卡尺抽检”把例程思路迁移过去的速度也很快。我下面会从业务需求、技术选型、算子细节到调试踩坑完整拆一遍。1. 例程背后的真实业务需求1.1 鱼条尺寸检测解决什么问题先说业务逻辑。鱼条生产出来要装袋或装罐如果尺寸参差不齐重量就会飘忽不定。举个例子一包标注净含量454克如果鱼条偏大实际重量可能超到480克厂家的成本就高了如果偏小装到450克市场监管抽检时又面临短斤缺两的风险。因此鱼条在切割完成后生产线上必须有一个环节对尺寸进行抽检或全检。这个例程本身是用静态图像来做测量模拟的就是抽检工位的视觉系统一条鱼条被送到相机视野内拍一张图计算机马上量出长度、宽度与标准值对比输出OK或NG。在实际产线上这张图往往来自光电传感器触发的高速相机而非手工拍摄但核心算法逻辑完全一样。除此之外还有一层潜在需求数据记录。人工量尺寸只能记录在纸面上而视觉系统可以输出每个周期内所有鱼条长度、宽度的统计数据比如CPK、均值、极差。生产管理人员可以直接利用这些数据调整切割机的参数做闭环控制这才是视觉测量价值的最大化。1.2 人工卡尺抽检的痛点很多人第一反应是鱼条尺寸难道不能用尺量吗能。但你产线一分钟过60条鱼条质检员要保持什么速度才能全检就算全检连续测量半小时后眼睛和手腕都会疲劳读数误差会增大。更重要的是人工测量是非实时、非连续的无法做到产品全检只能事后抽检发现问题时已经产生了一批不合格品。从成本上看一个专职工位一年的用工成本可能比一套视觉检测设备的折旧成本还高。从稳定性看人工测量的标准并不统一不同质检员量出来的结果可以差到1到2毫米。视觉测量则一次性标定好基准误差对所有人都是恒定的。1.3 到底要测哪些尺寸从例程名“check dimension”可以看出核心是测量尺寸。具体到生鱼条主要测三个量长度、宽度、面积。长度和宽度不是简单对图像左右上下比划而是先找到鱼条的主轴方向再沿主轴及其垂直方向取外接尺寸。Halcon中常用的做法是拟合一个最小外接矩形矩形的长边对应鱼条长度短边对应宽度。再补一个细节鱼条两端不一定是平齐的可能是斜切的这时候最小外接矩形依然能给出最高效的包络尺寸。但如果你要的是“切面到切面”的精确长度就得在两端各建一条测量线用测量算子拉距离。例程为了通用性用的是矩形拟合方案。公差判断原则是长度在[L_min, L_max]范围内、宽度在[W_min, W_max]范围内同时面积大于某个下限三个条件都满足才算合格。为什么面积也要管因为鱼条如果断成两截或者边缘残缺长度和宽度可能还在范围内面积却明显偏小面积可以兜底。2. 整体方案架构与技术选型2.1 从图像到测量结果的完整处理管线先梳理管线。Halcon例程的经典结构是图像读取 → 预处理分割 → 连通域分析 → 目标筛选 → 形态学处理 → 轮廓提取 → 矩形拟合 → 像素尺寸提取 → 物理标定 → 公差判断 → 结果展示我说的“读取”在你本地复现时可以把read_image换成grab_image从相机实时采图。预处理分割阶段最常用的就是阈值分割。因为鱼条颜色与背景形成明显对比时一个threshold就能得到目标区域。但真实产线里照明不稳定、污渍、水汽都会影响灰度分布所以例程后续还用了区域形态学算子做“修补”。目标筛选阶段用select_shape。一张图可能拍到多条鱼条也可能有鱼条碎片、水渍形成的伪区域通过面积、长度、宽度等几何特征把需要的鱼条挑出来。轮廓和测量阶段用edges_sub_pix提取亚像素级别边缘再用fit_rectangle2_contour_xld拟合矩形。这两个算子配合测量的重复精度可以到达亚像素层。最后通过标定将像素尺寸换算成毫米。Halcon里最常用的是pixels_to_plm和plm_to_pixels。这套算子的底层是建立一套图像坐标到物理坐标的映射关系具体在后面详解。2.2 为什么选Halcon而不是从零写算法在尺寸测量这个领域我见过很多人一上来就想自己写C加OpenCV实现。也不是不行但对于产线级项目成熟的视觉库在算法稳定性、边缘提取鲁棒性、标定工具链和调试体验上优势非常明显。Halcon的算子经过大量工业项目验证同样的边缘提取自己写的Canny参数调到崩溃换Halcon算子可能参数只需要两个。Halcon的另一个优势是HDevelop交互开发环境。你可以一边调阈值一边看区域变化这种“所见即所得”的调试方式在项目初期摸清图像特征时特别高效。例程本身是用HDevelop脚本写的稍加修改就能打包成C/C#工程从原型到上线路径很成熟。当然选型不是唯一的。如果你所在团队对开源栈有硬性要求OpenCV也完全能做只是需要投入更多时间做调参和边界情况处理。我的建议是项目周期紧、精度要求高、交付要快的优先考虑商业库做研究验证或预算受限再考虑开源方案。2.3 硬件侧的几个关键匹配点算法只是系统的一半。这里必须提醒一下硬件匹配。相机分辨率决定测量精度。假设视野是200毫米要求检测精度±0.5毫米那么每个像素对应的物理尺寸至少是0.5毫米实际上要留2到3倍余量也就是0.2毫米左右对应水平方向至少1000像素。但这只是最低标准如果还要测量宽度并保证重复性建议按更高密度选型。500万像素级别相机在这个视野下比较保险。镜头选择优先低畸变定焦镜头。畸变会影响边缘位置的真实度如果镜头畸变大标定模型再精确也会残留系统误差。照明方式上测量外轮廓的项目首选背光源也就是把光源放在被测物背面让鱼条在图像里形成黑色剪影。背光得到的边缘对比度极高阈值分割几乎不会出意外。如果因为结构限制只能用环形光源或条形光源就要做好灰度不均的准备后面阈值参数和形态学参数都要跟着调整。这是很多新人一上来就踩的坑拿环形光拍出来的图边缘一会亮一会暗阈值怎么调都稳定不下来。3. 核心算子解析与关键参数调优3.1 阈值分割从threshold到动态阈值的演进例程通常会用类似下面的方式把鱼条从背景中分出来threshold (Image, Regions, 80, 255)理解一下这个算子把灰度值介于80到255之间的所有像素划入区域。80这个下限是怎么来的在典型的背光照明下鱼条区域的灰度一般高于背景而且灰度分布比较集中。你可以打开灰度直方图找到波谷位置作为阈值下限。但是“80”这种固定值在产线调试里存在隐患光照波动或不同批次原料颜色改变都可能让阈值失效。所以例程给的是思路实际部署时我喜欢改成动态阈值或自动阈值。Halcon里可以用auto_threshold或者采用每个ROI内求均值加减倍数的策略让阈值跟着图像特征走。另外强调一点阈值分割只做一步就完事太理想化了。鱼条表面可能有纹理暗斑分割出来的区域中间会带小孔鱼条边缘可能有毛刺区域边界会坑坑洼洼。这些都要交给后面的形态学算子收拾。3.2 连通域分析与目标选择分割完成后图像里的目标可能不止一个。connection算子的作用是把连通的像素区域划分为一个个独立的区域对象相当于给每个连通块发一个编号。随后是select_shapeselect_shape (ConnectedRegions, Selected, [area, width, height], and, [5000, 20, 30], [999999, 999, 999])这里选了面积、宽度、高度三个特征。面积下限5000像素是为了去掉小噪点宽度和高度区间把太细的碎片排除掉。width和height是按照区域主轴方向的外接矩形尺寸不是简单轴向尺寸所以对于倾斜的鱼条也一样有效。有一个调试技巧select_shape 参数不要在代码里盲调先用 HDevelop 的“特征直方图”交互查看每个特征值分布确定阈值后再写死到代码里。这样选出来的参数有依据不会拍脑袋。3.3 形态学处理补洞和去毛刺分割得到的区域通常不够“干净”。鱼条表面纹理可能产生孔洞影响面积特征边缘毛刺会干扰后续矩形拟合。Halcon里常用fill_up把区域内部孔洞填掉再用closing_circle做闭运算把边缘细小凹陷磨平。fill_up (Selected, RegionFillUp) closing_circle (RegionFillUp, RegionClosing, 3.5)closing_circle 的半径3.5是个经验值具体要看毛刺宽度。半径太小毛刺压不干净半径太大会把鱼条真实轮廓磨圆导致后续测量偏小。这个值要在“去噪”和“保真”之间取平衡。这里顺便说一个容易忽略的点形态学操作会改变区域尺寸。如果后续需要测量区域的面积闭运算后面积会有小幅变化所以要谨慎控制结构元素尺寸。对于严格测量项目我更倾向于只做fill_up不去做closing宁可让边缘毛糙一点也要保证几何尺寸不失真。3.4 亚像素边缘提取与矩形拟合区域本身是像素级的直接量区域尺寸精度只能到1个像素。为了更精确要从区域边界生成亚像素轮廓gen_contour_region_xld (RegionClosing, Contours, border)从区域轮廓XLD已经可以做很多测量。但更专业的做法是直接在灰度图上提取亚像素边缘edges_sub_pix (ImageReduced, Edges, canny, 1.5, 20, 40)Canny算子的参数里1.5是高斯平滑系数20和40分别对应滞后阈值的低阈值和高阈值。低阈值20用于弱边缘跟踪高阈值40用于确定强边缘起点。这个组合在大多数测量场景是稳定起点再根据实际边缘噪声调整。拿到边缘后用fit_rectangle2_contour_xld拟合外形矩形fit_rectangle2_contour_xld (Edges, regression, -1, 0, 0, 3, 2, Row, Column, Phi, Length1, Length2)这个算子输出矩形的中心坐标Row、Column旋转角度Phi半长Length1和半宽Length2。注意Length1一定是长边的一半。因此鱼条长度就是2 * Length1宽度是2 * Length2。为什么用拟合而不是直接用区域的最小外接矩形因为亚像素边缘经过拟合后精度远高于像素级区域角度和宽度的重复性更好。3.5 像素到毫米标定与pixels_to_plm测量最终要输出物理尺寸必须做标定。Halcon中最常用的标定方式是用标定板。通过find_calib_object找到标定板特征点然后用get_calib_data提取相机内参和畸变系数最后建立像素坐标与物理坐标的映射。例程里简化了标定流程直接建立了一个位置无关的像素物理映射pixels_to_plm (Row, Column, 0.05, 0.05, 0, 0, RowPhysical, ColumnPhysical)0.05代表每个像素对应0.05毫米也就是当标定结果确认1个像素等于0.05毫米时填这个值。这个映射只含比例和旋转不包含畸变校正。它是快速测量的简化方案适合视野小、镜头畸变低、相机与物体垂直的场景。如果相机有透视角度或镜头畸变明显必须用完整的相机标定模型。否则你会发现鱼条放在视野中间和放在边缘测量结果会不一样这就是畸变造成的。标定的关键不只是“做了”而是要验证把标准量块放在视野多个位置分别测量结果都要落在标称值公差内。4. 实操过程与核心代码逐段解读4.1 读图与初始化显示以下是例程思路的完整落地你可以按顺序写入HDevelop验证* 读取图像这里以静态图模拟相机抓拍 read_image (Image, fish_stick_01) get_image_size (Image, Width, Height) * 初始化窗口 dev_close_window () dev_open_window (0, 0, Width/2, Height/2, black, WindowHandle) dev_display (Image)read_image后面跟的路径要换成你自己的样本路径。如果接相机用grab_image替换并且建议在循环里连续取帧。4.2 目标区域分割* 阈值分割灰度80到255 threshold (Image, Regions, 80, 255) * 分成独立连通区域 connection (Regions, ConnectedRegions) * 按几何特征筛选目标鱼条 select_shape (ConnectedRegions, SelectedRegions, [area, width, height], and, [5000, 20, 30], [999999, 999, 999]) * 补洞 fill_up (SelectedRegions, RegionFillUp)如果你拍的鱼条颜色较浅背景是深色传送带阈值方向就反过来了阈值应写成0到150。判断方向的标准很简单目标区域灰度高于背景就用高阈值低于背景就用低阈值甚至可以用灰度直方图一眼确认。4.3 提取亚像素边缘并拟合矩形* 将区域缩小到图像上减少计算量 reduce_domain (Image, RegionFillUp, ImageReduced) * 亚像素边缘提取 edges_sub_pix (ImageReduced, Edges, canny, 1.5, 20, 40) * 拟合矩形 fit_rectangle2_contour_xld (Edges, regression, -1, 0, 0, 3, 2, Row, Column, Phi, Length1, Length2)这一步是测量的核心。fit_rectangle2_contour_xld 的第三个参数 -1 表示所有边缘点都参与拟合最后两个参数2和3控制迭代次数和端点抑制在某些边缘缺损场景可以调整。例如鱼条中间有污渍产生断边可以适当提高迭代参数让拟合更稳健。4.4 计算物理尺寸并判断合格* 像素尺寸转物理尺寸 PixelLength : 2 * Length1 PixelWidth : 2 * Length2 pixels_to_plm (PixelLength, PixelWidth, 0.05, 0.05, 0, 0, PhysicalLength, PhysicalWidth) * 设定公差 LMin : 90.0 LMax : 110.0 WMin : 8.0 WMax : 12.0 * 判断 if (PhysicalLength LMin and PhysicalLength LMax and PhysicalWidth WMin and PhysicalWidth WMax) Result : OK else Result : NG endif这里的90到110毫米、8到12毫米只是示例规范实际以产线工艺文件为准。判定逻辑可以扩展比如面积也要参与判定或者输出超差偏差值让执行机构做分选。要注意 Halcon 的 if 条件写法是if (条件)不是if 条件小括号必不能少。4.5 显示与数据输出dev_display (Image) disp_message (WindowHandle, Length: PhysicalLength$5.2f mm, Width: PhysicalWidth$5.2f mm, Result: Result, window, 12, 12, black, true)结果显示用disp_message把测量数值动态拼到字符串里。这里的格式化写法PhysicalLength$5.2f是HDevelop常用的数值转字符串方式。实际项目中这段输出会替换成写数据库、发PLC信号或者存CSV基本逻辑相同。5. 现场调试中那些不得不说的坑5.1 鱼条粘连导致漏检产线输送过程中鱼条常常挨在一起阈值分割得到的是连成一片的大区域select_shape筛选时会把两条鱼并成一条长度直接翻倍。排查对策先从形态学分离用opening_rectangle1沿水平方向破开连接桥或者用watersheds做分水岭分割。如果鱼条朝向基本固定破开方向的矩形结构选宽度1到3像素效果立竿见影。5.2 水渍和反光让灰度阈值失效冷冻鱼条表面常有一层冰水膜在光源下形成镜面反光灰度值局部接近白色把鱼条区域直接“打断”成两截面积特征变小甚至被判为NG。对策是改用偏振光源或偏振片消除反光算法侧可把阈值模式改为“像素值分段”把反光区域重新合并回鱼条区域。测试时一定要准备带水渍的样品不要只测干燥状态否则现场一定翻车。5.3 标定误差比想象中更容易遇到标定板没放平、相机倾斜、标定图像拍糊都会引入标定误差。我见过量长度偏了0.8毫米的项目最后发现是标定板表面有轻微弯曲物理坐标系本身就不准。标定时要拍多张不同姿态的图算子会自动优化内参和外参减少随机误差。如果产线环境不允许放标定板可以用已知尺寸的标准块在视野内多个位置比对验证至少每季度复校一次。5.4 速度与精度的取舍在线检测对节拍有要求而精度又跟图像分辨率正相关两者天然矛盾。我的建议先算好预算精度再定相机分辨率不要上来就上最高分辨率。分辨率越高单帧数据量越大处理耗时越长成本也越高。如果产线节拍是每分钟60条单条处理时间必须小于1秒500万像素级别的图在普通工控机上跑这套流程通常没问题但要留足CPU余量。5.5 一个容易被忽略的细节边缘毛刺鱼条切面往往有碎肉和纤维毛刺提取边缘时会出现大量小尖刺参与拟合导致矩形拟合时角度偏移测量长度时好时坏。解决思路是先用smooth_contours_xld对轮廓做平滑或者选用拟合模式robust替代regression让离群点权重降低。这一条在测量任何切割类食品时都非常管用。最后再分享一个实用习惯验收视觉测量项目时我都会准备三种样品——标准合格品、尺寸临界品、带表面干扰的劣质品分别测几十次统计重复性和误判率。这套流程跑通比任何纸上参数都管用。这个习惯大家可以直接带走拿到任何一个新例程都适用。