OpenCV-Python教程:形态学变换~开闭操作,顶帽黑帽,形态学梯度,击中击不中(morphologyEx)
形态学变换除了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()构造。