
搞机器视觉的都知道等你把Halcon环境跑通、图像能读能显示之后真正开始处理问题的时候十有八九会碰上一个绕不过去的环节形态学运算和Blob分析。标题里说的“第3讲”其实就是直接从“能看图像”跨到“能从图像里把目标给我抠出来、数出来、量出来”的关键一步。不管你是刚入门的学生还是被项目逼着学Halcon的工程师这讲的内容都是你后面做缺陷检测、尺寸测量、定位引导的基础——说白了你后面写的每一个视觉项目核心都是这套东西打底。这一讲我会把形态学里的腐蚀、膨胀、开运算、闭运算讲清楚再带你走一遍Blob连通域分析的完整流程最后用一个垫片计数与测量的例子把这些算子串起来。看完你就能明白为什么网上那些Halcon例子里老是出现threshold、closing_circle、connection、select_shape这几个算子它们到底在干什么以及遇到工件粘连、背景不均、尺寸偏大偏小这类问题该怎么调。1. 形态学运算到底是什么为什么视觉项目里绕不开它很多人第一次看到“形态学运算”这个名词会以为是什么高深的数学理论其实它就是图像处理里的几个基本操作腐蚀、膨胀、以及由这两者组合出来的开运算和闭运算。它的核心思想从名字里就能感受到——处理的是“形态”也就是二值图像里前景区域的形状结构。再说白一点你用阈值把目标从背景里分出来后得到的二值图往往不干净。目标边缘可能有毛刺目标内部可能有小洞背景里可能有噪声点几个目标之间可能有一点不该有的黏连。这时候形态学运算就是用来修这些区域形状的。你可以把它想象成一块橡皮泥——一边用手捏掉多余的部分一边用手把缺口补上。先把形状修到你满意的状态后面做Blob分析才不会出错。1.1 四个基础运算腐蚀、膨胀、开运算、闭运算先说腐蚀Erosion它在Halcon里最常用的写法是erosion_circle、erosion_rectangle1这些。腐蚀的效果非常直观让前景区域“瘦一圈”。原本是一个10像素宽的垫片腐蚀之后可能变成8像素宽原来和主体只有1个像素相连的毛刺会被直接腐蚀断掉。我经常跟同事打比方这就像用刀子把一块木头边缘的茸毛全部刮掉一样目标是让边界光整牺牲一点点边缘尺寸。腐蚀用得多的地方是去掉孤立噪声点、断开细小的错误连接。然后是膨胀Dilation算子类似dilation_circle、dilation_rectangle1。它和腐蚀刚好相反让前景区域“胖一圈”。边缘往外扩原本断成两截的线段如果间距足够近膨胀之后就会被“焊”在一起原本区域内部有一个小孔膨胀之后孔会明显变小甚至消失。膨胀的作用包括连接断裂的区域、填充内部孔洞。不过要注意膨胀是无差别扩张如果区域之间间隔稍微近一点很容易把两个本不该相连的目标糊成一块这一点后面我会特别说。开运算Opening和闭运算Closing都是复合操作。开运算是先腐蚀、再膨胀。这句话看起来简单实际却非常关键。腐蚀把毛刺和小碎块去掉然后膨胀再把主体补回原来的尺寸所以开运算能清除毛刺和孤立小点但又不像单纯腐蚀那样让目标整体瘦一圈。闭运算反过来先膨胀、再腐蚀。膨胀把区域内的小洞填平、把断裂处接上腐蚀再把尺寸恢复回去所以闭运算能在修补空洞和连接断口的同时尽量保持目标原有大小。这四个运算你不需要背公式只需要记住想除小东西用开运算想补小洞用闭运算单纯缩小或放大尺寸用腐蚀或膨胀。实际工程里我用得最多的是闭运算因为很多二值化之后的目标往往因为打光不均或者反光在内部形成小黑洞、在边缘出现断裂闭运算一上去这些问题马上缓解。1.2 结构元怎么选圆形还是方形半径该给多大形态学运算里有个概念叫“结构元”Structuring Element说白了就是一把形状固定的刷子用它去图像上一扫扫到哪个区域就操作哪个区域。Halcon里最常见的是圆形结构元circle和矩形结构元rectangle参数就是半径或长宽。选择原则是这样的如果你的目标是各向同性的比如圆垫片、圆孔、颗粒用圆形结构元最稳妥至少在Halcon里90%的场景我给的是closing_circle或opening_circle因为圆不偏袒任何方向。如果目标有明显的方向性比如要消除水平方向的长条纹那用矩形结构元长边顺着条纹方向给才能既去掉条纹又不损伤目标。半径大小怎么给这个不能拍脑袋。先说一个关键原则结构元尺寸一定要小于目标本身的最小尺寸否则目标会被整个吃掉。比如一个垫片直径是50像素你去噪用的是半径30像素的开运算那垫片直接就没了好吗这种情况我见过不止一次新手一上来就喜欢把半径调大结果目标消失然后一脸懵。正确的做法是根据像素当量来倒推。假设你标定过相机知道一个像素对应0.05毫米目标上有一个0.3毫米的小毛刺要去除那毛刺大约是6个像素开运算半径给3到4就足够了。如果给到15或20毛刺倒是去掉了但目标的真实边缘也会被磨掉一圈后面的尺寸测量会明显偏小。再说一遍开运算再怎么保持尺寸也是有个限度的半径太大它磨掉的就是目标本身的真实边缘了。这个度靠的就是对像素当量的把握。2. Blob分析从二值图到“有用的东西”形态学运算帮你把二值图修干净了接下来怎么办接下来就是把“一堆连在一起的白色像素”当成一个整体来看待。这个整体业内叫Blob也叫连通域。所以你在标题里看到Blob和连通域分析两个词同时出现其实是同一件事的两个角度Blob就是那些像素团连通域分析就是把这些像素团一个个标成独立对象。为什么这一步重要因为视觉项目里你要的不只是“图像里有白色区域”而是“图像里有几个工件、每个工件中心在哪、面积多大、圆不圆”。这些信息全靠Blob分析来提取。我举个实际的例子你去数一版药板上的胶囊或者给传送带上的零件计数本质上都是同一套逻辑用阈值把目标从背景里分离用形态学把噪声去掉然后用连通域分析把每个目标独立出来最后统计数量、算位置和尺寸。这套逻辑放在Halcon里就是几个算子的组合。2.1 核心步骤拆解分割→处理→连通→筛选Blob分析的标准流程我习惯把它分成四步分割、形态学处理可选、连通、筛选测量。分割就是把目标和背景分开Halcon里最常用的是threshold或fast_threshold输入是一个灰度图输出是一个区域Region。这一步的关键是目标和背景之间要有足够的对比度。如果你拍出来的图像目标几乎和背景融为一体那你后面再牛的算法也是白搭所以打光才永远是第一位。形态学处理就是前面讲的腐蚀膨胀开闭根据二值图的质量来决定要不要做、做哪一步。多的我不说但记住一句话形态学处理是手段不是目的能不做就不做做的时候宁小勿大。第三步是连通把区域分成一个个独立的连通域Halcon里的算子是connection或connected_components新版里connected_components更常用。这个算子会把原本连在一起的区域拆成多个独立的Region每个Region就是一个目标。最后是筛选和测量。筛选用select_shape按面积、圆度、宽度、高度等特征把不要的杂质滤掉。测量用area_center求面积和中心坐标用smallest_circle求最小外接圆用smallest_rectangle2求最小外接矩形。对新手来说只要把 threshold → connected_components → select_shape 这条线走通了你已经能解决很多实际项目了。2.2 常用特征筛选有哪些怎么组合使用select_shape是Blob分析里最灵活、也最能体现经验的地方。它的原理很简单给每个连通域算出一堆特征值然后你设定条件满足的留下不满足的扔掉。常用的特征包括面积area、宽高width,height、圆度circularity、矩形度rectangularity、凸度convexity等。新手最容易犯的毛病是只用面积一个特征。你以为面积够大就能排除所有干扰实际上反光碎块、灰尘、边缘残缺目标的面积很可能落在你设定的区间内。我做过一个PCB焊点检测项目只按面积筛选过检率惨不忍睹。后来加上圆度限制误检少了一大半。举这个例子是想说筛选条件一定要根据目标的几何特征组合使用。圆形目标就加圆度条件矩形目标就加矩形度条件有条状纹理的目标要关注宽和高的比例。这是很符合直觉的事情就像你在一堆水果里挑苹果大小是一个标准颜色是一个标准有没有虫眼又是一个标准条件越多挑出来的东西越接近你真正想要的。后面实例里我会给你一个可以抄的组合方式。3. 实例跑一遍垫片计数与尺寸测量第3讲实操前面讲概念下面来点实际的。假设我们有这样一张图暗色传送带上放着若干个圆形金属垫片垫片表面有轻微反光部分垫片上有不明显的小锈点目标是要数出垫片的个数并测量每个垫片的圆心坐标和半径。这张图的光照是均匀的目标与背景对比明显属于典型的“入门但实用”场景。这个例子选得好是因为它把形态学运算和Blob分析都用上了而且每一步都看得见效果。你可以跟着下面的HDevelop脚本一步一步把流程跑通。3.1 完整HDevelop脚本* 读取图像 read_image (Image, gasket.png) * 如果采集到的是彩色图先转灰度 rgb1_to_gray (Image, GrayImage) * 灰度直方图查看目标和背景的峰值分布辅助确定阈值 gray_histo (GrayImage, GrayImage, AbsoluteHisto, RelativeHisto) * 全局阈值分割把亮色垫片从暗色背景中分离出来 threshold (GrayImage, Region, 128, 255) * 用闭运算填补垫片内部的锈点小洞和轻微断裂 closing_circle (Region, RegionClosing, 4.5) * 连通域分析把每个垫片拆成独立区域 connected_components (RegionClosing, ConnectedRegions) * 特征筛选面积在5000到50000像素之间圆度大于0.85 select_shape (ConnectedRegions, SelectedRegions, [area, circularity], and, [5000, 0.85], [50000, 1.0]) * 统计筛选后的区域数量 count_obj (SelectedRegions, Number) * 计算每个垫片的面积、圆心坐标和最小外接圆半径 area_center (SelectedRegions, Area, Row, Column) smallest_circle (SelectedRegions, RowCircle, ColumnCircle, Radius) * 在图像上逐个显示检测结果 dev_open_window (0, 0, 800, 600, black, WindowHandle) dev_display (Image) for i : 1 to Number by 1 disp_circle (WindowHandle, RowCircle[i-1], ColumnCircle[i-1], Radius[i-1]) endfor这段脚本不复杂但每一步都值得你细想一下为什么这么写。3.2 对每一步选择的“为什么”逐一拆解先看阈值分割。我用的是threshold (GrayImage, Region, 128, 255)上限设成255是因为垫片是亮色背景是暗色亮色目标的灰度值集中在高段。但128这个下限是不是固定能用当然不是。实际项目里光照一变128可能就废了。所以我习惯先执行一次gray_histo看一眼灰度直方图里代表背景和目标的两个峰到底在哪里再去确定阈值。如果你发现目标和背景两个峰几乎重叠说明对比度不够先回头调打光不要硬调阈值硬调出来的结果是不可靠的。再看闭运算。为什么是closing_circle而不是dilation_circle因为垫片表面有小锈点二值化之后会在垫片内部形成黑色空洞同时边缘如果有细微反光不均也可能出现细小的断裂。膨胀能填洞但会让整个垫片区域胖一圈直接影响后面的圆心和半径测量。闭运算先膨胀后腐蚀填完了洞又把尺寸恢复回来这才是我们想要的“只补洞、不改大小”。半径4.5也是特意选的垫片直径如果大约是200像素锈点直径约8像素那么半径4.5的圆盘结构元足够盖住锈点。如果你给的半径过大比如给到30那圆形垫片会被搞得不像圆形圆度反而下降。再看特征筛选。我用的是面积加圆度的组合。面积把过小的灰尘和过大的背景残留排除掉圆度0.85以上则把边缘不完整的残缺垫片、两个连在一起的异常区域排除掉。这里要解释一下如果两个垫片意外离得很近在连通域分析之后它们可能被算成一个整体而这个整体的圆度会明显低于0.85所以能被这个条件拦下来。如果只用面积两个垫片连成一个区域后面积刚好是单个垫片的两倍可能还在面积区间内那就漏过去了。最后看smallest_circle它求的是每个区域的最小外接圆返回圆心和半径。对于圆形垫片来说这个半径基本就是垫片的真实半径。如果你的目标是方形的这个算子就不合适应该换成smallest_rectangle2。选算子的原则很简单测量对象的几何形状决定外接模型。这条流程跑到这你就完成了从图像输入到结果输出的完整Blob分析。同样的逻辑你在C、C#、QT工程里调用Halcon的HDevEngine时也是这样组织的无非是把这段脚本封装成类、把参数开放出来给界面调而已。4. 踩坑实录形态学Blob分析常见问题排查理论讲完了实例也跑通了下面说说我这些年实际项目中踩过的坑。这些问题不一定都出现在这次例子中但都是Blob分析这条路上高频出现的坑我整理成一份速查表放在这里每个都值得你收藏。问题现象常见原因解决思路二值化后目标内部有很多黑点表面反光不均或锈斑闭运算补洞半径以黑点最大尺寸为准二值化后目标边缘毛刺严重光照过强或背景纹理先开运算去毛刺再看是否需要调打光两个目标被闭运算连成一片闭运算半径大于目标间隙减小半径如果仍粘连改用分水岭分割开运算后目标直接消失结构元尺寸大于目标尺寸重新计算目标像素直径让结构元远小于它筛选后数量忽多忽少只用了面积一个特征组合圆度、矩形度、宽高等多个特征测量出来的尺寸比实际偏大膨胀/闭运算半径过大减小形态学半径或改用不改变尺寸的修补方式光照变化后阈值全部失效固定阈值不可迁移用动态阈值或根据直方图自动选阈值这里我挑几个展开说说因为它们是真实场景里最容易让人怀疑人生的。第一个是“闭运算把两个目标焊在一起”。闭运算的半径如果大于两个目标之间的最小间隙膨胀阶段就会把间隙填满腐蚀阶段又不可能把它恢复两个目标就黏成一体了。遇到这种情况先别急着调算法先看你的打光和夹具。能不能让目标在图像里分得更开这是最省事的方案。如果物理上改不了再考虑把闭运算半径减小或者干脆不用闭运算改用分水岭分割。分水岭虽然要调参数但对贴在一起的同类目标往往更有效。这里要记住形态学处理是“修改图像形状”的操作它分不清哪个是目标哪个是间隙只要半径比间隙大它就“一视同仁”地糊上。第二个是“目标边缘反光导致圆度筛选误杀”。金属件反光很常见二值化之后圆的边缘会带着一圈锯齿本来应该是0.95的圆度硬是被降到0.7。很多人一看到圆度不够就往下调阈值比如从0.85调到0.6结果什么干扰都放进来了。正确的做法是先做一次轻微的开运算把边缘锯齿磨掉一点再计算圆度。如果你开完运算圆度从0.7升到了0.9说明原来的锯齿是噪声不是目标本身形状的真实反映。这种“先修再量”的顺序比直接放宽筛选阈值要可靠得多。第三个是关于“阈值失效”的问题。我见过不少项目在实验室里跑得好好的一到车间就全部崩盘原因就是那里的光照不是实验室的稳定光照。固定阈值128这种代码只有在光源和曝光完全可控的场景下才靠得住。车间环境稍微来点自然光变化、灯管老化二值化结果就完全变了。这时候有两个选择一是改用动态阈值dyn_threshold让每个局部的分割阈值跟随背景亮度走二是每次运行之前先采集一张无目标的背景图用背景图做差分再去阈值化。无论哪种都比死记一个128强。这个问题在你换产品、换机台的时候尤其常见几乎每个做过落地项目的工程师都有一把辛酸泪。第四个是低对比度目标带来的连锁反应。有时候你调阈值怎么调都调不出干净的效果然后就开始疯狂加形态学开完再闭、闭完再开最后图像倒是“干净”了目标纹理也全没了测量结果大得离谱或者小得离谱。坦白说形态学处理是修图工具不是起死回生丹如果阈值阶段已经病入膏肓后面再修都是硬撑。正确做法是回头检查打光方案加背光、加偏振片、调低曝光时间目标是让目标和背景的灰度差拉到50以上。灰度差越大后面阈值、形态学、特征筛选每一步都越稳。这条经验是我反复跟新人强调的也是我觉得整个Blob分析流程里最容易被忽视、但最值得先解决的问题。5. 最后分享一个我自己的习惯代码写到这流程跑通问题也聊了不少最后想分享一个我从项目里养成的习惯每到一个新项目、新拍摄环境我一定会先存一张“二值化中间图”。这张图就是做完阈值分割之后、还没做形态学处理的状态。为什么存它因为你后续调形态学参数、调筛选特征的时候遇到效果不对回头看一眼原始二值图立刻能判断出问题出在采集阶段还是处理阶段。二值图干净后面出的问题就一定是参数没调好二值图本身又脏又乱那你后面再怎么调代码都只是治标不治本。形态学运算和Blob分析这套组合拳我在Halcon里用了很多年后来换成OpenCV、写成C底层核心思路也没有变过先分割再修形然后提取特征最后筛选。不同工具的算子名字不一样但逻辑永远是这条线。你把它吃透了后面学模板匹配、学OCR识别、学深度学习分割都会觉得非常顺因为那些高级技术里照样藏着Blob分析的内核。做视觉这行不会这招你连入门的门槛都没摸到。