图像腐蚀与膨胀

我们在前两次教程中概述了OpenCV对于图像的滤波,通常对于一个实战项目而言,滤波之后的下一步操作就是图像的形态学处理了,从本次教程开始,我们正式步入了OpenCV图像形态学处理的部分。

形态学(morphology)一词通常表示生物学的一个分支,该分支主要研究动植物的形态和结构。而我们图像处理中指的形态学,往往表示的是数学形态学。下面一起来了解数学形态学的概念。

数学形态学是一门建立在格论和拓扑学基础之上的图像分析学科,是数学形态学图像处理的基本理论。其基本的运算包括:二值腐蚀和膨胀、二值开闭运算、骨架抽取、极限腐蚀、击中击不中变换、形态学梯度、Top-hat变换、颗粒分析、流域变换、灰值腐蚀和膨胀、灰值开闭运算、灰值形态学梯度等。

简单来讲,形态学操作就是基于形状的一系列图像处理操作。OpenCV为进行图像的形态学变换提供了快捷、方便的函数。最基本的形态学操作有二种,他们是:膨胀与腐蚀(Dilation与Erosion)。

膨胀与腐蚀能实现多种多样的功能,主要如下:

  • 消除噪声。
  • 分割出独立的图像元素,在图像中连接相邻的元素。
  • 寻找图像中的明显的极大值区域或极小值区域。
  • 求出图像的梯度。

腐蚀和膨胀是对白色部分(高亮部分)而言的,不是黑色部分。膨胀就是图像中的高亮部分进行膨胀,“领域扩张”,效果图拥有比原图更大的高亮区域。腐蚀就是原图中的高亮部分被腐蚀,“领域被蚕食”,效果图拥有比原图更小的高亮区域。

▼ 膨胀

其实,膨胀就是求局部最大值的操作。

按数学方面来说,膨胀或者腐蚀操作就是将图像(或图像的一部分区域,我们称之为A)与核(我们称之为B)进行卷积。

核可以是任何的形状和大小,它拥有一个单独定义出来的参考点,我们称其为锚点。多数情况下,核是一个小的中间带有参考点和实心正方形或者圆盘,其实,我们可以把核视为模板或者掩码。

而膨胀就是求局部最大值的操作,核B与图形卷积,即计算核B覆盖的区域的像素点的最大值,并把这个最大值赋值给参考点指定的像素。这样就会使图像中的高亮区域逐渐增长。如下图所示,这就是膨胀操作:

opencv开运算和闭运算 java opencv开运算和闭运算_锚点

我们来看一下函数原型:

cv2. dilate (img,kernel,iterations)->dst

  • 第一个参数:img指需要膨胀的图。
  • 第二个参数:kernel指膨胀操作的内核,默认是一个简单的3X3矩阵,我们也可以利用getStructuringElement()函数指明它的形状。
  • 第三个参数:iterations指的是膨胀次数,省略是默认为1。
  • dst则为返回的图像。

定义卷积核需要用到Numpy中的函数,它可以定义一个矩形的卷积核结构元素,我们来看一下代码:

import cv2import numpy as npimg = cv2.imread('01.jpg',0)kernel = np.ones((5,5),np.uint8)dict = cv2.dilate(img,kernel,iterations = 1)cv2.imshow("org",img)cv2.imshow("result", dict)cv2.waitKey(0)cv2.destroyAllWindows()

我们定义了一个5*5的矩形卷积核,效果:

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_02

事实上,在某些情况下,我们可能需要椭圆形/圆形的内核。因此,为此,OpenCV具有一个函数cv.getStructuringElement()。我们只需传递内核的形状和大小,即可获得所需的内核,函数原型:

retval=cv.getStructuringElement(shape, ksize[, anchor])

  • 这个函数的第一个参数表示内核的形状,有三种形状可以选择。
  • 矩形:MORPH_RECT
  • 交叉形:MORPH_CROSS
  • 椭圆形:MORPH_ELLIPSE

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_03

第二和第三个参数分别是内核的尺寸以及锚点的位置。一般在调用erode以及dilate函数之前,先定义一个变量来获得。

getStructuringElement函数的返回值: 对于锚点的位置,有默认值Point(-1,-1),表示锚点位于中心点。element形状唯一依赖锚点位置,其他情况下,锚点只是影响了形态学运算结果的偏移。

我们看一下代码:

import cv2import numpy as npimg = cv2.imread('01.jpg',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()

至于卷积核的形状我在这里选择了椭圆形,大家可以自己选择其他形状进行实验:

opencv开运算和闭运算 java opencv开运算和闭运算_卷积核_04

▼ 腐蚀

再来看一下腐蚀,膨胀和腐蚀是一对好友,是相反的一对操作,所以腐蚀就是求局部最小值的操作。我们一般都会把腐蚀和膨胀对应起来理解和学习。下文就可以看到,两者的函数原型也是基本上一样的。

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_05

我们来看一下函数原型:

cv2.erode(img,kernel,iterations)->dst

  • 第一个参数:img指需要腐蚀的图。
  • 第二个参数:kernel指腐蚀操作的内核,默认是一个简单的3X3矩阵,我们也可以利用getStructuringElement()函数指明它的形状。
  • 第三个参数:iterations指的是腐蚀次数,省略是默认为1。
  • dst则为返回的图像。

代码:

import cv2import numpy as npimg = cv2.imread('01.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE,(5,5))erode = cv2.erode(img,kernel,iterations = 1)cv2.imshow("org",img)cv2.imshow("result", erode)cv2.waitKey(0)cv2.destroyAllWindows()

效果:

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_06

腐蚀与膨胀是形态学处理中的基础操作,它们有着很重要的作用,也是后面开操作与闭操作的基础,所以必须熟练运用。

开运算与闭运算

图像的腐蚀与膨胀是本次教程的核心——开运算与闭运算的基础,如果结构元素为圆形, 则膨胀操作可填充图像中比结构元素小的孔洞以及图像边缘处小的凹陷部分。而腐蚀可以消除图像中的毛刺及细小连接成分, 并将图像缩小, 从而使其补集扩大。但是, 膨胀和腐蚀并非互为逆运算, 所以它们可以结合使用。在腐蚀和膨胀两个基本运算的基础上, 可以构造出形态学运算簇, 它由膨胀和腐蚀两个运算的复合与集合操作(并、 交、 补等)组合成的所有运算构成。例如, 可使用同一结构元素, 先对图像进行腐蚀然后膨胀其结果, 该运算称为开运算;或先对图像进行膨胀然后腐蚀其结果, 称其为闭运算。开运算和闭运算是形态学运算族中两种最为重要的运算。

对于图像X及结构元素S, 用符号

opencv开运算和闭运算 java opencv开运算和闭运算_opencv开运算和闭运算 java_07

表示S对图像X作开运算, 用符号

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_08

表示S对图像X作闭运算, 它们的定义为:

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_09

首先需要来了解一个函数:

cv2.morphologyEx(src, op, kernel)

  • src传入的图片。
  • op进行变化的方式。
  • kernel表示定义的卷积核的大小以及形状。
  • op =  cv2.MORPH_OPEN 进行开运算,指的是先进行腐蚀操作,再进行膨胀操作。
  • op = cv2.MORPH_CLOSE 进行闭运算, 指的是先进行膨胀操作,再进行腐蚀操作。

▼ 开运算

开运算指的就是对图像先进行腐蚀操作,然后再进行膨胀操作,而通常情况下,它是对图像的明亮的区域进行操作,可以消除图像中的白噪声,现在我们来看例子,先看一幅图像:

opencv开运算和闭运算 java opencv开运算和闭运算_卷积核_10

现在我们想要消除图像中的黑色的毛刺,但是如果直接对图像进行开运算是不行的,因为开运算是对图像的明亮区域进行操作,看一下直接进行开运算会有什么效果:

import cv2import numpy as npimg = cv2.imread('open.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))open =cv2.morphologyEx(img,cv2.MORPH_OPEN,kernel)cv2.imshow("img",img)cv2.imshow("result", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_opencv开运算和闭运算 java_11

可以看到,图像的毛刺没有被去除,现在我们需要将原图进行阈值化翻转,也就是黑白颠倒,这样才方便进行形态学的处理,我们在前面阈值部分讲过,这里就不再讲述了,直接看代码:

import cv2import numpy as npimg = cv2.imread('open.jpg',0)threshold = cv2.threshold(img,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]cv2.imshow("img",img)cv2.imshow("thres",threshold)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_锚点_12

现在图像已经被黑白颠倒了过来,现在我们可以开始进行开运算了,当然首先也是需要定义一个卷积核的,这在上个教程中已经谈到,在这里我们定义一个3*3的矩形卷积核:

import cv2import numpy as npimg = cv2.imread('open.jpg',0)threshold = cv2.threshold(img,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))open =cv2.morphologyEx(threshold,cv2.MORPH_OPEN,kernel)cv2.imshow("img",img)cv2.imshow("thres",threshold)cv2.imshow("result", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_锚点_13

这样效果就显而易见了,如果我们将卷积核改成5*5的呢:

import cv2import numpy as npimg = cv2.imread('open.jpg',0)threshold = cv2.threshold(img,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))open =cv2.morphologyEx(threshold,cv2.MORPH_OPEN,kernel)cv2.imshow("thres",threshold)cv2.imshow("result", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_14

这就说明操作过度了,所以对于形态学处理卷积核的适当选取是非常重要的,现在我们对处理之后的图像进行还原:

import cv2import numpy as npimg = cv2.imread('open.jpg',0)threshold = cv2.threshold(img,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(3,3))open =cv2.morphologyEx(threshold,cv2.MORPH_OPEN,kernel)result = cv2.threshold(open,0,255,cv2.THRESH_BINARY_INV|cv2.THRESH_OTSU)[1]cv2.imshow("img",img)cv2.imshow("thres",threshold)cv2.imshow("open", open)cv2.imshow("result",result)cv2.waitKey(0)cv2.destroyAllWindows()

看一下最终还原的结果:

opencv开运算和闭运算 java opencv开运算和闭运算_开运算和闭运算_15

事实上,卷积核的灵活运用将会极大的方便图像的形态学处理,我们来进行一个实战,比如现在给出一幅图像:

opencv开运算和闭运算 java opencv开运算和闭运算_opencv开运算和闭运算 java_16

我们将用开运算分别提炼出横线和竖线,我们使用13*1的卷积核进行实验:

import cv2import numpy as npimg = cv2.imread('hengshu.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(13,1))open =cv2.morphologyEx(img,cv2.MORPH_OPEN,kernel)cv2.imshow("img",img)cv2.imshow("open", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_17

是不是很神奇,现在我们使用1*13的卷积核进行实验:

import cv2import numpy as npimg = cv2.imread('hengshu.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(1,13))open =cv2.morphologyEx(img,cv2.MORPH_OPEN,kernel)cv2.imshow("img",img)cv2.imshow("open", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_18

竖线也被完美的提取出来了,在以后的项目实战中,我们将会用到这些知识,合理的过滤掉图像中多余的信息,事实上,我们还发现,处理之后的图像偏暗,没有原图那么明亮,这在下次教程中的顶帽——黑帽操作中可以进行处理。

▼ 闭运算

我们折腾了半天的开运算,现在我们来玩玩闭运算,闭运算跟开运算相反,是先膨胀再腐蚀,它通常被用来去除图像明亮区域内部的噪声。我们来看一幅图像:

opencv开运算和闭运算 java opencv开运算和闭运算_锚点_19

现在我们将要用闭运算去除图像明亮区域内部的黑点,定义一个7*7的卷积核,我们看代码:

import cv2import numpy as npimg = cv2.imread('close.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(7,7))open =cv2.morphologyEx(img,cv2.MORPH_CLOSE,kernel)cv2.imshow("img",img)cv2.imshow("open", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_卷积核_20

可以看到,效果很好,现在我们来进行另一个具有实战意义的实验,先看图片:

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_21

我们需要将这些字轮廓提炼出来,并且用方框标定出来,每一行字用一个方框标定出来,当然,这个涉及到以后将要讲解的轮廓提取以及轮廓近似,但是在这里我们先进行一个实验,如果我们想将每一行字用一个方框标定出来,那么首先需要满足的条件就是每一行的字必须连在一块,形成一个整体,这样的话才可以用OpenCV提取他们整体的轮廓,进而标定出来,但现在我们看到这些字都是独立的,它们并没有连在一起,这个时候我们就可以采用闭运算了。

我们来看代码:

import cv2import numpy as npimg = cv2.imread('text1.jpg',0)kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(21,5))open =cv2.morphologyEx(img,cv2.MORPH_CLOSE,kernel)cv2.imshow("img",img)cv2.imshow("open", open)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_高亮_22

这样的话所有的字体连在一起,这也方便了后期的轮廓提取。

我在这里给出综合代码,大家可以玩玩:

import cv2import numpy as npimg = cv2.imread('text1.jpg')test = img.copy()gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (19, 5))close = cv2.morphologyEx(gray, cv2.MORPH_CLOSE, kernel)contour, _ = cv2.findContours(close, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)cv2.drawContours(test, contour, -1, (0, 0, 255), 2)for c in contour:x, y, w, h = cv2.boundingRect(c)cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)cv2.imshow("result", img)cv2.imshow("test",test)cv2.imshow("close", close)cv2.waitKey(0)cv2.destroyAllWindows()

opencv开运算和闭运算 java opencv开运算和闭运算_opencv开运算和闭运算 java_23

第一个图是对图像的轮廓进行提取,第二个则是提取轮廓的外接矩形,是不是很有意思,这在以后都会慢慢讲述。

闭运算一般则被用于处理内部噪声情况,开运算处理外部噪声情况,所以合理的运用它们是非常重要的。