赞
踩
图像卷积就是卷积核(Filter)在图像上按行滑动遍历像素时不断的相乘求和的过程
步长就是卷积核在图像上移动的步幅.上面例子中卷积核每次移动一个像素步长的结果, 如果将这个步长修改为2, 结果会如何?
为了充分扫描图片, 步长一般设为1.
一般规律:当步长为1时,卷积核的大小为F> = 3(奇数),原来图像的大小为N*N,与卷积形成后新的图像大小为n * n。
则n = N - F + 1。
速记:当 F = 3 时,新图像大小为 N - 2;当 F = 5 时,新图像大小为 N - 4;……
从上面例子中我们发现, 卷积之后图片的长宽会变小. 如果要保持图片大小不变, 我们需要在图片周围填充0. padding指的就是填充的0的圈数.
我们可以通过公式计算出需要填充的0的圈数.
如果要保持卷积之后图片大小不变, 可以得出等式: ( N + 2 P − F + 1 ) = N (N + 2P - F + 1) = N (N+2P−F+1)=N从而可以推导出 P = F − 1 2 P = \frac{F -1}{2} P=2F−1
图片卷积中, 卷积核一般为奇数, 比如 3 * 3, 5 * 5, 7 * 7.为什么一般是奇数呢, 出于以下两个方面的考虑:
# OpenCV图像卷积操作 import cv2 import numpy as np #导入图片 img = cv2.imread('./dog.jpeg') # 相当于原始图片中的每个点都被平均了一下, 所以图像变模糊了. kernel = np.ones((5, 5), np.float32) / 25 # ddepth = -1 表示图片的数据类型不变 dst = cv2.filter2D(img, -1, kernel) # 很明显卷积之后的图片模糊了. cv2.imshow('img', np.hstack((img, dst))) cv2.waitKey(0) cv2.destroyAllWindows()
cv2.boxFilter(src, ddepth, ksize[, dst[, anchor[, normalize[, borderType]]]]) 方盒滤波.
方盒滤波的卷积核的形式如下:
不用手动创建卷积核, 只需要告诉方盒滤波, 卷积核的大小是多少.
src 是原图像
ddepth 是卷积之后图片的位深, 即卷积之后图片的数据类型, 一般设为-1, 表示和原图类型一致.即src.shape( )
ksize 是平均卷积核的尺寸,元组(x,y)即代表xy的矩阵,a = 1/xy
dst 是输出图像,与src有同样的大小和类型
anchor 锚点, 即卷积核的中心点, 是可选参数, 默认是(-1,-1)
normalize
borderType 边界类型.一般不设.
一般情况我们都使用normalize = True的情况. 这时 方盒滤波 等价于 均值滤波
# 方盒滤波
import cv2
import numpy as np
img = cv2.imread('dog.jpeg')
# 不用手动创建卷积核, 只需要告诉方盒滤波, 卷积核的大小是多少.
dst = cv2.boxFilter(img, -1, (5, 5), normalize=True)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
调用 blur(src, dst, ksize, anchor, borderType)函数相当于调用 boxFilter(src, dst, src.type(), anchor, true, borderType).
import cv2
import numpy as np
#导入图片
img = cv2.imread('./dog.jpeg')
# kernel = np.ones((5, 5), np.float32) / 25
# 均值滤波没有位深这个参数ddepth
dst = cv2.blur(img, (5, 5))
# 很明显卷积之后的图片模糊了.
cv2.imshow('img', np.hstack((img,dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
为了克服简单局部平均法的弊端(图像模糊),目前已提出许多保持边缘、细节的局部平滑算法。它们的出发点都集中在如何选择邻域的大小、形状和方向、参数加平均及邻域各店的权重系数等。
图像高斯平滑也是邻域平均的思想对图像进行平滑的一种方法,在图像高斯平滑中,对图像进行平均时,不同位置的像素被赋予了不同的权重。高斯平滑与简单平滑不同,它在对邻域内像素进行平均时,给予不同位置的像素不同的权值。
高斯滤波的核心思想是让临近的像素具有更高的重要度. 对周围像素计算加权平均值, 较近的像素具有较大的权重值.能有效去除高斯噪声
要理解高斯滤波首先要知道什么是高斯函数.高斯函数在是符合高斯分布(也叫正态分布)的数据的概率密度函数.画出来长这样子:
高斯函数的特点是以x轴某一点(这一点称为均值)为对称轴, 越靠近中心数据发生的概率越高, 最终形成一个两边平缓, 中间陡峭的钟型(有的地方也叫帽子)图形.
高斯函数的一般形式为:
高斯滤波就是使用符合高斯分布的卷积核对图片进行卷积操作. 所以高斯滤波的重点就是如何计算符合高斯分布的卷积核, 即高斯模板.
假定中心点的坐标是(0,0),那么取距离它最近的8个点坐标,为了计算,需要设定σ的值。假定σ=1.5,则模糊半径为1的高斯模板就算如下:
我们可以观察到越靠近中心, 数值越大, 越边缘的数值越小.符合高斯分布的特点.
通过高斯函数计算出来的是概率密度函数, 所以我们还要确保这九个点加起来为1,这9个点的权重总和等于0.4787147,因此上面9个值还要分别除以0.4787147,得到最终的高斯模板。
注: 有些整数高斯模板是在归一化后的高斯模板的基础上每个数除上左上角的值, 然后取整.
有了卷积核, 计算高斯滤波就简单了.假设现有9个像素点,灰度值(0-255)的高斯滤波计算如下:
将这9个值加起来,就是中心点的高斯滤波的值。对所有点重复这个过程,就得到了高斯模糊后的图像。
cv2.GaussianBlur(src, ksize, sigmaX[, dst[, sigmaY[, borderType]]])
相对均值滤波而言,均值滤波的平滑力度会更大,但高斯滤波保留细节的能力相对会更好一点,并且高斯滤波可以通过加权值的改变,调节其性能。
选择不同的sigma值会得到不同的平滑效果, sigma越大, 平滑效果越明显.
没有指定sigma时, ksize越大, 平滑效果越明显
高斯滤波实战
# 高斯滤波
import cv2
import numpy as np
#导入图片
img = cv2.imread('./gaussian.png')
dst = cv2.GaussianBlur(img, (5, 5), sigmaX=1)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
中值滤波原理非常简单, 假设有一个数组[1556789], 取其中的中间值(即中位数)作为卷积后的结果值即可.
前面的滤波器都是用计算得到的一个新值来取代中 心像素的值,而中值滤波是用中心像素周围(也可以使他本身)的值来取代他。
中值滤波处理椒盐滤波效果明显,处理高斯噪声效果较差,与均值滤波相反.
cv2.medianBlur(src.ksize[, dst])
src 是原图像
ksize 是孔径大小,必须是奇数且大于1,例如3,5,7…(可理解成卷积核大小)
dst 是输出图像,与src有相同的大小和类型
# 中值滤波
import cv2
import numpy as np
#导入图片
img = cv2.imread('./papper.png')
# 注意这里的ksize就是一个数字
dst = cv2.medianBlur(img, 5)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
双边滤波对于图像的边缘信息能过更好的保存。其原理为一个与空间距离相关的高斯函数与一个灰度距离相关的高斯函数相乘。
双边滤波在保持边缘清晰的同时,对噪声去除非常有效。但与其他过滤器相比,该操作速度较慢。
双边滤波本质上是高斯滤波, 双边滤波和高斯滤波不同的就是:双边滤波既利用了位置信息又利用了像素信息来定义滤波窗口的权重。而高斯滤波只用了位置信息.
对于高斯滤波,仅用空间距离的权值系数核与图像卷积后,确定中心点的灰度值。即认为离中心点越近的点,其权重系数越大。
双边滤波中加入了对灰度信息的权重,即在邻域内,灰度值越接近中心点灰度值的点的权重更大,灰度值相差大的点权重越小。此权重大小,则由值域高斯函数确定。
两者权重系数相乘,得到最终的卷积模板。由于双边滤波需要每个中心点邻域的灰度信息来确定其系数,所以其速度与比一般的滤波慢很多,而且计算量增长速度为核大小的平方。
双边滤波可以保留边缘, 同时可以对边缘内的区域进行平滑处理.
双边滤波的作用就相当于做了美颜.
双边滤波对椒盐噪声几乎没效果。
cv2.bilateralFilter(src, d, sigmaColor, sigmaSpace[, dst[, borderType]])
# 中值滤波
import cv2
import numpy as np
#导入图片
img = cv2.imread('./lena.png')
dst = cv2.bilateralFilter(img, 7, 20, 50)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
边缘是像素值发生跃迁的位置,是图像的显著特征之一,在图像特征提取,对象检测,模式识别等方面都有重要的作用。
人眼如何识别图像边缘?
比如有一幅图,图里面有一条线,左边很亮,右边很暗,那人眼就很容易识别这条线作为边缘.也就是像素的灰度值快速变化的地方.
sobel算子对图像求一阶导数。一阶导数越大,说明像素在该方向的变化越大,边缘信号越强。
因为图像的灰度值都是离散的数字, sobel算子采用离散差分算子计算图像像素点亮度值的近似梯度.
图像是二维的,即沿着宽度/高度两个方向.
我们使用两个卷积核对原图像进行处理:
这样的话,我们就得到了两个新的矩阵,分别反映了每一点像素在水平方向上的亮度变化情况和在垂直方向上的亮度变换情况.
综合考虑这两个方向的变化,我们使用以下公式反映某个像素的梯度变化情况.
G = G x 2 + G y 2 G= \sqrt{G^2_x+G^2_y} G=Gx2+Gy2
有时候为了简单起见,也直接用绝对值相加替代. G = ∣ G X ∣ + ∣ G Y ∣ G = |G_X| + |G_Y| G=∣GX∣+∣GY∣
cv2.Sobel(src, ddepth, dx, dy, [, dst[, ksize[, scale[, delta[, borderType]]]]])
src 是原图像
ddepth 是位深,一般是写cv2.CV_64F,也可以写-1
dx x方向求导阶数,所以可以使dx=1,dy=0实现x方向求导
dy y方向求导阶数,所以可以使dx=0,dy=1实现y方向求导
dst 是输出图像
ksize 是卷积核大小,必须取正奇数,即1、3、5…;若取了-1就是Scharr算子了
scale 是缩放大小,默认为1
delta 是增量数值(额外数值),默认为0
borderType 是边界类型
import cv2 import numpy as np img = cv2.imread('./chess.png') # 注意sobel算子要分别计算x,y的梯度 # 计算x轴方向的梯度, 只有垂直方向的边缘 dx = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=0, ksize=5) # 计算y轴方向的梯度, 只有水平方向的边缘 dy = cv2.Sobel(img, cv2.CV_64F, dx=0, dy=1, ksize=5) # sobel算子必须分开计算x,y轴, 不然话效果很差. # dst = cv2.Sobel(img, cv2.CV_64F, dx=1, dy=1, ksize=3) # 使用sobel算子, 别忘了把x,y的梯度合并在一起. dst = cv2.add(dx, dy) # 使用addWeighted也可以 # dst = cv2.addWeighted(dx, 0.5, dy, 0.5, gamma=0) cv2.imshow('chess', img) cv2.imshow('img', np.hstack((dx, dy))) cv2.imshow('dst', dst) cv2.waitKey(0) cv2.destroyAllWindows()
Sobel算子检测方法对灰度渐变和噪声较多的图像处理效果较好,Sobel算子对边缘定位不是很准确,图像的边缘不止一个像素,而且必须分开算x轴,y轴,不然效果很差;当对精度要求不是很高时,是一种较为常用的边缘检测方法。
cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])
当内核大小为 3 时, 以上Sobel内核可能产生比较明显的误差(毕竟,Sobel算子只是求取了导数的近似值)。 为解决这一问题,OpenCV提供了 Scharr函数,但该函数仅作用于大小为3的内核。该函数的运算与Sobel函数一样快,但结果却更加精确(原理是Scharr使用小的卷积核求解梯度角度时的优化).
Scharr算子和Sobel很类似, 只不过使用不同的kernel(卷积核)值, 放大了像素变换的情况(是对sobel算子的优化,有效解决sobel算子不能处理好边缘像素的问题):
Scharr算子只支持3 * 3 的kernel所以没有kernel参数了.
Scharr算子只能求x方向或y方向的边缘.
Sobel算子的ksize设为-1就是Scharr算子.
Scharr擅长寻找细小的边缘, 一般用的较少.
效果比3*3的Sobel算子好,而且速度相同,所以在使用 3x3 滤波器时应该尽量使用 Scharr 滤波器
cv2.Scharr(src, ddepth, dx, dy[, dst[, scale[, delta[, borderType]]]])
src 是原图像
ddepth 是位深,一般是写cv2.CV_64F,也可以写-1
dx x方向求导阶数,所以可以使dx=1,dy=0实现x方向求导
dy y方向求导阶数,所以可以使dx=0,dy=1实现y方向求导
dst 是输出图像
scale 是缩放大小,默认为1
delta 是增量数值(额外数值),默认为0
borderType 是边界类型
import cv2 import numpy as np img = cv2.imread('./lena.png') # 计算x轴方向的梯度, 只有垂直方向的边缘 dx = cv2.Scharr(img, cv2.CV_64F, dx=1, dy=0) # 计算y轴方向的梯度, 只有水平方向的边缘 dy = cv2.Scharr(img, cv2.CV_64F, dx=0, dy=1) # 使用Scharr算子, 别忘了把x,y的梯度合并在一起. dst = cv2.add(dx, dy) # 使用addWeighted也可以 # dst = cv2.addWeighted(dx, 0.5, dy, 0.5, gamma=0) cv2.imshow('img', np.hstack((dx, dy, dst))) cv2.waitKey(0) cv2.destroyAllWindows()
索贝尔算子是模拟一阶求导,导数越大的地方说明变换越剧烈,越有可能是边缘.
那如果继续对f’(t)求导呢?
可以发现"边缘处"的二阶导数=0, 我们可以利用这一特性去寻找图像的边缘. 注意有一个问题,二阶求导为0的位置也可能是无意义的位置,该位置很可能就是噪声,这时有可能需要用高斯滤波进行降噪.
拉普拉斯算子推导过程
以x方向求解为例:
一阶差分:
f
′
(
x
)
=
f
(
x
)
−
f
(
x
−
1
)
f'(x) = f(x) - f(x - 1)
f′(x)=f(x)−f(x−1)
二阶差分:
f
′
′
(
x
)
=
f
′
(
x
+
1
)
−
f
′
(
x
)
=
(
f
(
x
+
1
)
−
f
(
x
)
)
−
(
f
(
x
)
−
f
(
x
−
1
)
)
f''(x) = f'(x+1) - f'(x) = (f(x + 1) - f(x)) - (f(x) - f(x - 1))
f′′(x)=f′(x+1)−f′(x)=(f(x+1)−f(x))−(f(x)−f(x−1))
化简后:
f
′
′
(
x
)
=
f
(
x
−
1
)
−
2
f
(
x
)
+
f
(
x
+
1
)
f''(x) = f(x - 1) - 2 f(x) + f(x + 1)
f′′(x)=f(x−1)−2f(x)+f(x+1)
同理可得: f ′ ′ ( y ) = f ( y − 1 ) − 2 f ( y ) + f ( y + 1 ) f''(y) = f(y - 1) - 2 f(y) + f(y + 1) f′′(y)=f(y−1)−2f(y)+f(y+1)
把x,y方向的梯度叠加在一起.
f ′ ′ ( x , y ) = f x ′ ( x , y ) + f y ′ ( x , y ) f''(x,y) = f'_x(x,y) + f'_y(x,y) f′′(x,y)=fx′(x,y)+fy′(x,y)
f ′ ′ ( x , y ) = f ( x − 1 , y ) − 2 f ( x , y ) ) + f ( x + 1 , y ) + f ( x , y − 1 ) − 2 f ( x , y ) ) + f ( x , y + 1 ) f''(x,y) = f(x - 1, y) - 2 f(x,y)) + f(x + 1, y) + f(x, y - 1) - 2 f(x,y)) + f(x,y + 1) f′′(x,y)=f(x−1,y)−2f(x,y))+f(x+1,y)+f(x,y−1)−2f(x,y))+f(x,y+1)
$f’'(x,y) = f(x - 1, y) + f(x + 1, y) + f(x, y - 1) + f(x,y + 1) - 4 f(x,y)) $
这个等式可以用矩阵写成:
f
′
′
(
x
,
y
)
=
[
0
1
0
1
−
4
1
0
1
0
]
⨀
[
f
(
x
−
1
,
y
−
1
)
f
(
x
,
y
−
1
)
f
(
x
+
1
,
y
−
1
)
f
(
x
−
1
,
y
)
f
(
x
,
y
)
f
(
x
+
1
,
y
)
f
(
x
−
1
,
y
+
1
)
f
(
x
,
y
+
1
)
f
(
x
+
1
,
y
+
1
)
]
f''(x,y) = \left[
这样就得到了拉普拉斯算子的卷积核即卷积模板.
该运算符是每一行的对应元素的线性运算。例如第一行是0 * f(x-1, y-1) + 1 * f(x,y-1) + 0 * f(x+1,y-1)
cv2.Laplacian(src, ddepth[, dst[, ksize[, scale[, delta[, borderType]]]]])
src 时原图像
ddepth 是位深,一般设为-1或者cv2.CV_64F
dst 是输出图像
ksize 是卷积核大小,必须取奇数
scale 是缩放大小,默认为1
delta 是增量数值(额外数值),默认为0
borderType 是边界类型
可以同时求两个方向的边缘,不像Sobel和Scharr算子那样必须分别在x轴,y轴求导再相加。
对噪音敏感, 一般需要先进行去噪(高斯滤波)再调用拉普拉斯
import cv2
import numpy as np
img = cv2.imread('./chess.png')
dst = cv2.Laplacian(img, -1, ksize=3)
cv2.imshow('img', np.hstack((img, dst)))
cv2.waitKey(0)
cv2.destroyAllWindows()
Canny 边缘检测算法 是 John F. Canny 于 1986年开发出来的一个多级边缘检测算法,也被很多人认为是边缘检测的 最优算法, 最优边缘检测的三个主要评价标准是:
- 低错误率: 标识出尽可能多的实际边缘,同时尽可能的减少噪声产生的误报。
- 高定位性: 标识出的边缘要与图像中的实际边缘尽可能接近。
- 最小响应: 图像中的边缘只能标识一次。
Canny边缘检测的一般步骤
去噪. 边缘检测容易受到噪声影响, 在进行边缘检测前通常需要先进行去噪, 一般用5*5高斯滤波去除噪声.
计算梯度: 对平滑后的图像采用sobel算子计算梯度和方向.
G = G x 2 + G y 2 G = \sqrt{G_x^2+G_y^2} G=Gx2+Gy2 为了方便一般可以改用绝对值
θ = a r c t a n ( G y G x ) \theta = arctan(\frac{G_y}{G_x}) θ=arctan(GxGy)
梯度的方向总是垂直于边缘,它是四角之一,被归为四类: 垂直, 水平和两个对角线.
计算出来的梯度和方向大概如下图:
非极大值抑制(NMS)
在获取了梯度和方向后, 遍历图像, 去除所有不是边界的点.(梯度大的地方很可能是边缘)
实现方法: 逐个遍历像素点, 判断当前像素点是否是周围像素点中具有相同方向梯度的最大值.
下图中, 点A,B,C具有相同的方向, 梯度方向垂直于边缘.
点A在边缘上(垂直方向)。梯度方向垂直于边缘。点B和点C在梯度方向上,因此,点A与点B和点C进行检查,看它是否形成局部最大值,如果是,则将其考虑到下一阶段,否则将抑制它(使其为零)。
更形象的例子:
滞后阈值(解决误判的情况)
现在要确定那些边界是真正的边界,因此设定两个阈值maxVal和minVal。大于maxVal的梯度(肯定是边界)保留,小于minVal的梯度(肯定是非边界)被抛弃。在minVal和maxVal之间的梯度,若和大于maxVal的梯度相连的梯度(也可认为是边界点)保留,其他的被抛弃。
A 高于阈值 maxVal 所以是真正的边界点,D 低于阈值minVal 所以被抛弃。C虽然低于 maxVal 但高于 minVal 并且与 A 相连,所以也被认为是真正的边界点。而 B 就会被抛弃,因为他不仅低于 maxVal 而且不与真正的边界点相连。所以选择合适的 maxVal 和 minVal 对于能否得到好的结果非常重要。 在这一步一些小的噪声点也会被除去,因为我们假设边界都是一些长的线段。
cv2.Canny(image, threshold1, threshold2[, edges[, apertureSize[, L2gradient]]])
image 是原图像
threshold1 是minVal
threshold2 是maxVal
edges 是输出的边缘图,需要和原图片有一样的尺寸和类型
apertureSize 是Sobel算子的大小(可选参数),默认值是3
L2gradient 是一个计算图像幅值的标识,默认值为False。如果为True,则使用更精确的L2范数进行计算(即两个方向的倒数的平方和再开方),否则使用L1范数(直接将两个方向导数的绝对值相加)。
threshold2用于检测图像中明显的边缘,但一般情况下检测的效果不会那么完美,边缘检测出来是断断续续的。所以这时候用threshold1用于将这些间断的边缘连接起来(推荐的高低阈值比为2:1到3:1之间)。函数返回的是二值图,包含检测出的边缘
import cv2 import numpy as np # 导入图片 img = cv2.imread('./lena.png') # 阈值给的稍大一些, 绘制的边缘不够精细 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()
总结:
滤波一般是用来去除噪声(降噪)和模糊图像;算子一般用来找边界,识别图像边缘。
低通滤波器(LPF):方盒滤波、均值滤波、高斯滤波、中值滤波、双边滤波
高通滤波器(HPF):Sobel算子、Scharr算子、拉普拉斯算子、边缘检测Canny
附OpenCV目录:OpenCV总目录学习笔记
智科专业小白,写博文不容易,如果喜欢的话可以点个赞哦!
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。