滤波起源于信号和系统理论,这里不做过多解释,感兴趣的可以查看《信号与系统》这本书。【不建议花太多时间取研究信号与系统,在我们的学习中知道这样的概念就可以了;当然你特别感兴趣,也可以看哦】
在图像处理中,滤波是一个必不可少的概念,它可以去除图像中的噪声,提取图像中特征等等。
当我们看一幅图像时,图像中图案是由不同的灰度组成的,图像之间的区别就是他们具有不同的灰度级分布方式。
我们可以从图像中灰度的变化来解释。在一幅图像中,某个区域的灰度值几乎不变,例如下图中红色框圈出的部分;绿色框圈出的部分,红色线就是灰度值变化大的地方。因此,可以根据上述灰度变化的频率来描述图像的特征,这就叫做频域;通过灰度分布来描述图像特征称为空域。
频域把图像分为低频和高频,图像灰度值变化慢的区域我们认为他是低频, 变化快的区域认为是高频。傅里叶变换和余弦变化算法可以清楚的显示图像的频率,这里不做过多解释,感兴趣的可以自己百度,因为这部分对咱们后续课程没有太多作用。
图像是二维的,所以频率分为垂直频率(垂直方向的变化)和水平频率(水平方向的变化)。
滤波器分为低通滤波器和高通滤波器,高通滤波器就是通过图像中的高频信息,消除图像的低频信息,从而消除图像中的噪点。低通正好相反,使图像变的平滑。
卷积的原理:应用一个滤波器,也叫做内核,我们针对原始图像的每个像素点,利用滤波器进行处理,得到一个结果图像。如下图所示:
补充知识中又详细的卷积介绍。
白色的大框就是我们的图像矩阵为 9 9 9行 7 7 7列,蓝色部分就是我们的滤波器,提取出来就是 1 / 25 1/25 1/25那部分【后面会解释】,大小为 5 5 5行 5 5 5列,让滤波器中心点对应在图像上的每个像素格移动,直到最后一个像素格,最后的图的红色部分就是我们得到的结果图像。
在第一个像素格的时候,我们把滤波器的中心对应到第一个像素格,这时就会有多出来的部分没有图像像素对应,我们就需要在图像的最外围填充0,跟滤波器能够对应上。如下如所示:
均值滤波将每个像素的值用该像素邻域的平均值替换,如下图所示:
红色点的新得像素值:
( 197 + 25 + 106 + 156 + 159 + 149 + 40 + 107 + 5 + 71 + 163 + 198 + 226 + 223 + 156 + + 222 + 37 + 68 + 193 + 157 + 42 + 72 + 250 + 41 + 75 ) / 25 (197+25+106+156+159+149+40+107+5+71+163+198+226+223+156++222+37+68+193+157+42+72+250+41+75)/25 (197+25+106+156+159+149+40+107+5+71+163+198+226+223+156++222+37+68+193+157+42+72+250+41+75)/25
从以上公式中,我们可以看出,给源图像的每个像素值乘1,累加后在除以滤波器的大小,这里称1为权值,我们把这个1(权值)单拿出来,也就形成了一个模板,也就是上述所说的滤波器。如下图所示:
均值滤波在opencv
官网中的函数API
:
dst = cv2.blur(src, ksize[, dst[, anchor[, borderType]]])
src
:原始图像
ksize
:滤波器的大小,元组形式,例如 ( 3 , 3 ) (3,3) (3,3),
[] 中括号表示可选参数,实际应用中,我们后三个参数不用管,用默认值即可。
ksize
大小必须是奇数所以用的时候如下代码:
dst = cv2.blur(img, (3,3))
在均值滤波中,我们把滤波器中的值都设为了1,那么高斯滤波与均值滤波的区别就是:高斯滤波的的滤波器的值离中心点像素近的值就大一点,离中心像素点远得值就小一点。滤波器得值如下图所示:
下面来解释一下为什么这样操作,首先看一下高斯函数的图像
从上图中可以看到,距离均值较远的地方,已经接近0了,所以对于函数没有什么意义了,这就是高斯滤波权值设置的原因。
高斯滤波在opencv
官网中的函数API
:
dst = cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
src
:原始图像
ksize
:滤波器的大小,元组形式,例如 ( 3 , 3 ) (3,3) (3,3)
sigmaX
:X反向得方差
sigmaX
这个参数我们一般设为0,让程序根据我们设置的滤波器的大小自动计算出方差Y方向的方差与X方向保持一致
计算公式为 s i g m a X = 0 时 , s i g m a = 0.3 ∗ ( ( k s i z e − 1 ) ∗ 0.5 − 1 ) + 0.8 sigmaX=0时,sigma=0.3*((ksize-1)*0.5-1)+0.8 sigmaX=0时,sigma=0.3∗((ksize−1)∗0.5−1)+0.8
gaussian = cv2.GaussianBlur(img, (3, 3), 0)
中值滤波是把当前像素和它的邻域组成一个集合,然后计算这个集合的中间值,以此作为当前像素的值。
因为这样的形式,中值滤波就不能进行卷积操作了,如下图,图中布满了白色点点,我们称这种为噪声。
某个像素领域有这种噪声,在一幅灰度图中,像素值最大为255,也就是白色,所以白色的像素值永远不可能是中值,这样我们用邻域的中间值替换这个像素,那么白噪声就会被去掉,这就是中值滤波的优点,对于去除椒盐噪声非常有用。
197 、 25 、 106 、 156 、 159 、 149 、 40 、 107 、 5 、 71 、 163 、 198 、 226 、 223 、 156 、 222 、 37 、 68 、 193 、 157 、 42 、 72 、 250 、 41 、 75 197、25、106、156、159、149、40、107、5、71、163、198、226、223、156、222、37、68、193、157、42、72、250、41、75 197、25、106、156、159、149、40、107、5、71、163、198、226、223、156、222、37、68、193、157、42、72、250、41、75对这些数进行排序(从大到小,从小到大都可以),找到中值:149,我们就用149作为新得像素值。
中值滤波在opencv
官网中的函数API
:
dst = medianBlur(src, ksize[, dst])
src
:原始图像
ksize
:滤波器的大小,这里是整形数字
边缘检测可以说是一种定向滤波,我们最开始介绍了图像分为垂直频率(垂直方向的变化)和水平频率(水平方向的变化)两个方向,总体来说就是用后一个像素点的值减去前一个像素点的值得到的结果,我们称之为梯度,就是我们最后要求得边缘。也就是说用下图中第3列减去第二列得值,但是我们怎么实现这种操作呢,我们就用图像滤波方式来实现,区别在于滤波器得值我们在这里指定了大小,正负。
以一个 3 ∗ 3 3*3 3∗3大小的为例,中心点为 f ( x , y ) f(x,y) f(x,y),如图:
我们首先定义Robert算子得卷积核,如下图:
得到的X,Y方向的梯度就是:
E x = f ( x , y ) − f ( x − 1 , y − 1 ) E y = f ( x − 1 , y ) − f ( x , y − 1 ) E_x = f(x,y) - f(x-1,y-1) \\ E_y = f(x-1,y) - f(x,y-1) Ex=f(x,y)−f(x−1,y−1)Ey=f(x−1,y)−f(x,y−1)
上述公式是怎么计算出来的呢?我们看一幅图:
我们计算X方向的梯度:
E x = 0 ∗ f ( x , y − 1 ) + ( − 1 ) ∗ f ( x − 1 , y − 1 ) + 1 ∗ f ( x , y ) + 0 ∗ f ( x − 1 , y ) E x = f ( x , y ) − f ( x − 1 , y − 1 ) E_x = 0*f(x,y-1) + (-1)*f(x-1,y-1) + 1*f(x,y) + 0*f(x-1,y) \\ E_x = f(x,y) - f(x-1,y-1) Ex=0∗f(x,y−1)+(−1)∗f(x−1,y−1)+1∗f(x,y)+0∗f(x−1,y)Ex=f(x,y)−f(x−1,y−1)
Y方向同理。
卷积核要翻转 18 0 o 180^o 180o之后再参与计算的。
Robert算子没有相应的函数,其实在实际中也不怎么用。
感兴趣的话,可以研究一下自己写的Robert算子实现的代码。这里将算子定义为[[-1,-1],[1,1]]了,知道原理即可。
#coding=utf-8 import cv2 # robert 算子[[-1,-1],[1,1]] def robert_suanzi(img): r, c = img.shape r_sunnzi = [[-1, -1], [1, 1]] for x in range(r): for y in range(c): if (y + 2 <= c) and (x + 2 <= r): imgChild = img[x:x + 2, y:y + 2] list_robert = r_sunnzi * imgChild img[x, y] = abs(list_robert.sum()) # 求和加绝对值 return img img = cv2.imread('images/robert.png',0) dst = robert_suanzi(img) cv2.imshow('dst',dst) cv2.waitKey()
原理与上述Robert
算子一致,我们分别计算X和Y方向的梯度,但是整幅图像的梯度是什么呢?
G = E x 2 + E y 2 = ∣ E x ∣ + ∣ E y ∣ G = \sqrt{E_x^{2} + E_y^{2}} = |E_x|+|E_y| G=Ex2+Ey2=∣Ex∣+∣Ey∣
sobel
算子opencv
已经给我们写好了函数,我们直接调用即可。
函数API
:
dst = cv2.Sobel(src, ddepth, dx, dy[, dst[, ksize[, scale[, delta[, borderType]]]]])
参数解释:
src
:原始图像
ddepth
:处理结果图像的深度
dx
:X方向的梯度
dy
:Y方向的梯度
ksize
:滤波器大小
我们来具体讲解一下ddepth
参数,通常情况来说我们会设为-1,代表处理结果与原始图像保持一致;但是一般不用,因为会存在溢出问题。
下面有这样一幅图:
B这条边,我们用右面白色部分减去黑色部分,得到的是一个正数
A这条边,我们用右面黑色部分减去白色部分,得到的是一个负数
但是在灰度图中,像素值的范围是0-255,那么小于0的数值,我们就会自动赋值为0,称为截断。那么这条边就不会显示了,所以我们就需要把ddepth
参数的深度调大一点,即cv2.CV_64F
,代表64位的浮点数,之后再转换到正常图像的类型为cv2.CV_8U
,也就是np.uint8
类型,8位整型。
转换方法就是我们取A计算出来的绝对值,用到的函数API
:
dst = cv2.convertScaleAbs(src[, dst[, alpha[, beta]]])
虽然这个函数有很多参数,但是我们用的时候,只填第一个参数即可。
目标函数 = cv2.convertScaleAbs(原始图像)
在灰度图像中,也就是256色位图中,白色的像素值位255,黑色为0。
垂直边界的与水平原理一样。
当我们计算最后的结果时,我们需要把两个方向的梯度都加起来,有两种方式:
sobel_xY = cv2.Sobel(img, -1, 1, 1, ksize =3)
但是这样的效果并不是很好,同时求导对于精度的要求太高,使得有些边缘并没有计算出来。
单独对X方向和Y方向求导,再相加,需要用到cv2.addWeighted
函数
# Sobel算子边缘检测,分别对x,和y方向计算梯度
sobel_x = cv2.Sobel(img, cv2.CV_64F, 1, 0, ksize =3)
sobel_y = cv2.Sobel(img, cv2.CV_64F, 0, 1, ksize =3)
#经过卷积之后,会出现溢出的问题,需要转换
abs_x = cv2.convertScaleAbs(sobel_x)
abs_y = cv2.convertScaleAbs(sobel_y)
#第一个参数:第一幅图像
#第二个参数:第一幅图像的权重
#第三个参数:第二幅图像
#第四个参数:第二幅图像的权重
#第五个参数:修正值,一般为0
sobel_show = cv2.addWeighted(abs_x, 0.5, abs_y, 0.5, 0)
其实原理都是一样的,拉普拉斯就是需要减两次就可以了。
减几次就对应的求函数的几阶导数,因为图像也是一个函数嘛。
一阶导:sobel算子 |右-左| + |下-上|
二阶导:laplacian算子 |右-左| + |下-上| + |右-左| + |下-上|
它的卷积核长这个样子:
函数API
:
dst = Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
src
:原始图像
ddepth
:处理结果图像的深度,与Sobel算子的一样
ksize
:滤波器大小
代码演示:
laplacian = cv2.Laplacian(img, cv2.CV_64F)
lap_show = cv2.convertScaleAbs(laplacian)
canny算子的计算步骤:
去除噪声
边缘检测容易受到噪声的影响,所以先使用高斯滤波去除噪声
梯度计算
canny算子的梯度不仅有值还有方向,梯度计算上面已经说过了,一个像素点的梯度方向计算方式: A n g l e ( 角 度 ) = t a n − 1 ( E y E x ) Angle(角度) = tan^{-1}(\frac{E_y}{E_x}) Angle(角度)=tan−1(ExEy),因为这个方向是任意的,所以我们归为4个大类:水平,垂直和两个对角线。
非极大值抑制
得到梯度值和方向后,我们去除不是边界的点。
因为一个梯度方向上,有好多点,我们就判断当前像素点的梯度值是否是周围像素点中最大值,最大值就保留下来,否则就赋值为0。
双阈值
双阈值就是判断边缘的两个阈值,一个高阈值,一个低阈值,边缘点大于高阈值我们就认为他肯定是边缘,叫做强边缘点,小于低阈值,肯定不是边缘,我们可以忽略这部分边缘点;重要的是处于高低阈值中间的点的处理:需要判断这个点的8邻域中,是否有强边缘点,有就留下,没有就被忽略。
Canny算子的函数API
:
edges = Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
image
:原始图像
threshold1
:阈值1
threshold2
:阈值2
实验表明:高低阈值的比率最好为 2 : 1 2:1 2:1或者 3 : 1 3:1 3:1。
例如:
canny = cv2.Canny(img, 50, 150)
如果想要图像边缘的细节信息多一些,那么阈值就设置小一些。
一幅图像可以定义为一个二维函数 f ( x , y ) f(x,y) f(x,y),其中 x x x和 y y y是空间(平面坐标),二再任何一对空间坐标 ( x , y ) (x,y) (x,y)处的值 f ( x , y ) f(x,y) f(x,y)叫做该坐标处的灰度值。
图像的坐标跟我们以前学的平面坐标系有所差别,在图像中,我们以图像左上角为原点 ( 0 , 0 ) (0,0) (0,0),从左到右为 x x x轴,从上到下为 y y y轴。如下图所示:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3FVF7Ona-1607939104708)(图像滤波与边缘检测.assets/无标题.png)]
在图像滤波中,我们有两个重要的概念,一个是相关,一个是卷积。相关是滤波器模板移过图像并计算每个位置乘积之和的处理。卷积与相关类似,区别在于要先将滤波器模板翻转 18 0 o 180^o 180o。
翻转的原因:跟信号系统只是相关了,其实没有什么具体意义,就是为了得到某种效果而想到的一种运算。
参考链接:https://blog.csdn.net/weixin_39123145/article/details/82969261
https://blog.csdn.net/qq_26638113/article/details/98390833?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-2.nonecase
参考资料:《opencv计算机视觉编程攻略》第三章
《数字图像处理第三版》第 1 1 1页 和 89 − 90 89-90 89−90页