1.图像滤波
图像滤波是指去除图像中不重要的内容,而使关心的内容表现得更加清晰的方法,例如去除图像中的噪声、提取某些信息等。
根据图像滤波的目的不同,可以将图像滤波分为消除图像噪声的滤波和提取图像中部分特征信息的滤波。
去除图像中的噪声称作图像的平滑或者图像去噪。由于噪声信号在图像中主要集中在高频段,因此图像去噪可以看作去除图像中高频段信号的同时保留图像的低频段和中频段信号,此时使用的滤波器就是低通或者高阻滤波器。图像中纹理变化越明显的区域信号频率也就越高,因此使用高通滤波器对图像信号处理可以起到对图像边缘信息提取、增强和图像锐化的作用。
在部分图像处理书籍中,常用图像模糊来替代图像的低通滤波,因为图像的低通滤波在去除图像噪声的同时会将图像的边缘信息弱化,使得整幅图像看起来变得模糊。在低通滤波中,模糊可以与滤波等价,例如图像高斯模糊和图像高斯低通滤波是一个概念。
总之有:
噪声在高频 纹理信息在高频
图像平滑:去噪 -> 低通高阻滤波器 -> 卷积
图像锐化:提取纹理 -> 低阻高通滤波器 -> 微分
本文讲解使用低通滤波对图像进行去噪。
图像的滤波分为线性滤波和非线性滤波,常见的线性滤波包括均值滤波、方框滤波、高斯滤波,常见的非线性滤波主要包括中值滤波、双边滤波。
2.线性滤波
图像的线性滤波与图像的卷积类似,卷积操作中的卷积模版(卷积核)在图像滤波中称为滤波模版、滤波器或者领域算子。滤波器表示中心像素与滤波范围内其他像素之间的线性关系,通过滤波范围内所有像素值之间的线性组合,得到中心位置像素滤波后的像素值,因此这种方式称为线性滤波。
2.1均值滤波
均值滤波将滤波器内所有像素值都看作中心像素的测量,将滤波器内所有的像素值的平均值作为滤波器中心处图像像素值。
均值滤波的优点是,在像素值变换趋势一致的情况下,可以将受噪声影响而突然变化的像素值修正为周围邻近像素值的平均值,去除噪声影响。但是这种方式会缩小像素值之间的差距,使得细节信息变得更加模糊,滤波器范围越大,变模糊越明显。
均值滤波对于去除高斯噪声等均匀分布的噪声效果较好,但在处理图像中的细节和边缘时可能会导致模糊。此外,均值滤波不能有效消除椒盐噪声等离散噪声,因为离群值会对平均值产生较大影响。均值滤波是一种简单但计算量较大的滤波方法,特别是对于较大的滑动窗口。在实际应用中,可能会使用更高级的滤波方法来处理图像噪声。
OpenCV中提供了blur()
函数用于实现图像的均值滤波:
void blur(
InputArray src, // 待均值滤波的图像
OutputArray dst, // 均值滤波后的图像
Size ksize, // 滤波器尺寸
Point anchor = Point(-1,-1), // 内核基准点,默认为滤波器中心点
int borderType = BORDER_DEFAULT // 像素边界外推标志 在
);
输入滤波器的尺寸(3x3或5x5)后,函数会自动确定滤波器:
K
=
1
k
s
i
z
e
.
w
i
d
t
h
×
k
s
i
z
e
.
h
e
i
g
h
t
[
1
1
⋯
1
1
1
1
⋯
1
1
⋮
⋮
⋱
⋮
⋮
1
1
⋯
1
1
1
1
⋯
1
1
]
K = \frac{1}{ksize.width \times ksize.height} \begin{bmatrix} 1 & 1 & \cdots & 1 & 1 \\ 1 & 1 & \cdots & 1 & 1 \\ \vdots & \vdots & \ddots & \vdots & \vdots\\ 1 &1 & \cdots & 1 & 1\\ 1 &1 & \cdots & 1 & 1\\ \end{bmatrix}
K=ksize.width×ksize.height1
11⋮1111⋮11⋯⋯⋱⋯⋯11⋮1111⋮11
下面例子给出了利用不同尺寸的滤波器分别处理不含有噪声的图像、含有椒盐噪声的图像和含有高斯噪声的图像。通过结果可以发现,滤波器的尺寸越大,滤波后图像变得越模糊:
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
Mat equalLena = imread("equalLena.png", IMREAD_ANYDEPTH);
Mat equalLena_gauss = imread("equalLena_gauss.png", IMREAD_ANYDEPTH);
Mat equalLena_salt = imread("equalLena_salt.png", IMREAD_ANYDEPTH);
if (equalLena.empty() || equalLena_gauss.empty() || equalLena_salt.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result_3, result_9; //存放不含噪声滤波结果,后面数字代表滤波器尺寸
Mat result_3gauss, result_9gauss; //存放含有高斯噪声滤波结果,后面数字代表滤波器尺寸
Mat result_3salt, result_9salt; //存放含有椒盐噪声滤波结果,后面数字代表滤波器尺寸
//调用均值滤波函数blur()进行滤波
blur(equalLena, result_3, Size(3, 3));
blur(equalLena, result_9, Size(9, 9));
blur(equalLena_gauss, result_3gauss, Size(3, 3));
blur(equalLena_gauss, result_9gauss, Size(9, 9));
blur(equalLena_salt, result_3salt, Size(3, 3));
blur(equalLena_salt, result_9salt, Size(9, 9));
//显示不含噪声图像
imshow("equalLena ", equalLena);
imshow("result_3", result_3);
imshow("result_9", result_9);
//显示含有高斯噪声图像
imshow("equalLena_gauss", equalLena_gauss);
imshow("result_3gauss", result_3gauss);
imshow("result_9gauss", result_9gauss);
//显示含有椒盐噪声图像
imshow("equalLena_salt", equalLena_salt);
imshow("result_3salt", result_3salt);
imshow("result_9salt", result_9salt);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
2.2方框滤波
方框滤波是均值滤波的一般形式。在均值滤波中,将滤波器中所有的像素值求和后的平均值作为滤波后结果。方框滤波也是求滤波器内所有像素值的和,但是方框滤波可以选择不进行归一化,是将所有像素值的和作为滤波结果,而不是平均值。
OpenCV中提供了 boxFilter()
函数实现方框滤波:
void boxFilter(
InputArray src, // 待滤波图像
OutputArray dst, // 输出图像
int ddepth, // 输出图像数据类型,-1表示自动选择
Size ksize, // 卷积核尺寸
Point anchor = Point(-1,-1), // 内核的基准点
bool normalize = true, // 是否归一化标志,默认进行归一化
int borderType = BORDER_DEFAULT // 像素边界外推标志
);
该函数使用和 blur()
类似,不过可以选择输出图像的数据类型。在不考虑数据类型的情况下,方框滤波函数和均值滤波函数具有相同的滤波效果。
除对滤波器内每个像素值直接求和之外,OpenCV还提供了 sqrtBoxFilter()
函数实现对滤波器内每个像素值的平方求和,之后根据输入参数选择是否进行归一化:
void sqrBoxFilter(
InputArray src, // 参数同上
OutputArray dst,
int ddepth,
Size ksize,
Point anchor = Point(-1, -1),
bool normalize = true,
int borderType = BORDER_DEFAULT );
CV_8U数据类型的图像像素值为 0-255,计算平方后数据会变得更大,即使归一化操作也不能保证像素值不会超过最大值。CV_32F数据类型的图像像素值是 0-1的小数,平方之后会变得更小,但始终保持在0-1.因此该函数处理图像滤波的任务主要针对CV_32F数据类型的图像,在归一化后,图像在变模糊的同时亮度也会变暗。
下面例子给出了利用方框滤波分别处理矩阵数据和图相关的实例:
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
Mat equalLena = imread("equalLena.png", IMREAD_ANYDEPTH); //用于方框滤波的图像
if (equalLena.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
//验证方框滤波算法的数据矩阵
float points[25] = {
1,2,3,4,5,
6,7,8,9,10,
11,12,13,14,15,
16,17,18,19,20,
21,22,23,24,25 };
Mat data(5, 5, CV_32FC1, points);
//将CV_8U类型转换成CV_32F类型
Mat equalLena_32F;
equalLena.convertTo(equalLena_32F, CV_32F, 1.0 / 255);
Mat resultNorm, result, dataSqrNorm, dataSqr, equalLena_32FSqr;
//方框滤波boxFilter()和sqrBoxFilter()
boxFilter(equalLena, resultNorm, -1, Size(3, 3), Point(-1, -1), true); //进行归一化
boxFilter(equalLena, result, -1, Size(3, 3), Point(-1, -1), false); //不进行归一化
sqrBoxFilter(data, dataSqrNorm, -1, Size(3, 3), Point(-1, -1),
true, BORDER_CONSTANT); //进行归一化
sqrBoxFilter(data, dataSqr, -1, Size(3, 3), Point(-1, -1),
false, BORDER_CONSTANT); //不进行归一化
sqrBoxFilter(equalLena_32F, equalLena_32FSqr, -1, Size(3, 3), Point(-1, -1),
true, BORDER_CONSTANT);
cout << "data: " << endl << data << endl;
cout << "dataSqrNorm: " << endl << dataSqrNorm << endl;
cout << "dataSqr: " << endl << dataSqr << endl;
//显示处理结果
imshow("resultNorm", resultNorm);
imshow("result", result);
imshow("equalLena_32FSqr", equalLena_32FSqr);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
/*
data:
[1, 2, 3, 4, 5;
6, 7, 8, 9, 10;
11, 12, 13, 14, 15;
16, 17, 18, 19, 20;
21, 22, 23, 24, 25]
dataSqrNorm:
[10, 18.11111111111111, 24.77777777777778, 32.77777777777778, 24.66666666666666;
39.44444444444444, 66.33333333333333, 81.33333333333333, 98.33333333333333, 71.44444444444444;
99.44444444444444, 161.3333333333333, 186.3333333333333, 213.3333333333333, 151.4444444444444;
192.7777777777778, 306.3333333333333, 341.3333333333333, 378.3333333333333, 264.7777777777778;
163.3333333333333, 258.1111111111111, 284.7777777777778, 312.7777777777778, 218]
dataSqr:
[90, 163, 223, 295, 222;
355, 597, 732, 885, 643;
895, 1452, 1677, 1920, 1363;
1735, 2757, 3072, 3405, 2383;
1470, 2323, 2563, 2815, 1962]
*/
上面的dataSqr中:计算的597
[1, 2, 3
6, 7, 8
11, 12, 13]
平方和为597
上面的dataSqrNorm中:计算的66.33333333333333
597/9=66.33333333333333
2.3高斯滤波
高斯滤波器考虑了像素离滤波器中心距离的影响,以滤波器中心位置为高斯分布的均值,根据高斯分布公式和每个像素离中心位置的距离计算出滤波器内每个位置的数值。
OpenCV中提供了 GaussianBlur()
函数对图像进行高斯滤波:
void GaussianBlur(
InputArray src, // 待高斯滤波图像
OutputArray dst, // 输出图像
Size ksize, // 高斯滤波器的尺寸,可以不为正方形,但必须为正奇数,如果为0那么由标准偏差计算尺寸
double sigmaX, // X方向的高斯滤波器标准偏差
double sigmaY = 0, // Y方向的高斯滤波器标准偏差。如果为0,将其设置为sigmaX,如果两个标准偏差都为0,那么根据输入的ksize计算标准偏差
int borderType = BORDER_DEFAULT
);
该函数能够根据输入的参数自动生成高斯滤波器,实现对图像的高斯滤波。
高斯滤波器的尺寸和标准偏差存在着一定的互相转换关系,OpenCV中提供了输入滤波器单一方向尺寸和标准偏差生成单一方向高斯滤波器的 getGaussianKernel()
函数。
Mat getGaussianKernel(
int ksize, // 高斯滤波器尺寸
double sigma, // 高斯滤波器的标准差
int ktype = CV_64F
);
该函数给出了滤波器尺寸和标准偏差存在的关系,这个关系不是数学中存在的关系,而是OpenCV为了方便自己设定的关系。该函数用于生成指定尺寸的高斯滤波器,生成的是 ksize x 1
的Mat类矩阵。
标准差如果为负数,则使用如下公式计算标准差:
s
i
g
m
a
=
0.3
(
(
k
s
i
z
e
−
1
)
0.5
−
1
+
0.8
)
sigma = 0.3((ksize -1)0.5 -1 + 0.8)
sigma=0.3((ksize−1)0.5−1+0.8)
生成一个二维的高斯滤波需要调用两次 getGaussianKernel()
函数,将 X 方向的一维高斯滤波器和 Y 方向的以为高斯滤波器相乘得到最终的二维高斯滤波器。
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
cv::Mat x = cv::getGaussianKernel(3, -1, CV_32FC1);
cv::Mat y = cv::getGaussianKernel(3, -1, CV_32FC1);
x = x.reshape(1).t();
Mat matrix = x.t() * y;
std::cout << "x * y: " << std::endl << matrix << std::endl;
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
不知道为什么输出的不是一个矩阵?
下面的例子中中利用高斯滤波分别处理不含有噪声的图像、含有椒盐噪声的图像和含有高斯噪声的图像。通过处理结果发现,高斯滤波对高斯噪声去除效果较好,但是同样会对图像造成模糊,并且滤波器的尺寸越大,滤波后图像变得越模糊。
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
Mat equalLena = imread("equalLena.png", IMREAD_ANYDEPTH);
Mat equalLena_gauss = imread("equalLena_gauss.png", IMREAD_ANYDEPTH);
Mat equalLena_salt = imread("equalLena_salt.png", IMREAD_ANYDEPTH);
if (equalLena.empty() || equalLena_gauss.empty() || equalLena_salt.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result_5, result_9; //存放不含噪声滤波结果,后面数字代表滤波器尺寸
Mat result_5gauss, result_9gauss; //存放含有高斯噪声滤波结果,后面数字代表滤波器尺寸
Mat result_5salt, result_9salt; 存放含有椒盐噪声滤波结果,后面数字代表滤波器尺寸
//调用均值滤波函数blur()进行滤波
GaussianBlur(equalLena, result_5, Size(5, 5), 10, 20);
GaussianBlur(equalLena, result_9, Size(9, 9), 10, 20);
GaussianBlur(equalLena_gauss, result_5gauss, Size(5, 5), 10, 20);
GaussianBlur(equalLena_gauss, result_9gauss, Size(9, 9), 10, 20);
GaussianBlur(equalLena_salt, result_5salt, Size(5, 5), 10, 20);
GaussianBlur(equalLena_salt, result_9salt, Size(9, 9), 10, 20);
//显示不含噪声图像
imshow("equalLena ", equalLena);
imshow("result_5", result_5);
imshow("result_9", result_9);
//显示含有高斯噪声图像
imshow("equalLena_gauss", equalLena_gauss);
imshow("result_5gauss", result_5gauss);
imshow("result_9gauss", result_9gauss);
//显示含有椒盐噪声图像
imshow("equalLena_salt", equalLena_salt);
imshow("result_5salt", result_5salt);
imshow("result_9salt", result_9salt);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
2.4可分离滤波
前面介绍的滤波函数使用的滤波器都是固定形式的滤波器,有时需要根据实际需求调整滤波模版。例如,在滤波计算过程中,滤波器中心位置的像素值不参与计算、滤波器中参与计算的像素值不是一个矩形区域等。OpenCV无法根据每种需要求单独编写滤波函数,因此提供了根据自定义滤波器实现图像滤波的函数,即 图像卷积 一文中介绍的卷积函数 filter2D()
,其实应该叫滤波函数更为准确。
无论是图像卷积还是滤波,在原始图像上移动滤波器的过程中每一次的计算结果都不会影响到后面过程的计算结果,因此图像滤波是一个并行算法,在可以提供并行计算的处理器中可以极大地加快图像滤波的处理速度。
此外,图像滤波还具有可分离性,可分离性指的是先对 X(Y) 方向滤波,再对Y(X)方向滤波的结果与将两个方向的滤波器联合后整体滤波的结果相同。两个方向的的滤波器的联合就是将两个方向的滤波器相乘得到一个矩形的滤波器,如 X 方向滤波器为 x = [ x 1 , x 2 , x 3 ] x=[x_1,x_2,x_3] x=[x1,x2,x3] ,Y方向的滤波器为 y = [ y 1 , y 2 , y 3 ] T y = [y_1,y_2,y_3]^T y=[y1,y2,y3]T ,则两个方向的联合滤波器为:
x y = [ y 1 y 2 y 3 ] [ x 1 x 2 x 3 ] = [ x 1 y 1 x 2 y 1 x 3 y 1 x 1 y 2 x 2 y 2 x 3 y 2 x 1 y 3 x 2 y 3 x 3 y 3 ] xy = \begin{bmatrix} y_1 \\y_2 \\y_3 \\ \end{bmatrix} \begin{bmatrix}x_1 & x_2 & x_3 \\\end{bmatrix} =\begin{bmatrix} x_1y_1 & x_2y_1 & x_3y_1 \\ x_1y_2 & x_2y_2 & x_3y_2 \\ x_1y_3 & x_2y_3 & x_3y_3 \\ \end{bmatrix} xy= y1y2y3 [x1x2x3]= x1y1x1y2x1y3x2y1x2y2x2y3x3y1x3y2x3y3
因此,在高斯滤波中,可以利用 getGaussianKernel
函数分别得到 X 方向和 Y 方向滤波器,之后通过生成联合滤波器或者分别用两个方向的滤波器进行滤波,计算结果相同。
两个方向的滤波器可以使用 filter2D
计算滤波结果。OpenCV提供了可以输入两个方向的滤波器实现滤波:
void sepFilter2D(
InputArray src, // 带滤波图像
OutputArray dst, // 输出图像
int ddepth, // 输出图像数据类型
InputArray kernelX, // X方向滤波器
InputArray kernelY, // Y方向滤波器
Point anchor = Point(-1,-1), // 内核基准点
double delta = 0, // 偏值
int borderType = BORDER_DEFAULT // 像素边界外推标志
);
该函数将可分离的线性滤波器分离成 X 方向和 Y 方向进行处理,与 filter2D
函数不同之处在于,filter2D
函数需要通过滤波器的尺寸区分滤波操作是作用在 X 方向还是 Y 方向,例如滤波器尺寸为
K
×
1
K \times 1
K×1 时是Y方向滤波,
1
×
K
1 \times K
1×K 尺寸的滤波器是X方向滤波。而 sepFilter2D
函数通过不同参数区分是作用在 X 方向还是作用在 Y 方向。
下面例子中给出了利用 filter2D
函数依次进行 Y 方向和 X 方向滤波,将结果与两个方向联合滤波器滤波结果相比较,验证两种方式计算结果的一致性。同时,将两个方向的滤波器输入 sepFilter2D
函数汇总,验证该函数计算结果是否与前面的计算结果一致。
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
float points[25] = {
1,2,3,4,5,
6,7,8,9,10,
11,12,13,14,15,
16,17,18,19,20,
21,22,23,24,25 };
Mat data(5, 5, CV_32FC1, points);
//X方向、Y方向和联合滤波器的构建
Mat a = (Mat_<float>(3, 1) << -1, 3, -1);
Mat b = a.reshape(1, 1);
Mat ab = a * b;
//验证高斯滤波的可分离性
Mat gaussX = getGaussianKernel(3, 1);
Mat gaussData, gaussDataXY;
GaussianBlur(data, gaussData, Size(3, 3), 1, 1, BORDER_CONSTANT);
sepFilter2D(data, gaussDataXY, -1, gaussX, gaussX, Point(-1, -1), 0, BORDER_CONSTANT);
//输入两种高斯滤波的计算结果
cout << "gaussData=" << endl
<< gaussData << endl;
cout << "gaussDataXY=" << endl
<< gaussDataXY << endl;
//线性滤波的可分离性
Mat dataYX, dataY, dataXY, dataXY_sep;
filter2D(data, dataY, -1, a, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(dataY, dataYX, -1, b, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(data, dataXY, -1, ab, Point(-1, -1), 0, BORDER_CONSTANT);
sepFilter2D(data, dataXY_sep, -1, b, b, Point(-1, -1), 0, BORDER_CONSTANT);
//输出分离滤波和联合滤波的计算结果
cout << "dataY=" << endl
<< dataY << endl;
cout << "dataYX=" << endl
<< dataYX << endl;
cout << "dataXY=" << endl
<< dataXY << endl;
cout << "dataXY_sep=" << endl
<< dataXY_sep << endl;
//对图像的分离操作
Mat img = imread("lena.png");
if (img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat imgYX, imgY, imgXY;
filter2D(img, imgY, -1, a, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(imgY, imgYX, -1, b, Point(-1, -1), 0, BORDER_CONSTANT);
filter2D(img, imgXY, -1, ab, Point(-1, -1), 0, BORDER_CONSTANT);
imshow("img", img);
imshow("imgY", imgY);
imshow("imgYX", imgYX);
imshow("imgXY", imgXY);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
输出结果:
gaussData=
[1.7207065, 2.822206, 3.5481372, 4.2740688, 3.430702;
4.6296568, 7, 8, 9, 6.9852457;
8.2593136, 12, 13, 14, 10.614902;
11.888971, 17, 18, 19, 14.244559;
10.270683, 14.600147, 15.326078, 16.05201, 11.98068]
gaussDataXY=
[1.7207065, 2.822206, 3.5481372, 4.2740688, 3.430702;
4.6296568, 7, 8, 9, 6.9852457;
8.2593136, 12, 13, 14, 10.614902;
11.888971, 17, 18, 19, 14.244559;
10.270683, 14.600147, 15.326078, 16.05201, 11.98068]
dataY=
[-3, -1, 1, 3, 5;
6, 7, 8, 9, 10;
11, 12, 13, 14, 15;
16, 17, 18, 19, 20;
47, 49, 51, 53, 55]
dataYX=
[-8, -1, 1, 3, 12;
11, 7, 8, 9, 21;
21, 12, 13, 14, 31;
31, 17, 18, 19, 41;
92, 49, 51, 53, 112]
dataXY=
[-8, -1, 1, 3, 12;
11, 7, 8, 9, 21;
21, 12, 13, 14, 31;
31, 17, 18, 19, 41;
92, 49, 51, 53, 112]
dataXY_sep=
[-8, -1, 1, 3, 12;
11, 7, 8, 9, 21;
21, 12, 13, 14, 31;
31, 17, 18, 19, 41;
92, 49, 51, 53, 112]
3.非线性滤波
非线性滤波的滤波结果不是由滤波器内的像素值通过线性组合计算得到,其计算过程可能包含排序、逻辑计算等。
由于线性滤波是通过对所有像素值的线性组合得到滤波后的结果,因此含有噪声的像素点也会被考虑进去,噪声不会被消除,而是以更柔和的形式存在。
常见的非线性滤波有中值滤波和双边滤波。
3.1中值滤波
中值滤波就是用滤波器范围内所有像素值的中值来替代滤波器中心位置像素值的滤波方法,是一种基于排序统计理论的能够有效抑制噪声的非线性信号处理方法。
相比于均值滤波,中值滤波对于脉冲干扰信号和图像扫描噪声的处理效果更佳,同时,在一定条件下,中值滤波对图像的边缘信息保护效果更佳,可以避免图像细节的模糊,但是,当中值滤波尺寸变大之后,同样会产生图像模糊的效果。在处理时间上,中值滤波消耗的时间要远大于均值滤波消耗的时间。
OpenCV提供了 medianBlur
函数对图像进行中值滤波:
void medianBlur(
InputArray src, // 原图
OutputArray dst, // 中值滤波后图像
int ksize // 滤波器尺寸,奇数
);
下面代码对含有椒盐噪声的灰度图像和含有椒盐噪声的彩色图像进行中值滤波:
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
Mat gray = imread("equalLena_salt.png", IMREAD_ANYCOLOR);
Mat img = imread("lena_salt.png", IMREAD_ANYCOLOR);
if (gray.empty() || img.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat imgResult3, grayResult3, imgResult9, grayResult9;
//分别对含有椒盐噪声的彩色和灰度图像进行滤波,滤波模板为3×3
medianBlur(img, imgResult3, 3);
medianBlur(gray, grayResult3, 3);
//加大滤波模板,图像滤波结果会变模糊
medianBlur(img, imgResult9, 9);
medianBlur(gray, grayResult9, 9);
//显示滤波处理结果
imshow("img", img);
imshow("gray", gray);
imshow("imgResult3", imgResult3);
imshow("grayResult3", grayResult3);
imshow("imgResult9", imgResult9);
imshow("grayResult9", grayResult9);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
3.2双边滤波
前面介绍的滤波方法都会对图像造成模糊,使得边缘信息变弱或者消失,因此需要一种能够对图像边缘信息进行保留的滤波算法。
双边滤波是一种综合考虑滤波器内图像空域信息和滤波器内图像像素灰度值相似性的滤波算法,可以实现在保留区域信息的基础上实现对噪声的去除、对局部边缘的平滑。双边滤波对高频率的波动信号起到平滑作用,同时保留大幅值变化的信号波动,进而实现对保留图像中边缘信息的作用。
双边滤波器是两个滤波器的结合,分别考虑空域信息和值域信息,使得滤波器对边缘附近的像素进行滤波时,距离边缘较远的像素值不会对边缘上的像素值影响太多,进而保留边缘的清晰性。
双边滤波原理的数学表达式:
g
(
i
,
j
)
=
∑
k
,
l
f
(
k
,
l
)
ω
(
i
,
j
,
k
,
l
)
∑
k
,
l
ω
(
i
,
j
,
k
,
l
)
g(i,j) = \frac{\sum_{k,l}f(k,l) \omega(i,j,k,l)}{\sum_{k,l}\omega(i,j,k,l)}
g(i,j)=∑k,lω(i,j,k,l)∑k,lf(k,l)ω(i,j,k,l)
其中
ω
(
i
,
j
,
k
,
l
)
\omega(i,j,k,l)
ω(i,j,k,l) 为加权系数,其值取决于空域滤波器和值域滤波器的乘积。
空域滤波器表示如下:
d
(
i
,
j
,
k
,
l
)
=
e
x
p
(
−
(
i
−
k
)
2
+
(
j
−
l
)
2
2
σ
d
2
)
d(i,j,k,l) = exp({-\frac{(i-k)^2 + (j-l)^2}{2 \sigma_d^2}})
d(i,j,k,l)=exp(−2σd2(i−k)2+(j−l)2)
值域滤波器表示如下:
r
(
i
,
j
,
k
,
l
)
=
e
x
p
(
−
∣
∣
f
(
i
,
j
)
−
f
(
k
,
l
)
∣
∣
2
2
σ
d
2
)
r(i,j,k,l) = exp(- \frac{||f(i,j) - f(k,l)||^2}{2 \sigma_d^2})
r(i,j,k,l)=exp(−2σd2∣∣f(i,j)−f(k,l)∣∣2)
两者相乘后产生如下依赖于数据的双边滤波器:
ω
(
i
,
j
,
k
,
l
)
=
e
x
p
(
−
(
i
−
k
)
2
+
(
j
−
l
)
2
2
σ
d
2
−
∣
∣
f
(
i
,
j
)
−
f
(
k
,
l
)
∣
∣
2
2
σ
d
2
)
\omega(i,j,k,l) = exp({-\frac{(i-k)^2 + (j-l)^2}{2 \sigma_d^2} - \frac{||f(i,j) - f(k,l)||^2}{2 \sigma_d^2}})
ω(i,j,k,l)=exp(−2σd2(i−k)2+(j−l)2−2σd2∣∣f(i,j)−f(k,l)∣∣2)
OpenCV提供了 bilateralFilter()
函数对图像进行双边滤波:
void bilateralFilter(
InputArray src, // 单通道或者三通道图像
OutputArray dst, // 双边滤波后的输出图像
int d, // 滤波过程中每个像素邻域的直径。如果为非正数,则有第五个参数计算得到
double sigmaColor, // 颜色空间滤波器的标准差值。这个参数越大,表明该像素邻域内有越多的颜色被混合在一起,产生较大的半相等颜色区域。
double sigmaSpace, // 空间坐标中滤波器的标准差值。这个参数越大,表明越远的像素会互相影响,从而使更大领域内有足够相似的颜色获取相同的颜色。当d大于0时,领域范围由d确定,当d小于等于0时,领域范围由正比于这个参数的数值
int borderType = BORDER_DEFAULT // 像素边界外推标志
);
当滤波器直径大于5时,函数的运行速度会变慢,实时系统可以设为5,离线处理可以设为9。第4、5两个参数,可以将他们设置成相同的值,当他们小于10时,滤波器对图像的滤波作用较弱,当他们大于150时,滤波效果会非常强烈,使图像看起来具有卡通效果。该函数的缺点是运行时间比其他滤波方法要长。
下面给出了使用双边滤波对人脸图像进行滤波:
#include <opencv2/core/utils/logger.hpp>
#include <opencv2/opencv.hpp>
using namespace cv;
using namespace std;
int main()
{
cout << "OpenCV Version: " << CV_VERSION << endl;
utils::logging::setLogLevel(utils::logging::LOG_LEVEL_SILENT);
//读取两张含有人脸的图像
Mat img1 = imread("img1.png", IMREAD_ANYCOLOR);
Mat img2 = imread("img2.png", IMREAD_ANYCOLOR);
if (img1.empty() || img2.empty())
{
cout << "请确认图像文件名称是否正确" << endl;
return -1;
}
Mat result1, result2, result3, result4;
//验证不同滤波器直径的滤波效果
bilateralFilter(img1, result1, 9, 50, 25 / 2);
bilateralFilter(img1, result2, 25, 50, 25 / 2);
//验证不同标准差值的滤波效果
bilateralFilter(img2, result3, 9, 9, 9);
bilateralFilter(img2, result4, 9, 200, 200);
//显示原图
imshow("img1", img1);
imshow("img2", img2);
//不同直径滤波结果
imshow("result1", result1);
imshow("result2", result2);
//不同标准差值滤波结果
imshow("result3 ", result3);
imshow("result4", result4);
int k = waitKey(0); // Wait for a keystroke in the window
return 0;
}
从结果可以看出,滤波直径越大,滤波效果越明显,当滤波直径相同时,标准差值越大,滤波效果越明显。能对人脸起到美颜效果。