【OpenCV】OpenCV基础教程(4)—— 图像平滑

4. 图像平滑

4.1 二维离散卷积

4.1.1 卷积定义及矩阵形式

1. full卷积

I I I K K K的二维离散卷积的计算步骤如下:

  1. K K K逆时针旋转180°(上下翻转+左右翻转),得到 K flip {{K}_{\text{flip}}} Kflip
  2. K flip {{K}_{\text{flip}}} Kflip沿着 I I I按照先行后列的顺序移动,每移动到一个固定位置,对应位置就相乘,然后求和。

高为 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+H21、宽为 W 1 + W 2 − 1 W_1+W_2-1 W1+W21的矩阵,一般 H 2 ≤ H 1 H_2\le H_1 H2H1 W 2 ≤ W 1 W_2 \le W_1 W2W1

注意:full卷积得到的矩阵尺寸比原图的尺寸大

2. valid卷积

忽略 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 H1H2+1、宽为 W 1 − W 2 + 1 W_1-W_2+1 W1W2+1的矩阵。只有当 H 2 ≤ H 1 H_2\le H_1 H2H1 W 2 ≤ W 1 W_2 \le W_1 W2W1时才存在valid卷积。

注意:valid卷积得到的矩阵尺寸比原图的尺寸小

3. same卷积

在计算过程中给 K flip {{K}_{\text{flip}}} Kflip指定一个“锚点”,然后将“锚点”循环移至图像矩阵的 ( r , c ) (r,c) (r,c)处,其中 0 ≤ r < H 1 0 \le r \lt H_1 0r<H1 0 ≤ c < W 1 0 \le c \lt W_1 0c<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);
4. 边界扩充

对于full卷积和same卷积,矩阵 I I I边界处的值由于缺乏完整的邻接值,因此卷积运算在这些区域需要特殊处理,方法是边界扩充,有如下几种方式:

  1. 常数扩充:在矩阵 I I I边界外填充常数,通常进行的是0扩充;
  2. 边界复制:通过重复 I I I边界处的行和列,对输入矩阵进行扩充,使卷积在边界处可计算;
  3. 平铺扩充:卷绕输入矩阵,即矩阵的平铺;
  4. 以边界为中心的反射扩充:以矩阵边界为中心,令矩阵外某位置上未定义的灰度值等于图像内其镜像位置的灰度值。这种处理方式会令结果产生最小程度的干扰。

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);

对图像进行不同边界扩充后的效果如下:

【OpenCV】OpenCV基础教程(4)—— 图像平滑_第1张图片

4.1.2 可分离卷积核

可分离卷积核:一个卷积核至少由两个尺寸比它小的卷积核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 卷积是满足交换律和结合律的。

4.1.3 离散卷积的性质

在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) Ikernel1kernel2=I(kernel1kernel2)
在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) Ikernel1kernel2I(kernel1kernel2)
上式中出现的所有 same 卷积操作,锚点的位置默认在卷积核的中心。

“≈”两边的same卷积结果只有在四个边界范围内才可能有不同的值,而在“中间部分”的对应位置处的所有值都是相同的。在图像处理中,往往用到的卷积核的尺寸都很小,可以忽略边界处的不同,认为两者的结果是相同的。

等式左边的计算效率比等式右边的要高。

4.2 高斯平滑

高斯卷积核可分离成一维水平方向上的高斯核和一维垂直方向上的高斯核。基于这种分离性,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:边界扩充方式

4.3 均值平滑

对于快速均值平滑,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)代替

4.4 中值平滑

中值滤波最重要的能力是去除椒盐噪声。椒盐噪声是指在图像传输系统中由于解码误差等原因,导致图像中出现孤立的白点或者黑点。

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类型,blurGaussianBlur中的ksize是Size类型,需要区分。

4.5 双边滤波

背景:

高斯平滑均值平滑在去除图像噪声时,会使图像的边缘信息变得模糊。双边滤波导向滤波可以在图像平滑处理的过程中保持边缘。

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.

4.6 联合双边滤波

4.7 导向滤波

你可能感兴趣的:(OpenCV,opencv,计算机视觉,图像处理)