目录
概要:
众
嗦粥汁所周知,在如今计算机视觉(Computer Vision short for CV)是人工智能与机器人技术发展的一个重大研究方向,而opencv作为一个专门为机计算机视觉编程提供技术与函数支持的第三方库,自然是一个需要重点研究的内容。
本期首先要谈谈图像的开运算与闭运算的相关原理与函数解析,以及演示一些经典案例,以供大家参考。
话不多说,我是Kamen Black 君,让我们开始今天的学习!
正文部分:
print("祝大家每天快乐,love and peace!")
原理与函数:
— —初 窥 门 道
原理解析:
其实我们实现开运算与闭运算,主要是通过两个奇妙的函数:dilate与它的兄弟erode。
其中dilate用于图像的形态学膨胀,而erode则用于图像的形态学腐蚀。类比于我们上一期的比喻,那dilate的作用相当于下一场倾盆大雨,而erode的作用相当于发一次持续干旱。
也就是说:
dilate形象化的作用是将图像向外进行像素生长,达到面积增大的目的;
而erode形象化的作用是将图像向内进行像素退化,达到面积减小的效果。
但是要细究膨胀与腐蚀的原理的话,我们要了解两个最为关键的概念:结构元素、锚点。
(1)结构元素——相当于一个蒙版(也可以理解为一个窗口),它能在被操作的图像上进行滑动,每个时刻都能把被操作图像的一部分像素框选出来。结构元素通常为矩形,长与宽可以不相同。
(2)锚点——针对结构元素而言。顾名思义,在结构元素中如同船锚一般,固定重心的点位,是结构元素与被操作图像的连接位置。默认情况下,锚点就是结构元素形状上的中心点。锚点的选择方法将在下文的函数解析中有所介绍。
理解这两个概念后,我们就可以简单地说明膨胀与腐蚀的操作原理(以下称被操作图像为A,操作后图像为B,以避免文字繁杂):
(1)腐蚀——对A的一个像素做与结构元素的框选处理,即将结构元素的锚点与A的一个像素重合,然后观察被结构元素这个蒙版框选出来的A的所有像素的像素值,取其中的最小值作为B的这一像素的像素值。对A的每一个像素都实现以上的操作,以生成B。
用公式表达就是:
其中,dst(x,y)表示B中坐标为(x,y)的像素的像素值;
element(x,y)表示结构元素(其实可以直接表达为element,结构元素本身与每个像素并无关系,加上(x,y)只是为了更加形象);
(x',y')表示结构元素中各位置相对于锚点的坐标,如锚点的坐标为(0,0),而锚点下2行,上3列的坐标为(2,-3);
src(x+x',y+y')表示A中坐标为(x+x',y+y')的像素的像素值;
min表示取最小值。
(2)膨胀——对A的一个像素做与结构元素的框选处理,即将结构元素的锚点与A的一个像素重合,然后观察被结构元素这个蒙版框选出来的A的所有像素的像素值,取其中的最大值作为B的这一像素的像素值。对A的每一个像素都实现以上的操作,以生成B。
用公式表达就是:
其中,dst(x,y)表示B中坐标为(x,y)的像素的像素值;
element(x,y)表示结构元素(其实可以直接表达为element,结构元素本身与每个像素并无关系,加上(x,y)只是为了更加形象);
(x',y')表示结构元素中各位置相对于锚点的坐标,如锚点的坐标为(0,0),而锚点下2行,上3列的坐标为(2,-3);
src(x+x',y+y')表示A中坐标为(x+x',y+y')的像素的像素值。
max表示取最大值。
理解操作原理之后,还有几个点要简单说明一下,提前解决各位读者将来的疑惑:
(1)中心点坐标的计算方式:如果结构元素的行数与列数都是奇数,那中心点必然好找,但是倘若行数或列数出现了偶数的情况呢?这时,就以行数来举例,如果结构元素的总行数为偶数,那么中心点所在的行数可以这样计算:总行数/2+1,这就是中心点所在的行数。列数也是同理。注意:这里,行数(列数)的首位都是1,而非0。
其实无论行数(列数)是奇数还是偶数,都可以通过这样的公式来进行计算:
其中,ceil函数表示对操作数向上取整,如(2,3]范围内的操作数,ceil函数的结果都是3。
(2) 结构元素框选范围超出被操作图像:结构元素在被操作图像上滑动,若是没有超出范围,则可以根据公式算出操作后图像的像素值,但是倘若超出被操作图像呢?这时,要分为两种情况:一种是未扩充边界的情况,另一种是扩充了边界的情况。
①未扩充边界的情况:我们直接不考虑被操作图像外的情况,只考虑范围内的。若是腐蚀操作,只考虑被操作图像中的被框选的像素值中的最小值;若是膨胀操作,只考虑被操作图像中的被框选的像素值中的最大值。
②扩充了边界的情况:如果扩充了边界的话,则要考虑到扩充的边界是何种取值,根据相应情况进行操作。举个例子,如果扩充的边界类型(borderType,下文会有介绍)为cv2.BORDER_CONSTANT,且扩充值(borderValue,同样在下文会有说明)为0时,则相当于被操作图像外绕了一圈又一圈的0(
爱的魔力转圈圈)。只要能被蒙版框选的区域,且是范围外的区域,像素值一定显示为0。如果扩充值为3,则是绕了一圈又一圈的3。
直观展示:
只使用文字可能仍显得过于抽象,接下来通过图形进行一个较为直观的说明:
(1)结构元素如右图所示:,默认情况下锚点就在中心点。根据我们的中心点坐标计算方式,其中心点即在下
图中交叉斜向十字位置:。
(2)被操作图像如右图所示:。
(3)接下来进行腐蚀与膨胀操作后的图像展示:
①腐蚀后图像:;
②膨胀后图像:。
根据实际操作,可以证明,形态学的腐蚀与膨胀操作符合以上本文所描述的原理与规则。
函数解析:
①使用准备:
与前几期一样,依旧是调用我们的工具人老伙伴----opencv,作为我们使用rectangle的大前提。
import cv2
②使用情况:
该部分在上一期已经做过说明,故不在此再作赘述。
③语法说明:
该期有两个函数:erode与dilate,分开进行说明。
(1)腐蚀操作:
使用语法:dst = cv2.erode(src, kernel, dst, anchor, iterations, borderType, borderValue)
参数说明:首个dst:通过erode函数进行形态学腐蚀之后得到的目标图像,数据类型为(array)数组类型;
src:使用erode函数进行处理的源图像,数据类型也是(array)数组类型;
kernel:即上文所提到的结构元素。结构元素可以通过cv2.getStructuringElement函数进行构建,也可以通过numpy库中的ones或array函数进行构建。接下来对使用cv2.getStructuringElement函数构建结构元素进行进一步的简要说明:
使用语法:kernel = cv2.getStructuringElement(shape, ksize, anchor)
参数说明:kernel:即通过getStructuringElement函数构造得到的结构元素;
shape:结构元素的形状,有以下可供选择的取值:cv2.MORPH_RECT(矩形)、cv2.MORPH_CROSS(十字形)、cv2.MORPH_ELLIPSE(椭圆形);
ksize:结构元素的行列数,取值格式为(x,y),其中x表示列数,y表示行数(此处与直觉相反,要注意);
anchor:结构元素的锚点,不取值或取值为(-1,-1)则取结构元素的中心点作为锚点。此处,仅cv2.MORPH_CROSS(十字形)的蒙版形状与锚点的选择有关,其他形状与这里的锚点选择并无关系,可任意取值。
;
次个dst:与首个dst的说明相同,不再赘述,默认为None;
anchor:结构元素的锚点,不取值或取值为(-1,-1)则取结构元素的中心点作为锚点。与getStructuringElement函数的锚点有所区别,这里的锚点选择对所有形状的结构元素都有影响;
iterations:迭代次数。迭代的第N次使用迭代第N-1次的dst作为src来进行操作,其他参数不变;
borderType:扩充边界的方式,默认值为None,即表示不进行边界扩充。具体的类型如下图所示:
表格来源:opencv中borderType的类型,erode与dilate函数不支持BORDER_WRAP类型的取值;
borderValue:当borderType取值为cv2.BORDER_CONSTANT时,扩充边界的元素以borderValue填充。borderValue默认值为(0, 0, 0),当然你可以指定其他值,三个元素分别表示 BGR,注意不是RGB。
(2)膨胀操作:
使用语法:dst = cv2.dilate(src, kernel, dst, anchor, iterations, borderType, borderValue)
参数说明:与erode函数相同,故不再赘述。
下面另外附上erode与dilate函数相关说明的原生文档,以供参考:
def erode(src: Mat, kernel, dst: Mat = ..., anchor=..., iterations=..., borderType=..., borderValue=...) -> typing.Any:
'''
·"erode(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
·@brief Erodes an image by using a specific structuring element.
·The function erodes the source image using the specified structuring element that determines the
·shape of a pixel neighborhood over which the minimum is taken:
·\\f[\\texttt{dst} (x,y) = \\min _{(x',y'): \\, \\texttt{element} (x',y') \\ne0 } \\texttt{src} (x+x',y+y')\\f]
·The function supports the in-place mode. Erosion can be applied several ( iterations ) times. In
·case of multi-channel images, each channel is processed independently.
·@param src input image; the number of channels can be arbitrary, but the depth should be one of
·CV_8U, CV_16U, CV_16S, CV_32F or CV_64F.
·@param dst output image of the same size and type as src.
·@param kernel structuring element used for erosion; if `element=Mat()`, a `3 x 3` rectangular
·structuring element is used. Kernel can be created using #getStructuringElement.
·@param anchor position of the anchor within the element; default value (-1, -1) means that the
·anchor is at the element center.
·@param iterations number of times erosion is applied.
·@param borderType pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not supported.
·@param borderValue border value in case of a constant border
·@sa dilate, morphologyEx, getStructuringElement"
...
'''
def dilate(src: Mat, kernel, dst: Mat = ..., anchor=..., iterations=..., borderType=..., borderValue=...) -> typing.Any:
'''
·"dilate(src, kernel[, dst[, anchor[, iterations[, borderType[, borderValue]]]]]) -> dst
·@brief Dilates an image by using a specific structuring element.
·The function dilates the source image using the specified structuring element that determines the
·shape of a pixel neighborhood over which the maximum is taken:
·\\f[\\texttt{dst} (x,y) = \\max _{(x',y'): \\, \\texttt{element} (x',y') \\ne0 } \\texttt{src} (x+x',y+y')\\f]
·The function supports the in-place mode. Dilation can be applied several ( iterations ) times. In
·case of multi-channel images, each channel is processed independently.
·@param src input image; the number of channels can be arbitrary, but the depth should be one of
·CV_8U, CV_16U, CV_16S, CV_32F or CV_64F.
·@param dst output image of the same size and type as src.
·@param kernel structuring element used for dilation; if elemenat=Mat(), a 3 x 3 rectangular
·structuring element is used. Kernel can be created using #getStructuringElement\n.
·@param anchor position of the anchor within the element; default value (-1, -1) means that the
·anchor is at the element center.
·@param iterations number of times dilation is applied.
·@param borderType pixel extrapolation method, see #BorderTypes. #BORDER_WRAP is not suported.\
·@param borderValue border value in case of a constant border
·@sa erode, morphologyEx, getStructuringElement"
...
'''
案例与操作:
— —小 试 牛 刀
接下来我们仍以上一期的两湖小事作为我们的案例操作:
(1)故事背景:
话说,在很久很久很久以前,有两片湖泊,他们相邻很近,就像恋人一样相互依偎,中间还有一条小河连接着他们俩儿,作为他们连接的纽带。附近的人们管他们其中一片叫铁柱湖,另一片叫翠花泊,中间的小河就叫牵手河,因为这就好似两片湖泊仅仅牵着的小手。这样的日子,过了很久,似乎就要这样一直持续下去。。。(你知道我接下来要说什么:))
(2)故事1(开运算):
可是这样的日子过得久了,就好像人有七年之痒一般,两片湖泊也开始嫌恶互相之间一成不变的陪伴。于是他们向老天爷抱怨,希望能将他们俩儿分开,希望都能够给彼此一片空间,过一过各自的生活。老天爷聆听到了他们的请求,说可以,但是他们必须要接受考验,那就是一场旷日持久的干旱,这场干旱会断开他们之间的牵手河,也会蒸发他们自身的湖水。
两片湖早就已经对彼此心生厌烦,不由多说,便都接受了考验。很快,干旱到来了,刚开始,两片湖还算能够接受,也只是感觉自身的容量减少了一点。可是到了后来,干旱还是带来了巨大的影响,因为整日的干旱,植物不断地枯死,动物也纷纷去寻找其他的安身之所,生活的村民也渐渐地离开这片伤心地。终于,牵手河彻底蒸发完了,两片湖也算彻底分开了。但是周遭的环境已然是一片凋敝,湖泊也丧失了生气。铁柱湖与翠花泊也都开始回忆起过往的时光,相比于此时,那是无比的灿烂与美好。
渐渐地,时过境迁,干旱结束了,雨季到来了。随着雨水的不断倾注,两片湖的面积也开始慢慢地膨胀起来,他们心中又开始萌生互相连接的希望。可是随着雨季的结束,两片湖也只是恢复了与以前差不多的面积,但还是没能互相连接,牵手河的河床也早就成了两者之间无法逾越的高地。
import cv2
import numpy as np
img = cv2.imread("F://src.jpg")
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(10,10))
img1 = cv2.erode(img,kernel,iterations=2)
img2 = cv2.dilate(img1,kernel,iterations=3)
cv2.imwrite("F://rst1.jpg",img2)
cv2.imshow("src",img)
cv2.imshow("rst1",img2)
cv2.waitKey(5000)
cv2.destroyAllWindows()
(3)故事2(闭运算):
两片湖又向老天爷情愿,希望能将他们俩重新连接在一起。老天爷回应道,如果他们真想如此的话,这一次也可以满足你们的愿望,但是仍有一个条件,那就是你们这次不仅仅是靠一条牵手河来进行连接,而是要进行完全的融合,合成一个湖泊,达成你中有我,我中有你的情况。并且,这是最后的一个愿望,一旦合二为一,就再也不能分开。
这一次,铁柱与翠花都没有立即下结论,他们都经过了一段长时间的思考与斟酌。最后他们向老天爷说出了自己的选择:他们都愿意合二为一。老天爷表示可以,满足你们的愿望。接着,天上便降下了倾盆大雨。这场雨下得不仅势大,而且持久,久到将原本牵手河的河床重新灌满了河水,并且不断地扩张开来。雨势过大,环境再一次变得不适合动植物生存,湖边的周遭又一次了无生迹,可是这一次两片湖再无怨言,选择了默默接受,互相扶持,共同陪伴到最后一刻。
终于,雨停了,太阳出现,在已经合二为一的湖面上激起粼粼波光。随着雨季的结束,湖泊的面积慢慢恢复到正常的大小,可从此再也没有分离开来。后来,人们知道这里有两片湖合为一片,却不再记起两片湖分别叫什么名字,只知道这里有一片叫合心海的湖泊。每年,总有许多的情侣来到这里,在此祈愿,希望彼此之间的爱情也能如同合心海的由来一样,虽是历经万般艰险,归来仍是不离不弃,白首共婵娟。
import cv2
import numpy as np
img = cv2.imread("F://rst1.jpg")
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(10,10),anchor=(0,0))
img1 = cv2.dilate(img,kernel,iterations=6)
img2 = cv2.erode(img1,kernel,iterations=3)
cv2.imwrite("F://rst2.jpg",img2)
cv2.imshow("rst1",img)
cv2.imshow("rst2",img2)
cv2.waitKey(5000)
cv2.destroyAllWindows()
鄙人认为,最珍贵的爱情不是一见钟情,而是经过浪淘沙洗,依然能够熠熠生光的对于彼此的忠贞与责任。只有经过了风霜的考验与时间的沉积,才是光泽透亮的珍珠。诚如东汉崔瑗所作铭文《座右铭》中所言:“在涅贵不淄,暧暧内含光。”
结语:
本期文章,我们了解了膨胀与腐蚀的基本原理,也学习了操作过程中的一些注意点,比如中心点坐标的计算方式、结构元素框选超出被操作图像的各种情况等等,学习了erode与dilate函数中各参数的含义与设置方法,最后还用一个精美(
老掉牙)的爱情故事为背景,给大家演示了开运算与闭运算的实际案例。总的来说,本期还是内容比较充实的,希望大家都能学习到知识,也能收获到快乐。
好了以上就是所有的内容,希望大家多多关注,点赞,收藏,这对我有很大的帮助。谢谢大家了!
好了,这里是Kamen Black 君。祝国康家安,大家下次再见喽!!!
溜溜球~~