I I I与 K K K的二维离散卷积的计算步骤如下:
高为 H 1 H_1 H1、宽为 W 1 W_1 W1的矩阵 I I I与高为 H 2 H_2 H2、宽为 W 2 W_2 W2的卷积核 K K K的full卷积结果是一个高为 H 1 + H 2 − 1 H_1+H_2-1 H1+H2−1、宽为 W 1 + W 2 − 1 W_1+W_2-1 W1+W2−1的矩阵,一般 H 2 ≤ H 1 H_2\le H_1 H2≤H1, W 2 ≤ W 1 W_2 \le W_1 W2≤W1。
注意:full卷积得到的矩阵尺寸比原图的尺寸大。
忽略 I I I的边界,只是考虑 I I I能完全覆盖 K flip {{K}_{\text{flip}}} Kflip内的值的情况,该过程称为valid卷积。
高为 H 1 H_1 H1、宽为 W 1 W_1 W1的矩阵 I I I与高为 H 2 H_2 H2、宽为 W 2 W_2 W2的卷积核 K K K的full卷积结果是一个高为 H 1 − H 2 + 1 H_1-H_2+1 H1−H2+1、宽为 W 1 − W 2 + 1 W_1-W_2+1 W1−W2+1的矩阵。只有当 H 2 ≤ H 1 H_2\le H_1 H2≤H1, W 2 ≤ W 1 W_2 \le W_1 W2≤W1时才存在valid卷积。
注意:valid卷积得到的矩阵尺寸比原图的尺寸小。
在计算过程中给 K flip {{K}_{\text{flip}}} Kflip指定一个“锚点”,然后将“锚点”循环移至图像矩阵的 ( r , c ) (r,c) (r,c)处,其中 0 ≤ r < H 1 0 \le r \lt H_1 0≤r<H1, 0 ≤ c < W 1 0 \le c \lt W_1 0≤c<W1。
接下来对应位置的元素逐个相乘,最后对所有的积进行求和作为输出图像矩阵在 ( r , c (r,c (r,c)处的输出值。
显然,same卷积也是full卷积的一部分。
OpenCV中并没有直接给出卷积运算的函数,但是可以用其中的两个函数来实现same卷积。
第一步,使用函数flip
使得输入的卷积核逆时针翻转180°
void cv::flip(cv::InputArray src, cv::OutputArray dst, int flipCode)
flipCode = 0:绕x轴翻转
flipCode = 正数(例如1):绕y轴翻转
flipCode = -1:绕原点翻转
第二步,使用函数filter2D
void cv::filter2D(cv::InputArray src, cv::OutputArray dst, int ddepth, cv::InputArray kernel, cv::Point anchor = cv::Point(-1, -1), double delta = (0.0), int borderType = 4)
src:输入矩阵
dst:输出矩阵
ddepth:输出矩阵的数据类型(位深)
kernel:卷积核,数据类型必须为CV_32F或者CV_64F
anchor:锚点的位置
delta:默认值为0.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**;否则,即使程序不报错,计算出的卷积结果也有可能不正确。
为了方便使用这两个函数完成卷积运算,将它们封装在函数 conv2D 中,代码如下:
void conv2D(InputArray src, InputArray kernel, OutputArray dst, int ddepth, Point anchor = Point(-1, -1), int borderType = BORDER_DEFAULT)
{
//卷积运算的第一步:卷积核逆时针翻转180°
Mat kernelFlip;
flip(kernel, kernelFlip, -1);
//卷积运算的第二步
filter2D(src, dst, ddepth, kernelFlip, anchor, 0.0, borderType);
}
通过参数 anchor 指定锚点的位置,当参数 kernel 的宽、高均为奇数且让中心点作为锚点的位置时,也可以使用 Point(−1, −1) 代替 Point((kernel.cols−1) / 2, (kernel.rows−1) / 2)。
自定义函数conv2D
的使用方法如下:
Mat I = (Mat_<float>(2, 2) << 1, 2, 3, 4);
Mat K = (Mat_<float>(2, 2) << -1, -2, 2, 1); //卷积核
Mat c_same;
conv2D(I, K, c_same, CV_32FC1, Point(0, 0),BORDER_CONSTANT);
对于full卷积和same卷积,矩阵 I I I边界处的值由于缺乏完整的邻接值,因此卷积运算在这些区域需要特殊处理,方法是边界扩充,有如下几种方式:
OpenCV提供的边界扩充函数:
void cv::copyMakeBorder(cv::InputArray src, cv::OutputArray dst, int top, int bottom, int left, int right, int borderType, const cv::Scalar &value = cv::Scalar())
src:输入矩阵
dst:输出矩阵(对src边界扩充后的结果)
top:上侧扩充的行数
bottom:下侧扩充的行数
left:左侧扩充的行数
right:右侧扩充的行数
borderType:边界扩充类型
BORDER_REPLICATE:边界复制
BORDER_CONSTANT:常数扩充
BORDER_REFLECT:反射扩充
BORDER_REFLECT_101:以边界为中心反射扩充(默认)
BORDER_WRAP:平铺扩充
value:borderType=BORDER_CONSTANT时的常数
注意:copyMakeBorder
可以对多通道矩阵进行边界扩充,所以参数value是Scalar类。
Mat src = (Mat_<uchar>(3, 3) << 5, 1, 7, 1, 5, 9, 2, 6, 2);
Mat dst;
copyMakeBorder(src, dst, 2, 2, 2, 2, BORDER_REFLECT_101);
对图像进行不同边界扩充后的效果如下:
可分离卷积核:一个卷积核至少由两个尺寸比它小的卷积核full卷积(★表示full卷积)而成,并且在计算过程中在所有边界处均进行扩充零的操作。
例如:
[ 4 8 12 5 10 15 6 12 18 ] = [ 1 2 3 ] ★ [ 4 5 6 ] \left[ \begin{matrix} 4 & 8 & 12 \\ 5 & 10 & 15 \\ 6 & 12 & 18 \\ \end{matrix} \right]=\left[ \begin{matrix} 1 & 2 & 3 \\ \end{matrix} \right]\bigstar \left[ \begin{matrix} 4 \\ 5 \\ 6 \\ \end{matrix} \right] ⎣⎡45681012121518⎦⎤=[123]★⎣⎡456⎦⎤
或者
[ 4 8 12 5 10 15 6 12 18 ] = [ 4 5 6 ] ★ [ 1 2 3 ] \left[ \begin{matrix} 4 & 8 & 12 \\ 5 & 10 & 15 \\ 6 & 12 & 18 \\ \end{matrix} \right]=\left[ \begin{matrix} 4 \\ 5 \\ 6 \\ \end{matrix} \right]\bigstar \left[ \begin{matrix} 1 & 2 & 3 \\ \end{matrix} \right] ⎣⎡45681012121518⎦⎤=⎣⎡456⎦⎤★[123]
注意:full 卷积是不满足交换律的,但是一维水平方向和一维垂直方向上的卷积核的 full 卷积是满足交换律和结合律的。
在full卷积情况下:
I ★ k e r n e l 1 ★ k e r n e l 2 = I ★ ( k e r n e l 1 ★ k e r n e l 2 ) \mathbf{I}\bigstar \mathbf{kerne}{{\mathbf{l}}_{1}}\bigstar \mathbf{kerne}{{\mathbf{l}}_{2}}=\mathbf{I}\bigstar \left( \mathbf{kerne}{{\mathbf{l}}_{1}}\bigstar \mathbf{kerne}{{\mathbf{l}}_{2}} \right) I★kernel1★kernel2=I★(kernel1★kernel2)
在same卷积情况下:
I ★ k e r n e l 1 ★ k e r n e l 2 ≈ I ★ ( k e r n e l 1 ★ k e r n e l 2 ) \mathbf{I}\bigstar \mathbf{kerne}{{\mathbf{l}}_{1}}\bigstar \mathbf{kerne}{{\mathbf{l}}_{2}}\approx \mathbf{I}\bigstar \left( \mathbf{kerne}{{\mathbf{l}}_{1}}\bigstar \mathbf{kerne}{{\mathbf{l}}_{2}} \right) I★kernel1★kernel2≈I★(kernel1★kernel2)
上式中出现的所有 same 卷积操作,锚点的位置默认在卷积核的中心。
“≈”两边的same卷积结果只有在四个边界范围内才可能有不同的值,而在“中间部分”的对应位置处的所有值都是相同的。在图像处理中,往往用到的卷积核的尺寸都很小,可以忽略边界处的不同,认为两者的结果是相同的。
等式左边的计算效率比等式右边的要高。
高斯卷积核可分离成一维水平方向上的高斯核和一维垂直方向上的高斯核。基于这种分离性,OpenCV只给出了构建一维垂直方向上的高斯卷积核的函数:
cv::Mat cv::getGaussianKernel(int ksize, double sigma, int ktype = 6)
ksize:一维垂直方向上高斯核的行数,而且是正奇数
sigma:标准差
ktype:返回值的数据类型为CV_32F或CV_64F,默认是CV_64F
返回值就是一个 ksize × 1 的垂直方向上的高斯核,而对于一维水平方向上的高斯核,只需对垂直方向上的高斯核进行转置就可以了。
由于高斯卷积算子是可分离的,所以真正对图像进行高斯平滑时,可根据 same 卷积的结合律和卷积核的分离性对图像先进行一维水平方向上的高斯平滑,然后再进行一维垂直方向上的高斯平滑,或者反过来,先垂直后水平。
一维高斯卷积核对应的二项式指数如下:
窗口大小 | 二项式系数 |
---|---|
3×3 | 1 2 1 |
5×5 | 1 4 6 4 1 |
7×7 | 1 6 15 20 15 6 1 |
9×9 | 1 8 28 56 70 56 28 8 1 |
OpenCV实现的高斯平滑函数如下:
void cv::GaussianBlur(cv::InputArray src, cv::OutputArray dst, cv::Size ksize, double sigmaX, double sigmaY = (0.0), int borderType = 4)
src:输入矩阵,支持的数据类型为 CV_8U、CV_16U、CV_16S、CV_32F、CV_64F,通道数不限
dst:输出矩阵,大小和数据类型与 src 相同
ksize:高斯卷积核的大小,宽、高均为奇数,且可以不相同
sigmaX:一维水平方向高斯卷积核的标准差
sigmaY:一维垂直方向高斯卷积核的标准差,默认值为 0,表示与 sigmaX 相同
borderType:边界扩充方式
对于快速均值平滑,OpenCV 提供了 boxFilter 和 blur 两个函数来实现该功能,而且这两个函数均可以处理多通道图像矩阵,本质上是对图像的每一个通道分别进行均值平滑。
第一个函数:
void cv::boxFilter(cv::InputArray src, cv::OutputArray dst, int ddepth, cv::Size ksize, cv::Point anchor = cv::Point(-1, -1), bool normalize = true, int borderType = 4)
src:输入矩阵,数据类型为 CV_8U、CV_32F、CV_64F
sum:输出矩阵,其大小和数据类型与 src 相同
ddepth:位深
ksize:平滑窗口的尺寸
normalize:是否归一化
第二个函数(常用):
void cv::blur(cv::InputArray src, cv::OutputArray dst, cv::Size ksize, cv::Point anchor = cv::Point(-1, -1), int borderType = 4)
src:输入矩阵,数据类型为 CV_8U、CV_32F、CV_64F
dst:输出矩阵,其大小和数据类型与 src 相同
ksize:中值算子的尺寸,Size(宽, 高)
anchor:锚点,如果宽、高均为奇数,则 Point(-1,-1) 代表中心点
borderType:边界扩充类型
用法如下:
Mat dst;
blur(I,dst,Size(3,5),Point(1,2)); //Point(1,2) 可以用 Point(-1,-1)代替
中值滤波最重要的能力是去除椒盐噪声。椒盐噪声是指在图像传输系统中由于解码误差等原因,导致图像中出现孤立的白点或者黑点。
OpenCV中所提供的函数如下:
void cv::medianBlur(cv::InputArray src, cv::OutputArray dst, int ksize)
src:输入矩阵
dst:输出矩阵,其大小和数据类型与 src 相同
ksize:若为大于 1 的奇数,则窗口的大小为 ksize × ksize
用法如下:
Mat src = imread(argv[1], CV_LOAD_IMAGE_GRAYSCALE);
Mat dst;
medianBlur(src,dst,5);
注意:medianBlur
中的ksize为int类型,blur
和GaussianBlur
中的ksize是Size类型,需要区分。
背景:
高斯平滑、均值平滑在去除图像噪声时,会使图像的边缘信息变得模糊。双边滤波和导向滤波可以在图像平滑处理的过程中保持边缘。
void cv::bilateralFilter(cv::InputArray src, cv::OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType = 4)
src:输入图像,可以是Mat类型,图像必须是8位或浮点型单通道、三通道的图像。
dst:输出图像,和原图像有相同的尺寸和类型。
d:表示在过滤过程中每个像素邻域的直径范围。如果这个值是非正数,则函数会从第五个参数sigmaSpace计算该值。
sigmaColor:颜色空间过滤器的sigma值,这个参数的值越大,表明该像素邻域内有越宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
sigmaSpace:坐标空间中滤波器的sigma值,如果该值较大,则意味着颜色相近的较远的像素将相互影响,从而使更大的区域中足够相似的颜色获取相同的颜色。当d>0时,d指定了邻域大小且与sigmaSpace五官,否则d正比于sigmaSpace.
borderType:用于推断图像外部像素的某种边界模式,有默认值BORDER_DEFAULT.
略
略