
简介这份资源聚焦图像形态学中的开运算与闭运算面向具备一定图像处理基础、希望深入理解形态学操作的开发者与学习者帮助解决噪声去除、孔洞填充、边缘平滑等常见问题。压缩包内共1个文件为Python脚本大小约1KB可直接运行并观察处理前后的图像对比效果。目前已有2163人学习下载说明该主题在图像预处理领域具有较高的关注度。脚本基于OpenCV实现通过cv2.erode()与cv2.dilate()组合完成开运算与闭运算并演示了结构元素的定义方式如矩形核的构建。读者可从中掌握先腐蚀后膨胀消除孤立噪点、先膨胀后腐蚀填补小孔洞的核心逻辑理解结构元素形状与尺寸对结果的影响并可将代码直接迁移到自己的项目中作为形态学细化、边缘检测等任务的预处理基础。1. 开运算和闭运算为什么你的去噪结果总在“该留的丢了、该去的还在”做过工业视觉检测的人多半遇到过这种场景一张 PCB 板图像上既有细小的锡渣噪点又有元件引脚之间不该连通的微小毛刺。你上高斯滤波噪点糊了但引脚边缘也钝了你上中值滤波毛刺还在因为毛刺面积比中值核大。这时候真正管用的往往是形态学里的开运算和闭运算——它们不是“模糊”图像而是按结构元素的形状去“修剪”或“填补”图像。开运算和闭运算都由两个基础操作组合而成先腐蚀后膨胀叫开运算先膨胀后腐蚀叫闭运算。开运算擅长去掉比结构元素小的亮斑、断开细窄的连接闭运算擅长填补比结构元素小的暗洞、连接断裂的细线。它们解决的核心问题是在保留目标主体几何尺寸的前提下按尺寸筛选该留和该去的成分。适合做缺陷检测、字符分割、PCB 走线提取、医学细胞计数、遥感道路提取的从业者。这篇笔记按“原理选型 → OpenCV 落地 → 参数调优 → 避坑排查 → 进阶技巧”的顺序展开代码可直接抄参数给的是我产线上跑过的经验值。2. 开运算与闭运算的数学本质先腐蚀后膨胀到底在干什么2.1 腐蚀和膨胀形态学的两块基石腐蚀的直观含义是“让亮区域缩小”。它用一个结构元素kernel在图像上滑动只有当 kernel 覆盖的所有像素都是前景值为 1 或 255时中心像素才保留为前景。效果是边界向内收缩小于 kernel 的亮斑直接消失。膨胀则相反只要 kernel 覆盖范围内有一个前景像素中心就置为前景。效果是边界向外扩张小暗洞被填满断裂的细线被接上。用集合语言说腐蚀是前景集合与结构元素的闵可夫斯基差膨胀是闵可夫斯基和。但落到工程上你只需要记住一句话腐蚀做减法膨胀做加法减什么加什么由 kernel 的形状和尺寸决定。import cv2 import numpy as np # 读入灰度图二值化 img cv2.imread(pcb.png, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 127, 255, cv2.THRESH_BINARY) # 定义一个 3x3 矩形结构元素 kernel cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 单独腐蚀和膨胀 eroded cv2.erode(binary, kernel, iterations1) dilated cv2.dilate(binary, kernel, iterations1) cv2.imwrite(eroded.png, eroded) cv2.imwrite(dilated.png, dilated)这段代码里getStructuringElement的第二个参数是 kernel 尺寸iterations是迭代次数。迭代一次相当于用同样 kernel 连续做一次腐蚀效果约等于用更大 kernel 做一次但边界形状略有差异。我一般先用iterations1配合不同尺寸 kernel 试而不是直接堆迭代次数因为迭代次数多了计算量线性增长且边界会出现“方块化”伪影。2.2 开运算 先腐蚀后膨胀去掉小亮斑保留大目标开运算的公式是open(I) dilate(erode(I, K), K)。先腐蚀把小于 K 的亮斑和细连接去掉再膨胀把保留下来的大目标恢复到接近原始尺寸。注意“接近”二字——开运算不是无损的它会让目标的尖角变圆、细长突出物被削掉这是它的固有代价。开运算的典型用途去除背景中的孤立噪点比如白纸上的黑点反色后变成亮斑断开两个目标之间细窄的粘连比如重叠细胞之间的细桥提取水平或垂直走线用水平/垂直长条 kernel# 开运算先腐蚀后膨胀 opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 等价于手动两步 eroded cv2.erode(binary, kernel) opened_manual cv2.dilate(eroded, kernel) # 验证两者是否一致 print(np.array_equal(opened, opened_manual)) # TruemorphologyEx的MORPH_OPEN内部就是先腐蚀后膨胀OpenCV 做了优化比手动两步略快。参数上kernel 尺寸决定了“去掉多大的亮斑”3×3 去掉直径小于 3 像素的亮斑5×5 去掉小于 5 像素的以此类推。实际选型时先量一下你要去掉的噪点最大直径kernel 尺寸取比它大 1~2 像素即可取太大容易把目标本身的细节也削掉。2.3 闭运算 先膨胀后腐蚀填补小暗洞连接断裂细线闭运算的公式是close(I) erode(dilate(I, K), K)。先膨胀把小于 K 的暗洞和细缝填上再腐蚀把目标尺寸恢复。同样闭运算也不是无损的它会让目标之间的窄间隙被永久填死如果两个目标本来不该连闭运算之后就连上了这是最常见的翻车点。闭运算的典型用途填充目标内部的小孔洞比如金属表面反光造成的暗斑连接断裂的字符笔画或走线平滑目标轮廓的凹陷部分# 闭运算先膨胀后腐蚀 closed cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel) # 等价于手动两步 dilated cv2.dilate(binary, kernel) closed_manual cv2.erode(dilated, kernel) print(np.array_equal(closed, closed_manual)) # True闭运算的 kernel 尺寸决定了“填补多大的暗洞”。和开运算一样先量暗洞最大直径kernel 取比它大 1~2 像素。如果暗洞大小不一可以考虑用多个尺寸 kernel 做多级闭运算但要注意每级都会让目标间隙进一步缩小。2.4 结构元素形状怎么选矩形、椭圆、十字的适用边界OpenCV 提供三种常用结构元素形状创建方式适用场景注意矩形MORPH_RECT处理直角走线、PCB 焊盘会让斜边出现方块化椭圆MORPH_ELLIPSE处理圆形目标、细胞、焊球计算比矩形慢十字MORPH_CROSS保留水平垂直细节对斜向噪点抑制弱我一般默认用椭圆因为它在各个方向上各向同性不会引入方向性偏差。只有在明确知道目标只有水平垂直结构时比如提取表格线才用矩形或十字。椭圆 kernel 的尺寸参数是外接矩形宽高实际形状是内切椭圆所以 5×5 椭圆实际覆盖面积比 5×5 矩形小去噪力度也弱一些选型时要心里有数。3. 用 OpenCV 跑通开闭运算从二值图到缺陷标记的完整链路3.1 最小可复现脚本读图、二值化、开闭、保存下面这段代码是我在缺陷检测项目里常用的骨架输入一张灰度图输出开运算和闭运算后的结果并叠加显示差异区域。import cv2 import numpy as np def morphology_pipeline(img_path, kernel_size5, kernel_shapecv2.MORPH_ELLIPSE): # 1. 读图并转灰度 img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) if img is None: raise FileNotFoundError(f无法读取图像: {img_path}) # 2. 自适应二值化应对光照不均 binary cv2.adaptiveThreshold( img, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C, cv2.THRESH_BINARY_INV, blockSize31, C10 ) # 3. 构建结构元素 kernel cv2.getStructuringElement(kernel_shape, (kernel_size, kernel_size)) # 4. 开运算去小亮斑 opened cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 5. 闭运算填小暗洞 closed cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel) # 6. 保存结果 cv2.imwrite(binary.png, binary) cv2.imwrite(opened.png, opened) cv2.imwrite(closed.png, closed) return binary, opened, closed binary, opened, closed morphology_pipeline(defect.png, kernel_size5)逻辑说明先做自适应二值化而不是全局阈值是因为工业现场光照不均很常见全局阈值会把暗区目标整片丢掉。blockSize31是邻域大小必须是奇数我一般取图像宽度的 1/20 左右再取奇。C10是从均值里减去的常数值越大前景越少用来抑制背景噪声。参数说明kernel_size5是起点实际要根据缺陷尺寸调。如果缺陷是 2~3 像素的锡渣5×5 开运算能去掉如果缺陷是 8 像素的毛刺得用 9×9 或 11×11。kernel_shape默认椭圆处理圆形焊球时比矩形更保形。3.2 参数怎么调kernel 尺寸、迭代次数、二值化阈值的联动这三个参数不是独立的调一个往往要动另一个。我的经验顺序是先定二值化阈值把目标分出来再定 kernel 尺寸去掉噪点最后看是否需要迭代。kernel 尺寸的确定方法用cv2.connectedComponentsWithStats统计二值图中所有连通域的面积找到噪点面积分布和目标面积分布的分界点然后反推等效直径。num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(binary, connectivity8) areas stats[1:, cv2.CC_STAT_AREA] # 跳过背景 print(面积分布:, np.percentile(areas, [10, 50, 90, 99]))如果 10% 分位数面积对应直径 3 像素90% 分位数对应直径 20 像素那 kernel 取 5×5 就能去掉大部分噪点而不伤目标。迭代次数一般保持 1除非噪点直径刚好卡在 kernel 尺寸边缘迭代 2 次相当于用更大 kernel但边界会更方块化。二值化阈值和 kernel 的联动阈值调低前景变多噪点也变多需要更大 kernel阈值调高前景变少小目标可能被整片丢掉kernel 再小也救不回来。所以先保证目标不丢再考虑去噪。3.3 把开闭运算串进缺陷检测标记、计数、输出坐标实际项目里开闭运算只是预处理后面还要做连通域分析和缺陷标记。# 在闭运算结果上找连通域 num_labels, labels, stats, centroids cv2.connectedComponentsWithStats(closed, connectivity8) # 过滤掉面积过小的区域残留噪点和过大的区域背景 min_area, max_area 20, 5000 defects [] for i in range(1, num_labels): area stats[i, cv2.CC_STAT_AREA] if min_area area max_area: x, y, w, h stats[i, cv2.CC_STAT_LEFT], stats[i, cv2.CC_STAT_TOP], stats[i, cv2.CC_STAT_WIDTH], stats[i, cv2.CC_STAT_HEIGHT] defects.append((x, y, w, h, area, centroids[i])) # 在原图上画框 img_color cv2.imread(defect.png) for (x, y, w, h, area, centroid) in defects: cv2.rectangle(img_color, (x, y), (xw, yh), (0, 0, 255), 2) cv2.putText(img_color, f{area}, (x, y-5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 0, 255), 1) cv2.imwrite(marked.png, img_color) print(f检出缺陷数: {len(defects)})这段代码里min_area和max_area是二次过滤开闭运算不可能完美总会残留一些极小噪点或把大背景连成一片。min_area20是经验值对应约 5 像素直径比 kernel 去掉的稍大一点留余量。max_area5000根据实际目标尺寸设超过这个面积的基本是背景或误检。4. 开闭运算避坑排查5 个我踩过的血泪坑4.1 坑一闭运算把该断的连上了现象两个本应独立的焊盘闭运算后连成一片后续连通域分析把它们当成一个缺陷。原因闭运算是先膨胀后腐蚀膨胀阶段两个焊盘之间的窄间隙被填死腐蚀阶段因为已经连成整体无法再分开。这是闭运算的不可逆特性。解决如果两个目标之间的间隙小于 kernel 尺寸闭运算一定会连上。要么减小 kernel 尺寸到小于间隙宽度要么先做开运算断开细连接再做闭运算要么改用其他方法比如分水岭分割。我现在的习惯是只要目标之间有小于 5 像素的间隙闭运算 kernel 绝不超过 3×3。4.2 坑二开运算把细长目标整条削掉现象PCB 上的细走线开运算后整条消失因为走线宽度小于 kernel 尺寸。原因开运算先腐蚀腐蚀时走线宽度方向被削掉如果走线宽度小于 kernel 直径腐蚀后整条走线消失后续膨胀无法恢复。解决开运算 kernel 尺寸必须小于你要保留的最细目标宽度。如果最细走线 3 像素宽kernel 最大 3×3且椭圆 kernel 实际覆盖更小可能 3×3 也会伤到。这时候要么改用方向性 kernel水平走线用 1×N 的 kernel要么先做闭运算再做开运算顺序反过来。4.3 坑三kernel 尺寸设成偶数导致锚点偏移现象开运算后目标整体偏移半个像素边缘出现不对称。原因OpenCV 的getStructuringElement虽然接受偶数尺寸但锚点默认在(ksize.width/2, ksize.height/2)偶数时锚点不在正中心导致腐蚀和膨胀的参考点不一致。解决kernel 尺寸永远用奇数。3×3、5×5、7×7不要用 4×4、6×6。如果确实需要偶数尺寸的效果用奇数尺寸加anchor参数手动指定锚点但没必要直接奇数最省事。4.4 坑四在灰度图上直接做开闭运算现象在灰度图上做开运算结果图像整体变暗目标边缘出现奇怪的光晕。原因形态学操作本质是集合运算定义在二值图上。在灰度图上做形态学腐蚀变成取邻域最小值膨胀变成取邻域最大值效果是局部最小值/最大值滤波和去噪的预期不符。解决先二值化再做形态学。如果确实需要在灰度图上操作比如做顶帽变换要清楚你在做的是灰度形态学参数含义和二值形态学不同。顶帽变换是原图减开运算用来提取亮细节底帽变换是闭运算减原图用来提取暗细节。这两个是灰度形态学的典型用法但不要和去噪混淆。4.5 坑五迭代次数和 kernel 尺寸混用导致计算量爆炸现象为了去掉大噪点把 kernel 设成 15×15 还迭代 5 次单帧处理时间从 10ms 涨到 200ms产线节拍跟不上。原因形态学计算量和 kernel 面积成正比15×15 是 3×3 的 25 倍再迭代 5 次就是 125 倍。而且大 kernel 可以用多次小 kernel 迭代近似但计算量并不省。解决先用小 kernel 迭代 2~3 次近似大 kernel如果还不够说明噪点尺寸已经接近目标尺寸形态学不是合适工具应该换连通域面积过滤或频域滤波。我一般把单次形态学 kernel 控制在 11×11 以内迭代不超过 3 次超过就重新审视方案。5. 进阶技巧用开闭运算组合做顶帽底帽提取特定尺寸的亮暗细节5.1 顶帽变换原图减开运算提取比 kernel 小的亮细节顶帽变换Top-Hat的定义是tophat(I) I - open(I)。开运算去掉了比 kernel 小的亮细节原图减去开运算剩下的就是那些被去掉的亮细节。所以顶帽变换用来提取比 kernel 小的亮斑、亮线。# 顶帽变换提取亮细节 kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) tophat cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel) # 顶帽结果二值化后就是亮细节掩膜 _, detail_mask cv2.threshold(tophat, 30, 255, cv2.THRESH_BINARY)这里 kernel 尺寸决定了提取的细节尺度15×15 提取比 15 像素小的亮细节。阈值 30 是经验值根据顶帽结果的对比度调。顶帽变换在字符识别里很有用可以校正光照不均把暗背景上的亮字符提取出来。5.2 底帽变换闭运算减原图提取比 kernel 小的暗细节底帽变换Black-Hat的定义是blackhat(I) close(I) - I。闭运算填掉了比 kernel 小的暗细节闭运算减去原图剩下的就是那些被填掉的暗细节。底帽变换用来提取比 kernel 小的暗斑、暗线。# 底帽变换提取暗细节 blackhat cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel) # 底帽结果二值化后就是暗细节掩膜 _, dark_mask cv2.threshold(blackhat, 30, 255, cv2.THRESH_BINARY)底帽变换在检测亮背景上的暗缺陷时特别管用比如金属表面的暗划痕、LCD 面板的暗点。和顶帽一样kernel 尺寸决定提取尺度阈值决定灵敏度。5.3 组合用法先顶帽提取亮缺陷再底帽提取暗缺陷合并输出实际缺陷检测里亮缺陷和暗缺陷往往同时存在。我的做法是分别做顶帽和底帽各自二值化后合并再统一做连通域分析。def detect_defects(img_path, kernel_size15, thresh30): img cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) _, binary cv2.threshold(img, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (kernel_size, kernel_size)) tophat cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel) blackhat cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel) _, bright_mask cv2.threshold(tophat, thresh, 255, cv2.THRESH_BINARY) _, dark_mask cv2.threshold(blackhat, thresh, 255, cv2.THRESH_BINARY) # 合并亮暗缺陷 combined cv2.bitwise_or(bright_mask, dark_mask) # 开运算去掉合并后的孤立噪点 clean_kernel cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) combined cv2.morphologyEx(combined, cv2.MORPH_OPEN, clean_kernel) return combined result detect_defects(surface.png, kernel_size15, thresh30) cv2.imwrite(defects_combined.png, result)这段代码的关键在于顶帽和底帽的 kernel 尺寸要一致这样提取的亮暗细节尺度才匹配。阈值thresh30对亮暗掩膜共用如果亮缺陷和暗缺陷对比度差异大可以分开设。最后用 3×3 开运算去掉合并后的孤立噪点因为顶帽底帽对噪声敏感二值化后难免有零星亮点。我自己的习惯是顶帽底帽的 kernel 尺寸取目标正常纹理周期的 2~3 倍。比如金属表面纹理周期约 5 像素kernel 取 15×15这样正常纹理不会被当成缺陷而尺寸在 15 像素以下的异常亮暗斑会被提取出来。这个参数没有理论公式靠对具体产品的纹理做频谱分析或直接试。最后说一个我反复验证过的教训开运算和闭运算的顺序不能随便换。先开后闭和先闭后开结果完全不同前者先去噪再填洞后者先填洞再去噪填出来的洞可能又被当成噪点去掉。我一般先开运算去噪再闭运算填洞除非明确知道目标内部有暗洞需要先填。这个顺序我踩过坑换过来之后误检率翻了一倍。希望帮到你。本文还有配套的精品资源点击获取