1. 形态学操作到底在解决什么问题
很多人刚接触图像处理时,对腐蚀、膨胀这些词的第一反应是“听起来像化学实验”。其实说白了,形态学操作就是拿一个小模板(专业叫法叫结构元素)在图像上滑动,根据模板覆盖区域内像素的明暗关系,重新决定中心像素的值。它的核心价值就两个:去掉不该有的小噪点,补上不该缺的小空洞。
我最早用这套东西是在一个工业质检的小项目里。当时采集到的金属表面图像上总有零星的反光白点,用高斯模糊吧,边缘也跟着糊了;用中值滤波吧,稍微大一点的瑕疵又去不干净。后来换成先腐蚀后膨胀的组合,白点没了,边缘还保住了。从那以后我就养成了一个习惯:凡是遇到二值图像或者高对比度图像的形状调整问题,先想形态学,再想别的。
这套操作适合谁呢?如果你正在做缺陷检测、字符分割、细胞计数、车牌定位这类任务,那基本绕不开它。哪怕你只是想把扫描件上的印章和文字分开,形态学也能帮上忙。下面我会把腐蚀、膨胀、开运算、闭运算、形态学梯度、顶帽、黑帽这七个操作挨个拆开讲,每个都配上参数选择的思路和我自己踩过的坑。
2. 七个形态学操作的核心原理拆解
2.1 腐蚀与膨胀:一对相反相成的操作
腐蚀的直观效果是让亮区域变小、暗区域变大。它的运算规则是:结构元素覆盖的所有像素中,只要有一个是暗的(值为0),中心像素就变成暗的。你可以把它想象成用一块橡皮擦沿着亮区域的边缘往里擦,擦掉的宽度等于结构元素的半径。
膨胀则反过来,只要结构元素覆盖的像素里有一个是亮的,中心就变亮。效果是亮区域扩张,暗区域被挤压。这就像用一支粗笔沿着亮区域边缘往外描一圈。
这两个操作的关键参数只有一个:结构元素的形状和大小。OpenCV里用cv2.getStructuringElement()生成,常用的有矩形(MORPH_RECT)、椭圆(MORPH_ELLIPSE)和十字形(MORPH_CROSS)。我个人的经验是:
- 处理矩形物体(比如PCB板上的焊盘)用矩形核
- 处理圆形物体(比如细胞、孔洞)用椭圆核
- 处理细线状结构(比如血管、裂纹)用十字核
核的大小通常取奇数,3×3、5×5、7×7最常见。核越大,效果越猛,但细节丢失也越严重。我一般从3×3开始试,不够再往上加。
注意:腐蚀和膨胀不是互逆操作。先腐蚀再膨胀,信息已经丢了,回不到原样。所以别指望用膨胀把腐蚀掉的东西完全找回来。
2.2 开运算与闭运算:腐蚀膨胀的固定组合
开运算就是先腐蚀后膨胀。它的作用是去掉亮区域上的小突起和小噪点,同时保持主体形状基本不变。为什么能保持形状?因为腐蚀掉的部分,膨胀时会从主体边缘往外长回来,但那些孤立的小点腐蚀掉之后就再也长不回来了。
闭运算是先膨胀后腐蚀。它的作用是填补暗区域上的小孔洞和裂缝,同样保持主体形状。膨胀时孔洞被填上,腐蚀时主体边缘缩回来,但填上的孔洞不会再裂开。
这两个操作在实际项目里用得最多。举个例子:你有一张二值化的文字图像,文字笔画上有一些断裂,用闭运算就能把断笔连上;如果文字周围有一些孤立的小墨点,用开运算就能把它们清掉。
参数方面,开运算和闭运算都只需要一个结构元素。但这里有个容易忽略的点:结构元素的形状会影响保留细节的方向。比如用矩形核做开运算,水平方向的细线容易被保留,垂直方向的容易被去掉;用十字核则两个方向都会受影响。如果你发现某个方向的细节总是丢,换个核形状试试。
2.3 形态学梯度:提取轮廓的利器
形态学梯度的定义很简单:膨胀图减去腐蚀图。膨胀让亮区域往外扩,腐蚀让亮区域往里缩,两者相减,剩下的就是亮区域的边缘带。边缘带的宽度等于结构元素的直径。
这个操作我主要用来做粗定位。比如在缺陷检测里,先算梯度得到所有可疑边缘,再配合轮廓分析筛选出真正的缺陷区域。它的好处是计算量小,比Canny边缘检测快不少,而且对噪声不那么敏感。缺点是边缘比较粗,定位精度不如Canny。
结构元素的选择上,梯度对核大小很敏感。核越大,边缘越粗。如果你只是想要一个大概的轮廓,用5×5或7×7都行;如果要细一点的边缘,用3×3。但别指望用1×1,那等于没做。
2.4 顶帽与黑帽:分离局部亮暗细节
顶帽运算是原图减去开运算图。开运算去掉了亮区域上的小突起,那么原图减去开运算图,剩下的就是那些被去掉的小突起。所以顶帽用来提取比周围更亮的局部小区域。
黑帽运算是闭运算图减去原图。闭运算填上了暗区域上的小孔洞,那么闭运算图减去原图,剩下的就是那些被填上的小孔洞。所以黑帽用来提取比周围更暗的局部小区域。
这两个操作在光照不均匀的场景下特别有用。比如你拍了一张文档照片,中间亮四周暗,直接二值化肯定不行。这时候可以用顶帽把文字(比背景暗?不对,文字比背景暗,应该用黑帽)提取出来。等等,这里要分清楚:白纸黑字,文字是暗的,背景是亮的,文字属于“比周围更暗的小区域”,所以用黑帽。如果是黑纸白字,那就用顶帽。
我做过一个项目是检测液晶屏上的亮点缺陷。屏幕本身是均匀的灰白色,缺陷是比周围亮一点的小点。直接用阈值分割,阈值高了缺陷没了,阈值低了背景噪声全出来了。后来用顶帽运算,结构元素取15×15的椭圆核,一下子就把亮点缺陷凸显出来了,再二值化就非常干净。
提示:顶帽和黑帽的结构元素要选得比目标物体稍大一点。如果核比目标小,目标会被当成“主体”而不是“局部细节”,就提取不出来了。
3. OpenCV实操:从零搭建一套形态学处理流程
3.1 环境准备与基础代码框架
我平时用Python + OpenCV,版本建议4.x以上。安装就一行命令:
pip install opencv-python numpy matplotlib基础框架我习惯这样写,把读图、灰度化、二值化、形态学操作串起来:
import cv2 import numpy as np import matplotlib.pyplot as plt def show_images(titles, images, figsize=(15, 5)): plt.figure(figsize=figsize) for i, (title, img) in enumerate(zip(titles, images)): plt.subplot(1, len(images), i+1) plt.title(title) plt.imshow(img, cmap='gray') plt.axis('off') plt.tight_layout() plt.show() # 读图并转灰度 img = cv2.imread('sample.jpg', cv2.IMREAD_GRAYSCALE) # 二值化,这里用Otsu自动阈值 _, binary = cv2.threshold(img, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)这里有个细节:二值化时你要想清楚目标物体是白色还是黑色。OpenCV的阈值操作默认是“大于阈值变白,小于阈值变黑”。如果你要处理的物体在原图里是暗的,二值化后它会变成黑色(值为0),这时候做腐蚀膨胀就要注意方向了。我一般会加一句cv2.bitwise_not()把目标翻成白色,这样后续操作更符合直觉。
3.2 腐蚀与膨胀的参数选择与效果对比
先定义结构元素:
# 3x3矩形核 kernel_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 5x5椭圆核 kernel_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) # 7x7十字核 kernel_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (7, 7))然后分别做腐蚀和膨胀:
eroded = cv2.erode(binary, kernel_rect, iterations=1) dilated = cv2.dilate(binary, kernel_rect, iterations=1) show_images(['原二值图', '腐蚀后', '膨胀后'], [binary, eroded, dilated])iterations参数控制迭代次数,默认1次。迭代2次相当于用更大的核做1次,但计算上更灵活。我一般先用1次看效果,不够再加。
实测下来,3×3矩形核对大多数场景都够用。如果你发现腐蚀后目标碎成了好几块,说明核太大了,换3×3或者减少迭代次数。如果膨胀后相邻目标粘在一起了,也是同样的道理。
3.3 开闭运算的代码实现与场景适配
OpenCV提供了cv2.morphologyEx()函数,一个接口搞定所有形态学操作:
# 开运算 opened = cv2.morphologyEx(binary, cv2.MORPH_OPEN, kernel_ellipse) # 闭运算 closed = cv2.morphologyEx(binary, cv2.MORPH_CLOSE, kernel_ellipse) show_images(['原图', '开运算', '闭运算'], [binary, opened, closed])这里有个我踩过的坑:开运算和闭运算的顺序不能随便换。有人觉得先开后闭效果更好,其实要看你想要什么。先开后闭等于先去噪点再补孔洞,但开运算可能会把一些小目标整个去掉,闭运算就补不回来了。我的建议是:如果噪点和孔洞同时存在,先做开运算去噪,再做闭运算补洞,但结构元素要选小一点,比如3×3。
另外,morphologyEx还支持传入iterations参数,但我不建议在这里用多次迭代,因为开闭运算迭代多次的效果和用大核做一次不完全等价,而且更难控制。要调整强度,直接换核大小更直观。
3.4 形态学梯度、顶帽、黑帽的实战代码
# 形态学梯度 gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel_ellipse) # 顶帽 tophat = cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel_ellipse) # 黑帽 blackhat = cv2.morphologyEx(binary, cv2.MORPH_BLACKHAT, kernel_ellipse) show_images(['原图', '梯度', '顶帽', '黑帽'], [binary, gradient, tophat, blackhat])顶帽和黑帽对核大小非常敏感。我一般会写个循环,把核从5×5试到25×25,看看哪个尺寸下目标最明显:
for k in [5, 9, 15, 21, 25]: kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) tophat = cv2.morphologyEx(binary, cv2.MORPH_TOPHAT, kernel) # 计算顶帽结果的方差,方差越大说明目标越突出 print(f"核大小 {k}x{k}, 顶帽方差: {tophat.var():.2f}")这个方法是我自己琢磨出来的,原理是:当核大小和目标尺寸匹配时,顶帽结果中目标区域的对比度最高,整体方差也最大。实测下来,这个方法比肉眼一个个看快多了。
4. 参数调优与常见问题排查
4.1 结构元素选择的经验法则
结构元素的选择没有万能公式,但有一些经验规律可以遵循。我整理了一个速查表:
| 场景 | 推荐核形状 | 推荐核大小 | 理由 |
|---|---|---|---|
| 去除椒盐噪点 | 矩形 | 3×3 | 矩形核计算最快,小核足够 |
| 连接断裂笔画 | 椭圆 | 3×3或5×5 | 椭圆核各方向均匀,不会引入方向偏差 |
| 提取水平细线 | 矩形(1×N) | 1×15 | 水平方向长核只保留水平结构 |
| 提取垂直细线 | 矩形(N×1) | 15×1 | 垂直方向长核只保留垂直结构 |
| 分离粘连细胞 | 椭圆 | 5×5到9×9 | 椭圆核符合细胞形状,分离效果自然 |
| 检测亮点缺陷 | 椭圆 | 比缺陷大2-3倍 | 核必须大于目标才能提取局部细节 |
这个表是我做了十几个项目后总结的,不一定百分百适用,但能帮你快速起步。
4.2 迭代次数与核大小的等效关系
很多人问:迭代2次3×3核,和用1次5×5核,有什么区别?答案是:效果相似但不完全相同。
腐蚀操作中,迭代2次3×3核,相当于用了一个“菱形”的5×5核(因为3×3矩形核迭代两次,覆盖范围是5×5,但四个角是空的)。而直接用5×5矩形核,覆盖范围是完整的5×5。所以迭代多次的核形状会偏向菱形或十字形,取决于原始核的形状。
我的建议是:优先调整核大小,迭代次数保持1次。这样效果最可控,也最容易复现。只有在需要非矩形核形状时,才考虑用迭代来近似。
4.3 常见问题速查与排查思路
下面这几个问题是我在社区里被问得最多的,整理成速查表:
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 腐蚀后目标消失 | 核太大或目标太小 | 打印腐蚀前后像素统计 | 换小核,或先膨胀再腐蚀 |
| 膨胀后目标粘连 | 核太大或目标间距太小 | 测量目标最小间距 | 换小核,或改用开运算 |
| 开运算后目标断裂 | 核太大 | 逐步减小核看效果 | 换3×3核,或先闭后开 |
| 闭运算后噪点增多 | 核太大把噪点也连上了 | 检查闭运算前是否有噪点 | 先开运算去噪,再闭运算 |
| 顶帽结果全黑 | 核比目标小 | 检查核大小与目标尺寸 | 增大核到目标的2-3倍 |
| 黑帽结果全黑 | 同上 | 同上 | 同上 |
| 梯度边缘太粗 | 核太大 | 换3×3核 | 或改用Canny做精细边缘 |
注意:顶帽和黑帽结果全黑是最常见的坑。很多人以为是自己代码写错了,其实是核选小了。记住:顶帽提取的是“比结构元素小的亮细节”,核必须大于目标。
4.4 形态学操作的性能优化技巧
形态学操作本身计算量不大,但在高分辨率图像或视频流上,累积起来也很可观。我分享几个优化技巧:
第一,能用小核就不用大核。3×3核在OpenCV里有专门的优化路径,速度比5×5快不少。如果效果不够,先考虑增加迭代次数,而不是直接换大核。
第二,先降采样再处理。如果图像分辨率很高(比如4K),但目标物体也很大,可以先把图像缩小一半,做形态学操作,再把结果放大回去。这样速度能快4倍左右,精度损失在可接受范围内。
第三,复用结构元素。getStructuringElement()每次调用都会重新分配内存,如果在循环里反复调用,会有不必要的开销。我习惯在循环外定义好核,循环内直接传进去。
第四,用cv2.morphologyEx代替手动组合。OpenCV内部对morphologyEx做了优化,比你自己写erode然后dilate要快,而且代码更简洁。
5. 综合案例:用形态学操作做缺陷检测
5.1 案例背景与处理目标
假设你有一批金属零件表面图像,需要检测上面的小凹坑缺陷。图像特点:背景是均匀的金属灰色,缺陷是比背景暗的小圆点,直径大约5到10个像素。图像分辨率是1280×960,光照基本均匀但有轻微渐变。
处理目标:把缺陷区域分割出来,并标记位置和大小。
这个案例我做过类似的,下面把完整流程和参数选择思路写出来。
5.2 完整处理流程与参数记录
第一步,读图并灰度化。金属表面图像一般是彩色的,但缺陷检测用灰度就够了,还能减少计算量。
img = cv2.imread('metal_part.jpg', cv2.IMREAD_GRAYSCALE)第二步,光照校正。虽然光照基本均匀,但轻微渐变会影响后续阈值分割。我用一个大核做闭运算来估计背景:
# 用51x51大核做闭运算,得到背景估计 bg_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (51, 51)) background = cv2.morphologyEx(img, cv2.MORPH_CLOSE, bg_kernel) # 原图除以背景,消除光照影响 corrected = cv2.divide(img, background, scale=255)这一步的原理是:闭运算会填平暗的缺陷区域,得到的就是没有缺陷的背景。原图除以背景,缺陷区域就会变得更暗,背景变得更均匀。
第三步,黑帽运算提取暗缺陷。因为缺陷比背景暗,所以用黑帽:
# 核大小选15x15,比缺陷直径(5-10像素)大 defect_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (15, 15)) blackhat = cv2.morphologyEx(corrected, cv2.MORPH_BLACKHAT, defect_kernel)第四步,阈值分割。黑帽结果中缺陷区域亮度高,背景接近0。用Otsu自动阈值:
_, defect_mask = cv2.threshold(blackhat, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)第五步,形态学后处理。阈值分割后可能有一些小噪点,用开运算去掉:
clean_kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3)) defect_mask = cv2.morphologyEx(defect_mask, cv2.MORPH_OPEN, clean_kernel)第六步,轮廓分析并标记:
contours, _ = cv2.findContours(defect_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) result = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) for cnt in contours: area = cv2.contourArea(cnt) if area > 10: # 过滤太小的噪点 x, y, w, h = cv2.boundingRect(cnt) cv2.rectangle(result, (x, y), (x+w, y+h), (0, 0, 255), 2)5.3 参数调试过程与效果评估
这个案例里最关键的两个参数是:背景估计的核大小和黑帽的核大小。
背景估计核我试了31、51、71三个值。31的时候背景估计不够平滑,缺陷区域被部分填平了,导致校正后缺陷对比度下降;71的时候背景估计过度平滑,光照渐变没完全消除。最后51效果最好。
黑帽核我试了9、15、21。9的时候缺陷提取不完整,边缘有缺失;21的时候一些大的噪声也被提取出来了。15正好比缺陷直径大一点,效果最干净。
效果评估我用的是召回率和准确率。人工标注了50个缺陷,算法检测出48个,其中3个是误检。召回率96%,准确率94%。对于这个应用场景来说够用了。
提示:形态学操作的参数没有理论最优值,只有针对具体图像的最优值。我的习惯是写一个参数扫描脚本,把不同参数组合的结果保存成图片,快速翻看对比。这比一个个手动调快得多。
6. 形态学操作的扩展玩法与组合技巧
6.1 多尺度形态学:应对大小不一的缺陷
实际项目中,缺陷大小往往不一致。有的直径5像素,有的20像素。用单一尺度的黑帽,要么小缺陷提取不到,要么大缺陷提取不全。这时候可以用多尺度形态学:
def multi_scale_blackhat(img, scales=[9, 15, 21, 27]): results = [] for k in scales: kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (k, k)) bh = cv2.morphologyEx(img, cv2.MORPH_BLACKHAT, kernel) results.append(bh) # 取所有尺度的最大值,这样不同大小的缺陷都能保留 return np.max(results, axis=0)这个方法的思路是:每个尺度对应一个尺寸范围的缺陷,取最大值相当于把所有尺度的结果融合起来。实测下来,对于尺寸差异大的缺陷,召回率能提升10%以上。
6.2 形态学与滤波的配合使用
形态学操作对椒盐噪声很敏感,因为一个孤立的噪点会被当成小目标处理。所以如果图像噪声比较重,建议先做中值滤波:
# 先中值滤波去椒盐噪声 denoised = cv2.medianBlur(img, 3) # 再做形态学操作 blackhat = cv2.morphologyEx(denoised, cv2.MORPH_BLACKHAT, kernel)中值滤波的核大小一般取3或5。太大了会把小缺陷也滤掉,太小了去噪不干净。我一般从3开始试。
反过来,形态学也可以用来辅助滤波。比如你先用开运算去掉亮噪点,再用闭运算去掉暗噪点,效果比单纯的中值滤波更可控,因为你可以分别调整亮噪点和暗噪点的处理强度。
6.3 形态学在视频流中的实时处理
视频流处理对速度要求高,形态学操作虽然不慢,但也要注意优化。我的做法是:
第一,只在ROI区域做形态学。如果目标只出现在画面中间,就没必要处理整个画面。
第二,降低分辨率。视频流一般不需要全分辨率处理,缩小到640×480甚至320×240,速度能快好几倍。
第三,用cv2.UMat加速。OpenCV的UMat可以利用GPU或OpenCL加速,对于大核形态学操作提升明显:
img_umat = cv2.UMat(img) kernel_umat = cv2.UMat(kernel) result = cv2.morphologyEx(img_umat, cv2.MORPH_BLACKHAT, kernel_umat) result = result.get() # 转回numpy数组实测在1080p视频流上,用UMat后黑帽运算的耗时从15ms降到了4ms左右,提升还是很明显的。
6.4 形态学操作的边界处理细节
OpenCV的形态学操作默认使用BORDER_CONSTANT边界模式,即图像边界外填充0(黑色)。这会导致图像边缘的亮区域在膨胀时被“拉”向边界,产生不自然的扩张。如果你处理的物体靠近图像边缘,这个问题会很明显。
解决办法是改用BORDER_REPLICATE,即边界外复制边缘像素:
result = cv2.morphologyEx(img, cv2.MORPH_DILATE, kernel, borderType=cv2.BORDER_REPLICATE)这样膨胀时边缘像素会向外复制,不会产生虚假的扩张。这个细节在文档里不太起眼,但实际项目中影响很大。我早期做车牌定位时,车牌靠近图像边缘,膨胀后车牌和边界粘在一起,轮廓提取就出错了。后来改成BORDER_REPLICATE才解决。
7. 我踩过的坑与实操心得
7.1 二值化方向搞反导致操作失效
这是我最早踩的坑。当时处理的是白纸黑字的文档图像,文字是暗的。我直接二值化,文字变成了黑色(0),背景变成了白色(255)。然后我做腐蚀想去除噪点,结果文字直接消失了——因为腐蚀是让白色区域变小,文字本来就是黑色,腐蚀对它没影响,反而把背景的白色噪点去掉了。
正确的做法是先用cv2.bitwise_not()反转,让文字变成白色,再做形态学操作。或者直接用黑帽运算,因为黑帽专门提取暗细节。
这个坑的本质是:形态学操作默认处理的是白色前景。如果你的目标是暗的,要么反转,要么用黑帽。
7.2 核大小选错导致目标被当成噪点
有一次做细胞计数,细胞直径大约30像素。我用5×5的核做开运算去噪,结果细胞全没了。当时百思不得其解,后来才反应过来:开运算是去掉比核小的亮区域。5×5的核,能去掉的亮区域直径小于5像素。细胞直径30像素,按理说不应该被去掉啊。
再仔细一看,原来我的二值化阈值选高了,细胞内部有很多空洞,实际连通区域只有几个像素宽。开运算把这些细连接全切断了,细胞就碎成了小点,然后被当成噪点去掉了。
教训是:形态学操作之前,先检查二值化质量。如果目标内部有空洞或断裂,先做闭运算补上,再做开运算去噪。顺序反了,目标就没了。
7.3 顶帽黑帽的核必须大于目标
这个前面提过,但值得再强调一次。顶帽和黑帽的本质是“原图减去开运算”和“闭运算减去原图”。开运算去掉的是比核小的亮细节,所以顶帽提取的也是比核小的亮细节。如果核比目标小,目标会被开运算保留下来,顶帽结果里就没有目标。
我见过有人用3×3核做顶帽,然后抱怨结果全黑。一问目标多大,答曰直径20像素。3×3核当然提取不到20像素的目标。核至少要21×21才行。
7.4 迭代次数不是越多越好
有人觉得迭代次数越多效果越强,就无脑设成5次、10次。结果目标被腐蚀得渣都不剩。迭代次数和核大小是乘法关系,3×3核迭代5次,等效核大小接近11×11。如果你本来只想轻微腐蚀,迭代5次就过头了。
我的习惯是:迭代次数永远设1,要调强度就调核大小。这样每次调整的效果都是线性的,容易预测。
7.5 形态学梯度不能替代Canny
形态学梯度提取的边缘比较粗,而且对噪声敏感。我试过用梯度做精密测量,结果边缘宽度有5个像素,根本没法精确定位。后来还是换回Canny,配合亚像素边缘检测,精度能到0.1像素。
形态学梯度的正确用法是粗定位:先用梯度找到大概区域,再用Canny在局部做精细边缘。这样既快又准。
8. 从形态学出发的进阶学习路径
形态学操作是图像处理的基础工具,但它的思想可以延伸到很多地方。比如分水岭算法用形态学做标记提取,骨架化用形态学腐蚀迭代实现,击中击不中变换用形态学做特定形状检测。如果你把形态学吃透了,这些进阶算法理解起来会轻松很多。
我个人的学习路径是:先把这七个基本操作玩熟,然后去看OpenCV的morphologyEx源码,理解它是怎么用erode和dilate组合出各种操作的。再然后去读分水岭和骨架化的论文,看形态学在其中的作用。最后自己动手实现一个简单的形态学库,不用OpenCV,纯numpy写。写完你对形态学的理解会上一个台阶。
另外,形态学在深度学习时代并没有过时。很多语义分割网络的预处理和后处理都会用到形态学操作。比如分割结果有空洞,用闭运算补上;有噪点,用开运算去掉。甚至有些网络结构里直接嵌入了可学习的形态学层。所以这套东西学了不亏,用得上。
最后分享一个我常用的调试技巧:把每一步的中间结果都保存成图片,用cv2.imwrite写到磁盘上,然后快速翻看。形态学操作的参数影响很直观,看图比看数值快得多。我一般会建一个debug文件夹,每调一次参数就存一张图,文件名带上参数值,比如blackhat_k15.png。这样调完一轮,翻一遍图片就知道哪个参数最好了。