形态学,在此表示数学形态学内容,将数学形态学作为工具从图像中提取表达和描绘区域形状的有用图像分量,如边界、骨架和凸壳等。我们对预处理或后处理的形态学技术也感兴趣,比如形态学过滤、细化和修剪等。
数学形态学的语言是集合论。数学形态学中的集合表示图像中的对象。
一个集合B的反射表示为,定义如下:
如果B是描述图像中物体的像素的集合(二维点),则是B中(x,y)坐标被(-x,-y)代替的点的集合。下图显示了一个简单的集合及其反射。
结合B按照点z=(z1,z2)的平移,表示为(B)z,定义如下:
形态学中集合的反射和平移广泛用来表达基于结构元(SE)的操作:研究一幅图像中感兴趣特性所用的小集合或子图像。结构元就类似于空间滤波时的卷积核(模板)。
结构元的原点选取与具体问题有关,当SE对称且未显示原点时,则假定原点位于对称中心处。
对图像操作时,我们要求结构元是矩形阵列。这是通过添加最小适合数量的背景元素形成一个矩形阵列来实现的。
作为中的集合A和B,表示为 的B对A的腐蚀定义为
该式指出B对A的腐蚀是一个用z平移的B包含在A中的所有店z的集合。假定B是一个结构元。因为B必须包含在A中这一陈述等价于B不与背景共享任何公共元素,故可以将腐蚀表达为如下的等价形式:
语言描述:其中B为结构元,让B在整个Z2平面上移动,如果当B的原点平移值z点事B能完全包含与A中,则所有这样的z点构成的集合即为B对A的腐蚀图像。换句话说,就是让B的原点访问A中的每个点,寻找原点经过的位置中能使B完全被A包含的点的集合。
腐蚀的函数原型:
------------------不同腐蚀次数对照实验---------------------
import cv2
import numpy as np
img = cv2.imread(r'G:\4.png', 0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
erode1 = cv2.erode(img, kernel, iterations=1)
erode2 = cv2.erode(img, kernel, iterations=2)
erode3 = cv2.erode(img, kernel, iterations=3)
cv2.imshow("org", img)
cv2.imshow("result1", erode1)
cv2.imshow("result2", erode2)
cv2.imshow("result3", erode3)
cv2.waitKey(0)
cv2.destroyAllWindows()
------------------改变腐蚀内核对照实验-----------------------
import cv2
import numpy as np
img = cv2.imread(r'G:\4.png', 0)
kernel1 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
kernel2 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
kernel3 = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
erode1 = cv2.erode(img, kernel1, iterations=1)
erode2 = cv2.erode(img, kernel2, iterations=1)
erode3 = cv2.erode(img, kernel3, iterations=1)
cv2.imshow("org", img)
cv2.imshow("result1", erode1)
cv2.imshow("result2", erode2)
cv2.imshow("result3", erode3)
cv2.waitKey(0)
cv2.destroyAllWindows()
改变腐蚀次数对照实验:
改变腐蚀内核对照实验:
A和B是中的集合,表示为的B对A的膨胀定义为
这个公式是以B关于它的原点的映像,并且以z对映像进行平移为基础。B对A的膨胀是所有位移z的集合,这样,和A至少有一个元素是重叠的。
与腐蚀不同,腐蚀是一种收缩或细化操作,膨胀则会“增长”或“粗化”二值图像中的物体。这种特殊的方式和粗化的宽度由所用结构元来控制。
膨胀函数原型:
import cv2
import numpy as np
img = cv2.imread(r'G:\wenzi.png', 0)
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
dict = cv2.dilate(img, kernel, iterations=1)
cv2.imshow("org", img)
cv2.imshow("result", dict)
cv2.waitKey(0)
cv2.destroyAllWindows()
膨胀和腐蚀彼此关于集合求补运算和反射运算是对偶的,即
和
开操作一般会平滑物体的轮廓、断开较窄的狭颈并消除较细的突出物。闭操作同样也会平滑轮廓的一部分,但与开操作相反,它通常会弥合较窄的间断和细长的沟壑,消除较小的孔洞,填补轮廓线中的断裂。结构元B对集合A的开操作,表示为A○B,定义如下:
因此,B对A的开操作就是B对A的腐蚀,紧接着用B对结果进行膨胀。
import cv2
import numpy as np
#读取图片
src = cv2.imread(r'G:\jy.png', cv2.IMREAD_UNCHANGED)
#设置卷积核
kernel = np.ones((5,5), np.uint8)
#图像开运算
result = cv2.morphologyEx(src, cv2.MORPH_OPEN, kernel)
#显示图像
cv2.imshow("src", src)
cv2.imshow("result", result)
#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()
观察到原图像中噪点被去除。
类似地,用结构元B对集合A的闭操作,表示为A●B,定义如下:
上式说明,B对集合A的闭操作就是简单地用B对A膨胀,紧接着用B对结果进行腐蚀。
import cv2
import numpy as np
#读取图片
src = cv2.imread(r'G:\heidian.png', cv2.IMREAD_UNCHANGED)
#设置卷积核
kernel = np.ones((5,5), np.uint8)
#图像闭运算
result = cv2.morphologyEx(src, cv2.MORPH_CLOSE, kernel)
#显示图像
cv2.imshow("src", src)
cv2.imshow("result", result)
#等待显示
cv2.waitKey(0)
cv2.destroyAllWindows()
当卷积核设置为5X5时,观察到效果并不明显。
将卷积核更改为10X10,观察到黑点被完全消除。
如同膨胀和腐蚀的情形那样,开操作和闭操作彼此关于集合求补和反射也是对偶的,即
开操作满足下列性质:
类似地,闭操作满足下列性质:
算子应用一次后,一个集合的多次开操作或闭操作没有影响。
形态学击中或击不中变换是形状检测的基本工具。
简单来说击中-击不中运算常用于二值图像,它用于基于结构元素的配置,从图像中寻找具有某种像素排列特征的目标,如单个像素、颗粒中交叉或纵向的特征、直角边缘或其他用户自定义的特征等。计算时,只有当结构元素与其覆盖的图像区域完全相同时,中心像素的值才会被置为1,否则为0。下图给出了一个例子。
选取原图像相同的rgb(灰度)图像和二值图像进行击中击不中判断,只有二值图像能正常判断击中。
import numpy as np
import matplotlib.pyplot as plt
import cv2
plt.rc('font', family='Youyuan', size='9')
img_src = cv2.imread(r'G:\9-huidu.jpg', cv2.IMREAD_GRAYSCALE)
_, img_src_bin = cv2.threshold(img_src, 193, 255, 1)
# 构建kernel
img_kernel = cv2.imread(r'G:\jizhong3.png', 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()
表示为β(A)的集合A的边界可以通过先用B对A腐蚀,而后执行A和腐蚀的结果之间的差集得到,即
形态学梯度操作就是用膨胀图像减去腐蚀图像的结果,因为膨胀可以增大边沿,腐蚀会缩小边沿,所以形态学梯度变换就能将轮廓提取出来。
import matplotlib.pyplot as plt
import cv2
plt.rc('font', family='Youyuan', size='9')
img = cv2.imread(r'G:\1.png', 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()
孔洞定义为由前景像素相连接的边界所包围的背景区域。孔洞填充可视为边界提取的反过程,它是在边界已知的情况下得到边界包围的整个区域。
import numpy as np
import cv2 as cv
from matplotlib import pyplot as plt
img = cv.imread(r"G:\kongdong.jpg")
# 二值化
imgray = cv.cvtColor(img, cv.COLOR_BGR2GRAY)
imgray[imgray < 100] = 0
imgray[imgray >= 100] = 255
# 原图取补得到MASK图像
mask = 255 - imgray
# 构造Marker图像
marker = np.zeros_like(imgray)
marker[0, :] = 255
marker[-1, :] = 255
marker[:, 0] = 255
marker[:, -1] = 255
marker_0 = marker.copy()
# 形态学重建
SE = cv.getStructuringElement(shape=cv.MORPH_CROSS, ksize=(3, 3))
while True:
marker_pre = marker
dilation = cv.dilate(marker, kernel=SE)
marker = np.min((dilation, mask), axis=0)
if (marker_pre == marker).all():
break
dst = 255 - marker
filling = dst - imgray
# 显示
plt.figure(figsize=(12, 6)) # width * height
plt.subplot(2, 3, 1), plt.imshow(imgray, cmap='gray'), plt.title('src'), plt.axis("off")
plt.subplot(2, 3, 2), plt.imshow(mask, cmap='gray'), plt.title('Mask'), plt.axis("off")
plt.subplot(2, 3, 3), plt.imshow(marker_0, cmap='gray'), plt.title('Marker 0'), plt.axis("off")
plt.subplot(2, 3, 4), plt.imshow(marker, cmap='gray'), plt.title('Marker'), plt.axis("off")
plt.subplot(2, 3, 5), plt.imshow(dst, cmap='gray'), plt.title('dst'), plt.axis("off")
plt.subplot(2, 3, 6), plt.imshow(filling, cmap='gray'), plt.title('Holes'), plt.axis("off")
plt.show()
从二值图像中提取连通分量是许多自动图像分析应用的核心。令A是包含一个或多个连通分量的集合,并形成一个阵列X0(该阵列的大小与包含A的阵列的大小相同),除了在对应于A中每个连通分量中一个点的各个已知位置处我们已置为1(前景值)外,该阵列的所有其他元素均为0(背景值)。如下迭代过程可完成这一目的:
import cv2
import numpy as np
img_A = cv2.imread(r'G:\wenzi.png')
gray_A = cv2.cvtColor(img_A, cv2.COLOR_BGR2GRAY) #转换成灰度图
ret, thresh_A = cv2.threshold(gray_A, 50, 255, cv2.THRESH_BINARY_INV) #灰度图转换成二值图像
thresh_A_copy = thresh_A.copy() #复制thresh_A到thresh_A_copy
thresh_B = np.zeros(gray_A.shape, np.uint8) #thresh_B大小与A相同,像素值为0
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3,3))#3×3结构元
count = [ ] #为了记录连通分量中的像素个数
#循环,直到thresh_A_copy中的像素值全部为0
while thresh_A_copy.any():
Xa_copy, Ya_copy = np.where(thresh_A_copy > 0) #thresh_A_copy中值为255的像素的坐标
thresh_B[Xa_copy[0]][Ya_copy[0]] = 255 #选取第一个点,并将thresh_B中对应像素值改为255
#连通分量算法,先对thresh_B进行膨胀,再和thresh_A执行and操作(取交集)
for i in range(200):
dilation_B = cv2.dilate(thresh_B, kernel, iterations=1)
thresh_B = cv2.bitwise_and(thresh_A, dilation_B)
#取thresh_B值为255的像素坐标,并将thresh_A_copy中对应坐标像素值变为0
Xb, Yb = np.where(thresh_B > 0)
thresh_A_copy[Xb, Yb] = 0
#显示连通分量及其包含像素数量
count.append(len(Xb))
if len(count) == 0:
print("无连通分量")
if len(count) == 1:
print("第1个连通分量为{}".format(count[0]))
if len(count) >= 2:
print("第{}个连通分量为{}".format(len(count), count[-1] - count[-2]))
cv2.imshow("A", thresh_A)
cv2.imshow("A_copy", thresh_A_copy)
cv2.imshow("B", thresh_B)
cv2.waitKey(0)
如果在集合A内连接任意两个点的直线段都在A的内部,则称A是凸的。任意集合S的凸壳H是包含于S的最小凸集。差集H-S称为S的凸缺。
令,i=1,2,3,4表示4个结构元。
式中,。当该过程收敛时(即当时),我们令。则A的凸壳为
换句话说,该方法由反复使用B¹对A做击中或不击中变换组成;不在发生进一步变化时,我们执行与A的并集运算,结果称为D¹。这一过程使用B²重复(应用于A),直到不发生进一步的变化,如此往复。
结构元B对集合A的细化可表示为A B,它可以根据击中或击不中变换来定义:
对称地细化A的一种更有用表达方式是以结构元序列为基础的:
式中Bi是Bi-1旋转后的形式。可以使用一个结构元序列将细化定义为
这种处理是A被B¹细化一次,然后,得到的结果被B²细化一次,如此进行下去,直到A被Bn细化一次。整个过程不断重复,直到得到的结果不再发生变化。
粗化是细化的形态学对偶,定义如下:
式中B是适合于粗化处理的结构元。与细化一样,粗化处理也可以定义为一个系列操作:
如图集合A和骨架S(A)的概念很简单,由该图我们可以推出:
f(x,y)和b(x,y)为两个二位数字函数。f(x,y)表示一幅灰度级图像,b(x,y)表示结构元。
灰度级形态学中的结构元分为两类:非平坦或平坦的结构元。
当b的原点位于(x,y)处时,用一个平坦的结构元b在(x,y)处对图像f的腐蚀定义为图像f中与b重合区域的最小值。
b ̂ 的原点在(x,y)处时,平坦结构元b在任何位置(x,y)处对图像f的膨胀定义为:图像f中与b ̂ 重合区域的最大值。
其中b ̂ = b(-x,-y)。
非平坦结构元具有随定义域变化的灰度级。非平坦结构元bN对图像f的腐蚀定义如下:
使用非平坦结构元的膨胀定义如下:
腐蚀和膨胀是关于函数的补集和反射对偶的,即
灰度级图像的开操作和闭操作的表达式,与二值图像的对应操作具有相同的形式。结构元b对图像f的开操作表示为f○b,即
像之前那样,开操作先只用b对f做腐蚀,随后用b对所得结果做膨胀。类似地,b对f的闭操作表示为f●b,即
灰度级图像的开操作和闭操作关于函数的补集和结构元的反射是对偶的:
形态学平滑
因为开操作抑制比结构元小的量细节,而闭操作抑制暗细节,所以它们常常以形态滤波的形式结合起来平滑图像和去除噪声。
形态学梯度
腐蚀和膨胀可与图像相减结合起来得到一幅图像的形态学梯度,形态学梯度由g定义:
膨胀粗化一幅图像中的区域,腐蚀则细化它们,膨胀和腐蚀之差强调区域间的边界。同质区域不受影响(只要SE相对较小),因此相减操作趋于消除同质区域。最终结果是边缘被增强而同质区域的贡献则被抑制的图像。
顶帽变换和底帽变换
图像相减与开操作和闭操作相结合,可产生所谓的顶帽变换和底帽变换。灰度级图像f的顶帽变换定义为f减去其开操作:
类似地,f的底帽变换定义为f的闭操作减去f:
顶帽变换用于暗背景上的亮物体,可以得到图像中面积小于结构元且比周围亮的区域;底帽变换用于亮背景上的暗物体,可以得到图像中面积小于结构元且比周围暗的区域。故也常称为白顶帽变换和黑底帽变换。
粒度测定
粒度测定属于判断图像中颗粒的大小分布的领域。形态学可间接用于估计颗粒的大小分布,而不需要识别并测量图像中的每个颗粒。
对于比背景亮且具有规则形状的颗粒,基本方法是使用逐渐增大的结构元对图像执行开运算。对于每一个开运算,计算该开运算中像素值的和,该和有时称为表面区域。因为开运算会降低亮特征的灰度,故表面区域会睡着结构元的增大而减小。
纹理分割
以纹理内容为基础找到两个区域的边界。将一幅图像分为区域的处理称为分割。
令f和g分别代表标记图像和模板图像,假设f和g是大小相同的灰度级图像,且f≤g。f关于g的大小为1的测地膨胀定义为
其中∧代表点方式的最小算子。即先计算b对f的膨胀,然后选择在每个(x,y)点处该结果和g间的最小者。
根据b为平坦结构元还是非平坦结构元,膨胀的计算公式不同,可看前面的介绍。
f关于g的大小为n的测地膨胀为:
并有
类似地,f关于g的大小为1的测地腐蚀定义为
式中,∨代表点方式的最大算子。f关于g的大小为n的测地腐蚀为:
并有
灰度级标记图像f对灰度级模板图像g的膨胀形态学重建,定义为f关于g的测地膨胀反复迭代,直至达到稳定;即
且k应使
f对g的腐蚀的形态学重建类似地定义为
且k应使
图像大小为n的重建开操作定义为,先对f进行大小为n的腐蚀,再由f的膨胀重建
图像大小为n的重建闭操作定义为,先对f进行大小为n的膨胀,再由f的腐蚀重建
由于对偶性,图像的重建闭运算可以用图像的求补得到:对一幅图像求补,然后计算其重建开运算,再对其结果求补。