1. 形态学操作到底在做什么先聊点实际的。很多人第一次接触形态学是在用 OpenCV 做预处理或者轮廓分析的时候。看到cv2.morphologyEx()这个函数参数里又是MORPH_OPEN又是MORPH_CLOSE然后梯度、顶帽、黑帽一堆名词砸过来很容易直接懵掉。我当时也是这样。但真正用过几次之后就会发现形态学操作本质上就一句话用一个小窗口在图像上滑动根据窗口内的局部灰度或二值状态来决定中心像素的输出值。这个思路跟卷积很像区别在于卷积是加权求和形态学是“比较大小”。比如腐蚀就是取窗口内的最小值作为中心值膨胀就是取最大值。这就是最朴素的理解。形态学原本是生物学里研究动植物形状结构的学科计算机视觉把它借过来用来处理图像的“形状”。所以它天然就适合二值图像——也就是黑白图黑色背景、白色前景那种。你拿到一张灰度图一般也得先做二值化或者阈值处理再上形态学效果才明显。它的应用场景非常广。我举几个真实的例子文字识别前的预处理把粘连的笔画断开或者把断裂的笔画接上。车牌识别去掉边框、铆钉等干扰只保留字符区域。医学图像处理把散落的细胞区域合并成一个完整的连通域。工业缺陷检测突出划痕、麻点去除背景纹理。你会发现形态学操作几乎不会单独出现在一个完整项目里它永远是“流水线”里的一环。但这一环非常重要因为它直接影响后续轮廓查找、特征提取、OCR 识别的准确性。所以在 OpenCV 的学习路径里形态学跟滤波、阈值、边缘检测一样属于必须吃透的基础操作。这篇文章我会把开运算、闭运算、形态学梯度、顶帽、黑帽这五个操作从原理到代码再到实际踩坑一次性讲清楚。代码基于 OpenCV 的 Python 接口版本 4.x 通用。2. 从腐蚀和膨胀说起开闭操作、梯度、顶帽、黑帽本质上都是腐蚀和膨胀的组合或者差值运算。不把腐蚀膨胀搞明白后面全部是空中楼阁。2.1 腐蚀让白色区域“瘦”一圈腐蚀的操作是结构元素在图像上滑动只有当结构元素覆盖的区域全部是前景白色时中心点才保留为白色否则变成背景黑色。用灰度图的语言说就是取窗口内像素的最小值作为输出。import cv2 import numpy as np img cv2.imread(demo.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) kernel np.ones((3, 3), np.uint8) eroded cv2.erode(binary, kernel, iterations1)效果就是白色区域边缘被“吃掉”一圈。如果两个白色物体之间只有很细的连接腐蚀可以把连接断开实现“分离”。这在文字识别里很常见因为有些字的笔画之间距离太近或者扫描件有噪点把笔画连起来了先腐蚀一下就能让字符界限清晰。iterations参数表示腐蚀次数每多一次边缘就多吃掉一圈。但注意iterations等于 2 并不完全等价于用一个更大的核腐蚀一次。有些教材说等价实际测试你会发现边缘细节有差异因为每轮腐蚀其实是在上一轮结果上重新计算邻域。对于大多数应用两种写法结果差不多但如果你的核形状不规则还是尽量用“更大的核”而不是“多次迭代”。2.2 膨胀让白色区域“胖”一圈膨胀跟腐蚀正好相反结构元素覆盖的区域内只要有一个点是前景中心点输出就是前景。灰度图里就是取窗口内最大值。dilated cv2.dilate(binary, kernel, iterations1)膨胀的典型用途是“接续”——把断裂的线条接上把破碎的区域连成整体。比如拍照的时候笔画中间有一条细缝或者二值化之后文字内部出现空洞膨胀一把就能补上。但膨胀也有个副作用它会让所有白色区域变大如果后续要做轮廓面积统计结果会偏大。所以膨胀之后往往跟着腐蚀这就是开运算和闭运算的雏形。2.3 结构元素的选择结构元素就是那个“小窗口”OpenCV 里用cv2.getStructuringElement()生成。kernel_rect cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) kernel_ellipse cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) kernel_cross cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5))三种形状的效果差异我简单说一下MORPH_RECT矩形核四四方方腐蚀膨胀都最“暴力”适合去棱角不敏感的场景。MORPH_ELLIPSE椭圆核边缘过渡更圆润处理细胞、圆形目标更好用。MORPH_CROSS十字核只考虑上下左右适合处理对角线方向的目标对孤立噪点相对友好。选核的原则是核的形状要尽量接近你关心的目标形状。核的尺寸根据目标大小来定。比如文字笔画宽度大约是 3 个像素那核就不应该超过 3×3否则笔画会被磨平。提示核的尺寸没有绝对标准实践中我会先取一个经验值用滑动条实时调参在窗口里直接看效果比反复改代码重启效率高得多。这个习惯我从做图像处理第一天保持到现在。3. 开闭操作形态学的“磨皮”与“修复”开运算和闭运算都是腐蚀和膨胀的组合只是顺序不同。它们解决的是两类非常典型的问题。3.1 开运算先腐蚀再膨胀去噪点断粘连opening cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel)开运算的流程是腐蚀先把白色区域边缘削掉一圈同时把细小的白色噪点直接消除——因为这些噪点尺寸小于结构元素腐蚀一轮就整个消失了。然后膨胀把剩下的大区域恢复到接近原来的大小。我用生活中的例子解释开运算就像给一块路面做局部清理。你先用铲子把突出的小石子铲掉腐蚀再把路面整体铺平修复到原来的高度膨胀。结果是那些孤立的小突起没了大结构基本不受影响。它最常用的场景是去小白点噪点和断开物体之间的细连接。举一个实际案例。我处理过一批印刷品上的二维码识别原始图上有一些墨点二值化之后墨点变成一个个 1~2 像素的白色噪点。如果不处理二维码的定位图形会被噪点干扰识别率直线下降。用cv2.morphologyEx(binary, cv2.MORPH_OPEN, np.ones((3, 3), np.uint8))跑一遍噪点全消失二维码图案依然完整识别率从 60% 提到了 98%。另一个场景是芯片引脚检测。引脚之间如果有焊锡残留二值图里它们就连在一起了这时候开运算可以把粘连的小桥断开让每个引脚重新变成独立的连通域。但开运算也有局限。如果噪点比结构元素还大开运算就无能为力。这时候要么增大核但增大核会连带着把目标细节也磨掉要么换别的办法比如连通域分析加面积过滤。所以开运算适用于“噪点尺寸明显小于目标尺寸”的前提。3.2 闭运算先膨胀再腐蚀补空洞接断线closing cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel)闭运算顺序反过来先膨胀把目标区域向外扩一圈区域内的小空洞、小裂缝就会被填平——因为膨胀取最大值空洞内部被周围的白包围自然就被填上了。然后腐蚀把膨胀出去的边缘再收回来整体尺寸恢复。闭运算最常用的场景是填充物体内部空洞和连接断裂的线条。我再举一个做过的事。有一批单据需要做手写数字识别原始图片经过二值化后数字内部有不少白色空洞因为扫描时光线不均匀写的圆珠笔迹中间反光导致部分像素被判成背景。直接做轮廓提取一个“8”可能会被拆成好几个轮廓。用cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_ellipse)处理后空洞被填上每个数字变成一个完整连通域后续特征提取稳定多了。闭运算也非常适合修复文字笔画断裂。之前处理一个从书脊扫描出来的文字图像因为书脊是弯曲的文字在弯曲处有断笔。闭运算之后断笔被接上OCR 准确率提升很明显。我听说有些搞文档图像的人会把开运算和闭运算串联使用先闭运算接上断笔再开运算去掉孤立噪点效果比单独用任何一个都好。我自己试过确实能减少很多后续的轮廓清理工作。3.3 开闭操作的参数选择经验开闭操作有四个参数可以调结构元素形状、结构元素尺寸、迭代次数、处理的图像类型。参数经验取值注意事项结构元素形状文字/直线用RECT圆形目标用ELLIPSE十字核用的场景较少结构元素尺寸3×3 到 15×15超过目标尺寸会把目标磨没iterations1~3大于3基本说明核选小了图像类型二值图最佳灰度图也能跑但效果不可控结构元素尺寸的选择有个朴素办法先量一下目标在图中大概占几个像素。比如文字笔画粗约 5 像素那核设 3×3 或 5×5 比较安全如果设 15×15笔画连形状都保不住了。另外一个经验是处理灰度图时开闭运算的效果远不如二值图明显。因为灰度图里没有明确的“前景/背景”概念腐蚀膨胀只是在改局部亮度分布你肉眼可能看不出多少变化。所以如果你拿到一张灰度图第一件事应该是做阈值分割转成二值图再上形态学。4. 形态学梯度边缘提取的另一条路4.1 梯度到底是什么形态学梯度定义很简单膨胀结果减去腐蚀结果。gradient cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)膨胀让白色区域扩张腐蚀让白色区域收缩两者相减剩下的就是边缘那一圈“差值带”。从视觉效果看它提取的是目标的轮廓而且是一个闭合的、完整的边界。很多人第一次看到形态学梯度都会问这不就是边缘检测吗跟 Canny 比有什么优势我的回答是形态学梯度对二值图像的边缘提取更“干净”。Canny 算法包含高斯滤波、梯度计算、非极大值抑制、双阈值滞后处理链路长、参数多而且在二值图上反而可能因为边缘太锐利导致一些奇怪的双边缘。形态学梯度只有一次减法和一次形态学操作计算量小、参数少结果边界精确对齐原始目标边缘。我用一张表格对比一下特性形态学梯度Canny 边缘检测适用图像二值图、灰度图均可用灰度图边缘连续性连续闭合效果好可能出现断裂需要调阈值参数复杂度只需核尺寸高、低阈值ApertureSize 等计算速度快较慢边界定位边界为膨胀与腐蚀的差带亚像素级连续边缘但也要说清楚形态学梯度的边缘宽度受核尺寸影响。核大梯度带就宽定位精度下降核小梯度带窄但可能漏掉弱边缘。所以要做精细定位还是 Canny 更合适要做二值轮廓快速提取形态学梯度更顺手。4.2 一个坑梯度对灰度图效果不稳定有一段时间我图省事直接把灰度图丢给MORPH_GRADIENT没有预先二值化。结果出来的图噪声非常重。原因是灰度图里背景的纹理、光照渐变都会被膨胀腐蚀的差值放大变成一堆高亮噪点。后来我的标准流程是灰度图先做高斯模糊去噪再做自适应阈值二值化最后才上形态学梯度。这样出来的边缘非常清爽。高斯模糊的核一般取 3×3 或 5×5二值化的邻域大小根据图像分辨率调。gray cv2.imread(demo.png, cv2.IMREAD_GRAYSCALE) blur cv2.GaussianBlur(gray, (5, 5), 0) binary cv2.adaptiveThreshold(blur, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY, 11, 2) gradient cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, np.ones((3, 3), np.uint8))4.3 内部梯度、外部梯度与形态学梯度的区别严格来说cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel)计算的是“基本梯度”也就是膨胀减腐蚀。但论文和工作里还经常出现另外两个概念内部梯度原图减腐蚀结果。得到的边缘在目标内部比基本梯度更细适合精确定位目标边界内侧。外部梯度膨胀结果减原图。得到的边缘在目标外部适合分析目标外轮廓和周边背景。OpenCV 没有直接提供内部梯度和外部梯度的函数需要用cv2.erode和cv2.subtract手动组合eroded cv2.erode(binary, kernel) internal_gradient cv2.subtract(binary, eroded) dilated cv2.dilate(binary, kernel) external_gradient cv2.subtract(dilated, binary)注意这里我用的是cv2.subtract不是 NumPy 的直接相减。原因是cv2.subtract会把负值截断为 0而直接用binary - eroded在二值图上虽然多数情况没问题但遇到 uint8 溢出会出怪事。养成用cv2.subtract的习惯能少很多麻烦。4.4 形态学梯度在轮廓测量里的实战形态学梯度一个非常实用的场景是测量目标的边界宽度。比如我们要测一个金属圆环的外径和内径直接用二值图找轮廓再拟合椭圆速度比较慢用形态学梯度快速得到内外边缘再做霍夫圆检测效率高很多。我在做零件尺寸检测的时候用过一套流程binary cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU)[1] gradient cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))) contours, _ cv2.findContours(gradient, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)这样拿到的轮廓基本上就是目标的边界轮廓再做外接矩形或者最小外接圆就能得到尺寸信息。整个流程简单稳定不需要调 Canny 那两个阈值。如果你项目里边缘检测老是调参调不顺不妨试试形态学梯度这条路线。5. 顶帽与黑帽提取亮细节和暗细节5.1 顶帽原图减开运算保留比背景亮的小目标顶帽运算的公式是原图减去开运算结果。tophat cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel)开运算是先腐蚀再膨胀相当于提取了背景的“主体”。原图减掉这个主体留下来的就是那些没有被开运算保留的小凸起、小亮块。用生活经验理解开运算像一块粗砂纸把表面突出的颗粒磨平了顶帽就是“磨下来的碎屑”——也就是那些突出的颗粒本身。顶帽最典型的应用是不均匀光照下的缺陷或目标提取。举个例子有一张在强光下拍的工件照片中央区域反光很强四周光线暗直接二值化会导致中央和四周的亮度标准不一致。但是用顶帽运算可以先把不平坦的背景“减掉”剩下的亮细节不受背景亮度影响。我更常用的是从噪声背景中提亮斑。比如半导体晶圆表面检测落上去的灰尘或者刻印的字符在特定光照下表现为比背景更亮的小块。顶帽操作可以直接把它们从复杂的背景纹理中拉出来。gray cv2.imread(wafer.png, cv2.IMREAD_GRAYSCALE) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel) _, binary cv2.threshold(tophat, 50, 255, cv2.THRESH_BINARY)核的尺寸在这里是关键。核得比目标亮斑大一些但又要小于背景的起伏周期。我一般先做了连通域分析量一下亮斑大小再选核。核太小顶帽结果里会残留背景纹理核太大又会把目标也当背景减掉。5.2 黑帽闭运算减原图提取比背景暗的小目标黑帽运算的公式是闭运算结果减去原图。blackhat cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel)闭运算是先膨胀再腐蚀它会填平暗色的凹陷。闭运算结果减原图留下的是那些被填掉的“暗坑”。用砂纸类比闭运算像把表面凹陷填平黑帽就是你取下来的“填充料”——那些凹陷本身。黑帽最经典的应用是提取暗色缺陷比如印刷电路板上的断线、划痕或者金属表面细小的压凹。光照下这些缺陷比周围暗直接用阈值分割容易受到背景反光影响黑帽处理后暗缺陷变成高亮再用阈值就干净了。我也用过黑帽来做字符分割。有些金属铭牌是激光打标的字符其实是凹下去的暗纹背景是粗糙的亮面。直接用二值化凹纹沟壑跟背景亮面混在一起。黑帽运算先把暗纹提亮阈值分割后再做轮廓分析每个字符都能稳稳地切出来。5.3 顶帽黑帽在灰度图和二值图上的差异这里有个特别容易踩的坑顶帽和黑帽我建议在灰度图上用不要在二值图上用。原因是顶帽和黑帽的价值就在于“去除不平坦背景的影响”这是灰度图才有的问题。二值图只有 0 和 255背景要么全黑要么全白不存在“不平坦”的说法。你在二值图上做顶帽结果往往就是开运算去掉的噪点变成白色但这个过程用一次开运算的差值也能实现没必要绕这一圈。我个人的经验是灰度图 光照不均 要提取亮目标 → 用顶帽。灰度图 光照不均 要提取暗目标 → 用黑帽。二值图去噪 → 直接用开运算。二值图补洞 → 直接用闭运算。按照这个判断逻辑走基本不会用错。6. 详细代码实操一个去噪加目标提取的完整案例讲了这么多概念肯定得来一份能直接跑的代码。下面这个例子是我经常用的套路先开运算去噪再顶帽提亮细节最后找轮廓画框。假设场景是这样的我们有一张在复杂背景下拍摄的工件照片背景有轻微的纹理噪声工件表面有激光刻的编号字符需要把字符区域提取出来。import cv2 import numpy as np # 1. 读取图像 img cv2.imread(workpiece.jpg) gray cv2.cvtColor(img, cv2.COLOR_BGR2GRAY) # 2. 高斯滤波去噪建议核适中5x5比较通用 blur cv2.GaussianBlur(gray, (5, 5), 0) # 3. 阈值二值化这里用OTSU自动阈值 _, binary cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) # 4. 开运算去掉白色噪点核选3x3因为噪声很小 kernel_open cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) opening cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_open) # 5. 闭运算接上字符断笔核用5x5比字符笔画略宽一点 kernel_close cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) closing cv2.morphologyEx(opening, cv2.MORPH_CLOSE, kernel_close) # 6. 形态学梯度提取字符边缘轮廓 kernel_grad cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) gradient cv2.morphologyEx(closing, cv2.MORPH_GRADIENT, kernel_grad) # 7. 查找轮廓并绘制外接矩形 contours, _ cv2.findContours(gradient, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result img.copy() for c in contours: area cv2.contourArea(c) if area 50: # 过滤掉太小的噪声轮廓 continue x, y, w, h cv2.boundingRect(c) cv2.rectangle(result, (x, y), (x w, y h), (0, 255, 0), 2) cv2.imshow(result, result) cv2.waitKey(0) cv2.destroyAllWindows()来解释一下每一步的用意高斯滤波我用 5×5 核是因为 3×3 对轻微噪声压不住7×7 又会把细小字符磨糊。5×5 是大多数 300dpi 扫描图的折中选择。阈值选择 OTSU 自动阈值。很多场景光照不均固定阈值完全没法看。OTSU 能根据灰度直方图自动找一个类间方差最大的分割点省心很多。但如果你的图光照特别离谱OTSU 也会失效这时候要么分块做阈值要么先做顶帽预处理消除光照影响。开运算的核 3×3把细小的孤立噪点去掉。闭运算的核 5×5把字符之间因为光照造成的断裂接上。这两个尺寸都是经验值大家根据实际图改。我通常的做法是先开后闭先除噪再修复。如果你的图噪点不多但裂纹多那可以反过来先闭后开。顺序没有绝对视觉效果说了算。最后轮廓面积阈值area 50是过滤小噪声的。这个值取决于图像分辨率和目标大小。我做每张图第一步就是cv2.contourArea打印所有轮廓面积看一眼分布再定阈值基本不会错。运行完你会看到工件区域的字符被绿色外接矩形框出来背景纹理没有产生多余的框。这就是一个完整的小流程。7. 实战中总结的常见问题与排查技巧跟形态学打交道久了会积累一堆“当时想不明白后来一句话点醒”的问题。我把最常见的几个列出来基本覆盖了新手 80% 的疑惑。7.1 为什么开运算后目标也变“瘦”了如果你发现开运算后目标区域明显缩水说明结构元素尺寸选大了或者迭代次数太多。开运算的设计目标是“消除小于核的细节”核比你目标还大目标本身就是“细节”当然会被磨掉。解决办法量一下目标尺寸把核调整到目标的 1/3 到 1/2。比如目标宽度 15 像素核用 3×3 或 5×5 就合适别一上来就 11×11。7.2 为什么闭运算后目标边缘变得圆滚滚的闭运算先膨胀后腐蚀等于对目标的凸出尖角做了一次“打磨”。如果核太大原本尖锐的边缘会被抡圆方形的按键变圆角字符笔画变粗变形。如果你追求边缘锐利闭运算的核尽量小并且可以考虑在闭运算之后补一次开运算能恢复一部分尖锐感。7.3 梯度提取到的边缘太粗没法精确测量形态学梯度的边缘宽度大约等于核的尺寸。核 3×3边缘 3 像素左右核 5×5边缘就 5 像素。要更细的定位可以换成内部梯度或者直接上 Canny。测量场景下我的流程是小核梯度提取边缘 → 形态学细化cv2.ximgproc.thinning → 拟合几何参数。瘦身的边缘再拟合精度会好不少。7.4 顶帽结果里面全校是背景纹理看不到亮目标这说明背景的纹理周期跟目标尺寸差不多核没选对。我遇到过一种情况核是 15×15亮斑是 10×10结果亮斑也被包进了背景估计里顶帽输出一片灰。解决办法把核缩小到略小于目标尺寸试试。如果背景纹理本身就是大尺度的起伏顶帽很难一次性处理可以先做一次高斯差分DoG或者整幅图做灰度形态学重建。7.5 代码里cv2.bitwise_not要不要加有些人的预处理习惯是二值化之后白底黑字字符是黑色。这时候想做闭运算补断笔其实是要在“黑色区域”上做闭运算用反色后的图像操作完了再反色回来。inv cv2.bitwise_not(binary) closed_inv cv2.morphologyEx(inv, cv2.MORPH_CLOSE, kernel) binary_fixed cv2.bitwise_not(closed_inv)这个坑我在做 OCR 时踩过。直接在白底黑字上做闭运算补的不是字而是字周围的白色背景结果字越补越细。先想想你的前景到底是白还黑再决定要不要反色。7.6 参数调试的经验流程我现在的调试习惯是这样的先用一段代码把三次处理结果横向拼在一张图上显示原二值图、形态学处理图、最终轮廓图。然后写一个简单的滑动条绑几个核尺寸参数拖动观察。这个滑动条脚本保存着每个项目改改就能用。我把这段示意的滑动条代码贴出来方便直接套用def nothing(x): pass cv2.namedWindow(params) cv2.createTrackbar(kernel_size, params, 3, 31, nothing) cv2.createTrackbar(iterations, params, 1, 10, nothing) cv2.createTrackbar(area_min, params, 50, 5000, nothing) while True: ks cv2.getTrackbarPos(kernel_size, params) it cv2.getTrackbarPos(iterations, params) am cv2.getTrackbarPos(area_min, params) if ks % 2 0: ks 1 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (ks, ks)) opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel, iterationsit) contours, _ cv2.findContours(opened, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result cv2.cvtColor(opened, cv2.COLOR_GRAY2BGR) for c in contours: area cv2.contourArea(c) if area am: continue x, y, w, h cv2.boundingRect(c) cv2.rectangle(result, (x, y), (x w, y h), (0, 0, 255), 2) cv2.imshow(params, result) if cv2.waitKey(30) 0xFF ord(q): break cv2.destroyAllWindows()写完这个脚本调参会变得特别愉快。核心是永远不要把参数写死先用滑动条找到值域再固化成常量。8. 五个操作选型速查表与我的体会最后放一个我平时自己看的速查表。遇到图像处理需求先查表再动手效率能翻倍。操作公式主要用途典型核尺寸范围腐蚀最小值缩小前景、断开粘连3~7膨胀最大值扩大前景、连接断裂3~7开运算腐蚀膨胀去小白点、断细连接3~11闭运算膨胀腐蚀补小洞、接断笔3~11梯度膨胀-腐蚀提取闭合边缘轮廓3~5顶帽原图-开运算提取亮的细小目标大于目标尺寸黑帽闭运算-原图提取暗的细小目标大于目标尺寸说实话形态学这一块内容在 OpenCV 里算是“看起来不难实际用好多细节”的典型。函数就那么几个参数就核和迭代次数但真到了项目里核的形状、尺寸、先后顺序、要不要反色每个决策都影响最终结果。这也是我为什么建议用滑动条调参而不是闷头看文档——形态学操作的结果反馈非常直观动手调几遍比读十遍文档都管用。我在实际项目里的体会是形态学操作最强的组合是“开运算 连通域分析 闭运算”这条流水线。开运算清掉细碎的干扰连通域分析把目标区域挑出来闭运算再把目标内部的缺陷修补好。三个步骤下来图像质量基本能达到后续识别模型的要求。另外还有一个建议处理大图像时形态学操作是逐像素滑动遍历的核一大耗时增长明显。如果图像有上千像素宽尽量先做缩放或者分割别直接在原图上跑大核。实在要跑把图像缩到合理尺寸后再做速度差异天壤之别。最后再分享一个小技巧。如果你做的是实时视频流处理每一帧都跑形态学会很耗 CPU。可以降低处理帧率比如每 3 帧处理一次中间 2 帧直接用上一帧的结果视觉上一点都看不出来CPU 占用却能降一半。这个技巧在嵌入式设备上尤其重要。形态学操作本身不难难的是在合适的场景选择合适的操作。希望这篇梳理能帮你把思路理清楚。下次再遇到图像里的噪点、空洞、断线、边缘提取的需求别急着上深度学习先用形态学操作试一把说不定三行代码就搞定了。