OpenCV-Python教程:形态学变换~开闭操作,顶帽黑帽,形态学梯度,击中击不中(morphologyEx)

原文链接:http://www.juzicode.com/opencv-python-morphologyex

返回Opencv-Python教程

形态学变换除了OpenCV-Python教程:形态学变换~腐蚀和膨胀介绍的腐蚀和膨胀还有开操作、闭操作、顶帽变换、黑帽变换等,这些变换都是以morphologyEx()的接口函数调用的,该函数的接口形式如下:

cv2.morphologyEx(src, op, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) ->dst
  • 参数含义:
  • src:源图像,通道数任意;图像深度只能是CV_8U, CV_16U, CV_16S, CV_32F or CV_64F;其中op为cv2.MORPH_HITMISS时仅支持CV_8UC1;
  • op:变换方式;
  • kernel:可以由getStructuringElement()构建,op为cv2.MORPH_HITMISS时则由子图构建;
  • dst:输出图像,通道数和数据类型同src;
  • anchor:锚点,默认使用(-1,-1)表示中心点;
  • iterations:迭代次数;
  • borderType:边界类型;
  • borderValue:边界值;

参数含义和腐蚀、膨胀几乎一样,仅仅多了个op入参,用来表示形态学变换的方式,op的值和erode,dilate的关系如下:

形态学变换op标志位与erode和dilate关系
腐蚀cv2.MORPH_ERODE dst=erode(src,element)
膨胀cv2.MORPH_DILATE dst=dilate(src,element)
开操作cv2.MORPH_OPEN dst=dilate(erode(src,element))
闭操作cv2.MORPH_CLOSE dst=erode(dilate(src,element))
梯度cv2.MORPH_GRADIENT dst=dilate(src,element)−erode(src,element)
顶帽cv2.MORPH_TOPHAT dst=src−open(src,element)
黑帽cv2.MORPH_BLACKHAT dst=close(src,element)−src
击中击不中cv2.MORPH_HITMISS dst=erode(src,element) & erode(~src,~element)

1、开操作

开操作的实质是先进行腐蚀再膨胀,可以用来消除小于结构元大小的细小区域,在OpenCV-Python教程:形态学变换~腐蚀和膨胀(erode,dilate)一文中关于五线谱的例子先腐蚀后膨胀的过程可以用开操作实现。下面的例子分别进行腐蚀-膨胀和开操作,对比变换后的图像差异:

import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img = cv2.imread('..\\samples\\data\\notes.png',cv2.IMREAD_GRAYSCALE) 
_,img_bin = cv2.threshold(img,127,255,1)#二值反色
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(1,7))
img_erode = cv2.erode(img_bin,kernel,iterations=1) 
img_dilate = cv2.dilate(img_erode,kernel,iterations=1) 
img_open = cv2.morphologyEx(img_bin,cv2.MORPH_OPEN,kernel,iterations=1)
img_diff = cv2.absdiff(img_dilate,img_open)
print('countNonZero(img_diff):',cv2.countNonZero(img_diff))

#显示图像
fig,ax = plt.subplots(3,1)
ax[0].set_title('原图 (juzicode.com)')
ax[0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[1].set_title('img_erode_dilate') 
ax[1].imshow(cv2.cvtColor(img_dilate,cv2.COLOR_BGR2RGB))
ax[2].set_title('img_open') 
ax[2].imshow(cv2.cvtColor(img_open,cv2.COLOR_BGR2RGB)) 
ax[0].axis('off');ax[1].axis('off');ax[2].axis('off')
plt.show() 

运行结果:

VX公众号: 桔子code / juzicode.com
cv2.__version__: 4.5.3
countNonZero(img_diff): 0

这里用img_diff = cv2.absdiff(img_dilate,img_open)得到的2幅图像的差异,再用cv2.countNonZero(img_diff)统计差异图像的非0值,得到的数值为0,可以看到2个图像是完全相同的,另外从图像对比看开操作和先腐蚀后膨胀效果也是一样的。

在OpenCV4.5.3 morphologyEx()的源码中可以看到MORPH_OPEN分支表示的开操作实际就是先腐蚀后膨胀得到的,同样地MORPH_CLOSE分支表示的闭操作则是先膨胀再腐蚀得到的:

void morphologyEx( InputArray _src, OutputArray _dst, int op,
                       InputArray _kernel, Point anchor, int iterations,
                       int borderType, const Scalar& borderValue )
{
//......
    Mat src = _src.getMat(), temp;
    _dst.create(src.size(), src.type());
    Mat dst = _dst.getMat();
//......
    switch( op )
    {
//......
    case MORPH_OPEN:
        erode( src, dst, kernel, anchor, iterations, borderType, borderValue );
        dilate( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
    case MORPH_CLOSE:
        dilate( src, dst, kernel, anchor, iterations, borderType, borderValue );
        erode( dst, dst, kernel, anchor, iterations, borderType, borderValue );
        break;
//......
    default:
        CV_Error( CV_StsBadArg, "unknown morphological operation" );
    }
}

2、闭操作

闭操作实际上是先进行膨胀再腐蚀,因为膨胀可以用来填充孔洞、修复缺失的连接,但是同时也会导致白色轮廓增大,当用同样的结构元(kernel)再进行一次腐蚀操作后,就可以保持外形轮廓和原来的一致。下面是一个膨胀-腐蚀和闭操作修复孔洞对比的例子:

import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img = cv2.imread('..\\samples\\picture\\mnist-7-hole.jpg',cv2.IMREAD_GRAYSCALE) 
_,img_bin = cv2.threshold(img,127,255,0) 
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(11,11))
img_dilate = cv2.dilate(img_bin,kernel,iterations=1) 
img_erode = cv2.erode(img_dilate,kernel,iterations=1) 
img_close = cv2.morphologyEx(img_bin,cv2.MORPH_CLOSE,kernel,iterations=1)
img_diff = cv2.absdiff(img_close,img_close)
print('countNonZero(img_diff):',cv2.countNonZero(img_diff))

#显示图像
fig,ax = plt.subplots(2,2)
ax[0][0].set_title('原图 (juzicode.com)')
ax[0][0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[0][1].set_title('img_dilate') 
ax[0][1].imshow(cv2.cvtColor(img_dilate,cv2.COLOR_BGR2RGB))
ax[1][0].set_title('img_erode_dilate') 
ax[1][0].imshow(cv2.cvtColor(img_erode,cv2.COLOR_BGR2RGB))
ax[1][1].set_title('img_close') 
ax[1][1].imshow(cv2.cvtColor(img_close,cv2.COLOR_BGR2RGB)) 
ax[0][0].axis('off');ax[0][1].axis('off');ax[1][0].axis('off');ax[1][1].axis('off')
plt.show() 

运行结果:

原图数字7中存在黑色“孔洞”(图1),为了消除“孔洞”如果只做膨胀处理会导致外形增大(图2),但是在膨胀的基础上再进行一次腐蚀就能保证外形和原图一样(图3),闭操作的效果和图3一样(图4)。

3、形态学梯度

形态学梯度操作是用膨胀图像减去腐蚀图像的结果,因为膨胀可以增大边沿,腐蚀会缩小边沿,所以形态学梯度变换就能将轮廓提取出来:

import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img = cv2.imread('..\\samples\\picture\\mnist-7.jpg',cv2.IMREAD_GRAYSCALE) 
_,img_bin = cv2.threshold(img,127,255,0) 
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(11,11))
img_dilate = cv2.dilate(img_bin,kernel,iterations=1) 
img_erode = cv2.erode(img_bin,kernel,iterations=1) 
img_dilate_erode = img_dilate - img_erode
img_gradient = cv2.morphologyEx(img_bin,cv2.MORPH_GRADIENT,kernel,iterations=1)

#显示图像
fig,ax = plt.subplots(2,2)
ax[0][0].set_title('原图 (juzicode.com)')
ax[0][0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[0][1].set_title('img_dilate') 
ax[0][1].imshow(cv2.cvtColor(img_dilate,cv2.COLOR_BGR2RGB))
ax[1][0].set_title('img_erode') 
ax[1][0].imshow(cv2.cvtColor(img_erode,cv2.COLOR_BGR2RGB))
ax[1][1].set_title('img_gradient') 
ax[1][1].imshow(cv2.cvtColor(img_gradient,cv2.COLOR_BGR2RGB)) 
ax[0][0].axis('off');ax[0][1].axis('off');ax[1][0].axis('off');ax[1][1].axis('off')
plt.show() 

运行结果:

4、顶帽

顶帽变换是用原图减去开操作图像,因为开操作会去除小于结构元的小区域,原图减去开操作图像后,会将开操作去除的小区域保留下来:

import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img = cv2.imread('..\\samples\\picture\\mnist-7-noise.jpg',cv2.IMREAD_GRAYSCALE) 
_,img_bin = cv2.threshold(img,127,255,0) 
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(11,11))
img_open = cv2.morphologyEx(img_bin,cv2.MORPH_OPEN,kernel,iterations=1)
img_src_open = img_bin - img_open
img_tophat = cv2.morphologyEx(img_bin,cv2.MORPH_TOPHAT,kernel,iterations=1)

#显示图像
fig,ax = plt.subplots(2,2)
ax[0][0].set_title('原图 (juzicode.com)')
ax[0][0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[0][1].set_title('img_open') 
ax[0][1].imshow(cv2.cvtColor(img_open,cv2.COLOR_BGR2RGB))
ax[1][0].set_title('img_src_open') 
ax[1][0].imshow(cv2.cvtColor(img_src_open,cv2.COLOR_BGR2RGB))
ax[1][1].set_title('img_tophat') 
ax[1][1].imshow(cv2.cvtColor(img_tophat,cv2.COLOR_BGR2RGB)) 
ax[0][0].axis('off');ax[0][1].axis('off');ax[1][0].axis('off');ax[1][1].axis('off')
plt.show() 

运行结果:

5、黑帽

黑帽变换和顶帽变换则相反,是将闭操作后的图像减去原图,因为闭操作会填充孔洞(小的黑色区域),孔洞部分变成白色,而原图中仍然为黑色,这样就会将原图中的孔洞保留下来并变为白色区域。

import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img = cv2.imread('..\\samples\\picture\\mnist-7-hole.jpg',cv2.IMREAD_GRAYSCALE) 
_,img_bin = cv2.threshold(img,127,255,0) 
kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(11,11))
img_close = cv2.morphologyEx(img_bin,cv2.MORPH_CLOSE,kernel,iterations=1)
img_close_src = img_close-img_bin
img_blackhat = cv2.morphologyEx(img_bin,cv2.MORPH_BLACKHAT,kernel,iterations=1)

#显示图像
fig,ax = plt.subplots(2,2)
ax[0][0].set_title('原图 (juzicode.com)')
ax[0][0].imshow(cv2.cvtColor(img,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[0][1].set_title('img_close') 
ax[0][1].imshow(cv2.cvtColor(img_close,cv2.COLOR_BGR2RGB))
ax[1][0].set_title('img_close_src') 
ax[1][0].imshow(cv2.cvtColor(img_close_src,cv2.COLOR_BGR2RGB))
ax[1][1].set_title('img_blackhat') 
ax[1][1].imshow(cv2.cvtColor(img_blackhat,cv2.COLOR_BGR2RGB)) 
ax[0][0].axis('off');ax[0][1].axis('off');ax[1][0].axis('off');ax[1][1].axis('off')
plt.show() 

运行结果:

6、击中击不中

击中击不中变换可以用来在原图中查找子图,假设要查找的图像中包含了多种子图,可以利用某个子图构造出kernel,经过击中击不中变换就能在该子图中心保留一个非零的点。注意这里构造kernel不再是使用getStructuringElement(),而是需要用子图构造。一个构造kernel的例子如下,首先从子图中读取图像,然后和要做变换的原图做一样的阈值化,接下来构造一个和子图大小一样类型为np.int8型的kernel,其中子图阈值化后值为255的位置设置为1,阈值化后值为0的位置设置为-1:

#构建kernel
img_kernel = cv2.imread('..\\samples\\picture\\hitmiss-kernel.bmp',cv2.IMREAD_GRAYSCALE) 
_,img_kernel_bin = cv2.threshold(img_kernel,193,255,1) #阈值化,阈值和要做变换的原图一致
kernel = img_kernel.astype(np.int8) #构造一个和子图大小但是类型为int8型,可以保存负数
kernel[img_kernel_bin == 255] = 1 
kernel[img_kernel_bin == 0] = -1 

完整的例子如下,首先读取原图并进行阈值化,然后按照前面的方法构建kernel,接下来用morphologyEx()进行击中击不中变换,再用findNonZero()查找非零点并用circle()绘图显示出来:

import numpy as np
import matplotlib.pyplot as plt 
import cv2
print('VX公众号: 桔子code / juzicode.com')
print('cv2.__version__:',cv2.__version__)
plt.rc('font',family='Youyuan',size='9')

img_src = cv2.imread('..\\samples\\picture\\hitmiss.bmp',cv2.IMREAD_GRAYSCALE) 
_,img_src_bin = cv2.threshold(img_src,193,255,1) 
#构建kernel
img_kernel = cv2.imread('..\\samples\\picture\\hitmiss-kernel.bmp',cv2.IMREAD_GRAYSCALE) 
_,img_kernel_bin = cv2.threshold(img_kernel,193,255,1) #阈值化,阈值和要做变换的原图一致
kernel = img_kernel.astype(np.int8)#构造一个和子图大小但是类型为int8型,可以保存负数
kernel[img_kernel_bin == 255] = 1
kernel[img_kernel_bin == 0] = -1 
#击中击不中变换
img_hitmiss = cv2.morphologyEx(img_src_bin,cv2.MORPH_HITMISS ,kernel,iterations=1)
print('countNonZero(img_hitmiss):',cv2.countNonZero(img_hitmiss))
#绘制中心点
locations = cv2.findNonZero(img_hitmiss)
img_hitmiss_color = cv2.cvtColor(img_hitmiss,cv2.COLOR_GRAY2BGR)
if locations is not None:
    print(type(locations), locations.shape ,locations)
    print('locations:',locations[0][0]) # 第2个[0]固定,第1个[0]表示找到位置的个数
    center=locations[0][0][0],locations[0][0][1]
    cv2.circle(img_hitmiss_color,center, 15, (0,255,255), 5)
else:
    print('未击中')
    
#显示图像
fig,ax = plt.subplots(2,3)
ax[0][0].set_title('原图 (juzicode.com)')
ax[0][0].imshow(cv2.cvtColor(img_src,cv2.COLOR_BGR2RGB)) #matplotlib显示图像为rgb格式
ax[0][1].set_title('img_src_bin') 
ax[0][1].imshow(cv2.cvtColor(img_src_bin,cv2.COLOR_BGR2RGB))
ax[0][2].set_title('img_kernel') 
ax[0][2].imshow(cv2.cvtColor(img_kernel,cv2.COLOR_BGR2RGB))
ax[1][0].set_title('img_kernel_bin') 
ax[1][0].imshow(cv2.cvtColor(img_kernel_bin,cv2.COLOR_BGR2RGB))
ax[1][1].set_title('img_hitmiss') 
ax[1][1].imshow(cv2.cvtColor(img_hitmiss,cv2.COLOR_BGR2RGB)) 
ax[1][2].set_title('img_hitmiss_color') 
ax[1][2].imshow(cv2.cvtColor(img_hitmiss_color,cv2.COLOR_BGR2RGB))
plt.show() 

运行结果:

VX公众号: 桔子code / juzicode.com
cv2.__version__: 4.5.3
countNonZero(img_hitmiss): 1
<class 'numpy.ndarray'> (1, 1, 2) [[[319  91]]]
locations: [319  91]

小结:开操作,闭操作,顶帽,黑帽,形态学梯度,击中击不中变换都是以膨胀、腐蚀为基础。其中击中击不中变换需要从子图构造kernel,而其他几种形态学变换则用getStructuringElement()构造。

扩展阅读:

  1. OpenCV-Python教程
  2. OpenCV-Python教程:形态学变换~腐蚀和膨胀(erode,dilate)

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注