从做图像处理的第一天起,我就被“噪点”和“孔洞”这两件事折腾过。大部分二值图或者分割结果,离干净可用总差那么一步:要么背景上散落着一堆白色颗粒,要么目标内部莫名其妙缺了几块黑色空洞。拿这样的图直接做轮廓分析、OCR或者测量,结果一定飘。OpenCV里的形态学开运算和闭运算,就是为解决这对孪生问题专门准备的工具。这篇笔记不打算从头讲一遍所有形态学概念,而是围绕“开运算去噪点、闭运算补孔洞”这条主线,把我自己的操作习惯、选参逻辑和踩过的坑都写清楚,给正在处理二值图又拿噪点和孔洞没辙的朋友一个可以照着抄的流程。
1. 先搞清楚腐蚀和膨胀:开闭运算的积木
很多人学形态学时跳过了腐蚀和膨胀,直接冲去用开闭运算,结果出问题以后完全不知道往哪个方向调。开运算和闭运算本质上就是腐蚀和膨胀的排列组合,所以第一步必须把这两个基础操作吃透。
1.1 腐蚀:让前景边界往里缩
腐蚀干的活可以这样理解:拿一个固定形状和大小的“探针”(结构元素,也就是核)在图像上滑动,只有当核覆盖的区域全部都是前景像素时,核的锚点位置才保留为前景。换句话说,只要区域内混入一个背景点,中间这个位置就会被“开除”出前景。
这个规则带来的直接效果是:前景物体的边缘会向内收缩一圈,直径小于核尺寸的细小前景块会整体消失,本来连在一起的物体可能会被腐蚀成两段,前景内部那些细长通道会变得更宽。
我用一个生活类比来记这个规则:腐蚀就像是给涂了颜色的贴纸拿细砂纸从四周往里打磨,磨掉的是最外圈的毛边和凸起,小碎屑直接磨没了,但图案本身会变小一圈。
OpenCV里的调用方式很简单:
import cv2 import numpy asnp # 创建一个 5x5 的方形结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) eroded = cv2.erode(binary_image, kernel, iterations=1)这里的binary_image必须是二值图,常见取值是0和255。iterations是腐蚀迭代次数,每多一次就相当于再用同样核腐蚀一遍。实际效果近似于用更大的核腐蚀,但又不完全等价,这一点后面我会专门讲。
腐蚀的副作用也藏在这个规则里:如果你只是想清除噪点,腐蚀确实做到了,但你真正的目标物体也会被磨小一圈,原来内部的孔洞反而因为边缘内缩而变大。这就是为什么实战里几乎不单独用腐蚀。
1.2 膨胀:让前景边界往外扩
膨胀规则和腐蚀刚好相反:核在图像上滑动时,只要核覆盖的范围内存在至少一个前景像素,核的锚点位置就判为前景。等价地说,每一个前景像素都会“带节奏”,让周围的区域也跟着变成前景。
膨胀的直接效果是:物体边界向外扩展一圈,目标内部的小孔洞会被填上,外部断裂的线段可能因为膨胀而重新连接,孤立的小噪点反而会变大一圈。继续用贴纸比喻的话,膨胀等于拿一支粗画笔沿图案边缘往外描一圈,图案整体变胖,破洞会被画笔颜料盖住。
dilated = cv2.dilate(binary_image, kernel, iterations=1)膨胀单独用也一样痛苦:孔洞是填上了,但噪点被放大了,相邻物体可能粘连在一起,边缘轮廓变得圆鼓鼓的,面积信息完全失真。很多新手用了一次膨胀发现图像“变糊了”,其实就是没意识到膨胀会把噪点一起放大。
1.3 为什么单独用腐蚀或膨胀都很难收场
我在实际项目里见过不少同学拿着腐蚀和膨胀反复试,试图靠多迭代几次找到“既能去噪又不缩小目标”的魔法参数。结论是找不到,因为这两个操作天生就是一个缩一个放,方向完全相反。
单独腐蚀的问题很典型:图像里的椒盐噪点被清除干净了,但目标物体缩小了一圈,同时目标内部那些原本细小的孔洞因为外边界内缩而显得更大。你要是拿着腐蚀后的图去做面积测量,结果必然偏小。
单独膨胀的问题同样致命:目标内部孔洞被填上了,但外部背景上的噪点全都被放大,原来只有一两个像素的颗粒,膨胀两三次之后就变成能干扰后续分析的大白块,甚至会让两个本不相关的物体粘连到一起。
这正是开闭运算存在的意义:它们不引入新技术,而是通过“先缩后放”或者“先放后缩”的顺序,把“消除噪点/填充孔洞”和“恢复目标尺寸”这两件事同时办了。理解了这个逻辑,你就能明白为什么下面两个组合是标准打法。
2. 开运算和闭运算:组合拳的设计逻辑
2.1 开运算的本质:先腐蚀后膨胀
开运算的公式可以写成:先对图像做腐蚀,再用同一个结构元素对腐蚀结果做膨胀。也就是dilate(erode(src, kernel), kernel),OpenCV里用一行就能搞定:
opened = cv2.morphologyEx(binary_image, cv2.MORPH_OPEN, kernel)为什么这个顺序能去噪点而不明显改变目标面积?可以分成两步看:
第一步腐蚀把背景上的小颗粒、目标边缘的毛刺凸起统统磨掉,同时也把目标内部那些细的连接处断开,目标整体缩小一圈。 第二步膨胀是把处理后的前景重新往外扩一圈。因为用的是同一个核,扩出来的量大致能补回第一步缩掉的量,所以正常大小的目标面积能大致恢复。
但需要注意的是:已经被腐蚀掉的噪点,在第二步膨胀时不会“复活”。因为腐蚀已经让噪点消失了,膨胀只能在残存的前景基础上扩张,它没法无中生有地把已经消失的颗粒再变回来。这就是开运算的核心价值:小尺寸噪点被“删掉”,大目标尺寸被“还原”。
用一句不严谨但好记的话概括:开运算负责清除前景外面不该有的东西,适合处理“白底黑图上的小白点”或者“黑底白图上的小白碎块”。
2.2 闭运算的本质:先膨胀后腐蚀
闭运算就是反过来的组合:先膨胀后腐蚀,即erode(dilate(src, kernel), kernel):
closed = cv2.morphologyEx(binary_image, cv2.MORPH_CLOSE, kernel)这个顺序解决的是目标内部的问题。第一步膨胀会把目标内部细小的孔洞、缝隙填上,也会把临近的断裂区域连起来,代价是整个目标向外扩了一圈。第二步腐蚀再把边界向内回缩,目标是让填补后的轮廓恢复原状。
同样地,已经被填上的孔洞不会因为后面的腐蚀而重新露出来。因为膨胀之后孔洞区域已经是前景像素了,腐蚀只能磨外面那一圈,不会在已经连成片的区域里重新挖出空洞。闭运算由此实现了“保留目标外部轮廓、清除内部缺陷”的效果。
我习惯把闭运算当作“修复手术”:它处理的是前景内部的黑点、裂缝、断裂处,而不是外部那些多余颗粒。如果一张图既有大量外部噪点,又有内部孔洞,那正确的组合通常是先开运算再闭运算,因为如果先闭运算,外部噪点会被膨胀放大一圈,后面再开运算处理时,清除起来会更费劲。
2.3 数学符号和核的对称性
如果翻OpenCV文档,会看到开闭运算的数学记号:开运算是A ∘ B = (A ⊖ B) ⊕ B,闭运算是A • B = (A ⊕ B) ⊖ B。⊖是腐蚀,⊕是膨胀。
这里有个容易被忽略的细节:腐蚀和膨胀用的必须是同一个结构元素。只有当核完全对称时,先缩后放才能在理论上近似还原目标尺寸。如果你腐蚀用一个方形核,膨胀换成一个椭圆形核,那么“还原尺寸”的效果就会打折扣,最后得到的轮廓会和原来差别很大。所以没有特殊需求时,开闭运算请固定使用同一个kernel变量。
结构元素的对称性也很重要。OpenCV里常见的三种核:矩形核MORPH_RECT、十字核MORPH_CROSS、椭圆核MORPH_ELLIPSE。矩形核各向同性强,方向性弱,适合去除孤立噪点;椭圆核边缘更平滑,适合把轮廓修得更自然;十字核常用于需要保持横平竖直结构的场景,比如表格线和字符笔画。默认情况下选矩形核最稳。
3. 实操:噪声大扫除与孔洞修复完整流程
理论说得再多,都不如跑通一遍流程实在。这一节我直接给出一个从“原始灰度图”到“干净二值图”的完整开闭运算流程,包含代码和每一步应当观察的要点。整个流程的核心顺序是:灰度化 → 二值化 → 开运算去外部噪点 → 闭运算补内部孔洞。
3.1 准备测试图和二值化
项目里拿到的原始图往往是灰度图或彩色图。形态学操作本身可以直接作用于灰度图,但如果你处理的是分割结果或者需要干净的前景区域,我更建议先统一到二值图。原因很简单:灰度图上的开闭运算会改变像素的灰度值,导致边缘出现灰阶过渡,后续如果还要做连通域分析、面积统计,这种过渡会带来额外误差。
用一张合成的带噪点二值图举例:
import cv2 import numpy as np import matplotlib.pyplot as plt # 生成一张 400x400 的黑色底图 image = np.zeros((400, 400), dtype=np.uint8) # 画一个白色矩形作为目标主体 cv2.rectangle(image, (100, 100), (300, 300), 255, -1) # 在矩形内部挖几个黑色小孔洞 cv2.circle(image, (160, 160), 8, 0, -1) cv2.circle(image, (240, 240), 6, 0, -1) # 在背景上随机撒一些白色小噪点 rng = np.random.default_rng(42) noise_positions = rng.integers(0, 400, (80, 2)) for y, x in noise_positions: if 100 <= x <= 300 and 100 <= y <= 300: continue image[y, x] = 255这张图已经模拟了我们要解决的两个问题:背景上的白色小噪点,以及目标内部的黑色孔洞。如果你手头是真实图片,一般先做灰度化,再做二值化:
# 如果读的是彩色或灰度图 gray = cv2.imread("sample.png", cv2.IMREAD_GRAYSCALE) # 固定阈值 _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY) # 或者用 Otsu 自动阈值(适合前景背景对比明显的图) _, binary_otsu = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)二值化这一步最容易出问题的是前景背景搞反。开闭运算默认把白色像素当作前景,如果你要保留的目标是黑色区域,比如白底黑字的文档扫描图,那就得先取反:
binary_inv = cv2.bitwise_not(binary)我在这上面栽过不止一次,后来养成了习惯:凡是准备做形态学前,先打印一下目标区域像素值,确认前景是255再继续。
3.2 用开运算清噪点,用闭运算补孔洞
现在对二值图依次执行开运算和闭运算:
# 创建 5x5 矩形结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 开运算:清除目标外部的白色小噪点 opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel) # 闭运算:填充目标内部的黑色孔洞 closed = cv2.morphologyEx(opened, cv2.MORPH_CLOSE, kernel) # 保存结果方便对比 cv2.imwrite("step1_binary.png", binary) cv2.imwrite("step2_opened.png", opened) cv2.imwrite("step3_closed.png", closed)执行完把三张图并列放一起看,效果会非常直观:step1_binary.png里背景上零星分布的小白点,在step2_opened.png里基本消失;矩形内部那两个黑色圆形孔洞,在step3_closed.png里被填平,同时矩形轮廓尺寸没有明显变化。
如果核尺寸选得合适,后续再做轮廓检测时,就不会检索出几十个噪点轮廓或一个带凹洞的畸形轮廓。整个流程没有黑魔法,每一步的输入输出都是明确的二值图,特别适合写到图像预处理管线里,作为固定模块反复调用。
3.3 核尺寸怎么选:经典的经验法则
如果说开闭运算的代码调用是幼儿园级别,那核尺寸的选取就是真正拉开新手和老手差距的地方。核太大,目标细节会被抹平,小字变成墨团;核太小,噪点没清干净,孔洞也没填上。
我自己的经验是先分析噪点和孔洞的尺度,再定核。具体操作是:先用简单连通域分析看看噪点面积大概多大。以3×3核为例,它只能消除直径约3像素以下的小噪点;如果噪点是5×5的方块,用3×3核做开运算,腐蚀阶段根本没法把它整个消掉,后续膨胀还会让残余部分变大。
实用的选核流程我总结成三步:
- 在原始二值图上用
cv2.connectedComponentsWithStats统计所有前景连通域的面积。 - 排除最大那个目标区域,记录剩余小连通域的面积分布。如果最大噪点面积是20像素左右,那核尺寸选 5×5 或 7×7 比较合理。
- 从 3×3 开始,逐步放大核,每换一次都保存结果图,对比目标边缘细节的保留情况。记住一个原则:使用能完成任务的最小核。
为了快速给读者一个经验值,我做了一张选核对照表:
| 核尺寸 | 典型的清除能力 | 适合场景 |
|---|---|---|
| 3×3 | 清除直径≤3像素的孤立噪点,填充≤3像素的细孔 | 高分辨率图像里的轻微噪点、细小裂缝 |
| 5×5 | 清除直径≤5像素的噪点,填充≤5像素的孔洞 | 常规摄像头采集的标定图、普通文档二值图 |
| 7×7 | 清除直径≤7像素的块状噪点 | 低分辨率图像、颗粒较粗的噪点 |
| 9×9及以上 | 大面积平滑、连接长断裂 | 背景估计、粗分割结果修复 |
这只是起点,不是标准答案。实际项目里核尺寸一定要针对图像分辨率去缩放:同一张内容相同的图,放在1080p下噪点可能有10像素,放到4K下同样物理尺寸的噪点可能变成20像素,核如果不跟着放大,形态学就是无效的。
3.4 迭代次数 vs 大核,到底该调哪个
很多教程只说“迭代次数越多效果越强”,却不提它的坑。我实测下来,迭代次数和大核并不等价,主要体现在边缘处理上。
第一次腐蚀后图像边缘已经发生变化,第二次腐蚀是在“已经缩了一圈的物体”上再做一次腐蚀,等于连续缩两次。如果目标是消除噪点,连续迭代会让道路、字符这类线状结构的端点被快速“吃掉”,但核本身没有变大,对尺寸稍大的噪点又无能为力。
区别主要体现在形态学结构上:大核对角线方向的腐蚀程度和迭代多次不同。比如 5×5 方形核对一个45度倾斜的细线,可能一次就能腐蚀掉;而用 3×3 核迭代两次,虽然总作用范围接近,但对角线方向的破坏程度要轻一些。
所以我建议:优先调整核的形状和尺寸,iterations最多不要超过2。如果你发现要迭代5次才能清理干净,那说明核选小了,直接换大核重跑,别在迭代次数上硬凑。
3.5 串联操作顺序:先开还是先闭
开闭组合时,最常被问的问题就是“先开运算还是先闭运算”。我的默认答案是:先开运算,再闭运算。
理由可以这么推演:如果先做闭运算,膨胀阶段会把背景上每一个噪点都放大一圈,噪点变大之后再做开运算,要么需要更大的核才能清掉,要么在清掉噪点的过程中误伤目标边缘。反过来,先做开运算,把外部噪点清理掉,再做闭运算时膨胀阶段遇到的就是一个干净的外部背景,不会引入额外麻烦。
当然,这也不是铁律。如果你处理的图像“孔洞问题远重于噪点问题”,而且外部噪点很少,那先闭后开也可以,效果差异不大。但作为通用流程,灰度图 → 二值化 → 开运算 → 闭运算 → 后续分析这条链路在绝大多数场景下都是最稳的。
4. 踩坑记录与效果验证
4.1 常见问题速查表
把形态学开闭运算踩过的坑汇总成一张速查表,按“现象-原因-解决”来列,便于遇到问题直接对照:
| 现象 | 常见原因 | 解决思路 |
|---|---|---|
| 开运算后噪点还在 | 核尺寸小于噪点尺寸 | 统计噪点面积,放大核,或先连通域过滤再用形态学 |
| 目标边缘被严重侵蚀 | 核选得太大 / 迭代次数过多 | 换更小核,限制 iterations ≤ 2 |
| 闭运算后孔洞反而变大 | 前景背景颠倒了,真正目标被当作背景 | 用bitwise_not反转二值图 |
| 目标内部孔洞填不上 | 孔洞直径大于核尺寸 | 逐步放大核,但要注意外部噪声同步放大 |
| 处理彩色图 | 对RGB三个通道分别做形态学,结果混乱 | 先转灰度,做阈值得到二值图再处理 |
| 边缘多出影响结果的白边 | 默认边界填充方式在图像边界产生伪前景 | 扩展边界,或使用 BORDER_REFLECT 后用相同方式裁剪 |
| 结果边缘出现灰阶渐变 | 对灰度图做开闭运算而不是对二值图做 | 如果目标是二值分析,先用阈值转成0/255二值图 |
这些坑我基本都亲手踩过,其中“前景背景颠倒填不上孔洞”是我犯过最多次的低级错误。所以现在每处理一张图第一步一定是确认前景目标是什么颜色,255还是0。
4.2 如何量化判断开闭运算效果
光看图片说“看起来干净了”不够,万一你的目标其实被磨掉了太多,肉眼在低对比度下容易骗过自己。我习惯用三个量化指标来验证开闭运算的效果。
第一个是连通域数量变化。开运算之前统计一次连通域,开运算之后再统计一次,背景上的小噪点被清除后,连通域数量应当明显下降。如果下降不多,说明核不够大或者有噪点块太大。
第二个是目标区域的面积变化。闭运算之后,目标内部孔洞被填充,前景面积会略微增加;正常目标外轮廓面积不应该有显著变化。如果面积变化超过5%,我会认为核太大或者迭代过多,需要回头调整。
第三个是孔洞检测。对一个二值图,可以用找轮廓的方式,统计所有内层轮廓的数量和面积,闭运算后内层轮廓数量应该趋近于零。比如前面的合成图里有两个圆形孔洞,用cv2.findContours的cv2.RETR_LIST模式去查,处理后内部轮廓坐标集合会变得很小。
下面是连通域分析的参考代码:
def count_components(binary): num, labels, stats, centroids = cv2.connectedComponentsWithStats(binary, connectivity=8) return num - 1 # 去掉背景序号0 before = count_components(binary) after_open = count_components(opened) print(f"开运算前连通域数量: {before},开运算后: {after_open}")这条验证思路我建议固化到脚本里,而不是每次手动数。尤其当图像数量多、需要批量调参时,自动统计能帮你在几分钟内判断几十组参数的好坏。
4.3 我踩过的几个具体坑
第一坑:直接用高分辨率原图的开闭运算结果来评估。图像分辨率高时,同样物理尺寸的噪点在图像里像素数也多,但视觉上感觉“不大”。比如一张4K图里的噪点直径可能有20像素,我用5×5核一看,结果自然没变化。后来我统一在预处理脚本里加一步np.unique(binary)检查灰度级,同时统计每个连通域的平均面积,用数据代替肉眼判断,问题才解决。
第二坑:处理白底黑字文档时忘了取反。这类图前景是黑色笔画,背景是白色,直接做开运算会把笔画当背景噪声一样清掉,得到一张基本空白的图。正确做法是先cv2.bitwise_not,把笔画变成白色,做完开闭运算,如果需要再取反回去,或者直接在白前景上做后续分析。
第三坑:形态学边界效应导致边缘出现一圈异常像素。OpenCV默认的边界填充方式是BORDER_CONSTANT,也就是在图像外面补一圈常数,默认值是0。如果前景目标顶到图像边缘,那么开闭运算在图像边缘处的行为会和中间区域不一致,有时会产生意外的白边或残缺。解决方法是在操作前用copyMakeBorder给原图加几圈反射边界,处理完再裁掉:
bordered = cv2.copyMakeBorder(binary, 10, 10, 10, 10, cv2.BORDER_REFLECT) result_bordered = cv2.morphologyEx(bordered, cv2.MORPH_OPEN, kernel) clean = result_bordered[10:-10, 10:-10]这个方法对边缘目标特别重要。我在处理一张芯片引脚图时,边缘引脚总是出现半截或膨胀粘连,排查到最后就是边界填充方式在作怪,补齐边界后问题消失。
4.4 效果验证的额外心得
形态学参数选定以后,别急着删掉中间产物。我习惯保留每个阶段的输出图,比如binary.png、opened.png、closed.png、final.png,一是方便自己复盘参数选择,二是发给同事或客户时,别人能直观地看到每步变化,减少“你到底干了什么”的沟通成本。
我还会写一个简单的参数扫描脚本,把核尺寸从3到15按奇数遍历一遍,自动输出每个核下的连通域数量、平均轮廓面积、内部孔洞数。这个脚本帮我节省了大量时间,也让我逐步积累了“不同分辨率图像应该配多大核”的经验库。对于没有标注的真实验证数据,这种基于统计的验证模式基本就是最优解。
5. 进阶玩法:形态学不只是“去噪补洞”
5.1 用超大核开闭运算做背景估计
开闭运算一个容易被忽略的功能是用来估计背景光照。比如一张光照不均匀的文档图,亮处和暗处的灰度差异很大,直接二值化效果很差,把白色前景和白色背景混在一起。
这时候可以选一个特别大的核,比如图像短边的十分之一尺寸,对灰度图做闭运算,得到一张“背景图”。这张背景图反映的是大尺度光照变化趋势,不包含文字等小尺度细节。用原图减去背景图,再归一化,光照不均匀的问题会大大缓解。
large_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (50, 50)) background = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, large_kernel) flat = cv2.subtract(gray, background)这个技巧在很多OCR项目里特别实用。它的本质是利用闭运算对“小缝小洞”的填充能力,只不过这里的“孔洞”变成了字体细节,真正想保留的是背景本身。
5.2 用定向核连接断裂的字符和边缘
有时候文字或者图表里的线段看起来断断续续,尤其是扫描件里的细横线、表格线。通用5×5方形核做闭运算会把字符笔画也补粗,效果不够好。
更好的做法是使用长条形的定向核,例如 1×15 的水平核或 15×1 的垂直核。这样闭运算只连接水平方向或垂直方向的断裂,不会横向扩张目标:
horizontal_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (15, 1)) vertical_kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 15)) joined_h = cv2.morphologyEx(binary_word, cv2.MORPH_CLOSE, horizontal_kernel) joined_v = cv2.morphologyEx(joined_h, cv2.MORPH_CLOSE, vertical_kernel)我处理身份证复印件扫描图时,表格横线总在边缘处断开,用1×15核闭运算后线条衔接非常自然,而字符本身的粗细几乎不受影响。方向性核是开闭运算工具箱里很容易被忽略但很有价值的一项。
5.3 形态学梯度、顶帽和黑帽
开闭运算延伸出去的还有三个常用操作:形态学梯度、顶帽、黑帽。
形态学梯度的效果本质是dilate(src) - erode(src),得到的是目标边缘的高亮轮廓,和 Sobel 那种边缘检测不同,它对二值图边缘提取更直接。
顶帽运算等于原图 - 开运算结果,因为开运算清掉了小尺寸亮结构,差值里剩下的正是那些小亮细节;反过来说,它常被用来提取图像里的细小亮斑。
黑帽运算等于闭运算结果 - 原图,闭运算填掉了暗孔洞,差值里剩下的就是那些小暗缺陷。所以黑帽可以当作“孔洞探测器”,它比直接做连通域分析更擅长找到细小的暗缝。
这三个操作在OpenCV里同样是morphologyEx的一个类型参数:
gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel) tophat = cv2.morphologyEx(gray, cv2.MORPH_TOPHAT, kernel) blackhat = cv2.morphologyEx(gray, cv2.MORPH_BLACKHAT, kernel)这套组合在工业缺陷检测里很常见:先黑帽提取缺陷候选,再闭运算补全缺陷,再用面积过滤筛掉伪缺陷。
5.4 开闭运算与连通域分析的配合
形态学开闭运算不是终点,它通常只是预处理器。一个更完整的处理流水线是:
灰度图 → 二值化 → 开运算去噪 → 闭运算补洞 → 连通域分析 → 按面积/宽高比过滤 → 输出干净目标开闭运算把“颗粒化噪点”和“空洞缺陷”这两类最常见问题解决之后,剩下的问题一般就是“大块粘连物”“细长裂痕”这些,可以用连通域分析里的面积阈值、宽高比、填充率来过滤。我自己做细胞统计项目时,就是先闭运算填掉细胞内部的黑洞,再开运算清除碎片,最后用连通域面积筛选,精确度比单做任何一步都高很多。
整套流程并不复杂,但它非常稳定。严格遵循“先分析尺度,再选核尺寸,最后用统计验证效果”的顺序,开闭运算在图像预处理中的价值能发挥得相当充分。
最后再分享一点个人体会:开闭运算并不是图像处理的银弹,有些图噪点和目标本身尺度接近,靠形态学怎么调都容易误伤。这种情况我通常会换思路,先做连通域分析,把小于面积阈值的连通域直接抹掉,再配合开闭运算修细节。两者结合比单纯调形态学参数要灵活得多。另一个习惯是,所有预处理结果都随手保存一份,用放大镜工具认真观察一轮再进下一步。别嫌麻烦,形态学参数这种看起来“差不多就行”的东西,恰恰是后面所有分析精度的起点。