本系列文章是上篇OpenCV库操作的后续,主要内容还是有关图像的处理,建议先看完上篇内容后再来食用,如果你要问为什么不搞一个合集,那我只能说苦逼大学生没有一天空闲时间,只能某天抽出一点时间去更新(虽然我的更新频率也不是很快)
如果传入的图像有一些特别亮的点或者其他的能形成对图像检测干扰的点,我们要在图像检测前,先进行图像平滑处理,将这些点的干扰效果尽可能减小
均值滤波:我们要先指定一个卷积核,这个卷积核其实就是一个正方形的矩阵然后除以矩阵中元素的个数,我们以3x3的矩阵为例。用卷积核窗口中所有像素值的平均值替换目标像素值,对于上图的卷积核矩阵,58是中心点,他应该被替换为这九个数的平均数
# img就是图像,后面是卷积核的维度,blur就是处理过的图像
blur=cv2.blur(img,(3,3))
均值滤波的优缺点:
基本上于均值滤波一样,可以选择是否进行归一化,归一化就是求和除个数,而不做归一化就是只求和
如果和超过255,直接取255
blur=cv2.blur(img,(3,3),normalize=False)
** 大家听过高斯函数吗,高斯函数就是正态分布函数。我们可以这么想,在上面的均值滤波中,我们把卷积核中所有的值都相加除 9了,但是对于中心点来说,肯定不是所有的点对他的影响一样,肯定有部分点对他的影响很大,有部分点几乎没影响,那现在是不是感觉均值滤波是把所有点对中心点的影响看成了一样的,现在看来是不太合理的。我们应该给每一个数加一个权重,让影响大的数权重大,对中心的的影响大。而高斯函数说明了越靠近中心点,对中心点的影越大**
# 前两个参数就不说了,后面的那个1表示高斯核函数在X方向的的标准偏差,我们这里没有写y方向的,那么
# x方向和y方向一致,如果σ较小,这样对图像的平滑效果就不是很明显;
# 相反,σ较大时,比较类似于均值模板,对图像的平滑效果就比较明显。至于具体作用,可以看一下高斯函数
cv2.GaussianBlur(img,(5,5),1)
其实操作跟上面都一样,都是先取一个卷积核,但是替代的数是卷积核中所有元素的中位数
# 5就是卷积核的长度和宽度,必须是比1大的奇数
cv2.medianBlur(img,5)
这部分内容主要涉及到形态学的知识,而形态学的基本思想就是用一定形态的结构元素,取量度和提取图像中的对应形状,达到分析和识别的目的
腐蚀的作用:消除物体的边界点,使边界向内收缩,可以把小于结构元素的物体去除。可将两个有细小连通的物体分开。该方法可以用来去除毛刺,小突起等。如果两个物体间有细小的连通,当结构足够大时,可以将两个物体分开
# 这里我们先导进来一张图像
img = cv2.imread('dige.png')
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 我们可以看到这里面有许多毛刺,我们现在就要给这个图片加上黑色的背景,把这些毛刺“腐蚀掉”
# 我们在这里先创建了一个3x3的矩阵,矩阵的元素都为1,这个矩阵就是我们的结构元素,
# 通过这个矩阵去腐蚀
kernel = np.ones((3,3),np.uint8)
# 这个erode函数就是腐蚀函数,img就是图片,kernel就是用于腐蚀的结构元素,iterations就是腐蚀的次数
erosion = cv2.erode(img,kernel,iterations = 1)
# 我们看结果,毛刺被去除了。
cv2.imshow('erosion', erosion)
cv2.waitKey(0)
cv2.destroyAllWindows()
那腐蚀操作就是,结构元素矩阵在图像矩阵上依次移动,每个位置上结构元素矩阵所覆盖元素的最小值替换B的中心位置值。那我们知道,在灰度图中越暗的点值越小,越亮的点值越大,那我们可以再来简单总结一下
腐蚀操作是针对图像中的白色元素的,如果我们想去掉图像中,对图像有干扰的白色元素,我们就可以用腐蚀操作
膨胀操作我们可以类比腐蚀操作,就是让白色元素变得更大,像是膨胀一样,那腐蚀操作是取最小值,那膨胀操作就是取最大值
# 这个我们还是把刚才的照片导进来,然后进行一个腐蚀操作
img = cv2.imread('dige.png')
cv2.imshow('img', img)
cv2.waitKey(0)
cv2.destroyAllWindows()
kernel = np.ones((3,3),np.uint8)
dige_erosion = cv2.erode(img,kernel,iterations = 1)
cv2.imshow('erosion', erosion)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 然后我们对进行腐蚀操作后的图像进行膨胀操作,dilate()就是膨胀,与腐蚀函数基本一致
kernel = np.ones((3,3),np.uint8)
dige_dilate = cv2.dilate(dige_erosion,kernel,iterations = 1)
cv2.imshow('dilate', dige_dilate)
cv2.waitKey(0)
cv2.destroyAllWindows()
开运算就是先腐蚀,再膨胀:因为我们腐蚀后,虽然毛刺去掉了,但是相应内容部分也变小了,因为我们是拿矩阵去腐蚀的,毛刺都是不规则形状的,所以肯定主要内容部分也会减小一部分,那膨胀操作就可以把减少的这部分补回来。
**闭运算就是先膨胀,再腐蚀:**它在填充前景对象内的小孔或对象上的小黑点时很有用
# 开:先腐蚀,再膨胀
img = cv2.imread('dige.png')
kernel = np.ones((5,5),np.uint8)
# morphology就是形态学的意思
opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)
cv2.imshow('opening', opening)
cv2.waitKey(0)
cv2.destroyAllWindows()
# 闭:先膨胀,再腐蚀
img = cv2.imread('dige.png')
kernel = np.ones((5,5),np.uint8)
closing = cv2.morphologyEx(img, cv2.MORPH_CLOSE, kernel)
cv2.imshow('closing', closing)
cv2.waitKey(0)
cv2.destroyAllWindows()
将这个梯度运算前,我们需要先来补一下图像梯度的概念
图像梯度的解释:https://blog.csdn.net/literacy_wang/article/details/109640726
我们先来回顾一下高数中的梯度概念
梯度不是一个实数,它是一个有大小有方向的向量。现在以一个二元函数举例,假设一个二元函数f(x,y),在某点的梯度有:
其实就是方向导数,梯度的方向是函数变化最快的方向,沿着梯度的方向容易找到最大值。
有时候,在一副模糊图像中的物体的轮廓不是很明显,轮廓边缘灰度变化不是很强烈,从而导致我们看的时候感觉层次感不强,而在清晰图像中的物体轮廓边缘灰度变化明显,层次感强。那这种灰度变化明显不明显怎么去定义呢?
可以使用导数,导数的定义不就是函数在某点的变化率嘛,而梯度的方向是函数变化最快的方向。那如果我们把图像看成二维离散函数,那图像梯度就是这个二维离散函数的导数嘛
在上边这幅图可以看出,如果一副图像的相邻灰度值有变化,那么梯度存在,如果图像相邻的像素没有变化,那么梯度就是0
把梯度值和相应的像素相加,那么灰度值没有变化的,像素就没有变化,灰度值遍历,像素值也变了。
我们来看,原值和梯度对应位置相加后的,原图像素点100与90只相差10,而现在是110与90,相差20,对比度显然增强了,尤其是图像中物体的轮廓和边缘,与背景大大加强了区别,这就是梯度来增强图像的原理。
通过上面的讲解,相信大家已经理解了图像梯度了,那这里再总结一下
图像梯度运算简单来算就是把像素点之间的差值放大,这样图像的层次感就增强了
# 导进来一张图片,是一个圆
pie = cv2.imread('pie.png')
# 还是这个形态学操作,只是其中一个参数换成了cv2.MORPH_GRADIENT
kernel = np.ones((7,7),np.uint8)
gradient = cv2.morphologyEx(pie, cv2.MORPH_GRADIENT, kernel)
cv2.imshow('gradient', gradient)
cv2.waitKey(0)
cv2.destroyAllWindows()
我们看最后的结构是一个圆环,其实应该叫圆的轮廓更合适,形成这个的原因也很简单,我们打印一下这个图像就可以,见下图,可以发现很多都是0,但也有不少0的,只是数据太多,没显示完全。下面这个数组就是源图像的梯度数组,因为源图像,除了圆,都是黑色,那这些黑色的梯度肯定为0,还有圆的中心部分,都是白色相减也都是0,那有梯度的只有圆的轮廓了,那通过这个案例,我们也能看出,一个图像的梯度数组形成的图像就是这个图像的轮廓
礼帽=源图像-开运算,开运算后,我们把毛刺去掉了,那用源图像减去开运算后的图像,那剩下的不就是毛刺嘛
黑帽=闭运算-源图像,闭运算后,毛刺还在,只是闭运算后的图像比源图像胖一点,减去源图像后,刺没了,只剩下闭运算比源图像大的那一部分了,类似得到了一个没有毛刺的小轮廓,但是轮廓不是很清晰
现在我们还看不出礼帽和黑帽的作用,后面我们学的多了就可以看到了,这里可以先看一篇文章感受一下作用
礼貌和黑帽的作用:https://blog.csdn.net/great_yzl/article/details/119594716
对于各种复杂图像的梯度计算,仅靠形态学上的梯度计算是无法精确得出的,那这也是众多梯度算子产生的背景之一,接下来我们会为大家介绍各种梯度算子
Sobel算子包括两个矩阵,一个是计算水平方向的梯度,一个是计算垂直方向的梯度
那这个 * 运算是计算两个矩阵的点乘,就是矩阵中对应位置元素相乘然后相加。
而且这里有的是1,有的是2,其实这就是高数函数的体现,我们看算子中值为2或-2的离中心点较近
#dst = cv2.Sobel(src, ddepth, dx, dy, ksize)
# ddepth:图像的深度,处理结果图像深度,
# 一般我们都填-1,即与原图深度相同。但在这里我们需要填写cv2.CV_64F。
# 简单来说就是如果填写-1,我们在计算梯度时产生的负数就会被程序默认为0,
# 导致有一边的边缘出不来。而cv2.CV_64F范围更大,可以保留负数
# dx和dy分别表示水平和竖直方向
# ksize是Sobel算子的大小
def cv_show(img,name):
cv2.imshow(name,img)
cv2.waitKey()
cv2.destroyAllWindows()
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
cv_show(sobelx,'sobelx')
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
# 把算出来的soblex和sobley合并
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
cv_show(sobelxy,'sobelxy')
# 直接把x,y都计算出来,但是效果不是很好,轮廓出来是断断续续的,像是被等分成了4份
sobelxy=cv2.Sobel(img,cv2.CV_64F,1,1,ksize=3)
sobelxy = cv2.convertScaleAbs(sobelxy)
cv_show(sobelxy,'sobelxy')
# 下面我们用一个具体的例子看一下效果
img = cv2.imread('lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobely = cv2.convertScaleAbs(sobely)
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
cv_show(sobelxy,'sobelxy')
这是经过sobel算子后的轮廓
这是形态学的求的轮廓,是不是很模糊。这也说明了对应复杂图片,普通形态学求得的轮廓效果不是很好,sobel等轮廓算子还是很有必要的
我们看这个Scharr算子跟Sobel算子整体结构差不多,Scharr算子是对Sobel算子差异性的增强,因此两者之间的在检测图像边缘的原理和使用方式上相同。Scharr算子的边缘检测滤波的尺寸为3×3,因此也有称其为Scharr滤波器。可以通过将滤波器中的权重系数放大来增大像素值间的差异,弥补Sobel算子对图像中较弱的边缘提取效果较差的缺点。
拉普拉斯算子是图像领域内像素灰度差分计算的基础,通过二阶微分推导出的一种图像邻域增强算法。基本思想是当邻域的中心像素灰度低于他所在邻域内的其他像素的平均灰度时,此中心像素的灰度应该进一步降低,当高于时进一步提高中心像素的灰度。
具体的原理大家可以看下面这篇文章,主要理解离散点的求导就可以理解 laplacin算子是怎么来的了。
Laplacin算子的原理:https://www.jianshu.com/p/ca96bf5b4ad7
下面我们看不同算子的差异
#不同算子的差异
img = cv2.imread('lena.jpg',cv2.IMREAD_GRAYSCALE)
sobelx = cv2.Sobel(img,cv2.CV_64F,1,0,ksize=3)
sobely = cv2.Sobel(img,cv2.CV_64F,0,1,ksize=3)
sobelx = cv2.convertScaleAbs(sobelx)
sobely = cv2.convertScaleAbs(sobely)
sobelxy = cv2.addWeighted(sobelx,0.5,sobely,0.5,0)
scharrx = cv2.Scharr(img,cv2.CV_64F,1,0)
scharry = cv2.Scharr(img,cv2.CV_64F,0,1)
scharrx = cv2.convertScaleAbs(scharrx)
scharry = cv2.convertScaleAbs(scharry)
scharrxy = cv2.addWeighted(scharrx,0.5,scharry,0.5,0)
laplacian = cv2.Laplacian(img,cv2.CV_64F)
laplacian = cv2.convertScaleAbs(laplacian)
res = np.hstack((sobelxy,scharrxy,laplacian))
cv_show(res,'res')
第一个是 Sobel算子
第二个是Scharr算子:我们看Scharr算子相比Sobel算子描绘图像更细致,可以捕捉跟细致的梯度
第三个是Laplacin算子:我们看laplacin算子描绘的不少很清楚,是因为laplacin算子比较敏感,用laplacin算子之前,还要把噪音点去除掉。
步骤:
我们知道梯度算子可以用于增强图像,增强图像的层次感,本质上是通过增强边缘轮廓来实现的,也就是说是可以检测到边缘的。但是,它们受噪声的影响很大。因为噪声就是灰度变化很大的地方,所以很容易被识别为伪边缘。所以我们要先把噪音点去掉
经过降噪后,我们计算图像梯度,就可以得到可能的边缘集合(因为梯度是灰度变化明显的地方,而边缘也是灰度变化明显的地方。)但是得到的是可能的边缘,不一定就是边缘。因为灰度变化的地方可能是边缘,也可能不少边缘。
通常灰度变化的地方都比较集中,在局部范围内的梯度上,灰度变化最大的保留下来,其他的不保留,这也可以提出掉一大部分,将有多个像素宽的边缘变成一个单像素宽的边缘。即“胖边缘”变成“廋边缘”。我们上此说为什么不用形态学的梯度计算而用算子计算,也有这个原因。如果都保留,那整个图像的轮廓看上去就像挤到一起似的。
通过非极大值抑制后,仍然有很多的可能边缘点,进一步设置一个双阈值,即低阈值(low),高阈值(high)。梯度大于high的,设置为强边缘像素,低于low的,剔除。在low和high自己的设为弱边缘。进一步判断,如果其领域内有强边缘像素,保留,如果没有,剔除
img=cv2.imread("lena.jpg",cv2.IMREAD_GRAYSCALE)
v1=cv2.Canny(img,80,150)
v2=cv2.Canny(img,50,100)
res = np.hstack((v1,v2))
cv_show(res,'res')
本篇文章主要还是讲述一些有关图像处理的操作,下期应该就可以把图像操作更新完,下下期会带来一个银行卡号识别的项目,敬请期待
我是Mayphry,从一点点到亿点点,我们下次再见