本节摘要:形态学操作在二值图上"按形状修正形状":腐蚀收缩白区、膨胀扩张白区,两者组合出开运算(去小白点)、闭运算(填小洞、连断裂)、梯度(描边)、礼帽黑帽(提取亮暗细节)。本节先跑最小例子再讲结构元素的作用,核心是"结构元素形状决定操作的方向性",最后给出噪声点、断裂、空洞三类问题的标准组合拳。
阅读完本节,你应当能够:
造一张有白色方块的二值图,先腐蚀再膨胀,各看一眼:
import cv2 import numpy as np img = np.zeros((100, 100), dtype=np.uint8) cv2.rectangle(img, (20, 20), (79, 79), 255, -1) # 实心白色方块 kernel = np.ones((5, 5), np.uint8) # 5x5 方形结构元素 eroded = cv2.erode(img, kernel) # 腐蚀:白区四周各缩2像素 dilated = cv2.dilate(img, kernel) # 膨胀:白区四周各扩2像素
运行后一目了然:腐蚀把 60×60 的方块削成约 50×50,膨胀把它扩成约 70×70。方向记法很朴素:腐蚀是"白区被啃,边界收缩",膨胀是"白区长胖,边界外扩"。这个方向性是相对于"前景=白色"的约定,而 OpenCV 的默认约定正是亮区为前景——如果你把目标画成黑底白字,一切叙述都成立;若你的目标是暗区,先反相再操作,否则方向全反。
再跑组合操作,看一次就能记住它们各干什么:
opened = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel) # 开:先腐蚀后膨胀 closed = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel) # 闭:先膨胀后腐蚀 grad = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) # 梯度:膨胀减腐蚀
在方块图上,开和闭的结果看起来都跟原图差不多——因为没有噪声点和空洞,它们的"修正作用"无事可做。往图上撒几个孤立白点、在方块里戳几个黑洞,差异立刻显现:开运算后白点消失、方块原样;闭运算后黑洞被填、方块原样。"先损后补"的设计哲学就在这里:腐蚀那一步本来会把边界啃掉一圈,但随后的膨胀又把边界推回去——只有那些尺寸小于结构元素的干扰物(腐蚀一步就整根拔掉的噪点、膨胀一步就能合拢的窄缝)回不来,于是被精确清除。
所有形态学操作都围绕一个形状模板展开——结构元素,它决定"以什么形状的邻域为单位做与/或运算"。3×3 全一是方形笔刷,cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (9,9)) 是圆笔刷,MORPH_RECT 与 MORPH_CROSS 分别给出矩形与十字。
笔刷的形状会"印"在结果上:方形结构元素腐蚀出的边界带直角,圆形结构元素处理出的边界圆润。处理圆形目标(细胞、气泡、硬币)优先椭圆笔刷;处理方形目标(集成电路、建筑平面)用方笔刷;十字笔刷用于只想在横竖方向起作用的场景,比如修复水平细线。
尺寸是力度旋钮:结构元素越大,腐蚀膨胀越猛,开运算能清掉的干扰物尺寸上限也越大。经验起点是 3×3 或 5×5,按"要清除的目标最大有多大"来定——想擦掉直径 8 像素的噪声团,结构元素就得比 8 大,否则擦不干净。
腐蚀和膨胀是两块基本砖,砌墙的规则:
| 操作 | 定义 | 效果 | 典型用途 |
|---|---|---|---|
| 开运算 open | 先腐蚀后膨胀 | 去掉小的白色孤立点与毛刺,白区整体不变 | 清除背景噪声、平滑目标轮廓 |
| 闭运算 close | 先膨胀后腐蚀 | 填充小黑洞、连接窄缝断裂 | 补全目标内部空洞、粘连断裂字符 |
| 梯度 gradient | 膨胀减腐蚀 | 得到白色区域边界线 | 快速提取轮廓骨架 |
| 礼帽 tophat | 原图减开运算 | 提取被开运算去掉的亮细节 | 分离明亮小目标、光照校正 |
| 黑帽 blackhat | 闭运算减原图 | 提取闭运算填掉的暗细节 | 提取暗孔洞、阴影结构 |
礼帽和黑帽不那么直观,值得多说一句:开运算会把"比结构元素小的亮斑"整体抹掉,那么原图减去开运算的结果,剩下的恰好就是这些亮斑——礼帽等于一个"亮细节滤镜"。照度不均的图上做阈值分割前,礼帽常被用来拉平亮场,这比全局阈值稳得多(与下一节的自适应阈值殊途同归)。
腐蚀与膨胀在灰度图上依然有定义:腐蚀取邻域最小值(整体变暗),膨胀取最大值(整体变亮)。开运算于是变成"抹掉亮的小突起",闭运算变成"填掉暗的小坑"。这个扩展让形态学可以不二值化就直接压灰度噪声,但也让效果更难预料——我建议只在二值图上用形态学做形状修正,灰度图上的去噪优先交给第 2 章第 1 节的滤波,各司其职。
直接给结论,配方可以背下来:
实际项目里两者常常连用:先开运算清背景,再闭运算补目标,顺序不要反——先闭会把噪声点也"补"得更结实,给后面添堵。
morphologyEx 的 iterations 参数可以重复执行,开运算迭代 2 次的效果接近用更大结构元素开 1 次,但不完全等价(细节上有差异)。需要强力度时,我更倾向直接换大结构元素,语义清晰;迭代次数留给"同一核稍微多修一遍"的微调场景。
如果你的前景是暗的(白底黑字),腐蚀与膨胀的效果整体反过来:腐蚀会"加粗"黑字。两个选择:先 bitwise_not 反相成白前景,操作完再反回去;或者接受反方向语义并在注释里写明。团队里统一"亮前景"约定,能省掉无数沟通成本。
⚠️ 常见坑:结构元素开得太大,把目标本体也啃掉。比如 5×5 的核开运算,会把所有宽度小于 5 的白色细线整条删除——细线检测场景用开运算提纯时,务必先量一下线宽。另一个坑是对彩色图直接做形态学,三通道各自运算后颜色交界处会串色,先转灰度或 HSV。

输入前提不同:形态学梯度只对二值图有意义(白区的边界线),Canny 作用于灰度图(灰度突变的边缘)。二值图的边界用形态学梯度一步到位且没有参数可调;灰度图的结构提取必须走 Canny 的四阶段(详见第 2 章第 5 节)。混用两者的输出做下游,是初学者常见的"图不对题"。
方与椭圆核是各向同性的(旋转不变),十字核有明确的方向性。想"只清理水平方向的细刺、不动竖直结构"这类需求,用横放的矩形核或十字核就能实现方向选择性。核的形状是"方向偏好"的开关,比想象中常用。
不必。清背景的噪声直径与目标内空洞的尺寸通常不同,两步各用各的核更合理。连用的顺序(先开后闭)是共识,但两步的力度独立标定——用同一核"一档到底"是偷懒,效果上限受限。
第 2 章第 4 节产出的二值掩码,到第 4、5 章可用目标框之间,隔着一段几乎固定的净化流程,把它固化成配方。
第一步开运算清背景碎斑。结构元素按"最大噪声团直径加一"取奇数核,椭圆形状对无方向性的斑点最稳。清完检查目标本体有没有被啃(本节 3.3 的线宽警告):目标出现缺口说明核过大,回调。
第二步闭运算补目标空洞。核按"最大空洞口径"取,必要时迭代两次(小核两次比超大核一次温和)。这一步之后再轮廓提取,得到的边界连续性会好得多。
第三步轮廓级过滤兜底。findContours 找连通域,按面积与长宽比过滤:面积下限砍掉残余碎块,长宽比过滤掉细长的伪目标(划痕、反光条)。到这里,"可用的目标候选框"就交付了——第 4 章的传统检测与第 5 章的运动分析,吃的都是这个规格的输入。
三步的顺序与力度标定法(按清除目标的尺寸定核)都来自本节原理,配方本身没有新知识,新的是把"判断→选核→验证"的循环压缩成肌肉记忆。
二值图是形态学的主场,那灰度图怎么变成二值图?下一节的阈值分割正是这座桥——固定、自适应、Otsu 三档怎么选,一次说清。
不必。椭圆形结构元素本身就是不对称的,用它做膨胀时朝长轴方向的扩张更明显,处理斜向条带状目标时比矩形核更好用。还能用 getStructuringElement 自定义任意形状,甚至定义一个"只有左边有像素"的偏置核,专门做特定方向的桥接。核的形状、尺寸两个维度都值得一试,很多"开闭运算不奏效"的场景,换个形状就通了。