OpenCV计算机视觉学习(5)——形态学处理(腐蚀膨胀,开闭运算,礼帽黑帽,边缘检测)
如果需要处理的原图及代码,请移步小编的GitHub地址
传送门:请点击我
如果点击有误:https://github.com/LeBron-Jian/ComputerVisionPractice
形态学操作简单来说,就是改变物体的形状,下面学习一下,首先本文的目录如下:
- 1,定义结构元素
- 2,腐蚀和膨胀
- 3,开运算和闭运算
- 4,礼帽/顶帽,黑帽算法
- 5,梯度运算
- 6,形态学运算 检测边和角点(1,检测边缘 ; 2,检测拐角)
1,定义结构元素
形态学操作的原理:在特殊领域运算形式——结构元素(Structure Element),在每个像素位置上与二值图像对应的区域进行特定的逻辑运算。运算结构是输出图像的相应像素。运算效果取决于结构元素大小内容以及逻辑运算性质。
结构元素:膨胀和腐蚀操作的最基本组成部分,用于测试输出图像,通常要比待处理的图像小很多,二维平面结构元素由一个数值为0或1的矩阵组成。结构元素的原点指定了图像中需要处理的像素范围,结构元素中数值为1的点决定结构元素的领域像素进行膨胀或腐蚀操作时是否需要参与计算。
形态学处理的核心就是定义结构元素,在OpenCV-Python中,可以使用其自带的 getStructuringElement 函数,也可以直接使用 Numpy 的 ndarray 来定义一个结构元素,形象图如下:
下面代码为上图的十字形,代码如下:
#_*_coding:utf-8_*_ import cv2 import numpy as np def show_element(): element_cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) print(element_cross) element_ellipse = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5)) print(element_ellipse) element_rect = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) print(element_rect) ''' [[0 0 1 0 0] [0 0 1 0 0] [1 1 1 1 1] [0 0 1 0 0] [0 0 1 0 0]] [[0 0 1 0 0] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [0 0 1 0 0]] [[1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1]]''' def define_cross_structure(): NpKernel = np.uint8(np.zeros((5, 5))) for i in range(5): NpKernel[2, i] = 1 NpKernel[i, 2] = 1 print("NpKernel", NpKernel) ''' NpKernel [[0 0 1 0 0] [0 0 1 0 0] [1 1 1 1 1] [0 0 1 0 0] [0 0 1 0 0]] '''
上面我们自定义了一个结构元素 kernel,先声明一个矩阵,然后对其进行赋值,这种方法灵活但是略显复杂。OpenCV提供了一个函数 也就是上面展示的,可以获取常用结构元素的性质:矩形(包括线形),椭圆(包括圆形)以及十字形。下面具体学习一下此方法
1.1 定义一些基本符号和关系
1,元素
设有一幅图像X,若点 a 在 X 的区域以内,则称 a 为 X 的元素,记做 a 属于 X,如图 6.1所示。
2,B包含于X
设有两幅图像 B, X。对于 B中所有的元素 ai, 都有 ai 属于 X,则称B包含于 (included in)X ,记做 B 属于 X,如图6.2所示。
3,B击中 X
设有两幅图像B, X。若存在这一一个点,它即是B的元素,又是 X 的元素,则称 B 击中(hit)X,记做 B ↑ X,如图6.3所示。
4,B不击中 X
设有两幅图像B, X。若不存在任何一个点,它既是B的元素,又是 X的元素,即 B和 X的交集是空,则称 B 不击中(miss)X,记做 B ∩ X = Φ;其中 ∩ 是集合运算相交的符号,Φ 表示空集,如图6.4所示。
5,补集
设有一幅图像 X,所有 X 区域以外的点构成的集合称为 X 的补集,记做 Xc,如下图所示。显然,如果B ∩ X = Φ,则 B 在 X的补集内,即 B 属于 Xc。
6,结构元素
设有两幅图像B,X。若X是被处理的对象,而B是用来处理X的,则称B为结构元素(structure element),又被形象的称作刷子。结构元素通常都是一些比较小的图像。
7,对称集
设有一幅图像B,将B中所有元素的坐标取反,即令(x, y)变为(-x, -y),所有这些点构成的新的集合称为B的对称集,记做 Bv,如下图6.6所示。
8,平移
设有一幅图像B,有一个点a(x0, y0),将B平移a后的结果是,把B中所有元素的横坐标加 x0,纵坐标加 y0,即令(x, y)变成(x + x0, y+y0),所有这些点构成新的集合称为B的平移,记做 Ba,如图6.7所示。
1.2 getStructuringElement 方法
getStructuringElement 是OpenCV提供的一个函数,getStructuringElement 的内部并没有什么优化实现,只是封装了一些功能,其原理同样是声明了一个矩阵,然后求形状,指定矩阵的值。而我们只需要直接调用即可。
函数原型如下:
def getStructuringElement(shape, ksize, anchor=None):
参数的意思:
- shape 表示内核的形状,有三种形状可以选择:
——十字形:cv2.getStructuringElement(cv2.MORPH_CROSS,(5,5))
——椭圆:cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))
——矩形:cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
- ksize 表示内核的尺寸(n, n)
- anchor 锚点的位置
此函数最终会返回指定形状和尺寸的结构元素。
下面代码实现一下,这里同时展示一下自己写的:
kernel1 = np.ones((3, 3), np.uint8) kernel2 = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) print(kernel1) print(kernel2) print(kernel1 == kernel2) ''' [[1 1 1] [1 1 1] [1 1 1]] [[1 1 1] [1 1 1] [1 1 1]] [[ True True True] [ True True True] [ True True True]] '''
这里其实再证明一次。
2,腐蚀和膨胀
图像的膨胀(Dilation)和腐蚀(Erosion)是两种基本的形态学运算,主要用来寻找图像中的极大区域和极小区域。其中膨胀类似于“领域扩张”,将图像中的高亮区域或白色部分进行扩张,其运行结果图比原图的高亮区域更大;腐蚀类似于“领域被蚕食”,将图像中的高亮区域或白色部分进行缩减细化,其运行结果图比原图的高亮区域更小。
形态学各种功能实现,都归结为腐蚀 erode 和 膨胀 dilate 的组合,形象理解一下就是腐蚀等于变瘦,膨胀等于变胖,所以下面学习一下腐蚀和膨胀。
注意:腐蚀和膨胀主要针对二值化图像的白色部分。
2.1 腐蚀
腐蚀就是把结构元素B平移a后得到Ba,若Ba包含于X,我们记下这个a点,所有满足上述条件的 a点组成的集合称为X被B腐蚀(Erosion)的结果。
上图 X 是被处理的对象,B是结构元素,不难知道,对于任意一个在阴影部分的点 a,Ba包含于X,所以 X被B腐蚀的结果就是那个阴影部分,阴影部分在 X的范围之内,且比 X小,就像 X 被剥掉了一层似的,这0就是为什么叫腐蚀的原因。
腐蚀的运算符为 “ - ”,其定义如下:
该公式表示图像A用卷积模板B来进行腐蚀处理,通过模板B与图像A进行卷积计算,得到B覆盖区域的像素点最小值,并用这个最小值来替代参考点的像素值。如图所示,将左边的原始图像A腐蚀处理为右边的效果图A-B。
腐蚀:腐蚀会把物体的边界腐蚀掉,卷积核沿着图像滑动,如果卷积核对应的原图的所有像素值为1,那么中心元素就保持原来的值,否则变为零。主要应用在去除白噪声,也可以断开连在一起的物体。
在原图的每一个区域中取最小值,由于是二值化图像,只要有一个点为0,则为0,来达到瘦身的目的。
腐蚀的作用:
- 1,对象大小减少1个像素(3*3)
- 2,平滑对象边缘
- 3,弱化或者分割图像之间的半岛型连接
2.2 膨胀(Dilate)
膨胀可以看做是腐蚀的对偶运算,其定义是:把结构元素B平移 a 后得到 Ba,若Ba击中X,我们记下这个 a 点。所有满足上述条件的 a点组成的集合称为 X被B膨胀的结果。
膨胀的方法是:拿B的中心点和X上的点及X周围的点一个一个的对,如果B上有一个点落在X的范围内,则该点为黑,可以看出X的范围就像X膨胀一圈似的。
图像膨胀的运算符是“⊕”,其定义如下:
这个公式表示用B来对图像A进行膨胀处理,其中B是一个卷积模板或卷积核,其形状可以为正方形或圆形,通过模板B与图像A进行卷及计算,扫描图像中的每一个像素点,用模板元素与二值图像元素做“与”运算,如果都为0,那么目标像素点为0,否则为1。从而计算B覆盖区域的像素点最大值,并用该值替换参考点的像素值实现膨胀。下图是将左边的原始图像A膨胀处理为右边的效果图 A⊕B。
膨胀:卷积核所对应的原图像的像素值只要有一个是1,中心像素值就是1.一般在除噪声,先腐蚀再膨胀,因为腐蚀在去除白噪声的时候也会使图像缩小,所以我们之后要进行膨胀。当然也可以用来将两者物体分开。
膨胀的作用:
- 1,对象大小增加一个像素 (3*3)
- 2,平滑对象边缘
- 3,减少或者填充对象之间的距离
2.3 代码展示
代码如下:
import cv2 import numpy as np def erode_image(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定义的结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 腐蚀图像 eroded = cv2.erode(gray_img, kernel) # 显示腐蚀后的图像 cv2.imshow('Origin', origin_img) cv2.imshow('Erode', eroded) cv2.waitKey(0) cv2.destroyAllWindows() def dilate_image(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定义的结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 膨胀图像 dilated = cv2.dilate(gray_img, kernel) # 显示腐蚀后的图像 cv2.imshow('Dilate', dilated) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'origin.jpg' erode_image(img_path) dilate_image(img_path)
如上所示,腐蚀和膨胀的处理很简单,只需设置好结构元素,然后分别调用 cv2.erode() 和 cv2.dilate()函数,其中第一个参数为需要处理的图像,第二个是结构元素,返回处理好的图像。
下图从左到右依次是 原图,腐蚀,膨胀(我们可以看出腐蚀是将线条变瘦,膨胀是变胖):
2.4 腐蚀和膨胀的知识点补充
1,可以看做膨胀是将白色区域扩大,腐蚀是将黑色区域扩大。
2,可以不进行灰度处理,对彩色图像进行处理
腐蚀 cv2.erode(src, # 输入图像 kernel, # 卷积核 dst=None, anchor=None, iterations=None, # 迭代次数,默认1 borderType=None, borderValue=None) 膨胀 cv2.dilate(src, # 输入图像 kernel, # 卷积核 dst=None, anchor=None, iterations=None, # 迭代次数,默认1 borderType=None, borderValue=None)
3,开运算和闭运算
开运算和闭运算就是将腐蚀和膨胀按照一定的次序进行处理。但是这两者并不是可逆的,即先开后闭并不能得到原来的图像。
为了获取图像中的主要对象:对一幅二值图连续使用闭运算和开运算,或者消除图像中的噪声,也可以对图像先用开运算后用闭运算,不过这样也会消除一些破碎的对象。
- 开运算:先腐蚀后膨胀,用于移除由图像噪声形成的斑点
- 闭运算:先膨胀后腐蚀,用来连接被误分为许多小块的对象
3.1 开运算
开运算 = 先腐蚀运算,再膨胀运算(看上去把细微连在一起的两块目标分开了)
开运算的效果图如下图所示:
开运算总结:
- (1)开运算能够除去孤立的小点,毛刺和小桥,而总的位置和形状不变。
- (2)开运算是一个基于几何运算的滤波器
- (3)结构元素大小的不同将导致滤波效果的不同
- (4)不同的结构元素的选择导致了不同的分割,即提取出不同的特征。
3.2 闭运算
闭运算=先膨胀运算,再腐蚀运算(看上去将两个细微连接的图封闭在一起)
闭运算的效果如下图所示:
闭运算总结:
- (1)闭运算能够填平小湖(即小孔),弥合小裂缝,而总的位置和形状不变。
- (2)闭运算是通过填充图像的凹角来滤波图像的。
- (3)结构元素大小的不同将导致滤波效果的不同。
- (4)不同结构元素的选择导致了不同的分割。
代码如下:
import cv2 import numpy as np def Open_operation(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定义的结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 开运算 open = cv2.morphologyEx(gray_img, cv2.MORPH_OPEN, kernel) # 显示腐蚀后的图像 cv2.imshow('Open', open) cv2.waitKey(0) cv2.destroyAllWindows() def Closed_operation(img_path): origin_img = cv2.imread(img_path) gray_img = cv2.cvtColor(origin_img, cv2.COLOR_BGR2GRAY) # OpenCV定义的结构元素 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) # 闭运算 closed = cv2.morphologyEx(gray_img, cv2.MORPH_CLOSE, kernel) # 显示腐蚀后的图像 cv2.imshow('Closed', closed) cv2.waitKey(0) cv2.destroyAllWindows() def show_origin(origin_path): # img = cv2.imread(origin_path, ) # 灰度化 img = cv2.imread(origin_path, 0) cv2.imshow('origin', img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': # 此图为加了高斯噪声的图片 img_path = 'butterfly_Gaussian.jpg' show_origin(img_path) Closed_operation(img_path) Open_operation(img_path)
效果如下:(分布是原图,开运算的图,闭运算的图)
闭运算用来连接被误分为许多小块的对象,而开运算用于移除由图像噪声形成的斑点。因此,某些情况下可以连续运用这两种运算。如对一幅二值图连续使用闭运算和开运算,将获得图像中的主要读写。同样,如果想消除图像中噪声(即图像中的“小点”),也可以对图像先用开运算后用闭运算,不过这样也会消除一些破碎的对象。
3.3 开运算和闭运算的知识点补充
这里主要补充函数原型
开运算 图像开运算主要使用的函数morphologyEx,它是形态学扩展的一组函数, 其参数cv2.MORPH_OPEN对应开运算。其原型如下: dst = cv2.morphologyEx(src, cv2.MORPH_OPEN, kernel) 参数dst表示处理的结果,src表示原图像,cv2.MORPH_OPEN表示开运算,kernel表示卷积核 闭运算 图像闭运算主要使用的函数morphologyEx,其原型如下: dst = cv2.morphologyEx(src, cv2.MORPH_CLOSE, kernel) 参数dst表示处理的结果,src表示原图像, cv2.MORPH_CLOSE表示闭运算,kernel表示卷积核
4,礼帽/顶帽,黑帽算法
礼帽 :原始图像与其进行开运算后的图像进行一个差
黑帽:原始图像与其闭运算后的图像进行一个差
礼帽运算 = 原始图像 - 开运算
黑帽运算 = 闭运算 - 原始图像
代码如下:
import cv2 def hat_algorithm(img_path): original_img0 = cv2.imread(img_path) original_img = cv2.imread(img_path, 0) kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 定义矩形结构元素 TOPHAT_img = cv2.morphologyEx(original_img, cv2.MORPH_TOPHAT, kernel) # 顶帽运算 BLACKHAT_img = cv2.morphologyEx(original_img, cv2.MORPH_BLACKHAT, kernel) # 黒帽运算 # 显示图像 cv2.imshow("original_img0", original_img0) cv2.imshow("original_img", original_img) cv2.imshow("TOPHAT_img", TOPHAT_img) cv2.imshow("BLACKHAT_img", BLACKHAT_img) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'butterfly_Gaussian.jpg' hat_algorithm(img_path)
效果如下:(依次是原图,灰度图图片(两个类似,是因为我将原图做了灰度化高斯处理,所以灰度化之后和原图类似),顶帽图片,黑帽图片)
该算法可以用于图像识别的预处理,用于图像二值化后取出孤立点,代码如下:
import cv2 def deal_isolated(img_path): original_img = cv2.imread(img_path, 0) gray_img = cv2.resize(original_img, None, fx=0.8, fy=0.8, interpolation=cv2.INTER_CUBIC) # 图形太大了缩小一点 kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) # 定义矩形结构元素(核大小为3效果好) TOPHAT_img = cv2.morphologyEx(gray_img, cv2.MORPH_TOPHAT, kernel) # 顶帽运算 BLACKHAT_img = cv2.morphologyEx(gray_img, cv2.MORPH_BLACKHAT, kernel) # 黒帽运算 bitwiseXor_gray = cv2.bitwise_xor(gray_img, TOPHAT_img) # 显示如下腐蚀后的图像 cv2.imshow("gray_img", gray_img) cv2.imshow("TOPHAT_img", TOPHAT_img) cv2.imshow("BLACKHAT_img", BLACKHAT_img) cv2.imshow("bitwiseXor_gray", bitwiseXor_gray) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = 'lena.jpg' deal_isolated(img_path)
可以看出,最后效果更加明显了一些:
5,梯度运算
梯度 = 膨胀 - 腐蚀
下面看一个示例:
import cv2 import numpy as np import matplotlib.pyplot as plt img = cv2.imread('circle.jpg') kernel = np.ones((7, 7), np.uint8) # kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (7, 7)) erosion = cv2.erode(img, kernel, iterations = 5) dilation = cv2.dilate(img, kernel, iterations = 3) gradient = cv2.morphologyEx(img, cv2.MORPH_GRADIENT, kernel) result = [img, erosion, dilation, gradient] titles = ['origin img', 'erosion img', 'dilate img', 'gradient img'] for i in range(4): plt.subplot(2, 2, i+1), plt.imshow(result[i]) plt.title(titles[i]) plt.xticks([]), plt.yticks([]) plt.show()
效果如下:
6,用形态学运算检测边和角点
这里通过一个较复杂的例子学习如何用形态学算子检测图像中的边缘和拐角(这里只做形态学处理例子,实际使用请参考Canny和Harris等算法:请参考博文:深入学习OpenCV中几种图像边缘检测算子)
6.1 检测边缘
形态学检测边缘的原理很简单,在膨胀时,图像中的物体会向周围“扩张”;腐蚀时,图像的额物体会“收缩”。比较两幅图像,由于其变化的区域只发生在边缘。所以这时将这两幅图像相减,得到的就是图像中的边缘。这里用的依然是参考资料《Opencv2 Computer Vision Application Programming Cookbook》中相关章节的图片:
代码如下:
# coding=utf-8 import cv2 import numpy def detection_edge(img_path): image = cv2.imread(img_path, 0) # 构造一个3×3的结构元素 element = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)) dilate = cv2.dilate(image, element) erode = cv2.erode(image, element) # 将两幅图像相减获得边,第一个参数是膨胀后的图像,第二个参数是腐蚀后的图像 # cv2.absdiff参数:(膨胀后的图像,腐蚀后的图像) result = cv2.absdiff(dilate, erode) # 上面得到的结果是灰度图,将其二值化以便更清楚的观察结果 retval, result = cv2.threshold(result, 40, 255, cv2.THRESH_BINARY) # 反色,即对二值图每个像素取反 result = cv2.bitwise_not(result) # 显示图像 cv2.imshow("result", result) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = "building.jpg" detection_edge(img_path)
结果如下:
6.2 检测角点(拐角)
与边缘检测不同,拐角的检测过程稍稍有些复杂。但是原理相同,所不同的是先用十字形的结构元素膨胀像素,这种情况下只会在边缘处“扩张”,角点不发生变化。接着用菱形的结构元素腐蚀原图像,导致只有在拐角处才会“收缩”,而直线边缘都未发生变化。
第二步是用X行膨胀原图像,焦点膨胀的比边要多。这样第二次用方块腐蚀时,角点恢复原状,而边要腐蚀的更多。所以当两幅图像相减时,只保留了拐角处,示意图如下(示意图来自参考资料《Opencv2 Computer Vision Application Programming Cookbook》):
代码如下:
# coding=utf-8 import cv2 import numpy def detection_inflexion(img_path): image = cv2.imread(img_path, 0) origin = cv2.imread(img_path) # 构造5×5的结构元素,分别为十字形、菱形、方形和X型 cross = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 菱形结构元素的定义稍麻烦一些 diamond = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) diamond[0, 0] = 0 diamond[0, 1] = 0 diamond[1, 0] = 0 diamond[4, 4] = 0 diamond[4, 3] = 0 diamond[3, 4] = 0 diamond[4, 0] = 0 diamond[4, 1] = 0 diamond[3, 0] = 0 diamond[0, 3] = 0 diamond[0, 4] = 0 diamond[1, 4] = 0 square = cv2.getStructuringElement(cv2.MORPH_RECT, (5, 5)) x = cv2.getStructuringElement(cv2.MORPH_CROSS, (5, 5)) # 使用cross膨胀图像 dilate_cross_img = cv2.dilate(image, cross) # 使用菱形腐蚀图像 erode_diamond_img = cv2.erode(dilate_cross_img, diamond) # 使用X膨胀原图像 dilate_x_img = cv2.dilate(image, x) # 使用方形腐蚀图像 erode_square_img = cv2.erode(dilate_x_img, square) # result = result1.copy() # 将两幅闭运算的图像相减获得角 result = cv2.absdiff(erode_square_img, erode_diamond_img) # 使用阈值获得二值图 retval, result = cv2.threshold(result, 40, 255, cv2.THRESH_BINARY) # 在原图上用半径为5的圆圈将点标出。 for j in range(result.size): y = int(j / result.shape[0]) x = int(j % result.shape[0]) if result[x, y] == 255: cv2.circle(image, (y, x), 5, (255, 0, 0)) cv2.imshow("Result", image) cv2.waitKey(0) cv2.destroyAllWindows() if __name__ == '__main__': img_path = "building.jpg" detection_inflexion(img_path)
通过上面的代码就能检测出图像的拐角并标出来,效果如下:
当然这个只是一个示例,效果不是很好。
参考文献:https://www.cnblogs.com/ssyfj/p/9276999.html
https://blog.csdn.net/wsp_1138886114/article/details/82917661
https://blog.csdn.net/JohinieLi/article/details/81041276
https://blog.csdn.net/hanshanbuleng/article/details/80657148
1、《Opencv2 Computer Vision Application Programming Cookbook》
2、《OpenCV References Manule》
最初的来源: https://blog.csdn.net/sunny2038/article/details/9137759
https://blog.csdn.net/gbxvip/article/details/50844007