图像卷积就是卷积核在图像上按行滑动遍历像素时不断的相乘求和的过程。
步长就是卷积核在图像上移动的步幅。
上面的例子中卷积核每次移动一个像素步长的结果,如果将这个步长修改为2,结果会如何?
为了充分扫描图像,步长一般设为1。
从上面例子中我们发现,卷积之后图像的长宽会变小,如果要保持图像大小不变,我们需要在图像周围填充0,padding指的就是填充0的圈数。
我们可以通过公式计算出需要填充0的圈数:
如果要保持卷积之后图像大小不变,可以得出等式:(N + 2P - F + 1) = N 从而可以推导出 P = F − 1 2 P = \frac {F-1} {2} P=2F−1
图像卷积中,卷积核一般为奇数,比如 3 * 3,5 * 5,7 * 7,为什么一般是奇数呢?出于以下两个方面的考虑:
1、根据上面padding的计算公式,如果要保持图像大小不变,采用偶数卷积核的话,比如 4 * 4,将会出现填充1.5圈零的情况。
2、奇数维度的滤波器有中心,便于指出滤波器的位置,即OpenCV卷积中的锚点。
filter2D()
用法:
cv2.filter2D(src, ddepth, kernel, dst, anchor, delta, borderType)
参数说明:
代码实现(锐化)
import cv2
import numpy as np
img = cv2.imread('../resource/r_cat.jpg')
# 相当于原始图像中的每个点都被平均了一下,所以图像变模糊了
# kernel = np.ones((5, 5), np.float32) / 25
# 突出轮廓
# kernel = np.array([[-1, -1, -1], [-1, 8, -1], [-1, -1, -1]])
# 浮雕效果
# kernel = np.array([[-2, 1, 0], [-1, 1, 1], [0, 1, 2]])
# 锐化
kernel = np.array([[0, -1, 0], [-1, 5, -1], [0, -1, 0]])
dst = cv2.filter2D(img, -1, kernel)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
boxFilter()
用法:
cv2.boxFilter(src, ddepth, ksize, dst, anchor, normalize, borderType)
参数说明:
ddepth:卷积之后图像的位深,即卷积之后图像的数据类型,一般设为-1,表示和原图像类型一致。
Ksize:方盒滤波卷积核大小
方盒滤波的卷积核形式如下:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/r_cat.jpg')
# 不用手动创建卷积核,只需要告诉方盒滤波,卷积核的大小是多少
dst = cv2.boxFilter(img, -1, (5, 5), normalize=True)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
blur()
用法:
cv2.blur(src, ksize, dst, anchorborderType)
参数说明:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/r_cat.jpg')
dst = cv2.blur(img, (5, 5))
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
要理解高斯滤波首先要知道什么是高斯函数:
高斯函数是符合高斯分布(也叫正态分布)数据的概率密度函数。
高斯函数的特点是以x轴某一点(这一点称为均值)为对称轴,越靠近中心数据发生的概率越高,最终形成一个两边平缓,中间陡峭的钟型(也被称为帽子)图形。
高斯函数的一般形式为:
高斯滤波就是使用符合高斯分布的卷积核对图像进行卷积操作,所以高斯滤波的重点是如何计算符合高斯分布的卷积核,即高斯模板。
假定中心点的坐标是(0, 0),那么取距离它最近的8个点坐标,为了计算,需要设 定 σ \sigma σ 的值。假定 σ \sigma σ = 1.5,则模糊半径为1的高斯模板如下:
我们可以观察到越靠近中心,数值越大,越边缘的数值越小,符合高斯分布的特点。
通过高斯函数计算出来的是概率分布函数,所以我们还要确保这九个点加起来为1,这九个点的权重总和等于0.4787147,因此,上面九个值还要分别除以0.4787147,得到最终的高斯模板。
注:有些整数高斯模板是在归一化后的高斯模板的基础上每个数除以左上角的值,然后取整。
有了卷积核,计算高斯滤波就简单了。假设现在有9个像素点,灰度值(0 ~ 255)的高斯滤波计算如下:
将这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。
GaussianBlur()
用法:
cv2.GaussianBlur(src, ksize, sigmaX, dst, sigmaY, borderType)
参数说明:
代码实现一: 模糊
import cv2
import numpy as np
img = cv2.imread('../resource/r_cat.jpg')
# dst = cv2.GaussianBlur(img, (9, 9), sigmaX=100)
# 如果不指定sigmaX,会使用ksize计算sigma
dst = cv2.GaussianBlur(img, (9, 9), sigmaX=0)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
代码实现二: 去噪
import cv2
import numpy as np
img = cv2.imread('../resource/cv.webp')
dst = cv2.GaussianBlur(img, (5, 5), sigmaX=1)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
中值滤波原理非常简单,假设有一个数组[1 5 5 6 7 8 9],取1其中的中间值(即中位数)作为卷积后的结果值即可。
中值滤波对胡椒噪音(也叫椒盐噪音)效果明显。
medianBlur()
用法:
cv2.medianBlur(src, ksize, dst)
参数说明:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/cv_noise.webp')
dst = cv2.medianBlur(img, 5)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
双边滤波对于图像的边缘信息能够更好的保存,其原理为一个与空间距离相关的高斯函数与一个灰度距离相关的高斯函数相乘。
空间距离:
指的是当前点与中心点的欧式距离。空间域高斯函数其数学形式为: e − ( x i − x c ) 2 + ( y i − y c ) 2 2 σ 2 e^{-\frac {(x_i-x_c)^2 + (y_i-y_c)^2} {2\sigma^2}} e−2σ2(xi−xc)2+(yi−yc)2
其中, ( x i , y i ) (x_i, y_i) (xi,yi)为当前位置, ( x c , y c ) (x_c, y_c) (xc,yc)为中心点的位置, σ \sigma σ 为空间域标准差。
灰度距离:
指的是当前点灰度与中心点灰度的差的绝对值。值域高斯函数其数学形式为: e − ( g r a y ( x i , y i ) − g r a y ( x c , y c ) ) 2 2 σ 2 e^{-\frac {(gray(x_i, y_i)-gray(x_c, y_c))^2}{2\sigma^2}} e−2σ2(gray(xi,yi)−gray(xc,yc))2
其中, g r a y ( x i , y i ) gray(x_i, y_i) gray(xi,yi)为当前点灰度值, g r a y ( x c , y c ) gray(x_c, y_c) gray(xc,yc)为中心点灰度值, σ \sigma σ 为值域标准差。
注意:双边滤波本质上是高斯滤波,不同的是:
1、双边滤波既利用了位置信息又利用了像素信息来定义滤波窗口的权重。
2、高斯滤波只利用了位置信息。
对于高斯滤波,仅用空间距离的权值系数核与图像卷积后,确定中心点的灰度值。即认为离中心点越近的点,其权重系数越大。
双边滤波中加入了对灰度信息的权重,即在邻域内,灰度值越接近中心点灰度值点的权重更大,灰度值相差大的点的权重越小。此权重大小,则由值域高斯函数确定。
两者权重系数相乘,得到最终的卷积模板。由于双边滤波需要每个中心点邻域的灰度信息来确定其系数,所以其速度比一般的滤波慢很多,而且计算量增长速度为核大小的平方。
双边滤波可以保留边缘,同时可以对边缘内的区域进行平滑处理。双边滤波的作用就相当于做了美颜。
bilateralFilter()
用法:
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace, dst, borderType)
参数说明:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/lena.bmp')
dst = cv2.bilateralFilter(img, 10, sigmaColor=20, sigmaSpace=50)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
边缘是像素发生跃迁的位置,是图像的显著特征之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。
人眼如何识别图像边缘?
比如有一幅图像,里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘,也就是像素的灰度值快速变化的地方。
sobel算子对图像求一阶导数,一阶导数越大,说明像素在该方向的变化越大,边缘信号越强。
因为图像的灰度值都是离散的数字,sobel算子采用离散差分算子计算图像像素点亮度值的近似梯度。
图像是二维的,即沿着宽度/高度两个方向,我们使用两个卷积核对原图像进行处理。
G x = [ − 1 0 + 1 − 2 0 + 2 − 1 0 + 1 ] ∗ I (水平方向) G_x= \begin{bmatrix} -1 & 0 & +1 \\ -2 & 0 & +2 \\ -1 & 0 & +1 \\ \end{bmatrix} *\ I \tag{水平方向} Gx=⎣ ⎡−1−2−1000+1+2+1⎦ ⎤∗ I(水平方向)
G y = [ − 1 − 2 − 1 0 0 0 + 1 + 2 + 1 ] ∗ I (垂直方向) G_y= \begin{bmatrix} -1 & -2 & -1 \\ 0 & 0 & 0 \\ +1 & +2 & +1 \\ \end{bmatrix} *\ I \tag{垂直方向} Gy=⎣ ⎡−10+1−20+2−10+1⎦ ⎤∗ I(垂直方向)
这样的话,我们就得到了两个新的矩阵,分别反映了每一点像素在水平方向上的亮度变化和在垂直方向上的亮度变化情况。
综合考虑这两个方向的变化,我们使用以下公式反映某个像素的梯度变化情况: G = G x 2 + G y 2 G = \sqrt {G_x^2 + G_y^2} G=Gx2+Gy2
有时候为了简单起见,也直接使用绝对值相加替代: G = ∣ G x ∣ + ∣ G y ∣ G = \vert G_x \vert + \vert G_y \vert G=∣Gx∣+∣Gy∣
Sobel()
用法:
cv2.Sobel(src, ddepth, dx, dy, dst, ksize, scale, delta, borderType)
参数说明:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/chess.webp')
# x轴方向,获取的是垂直边缘
dx = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=1)
# y轴方向,获取的是水平边缘
dy = cv2.Sobel(img, cv2.CV_64F, dx=0, dy=1, ksize=1)
# x y 方向合并
dst = cv2.add(dx, dy)
cv2.imshow('img', img)
cv2.imshow('dx dy', np.hstack((dx, dy)))
cv2.imshow('dst', dst)
cv2.waitKey(0)
cv2.destroyAllWindows()
原图:
x,y方向上的图像:
x,y方向上的图像合并之后:
Scharr()
用法:
cv2.Scharr(src, ddepth, dx, dy, dst, scale, delta, borderType)
说明:
G x = [ − 3 0 + 3 − 10 0 + 10 − 3 0 + 3 ] ∗ I (水平方向) G_x= \begin{bmatrix} -3 & 0 & +3 \\ -10 & 0 & +10 \\ -3 & 0 & +3 \\ \end{bmatrix} *\ I \tag{水平方向} Gx=⎣ ⎡−3−10−3000+3+10+3⎦ ⎤∗ I(水平方向)
G y = [ − 3 − 10 − 3 0 0 0 + 3 + 10 + 3 ] ∗ I (垂直方向) G_y= \begin{bmatrix} -3 & -10 & -3 \\ 0 & 0 & 0 \\ +3 & +10 & +3 \\ \end{bmatrix} *\ I \tag{垂直方向} Gy=⎣ ⎡−30+3−100+10−30+3⎦ ⎤∗ I(垂直方向)
注意:Scharr算子
(1) 只支持3 * 3 的kernel,所以没有kernel参数。
(2) 只能求 x 方向或 y 方向的边缘。
(3) ksize 设为 -1 就是 Scharr 算子。
(4) 擅长寻找细小的边缘,一般用的比较少。
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/lena.bmp')
# x轴方向,获取的是垂直边缘
dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0)
# y轴方向,获取的是水平边缘
dy = cv2.Scharr(img, cv2.CV_64F, dx=0, dy=1)
# x y 方向合并
dst = cv2.add(dx, dy)
cv2.imshow('lena', np.hstack((dx, dy, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
索贝尔算子是模拟一阶求导,导数越大的地方说明变换越剧烈,越有可能是边缘。
那如果继续对 f’(t) 求导呢?
可以发现边缘处的二阶导数为0,我们可以利用这一特性去寻找图像的边缘。
注意有一个问题:二阶导数为0的位置也可能是无意义的位置。
拉普拉斯算子推导过程:(以 x 方向求解为例)
这样就得到了拉普拉斯算子的卷积核即卷积模板。
Laplacian()
用法:
cv2.Laplacian(src, ddepth, dst, ksize, scale, delta, borderType)
说明:
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/chess.bmp')
dst = cv2.Laplacian(img, -1, ksize=3)
cv2.imshow('chess', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
Canny边缘检测算法 是 John F. Canny 于1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的最优算法,最优边缘检测的三个主要评价标准是:
1、低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
2、高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
3、最小响应: 图像中的边缘只能标识一次。
Canny边缘检测的一般步骤:
Canny()
用法:
cv2.Canny(image, threshold1, threshold2, edges, apertureSize, L2gradient)
代码实现:
import cv2
import numpy as np
img = cv2.imread('../resource/lena.bmp')
lena1 = cv2.Canny(img, 100, 200)
lena2 = cv2.Canny(img, 64, 128)
lena3 = cv2.Canny(img, 80, 150)
cv2.imshow('lena', np.hstack((lena1, lena2, lena3)))
cv2.waitKey(0)
cv2.destroyAllWindows()