每一幅图像都包含某种程度的噪声,噪声可以理解为由一种或者多种原因造成的灰度值的随机变化,如由光子通量的随机性造成的噪声等,在大多数情况下,通过平滑技术(也常称为滤波技术)进行移植或者去除,其中具备保持边缘作用的平滑技术得到了更多的关注。常用的平滑处理算法包括基于二维离散卷积的高斯平滑、均值平滑,基于统计学方法的中值平滑,具备保持边缘作用的平滑算法的双边滤波。
在介绍基于二维离散卷积的平滑算法之前,先来介绍一下二维离散卷积的定义及性质。
二维离散卷积是基于两个矩阵的一种计算方式,通过以下示例进行理解。假设
那么和的二维离散卷积的计算步骤如下:
第一步:将逆时针旋转,即
第二步:沿着按照先行后列的顺序移动,每移动到一个固定位置,对应位置就相乘,然后求和。为了方便演示整个过程,将矩阵和的数值依次放入栅格中,过程如下:
在移动过程中,将对应位置积的和依次存图存入矩阵中,即,该矩阵就是和“full卷积”的结果,用符号表示,记,其中通常称为卷积核,或者卷积掩码,或者卷积算子。
显然,高为、宽为的矩阵与高为、宽为的卷积核的full卷积结果是一个高为,宽为的矩阵,一般。
从full卷积的计算过程可知,如果靠近的边界,那么就会有部分延伸到之外而导致访问到未定义的值,忽略边界,只是考虑能完全覆盖内的值的情况,该过程称为valid卷积。还是上面的示例,满足情况的只有
所以该示例中的与 的valid卷积。
高为、宽为的矩阵与高为、宽为的卷积核的valid卷积结果是一个高为,宽为的矩阵,当然,只有当时才会存在valid卷积。如果存在valid卷积,那么显然是的一部分,用Python语法表示两者的关系如下(后边的程序代码中会用到):
而对于图像处理来说,图像矩阵与卷积核无论是full卷积还是valid卷积,得到的矩阵的尺寸都要么比原图的尺寸大,要么比原图的尺寸小,这都不是我们想要的结果,same卷积就可以解决这个问题。
为了使得到的卷积结果和原图像的高、宽相等,所以通常在计算过程中给指定一个“锚点”,然后将“锚点”循环移至图像矩阵的处,其中,,接下来对应位置的元素逐个相乘,最后对所有所有的积进行求和作为输出图像矩阵在处的输出值。这个卷积过程称为same卷积。还是以上面提到的两个矩阵为例,假设将的左上角即第0行第0列作为锚点的位置,则same卷积的过程如下:
将得到的每一个值按照行列的顺序存入矩阵中,即为same卷积的结果:。显然,same卷积也是full卷积的一部分,假设的锚点的位置在第行第列(注意:这里说的位置是从索引0开始的),用Python语法表示两个矩阵的关系为:
大部分时候,为了更方便地指定卷积核的锚点,通常卷积核的宽、高为奇数,那么可以简单地令中心点为锚点的位置。对于full卷积和same卷积,矩阵边界处的值由于缺乏完整地邻接值,因此卷积运算在这些区域需要特殊处理。方法是进行边界扩充,有如下几种方式:
(1)在矩阵边界外填充常数,通常进行的是0扩充
(2)通过重复边界处的行和列,对输入矩阵进行扩充,使卷积在边界处可计算
(3)卷绕输入矩阵,即矩阵的平铺
(4)以矩阵边界为中心,令矩阵外某位置上未定义的灰度值等于图像内其镜像位置的灰度值,这种处理方式会令结果产生最小程度的干扰
利用上述不同的边界扩充方式得到的same卷积只是在距离上下左右四个边界小于卷积核半径的区域内值会不同,所以只要在运用卷积运算进行图像处理时,图像的重要信息不要落在距离边界小于卷积核半径的区域内就行。最常用的是第四种方式。
OpenCV提供了函数对矩阵边界进行扩充:
dst=cv.copyMakeBorder(src, top, bottom, left, right, borderType[, dst[, value]])
参数 | 解释 |
src | 输入矩阵 |
dst | 输出矩阵:对src边界扩充后的结果 |
top | 上侧扩充的行数 |
bottom | 下侧扩充的行数 |
left | 左侧扩充的行数 |
right | 右侧扩充的行数 |
borderType(边界扩充类型) | BORDER_REPLICATE:边界复制 BORDER_CONSTANT:常数扩充 BORDER_REFLECT:反射扩充 BORDER_REFLECT_101:以边界为中心反射扩充 BORDER_WRAP:平铺扩充 |
value | borderType=BORDER_CONSTANT时的常数 |
注意:函数copyMakeBorder可以对多通道矩阵进行边界扩充。表中borderType的类型还不全,更详细的可以查看官网。
import cv2 as cv
import numpy as np
img = cv.imread('phone.jpg', 0)
src = np.array([[5, 1, 7], [1, 5, 9], [2, 6, 2]])
dst = cv.copyMakeBorder(src, 2, 2, 2, 2, cv.BORDER_REFLECT_101)
print(dst)
下图显示的是设置不同的边界扩充类型后的输出值,其中图(a)采用的是复制边界的方式,图(b)采用的是常数为0的边界扩充方式,图(c)采用的是反射(镜像)扩充方式,图(d)采用的是以边界为轴的反射扩充方式。BORDER_REFLECT_101是默认的扩充方式,也是最理想的一种扩充方式。注意观察BORDER_REFLECT和BORDER_REFLECT_101的细微区别。
对于二维离散卷积的运算,Python的科学计算包Scipy提供了函数实现该功能:
convolve2d(in1, in2, mode="full", boundary="fill", fillvalue=0)
参数 | 解释 |
in1 | 输入的二维数组 |
in2 | 输入的二维数组,代表卷积核 |
mode | 卷积类型:"fulll","valid","same" |
boundary | 边界填充方式:"fill","warp","symm" |
fillvalue | 当boundary="fill"时,设置边界填充的方式,默认为0 |
对于函数convolve2d,当参数mode="same"时,卷积核in2的锚点的位置因为其尺寸不同而不同,假设将他的宽、高分别记为、:
(4)当和均为偶数时,锚点的位置默认中心点
而对于边界扩充,值"fill"等价于函数copyMakeBorder的BORDER_CANSTANT,值"symm"等价于BORDER_REFLECT,值"warp"等价于BORDER_WARP。
使用convolve2d计算任意卷积核且任意指定锚点的same卷积,需要首先计算出full卷积,然后利用same卷积和full卷积的关系,从full卷积中截取就可以了。代码如下:
import cv2 as cv
import numpy as np
from scipy import signal
# 输入矩阵
inp = np.array([[1, 2], [3, 4]], np.float32)
# inp的高和宽
H1, W1 = inp.shape[:2]
# 卷积核
K = np.array([[-1, -2], [2, 1]], np.float32)
# K的高和宽
H2, W2 = K.shape[:2]
# 计算full卷积
c_full = signal.convolve2d(inp, K, mode='full')
# 指定锚点的位置
kr, kc = 0, 0
# 根据锚点的位置,从full卷积中截取得到same卷积
c_same = c_full[H2 - kr - 1:H1 + H2 - kr - 1, W2 - kc - 1:W1 + W2 - kc - 1]
print(c_same)
'''
[[ -5. -6.]
[ 11. 4.]]
'''
对于same卷积的Python实现,也可以使用flip和filter2D这两个函数来代替Scipy中的convolve2d。
import cv2 as cv
import numpy as np
# 输入矩阵
inp = np.array([[1, 2], [3, 4]], np.float32)
# 卷积核
K = np.array([[-1, -2], [2, 1]], np.float32)
# 将卷积核旋转180度
K_flip = cv.flip(K, -1)
# 使用函数filter2D算出same卷积
c_same = cv.filter2D(inp, -1, K_flip,
anchor=(0, 0), borderType=cv.BORDER_CONSTANT)
print(c_same)
'''
[[ -5. -6.]
[ 11. 4.]]
'''
flip函数:dst=cv.flip(src, flipCode[, dst]) 其中src表示原图像矩阵;flipCode可取0,正数,负数,当flipCode=0时,表示src绕x轴翻转;当flipCode=-1(取负数)时,表示src绕y轴反转;当flipCode=1(取正数)时,表示src绕x轴和y轴翻转,即旋转180度。
filter2D函数:dst=cv.filter2D(src, ddepth, kernel[, dst[, anchor[, delta[, borderType]]]])
参数 | 解释 |
src | 输入矩阵 |
dst | 输出矩阵 |
ddepth | 输出矩阵的数据类型(位深) |
kernel | 卷积核,且数据类型为CV_32F/CV_64F |
anchor | 锚点的位置,默认为(-1,-1) 即中心 |
delta | 默认值为0 |
borderType | 边界扩充类型 |
对于该函数需要特别注意的是输入矩阵和输出矩阵的数据类型的对应:
当src.depth()=CV_8U时,ddepth=-1/CV_16S/CV_32F/CV_64F
当src.depth()=CV_16U/CV_16S时,ddepth=-1/CV_32F/CV_64F
当src.depth()=CV_32F时,ddepth=-1/CV_32F/CV_64F
当src.depth()=CV_64F时,ddepth=-1/CV_64F
其中,当参数ddepth=-1时,代表输出矩阵和输入矩阵的数据类型一样,而对于输入的卷积核kernel的数据类型必须是CV_32F或者CV_64F;否则,即使程序不报错,计算出的卷积结果也有可能不正确。
如果一个卷积核至少由两个尺寸比他小的卷积核full卷积而成,并且在计算过程中在所有边界处均进行扩充零的操作,且满足
其中均比的小,,则称该卷积核是可分离的。
在图像处理中经常使用这样的卷积核,它可以分离为一维水平方向和一维垂直方向上的卷积核,例如:
需要注意的是,full卷积是不满足交换律的,但是一维水平方向和一维垂直方向上的卷积核的full卷积是满足交换律的。Python实现如下:
import cv2 as cv
import numpy as np
from scipy import signal
kernel1 = np.array([[1, 2, 3]], np.float32)
kernel2 = np.array([[4], [5], [6]], np.float32)
# 计算两个核的全卷积
kernel = signal.convolve2d(kernel1, kernel2, mode="full")
print(kernel)
'''
[[ 4. 8. 12.]
[ 5. 10. 15.]
[ 6. 12. 18.]]
'''
(1)full卷积的性质
如果卷积核是可分离的,且,则有:
(2)same卷积的性质
对于same卷积。矩阵和卷积核的same卷积,其中的宽为,高为,且和均为奇数,可分离为水平方向上的的卷积核和垂直方向上的的卷积核,即。与full卷积的结合律类似,针对可分离卷积核的same卷积由类似性质:
分离性卷积核的优势:假设图像矩阵的尺寸为高、宽为,卷积核Kernel的尺寸为高,宽,那么进行same卷积的运算量大概为,可以看出卷积运算是非常耗时的,而且随着卷积核尺寸的增大耗时会越来越多。如果可分离为一维水平方向上的的卷积核和一维垂直方向上的的卷积核,则可使卷积运算量减少到,这里就体现出了分离性卷积核的优势。
掌握了二维离散卷积之后,接下来的文章就来介绍常用的基于卷积运算的图像平滑算法------高斯平滑和均值平滑,而其中的高斯卷积核和均值卷积核均是可分离的。