参考文章:OpenCV基础应用2.图像滤波
一、图像滤波
图像滤波,即在尽量保留图像细节特征的条件下对目标图像的噪声进行抑制,是图像预处理中不可缺少的操作,其处理效果的好坏将直接影响到后续图像处理和分析的有效性和可靠性。
图像滤波既可以在时域进行,也可以在频域进行。图像滤波可以更改或者增强图像。通过滤波,可以强调一些特征或者去除图像中一些不需要的部分。滤波是一个邻域操作算子,利用给定像素周围的像素的值决定此像素的最终的输出值。
1.1 均值滤波
1.1.1 均值滤波核
用周围像素的平均值代替原像素值,均值滤波的核定义为:
1 ksize.width ∗ ksize.height [ 1 1 ⋯ 1 1 1 ⋯ 1 ⋮ ⋮ ⋱ ⋮ 1 1 ⋯ 1 ] \frac{1}{\text{ksize.width} * \text{ksize.height}} \begin{bmatrix} 1 & 1 & \cdots & 1 \\ 1 & 1 & \cdots & 1 \\ \vdots & \vdots & \ddots & \vdots \\ 1 & 1 & \cdots & 1 \\ \end{bmatrix} ksize.width∗ksize.height1 11⋮111⋮1⋯⋯⋱⋯11⋮1
对于图像中的像素,滤波后的像素为:
g ( x , y ) = 1 M ∑ f ∈ S f ( x , y ) g(x,y) = \frac{1}{M} \sum_{f \in S} f(x,y) g(x,y)=M1f∈S∑f(x,y)
计算示例如下图:边框保留不变,遍历图像所有像素,计算像素周围像素的均值并替代值。

均值滤波不能保护图像的细节,在图像去燥的同时也破坏了图像的细节部分。
1.1.2 OpenCV实现
blur()函数实现了均值滤波。该函数定义如下:
cpp
void blur( InputArray src, OutputArray dst,
Size ksize, Point anchor=Point(-1,-1),
int borderType=BORDER_DEFAULT );
参数说明:
- src: 输入的图像。
- dst: 输出处理后的图像。
- ksize:核大小。
- anchor:表示锚点,即被平滑的点。如果是负值则表示取核的中心为锚点。
- borderType:处理边界的模式,BORDER_DEFAULT为无处理。
代码处理演示:
cpp
#include <iostream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
using namespace std;
using namespace cv;
#define IMAGE_PATH "../ImageSet/wgj_5.jpg"
void AverageFilter(Mat img)
{
Mat dstImage;
Mat img_resize;
resize(img, img_resize, cv::Size(img.cols/4, img.rows/4),
0.0,0.0, cv::INTER_LINEAR);
imshow("Org Image", img_resize);
blur(img_resize, dstImage, Size(5,5));
imshow("Average Filter", dstImage);
}
int main()
{
Mat img = imread(IMAGE_PATH);
if(img.empty())
{
cout<<"Read image failed"<<endl;
return 0;
}
AverageFilter(img);
waitKey(0);
}
对比结果如下:

核越大,处理后的图像越模糊。
1.2 中值滤波
1.2.1 中值滤波核
中值滤波是一种非线性滤波器,以3*3大小的核为中值滤波核,则取9个像素的中值像素替换原像素。核定义如下:
\\text{mid} \\begin{bmatrix} 1 \& 1 \& \\cdots \& 1 \\ 1 \& 1 \& \\cdots \& 1 \\ \\vdots \& \\vdots \& \\ddots \& \\vdots \\ 1 \& 1 \& \\cdots \& 1 \\ \\end{bmatrix}
计算方法如下:
g ( x , y ) = median { 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 ) } g(x,y) = \text{median} \left\{ \begin{aligned} &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) \end{aligned} \right\} g(x,y)=median⎩ ⎨ ⎧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)⎭ ⎬ ⎫
计算示例如下图所示:

中值滤波对脉冲噪声有良好的滤除作用,可以保留图像的边缘信息。
1.2.2 OpenCV实现
OpenCV将均值滤波封装在medianBlur()函数中,定义如下:
cpp
void medianBlur( InputArray src, OutputArray dst, int ksize );
- 参数src:输入原始图像。
- 参数dst:输出处理后的图像。
- 参数ksize:中值滤波核的大小,必须设置为奇数。
代码处理演示:
cpp
#include <iostream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
using namespace std;
using namespace cv;
#define IMAGE_PATH "../ImageSet/wgj_3.jpg"
void MedianFilter(Mat img)
{
Mat dstImage;
medianBlur(img, dstImage, 3);
imshow("medianFilter", dstImage);
}
int main()
{
Mat img = imread(IMAGE_PATH);
if(img.empty())
{
cout<<"Read image failed"<<endl;
return 0;
}
Mat img_resize;
resize(img, img_resize, cv::Size(img.cols/4, img.rows/4),
0.0,0.0, cv::INTER_LINEAR);
imshow("Raw Image", img_resize);
MedianFilter(img_resize);
waitKey(0);
}
处理效果如下图,可以发现一些小颗粒都会被滤除,但线条不会模糊。

1.3 高斯滤波
高斯滤波是一种线性滤波器,能够有效抑制噪声,平滑图像。其作用原理和均值滤波类似,都是取滤波器窗口内的像素均值作为输出,只是窗口模板的系数不同,均值滤波模块系数全部为1,而高斯滤波器的系数随着距离模板中心的增大而减小。所以高斯滤波器相比于均值滤波器对图像的模糊程度较小。
1.3.1 高斯分布
一维的高斯分布定义如下:

二维的高斯分布:

对于窗口大小为(2k+1)*(2k+1)的模板,高斯核计算公式如下:
H i , j = 1 2 π σ 2 e − ( i − k − 1 ) 2 + ( j − k − 1 ) 2 2 σ 2 H_{i,j} = \frac{1}{2\pi\sigma^2} e^{-\frac{(i - k - 1)^2 + (j - k - 1)^2}{2\sigma^2}} Hi,j=2πσ21e−2σ2(i−k−1)2+(j−k−1)2
高斯核有小数形式和整数形式,如果是整数形式则需对其归一化处理,将左上角的数值归一化为1,例如33和55的高斯滤波器核:

1.3.2 OpenCV实现
OpenCV将高斯滤波封装在GaussianBlur()函数中,定义如下:
cpp
void GaussianBlur(InputArray src,OutputArray dst, Size ksize,
double sigmaX, double sigmaY = 0,
int borderType = BORDER_DEFAULT);
- 参数src:输入图像。
- 参数dst:输出图像。
- 参数ksize:高斯核大小,一般用Size(w,h)表示核的大小,w为像素宽度,h为像素高度,w与h可以不同,但必须是奇数或者0。
- 参数sigmaX:表示高斯核函数在X方向的标准差。
- 参数sigmaY:表示高斯核函数在Y方向的标准差。
- 参数borderType:用于推断图像外部像素的某种边界模式,一般忽略。
代码实现:
cpp
#include <iostream>
#include <vector>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
using namespace std;
using namespace cv;
#define IMAGE_PATH "../ImageSet/wgj_4.png"
void Gaussian(Mat img)
{
Mat dstImage;
Mat img_ori;
img_ori = img(Rect(0,0, img.cols, img.rows/2));
GaussianBlur(img_ori, dstImage, cv::Size(5,5), 0,0);
dstImage.copyTo(img_ori);
imshow("GaussianFilter", img);
}
int main()
{
Mat img = imread(IMAGE_PATH);
if(img.empty())
{
cout<<"Read image failed"<<endl;
return 0;
}
Mat img_resize;
resize(img, img_resize, cv::Size(img.cols/4, img.rows/4),
0.0,0.0, cv::INTER_LINEAR);
imshow("Raw Image", img_resize);
Gaussian(img_resize);
waitKey(0);
}
处理效果如下图:图像上半部分模糊处理了,下半部分未处理。

1.4 双边滤波
双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折衷处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。
1.4.1 算法原理
双边滤波的改进就在于在采样时不仅考虑像素在空间距离上的关系,同时加入了像素间的相似程度考虑。双边滤波器比高斯滤波多了一个高斯方差sigma-d,它是基于空间分布的高斯滤波函数,所以在边缘附近,离的较远的像素不会太多影响到边缘上的像素值,这样就保证了边缘附近像素值的保存。但是由于保存了过多的高频信息,对于彩色图像里的高频噪声,双边滤波器不能够干净的滤掉,只能够对于低频信息进行较好的滤波。对于脉冲噪声,双边滤波会把它当成边缘从而不能去除。
双边滤波器输出像素的值依赖于领域像素值的加权值组合,定义如下:
H i , j = 1 2 π σ 2 e − ( i − k − 1 ) 2 + ( j − k − 1 ) 2 2 σ 2 H_{i,j} = \frac{1}{2\pi\sigma^2} e^{-\frac{(i - k - 1)^2 + (j - k - 1)^2}{2\sigma^2}} Hi,j=2πσ21e−2σ2(i−k−1)2+(j−k−1)2
加权系数w(i, j, k, l)取决于定义域核和值域核的乘积。其中定义域核表示如下:
d ( i , j , k , l ) = exp ( − ( i − k ) 2 + ( j − l ) 2 2 σ d 2 ) d(i,j,k,l) = \exp\left(-\frac{(i - k)^2 + (j - l)^2}{2\sigma_d^2}\right) d(i,j,k,l)=exp(−2σd2(i−k)2+(j−l)2)
定义域核模板如图所示:

值域核定义:
d ( i , j , k , l ) = exp ( − ( i − k ) 2 + ( j − l ) 2 2 σ d 2 ) d(i,j,k,l) = \exp\left(-\frac{(i - k)^2 + (j - l)^2}{2\sigma_d^2}\right) d(i,j,k,l)=exp(−2σd2(i−k)2+(j−l)2)
值域核模板:

则它们的乘积为:
w ( i , j , k , l ) = exp ( − ( i − k ) 2 + ( j − l ) 2 2 σ d 2 − ∥ f ( i , j ) − f ( k , l ) ∥ 2 2 σ r 2 ) w(i,j,k,l) = \exp\left(-\frac{(i - k)^2 + (j - l)^2}{2\sigma_d^2} - \frac{\|f(i,j) - f(k,l)\|^2}{2\sigma_r^2}\right) w(i,j,k,l)=exp(−2σd2(i−k)2+(j−l)2−2σr2∥f(i,j)−f(k,l)∥2)
1.4.2 OpenCV实现
OpenCV将双边滤波封装在bilateralFilter()函数中,定义如下:
cpp
void bilateralFilter( InputArray src, OutputArray dst, int d,
double sigmaColor, double sigmaSpace,
int borderType=BORDER_DEFAULT );
- 参数src:输入图像。
- 参数dst:输出图像。
- 参数d:表示在滤波过程中每个像素邻域的直径。
- 参数sigmaColor:表示颜色空间滤波器的sigma值,这个参数的值越大,就表明该像素邻域内有更宽广的颜色会被混合到一起,产生较大的半相等颜色区域。
- 参数sigmaSpace:坐标空间的标准差。它的数值越大,意味着越远的像素会相互影响,从而使更大的区域足够相似的颜色获取相同的颜色。当d>0,d指定了邻域大小且与sigmaSpace无关。否则,d正比于sigmaSpace。
- 参数borderType:处理边界像素的模式。
代码实现:
cpp
void bilateralFilter( InputArray src, OutputArray dst, int d,
double sigmaColor, double sigmaSpace,
int borderType=BORDER_DEFAULT );
效果如图所示。

1.5 导向滤波
均值滤波、中值滤波、高斯滤波等简单的滤波,都有一个共同的弱点,即他们都属于各向同性滤波。一幅图像可以被看成是有区域(过渡平缓,也就是梯度较小)和边缘(图像的纹理、细节等)共同组成。噪声的特点通常是以其为中心的各个方向上的梯度都比较大而且相差不多。边缘则不同,边缘只有在其法向方向上才会出现较大的梯度,而在切线方向上梯度较小。
导向滤波能保边平滑,抠图,可应用在图像增强、HDR压缩、图像抠图以及图像去雾等场景。
1.5.1 算法原理
导向滤波之所以叫这个名字,是因为在算法框架中,要对图像p进行滤波得到图像q,还需要一个引导图像I。滤波输出定义为:
q i = ∑ j ∈ w i W i , j ( I ) ⋅ p j q_i = \sum_{j \in w_i} W_{i,j}(I) \cdot p_j qi=j∈wi∑Wi,j(I)⋅pj
引导图像I可以是单独的一幅图像也可以是输入图像p本身。
导向滤波的示意图如下图所示。

该模型认为,在导向图像与滤波输出之间的一个二维窗口内是一个局部线性模型,即:
q i = a i I i + b i , ∀ i ∈ w k qi=aiIi+bi,∀i∈wk qi=aiIi+bi,∀i∈wk
导引图像与q之间存在线性关系,这样设定是因为我们希望导引图像提供的是信息主要用于指示哪些是边缘哪些是区域,所以在滤波时,如果导引图告诉我们这里是区域,那么就将其磨平。如果导引图告诉我们这里是边缘,这在最终的滤波结果里就要设法保留这些边缘信息。只有当I和q之间是线性关系的这种引导关系才有意义。
两边同时取梯度可以得到:
∇ q = a ∇ I ∇q=a∇I ∇q=a∇I
即输入图像与输出图像有类似的梯度,也就是导向滤波有边缘保持特性。
现在已知I和p,要求出q。如果能求的参数a和b,显然能通过I和q之间的线性关系求出q。由于p是q受到噪声污染而产生的退化图像,假设噪声是n,则有:
q i = p i − n i qi=pi−ni qi=pi−ni
我们希望求出q与真实p之间的差距最小,于是转化为最优化问题,即计算:
a r g m i n ∑ i ∈ ω k ( a k I i + b k − p i ) 2 argmin∑i∈ωk(akIi+bk−pi)2 argmin∑i∈ωk(akIi+bk−pi)2
于是便得到了最小二乘问题。即求解下式对应的参数a和b。
a ‾ i = 1 ∣ ω ∣ ∑ k ∈ ω i a k \overline{a}i = \frac{1}{|\omega|} \sum{k \in \omega_i} a_k ai=∣ω∣1k∈ωi∑ak
b ‾ i = 1 ∣ ω ∣ ∑ k ∈ ω i b k \overline{b}i = \frac{1}{|\omega|} \sum{k \in \omega_i} b_k bi=∣ω∣1k∈ωi∑bk
其中ε为防止a过大的归一化参数,可以求得:
a k = 1 ∣ ω ∣ ∑ i ∈ ω k I i p i − μ k p ‾ k σ k 2 + ϵ a_k = \frac{\frac{1}{|\omega|} \sum_{i \in \omega_k} I_i p_i - \mu_k \overline{p}_k}{\sigma_k^2 + \epsilon} ak=σk2+ϵ∣ω∣1∑i∈ωkIipi−μkpk
b k = p ‾ k − a k μ k b_k = \overline{p}_k - a_k \mu_k bk=pk−akμk
其中μk是图像I在窗口ωk中的平均值,Pk是待滤波图像p在窗口ωk中的均值,σk是I在窗口ωk中的标准差|ω|是窗口ωk中像素的数量。
在计算每个窗口的线性系数时,我们可以发现一个像素会被多个窗口包含,也就是说,每个像素都由多个线性函数所描述。因此,如之前所说,要具体求某一点的输出值时,只需将所有包含该点的线性函数值平均即可,如下:
q i = 1 ∣ ω ∣ ∑ k : i ∈ ω k ( a k I i + b k ) = a ‾ i I i + b ‾ i q_i = \frac{1}{|\omega|} \sum_{k: i \in \omega_k} (a_k I_i + b_k) = \overline{a}_i I_i + \overline{b}_i qi=∣ω∣1k:i∈ωk∑(akIi+bk)=aiIi+bi
其中:
a ‾ i = 1 ∣ ω ∣ ∑ k ∈ ω i a k \overline{a}i = \frac{1}{|\omega|} \sum{k \in \omega_i} a_k ai=∣ω∣1k∈ωi∑ak
b ‾ i = 1 ∣ ω ∣ ∑ k ∈ ω i b k \overline{b}i = \frac{1}{|\omega|} \sum{k \in \omega_i} b_k bi=∣ω∣1k∈ωi∑bk
一些特殊情况:
- 当引导图为输入图像时,引导滤波就成为一个保边滤波操作。
- 如果ε=0,则a=1,b=0,这是滤波器没有任何作用,输出与输入一致。
- 如果ε>0,在像素强度变化小的区域,a近似为0,而b近似pk,即加权滤波。在变化大的区域,a近似为1,b近似为0,对图像的滤波效果很弱,有助于保持边缘。
1.5.2 OpenCV实现
OpenCV中将导向滤波封装在cv::ximgproc::guidedFilter()函数中,该函数定义如下:
cpp
void guidedFilter(InputArray guide, InputArray src, OutputArray dst,
int radius, double eps, int dDepth = -1);
- 参数guide:导向图。
- 参数src:输入图像。
- 参数dst:输出图像。
- 参数radius:窗口半径。
- 参数eps: 归一化的ε参数。
- 参数dDepth:输出图像的深度。
代码实现:
cpp
#include <iostream>
#include "opencv2/highgui/highgui.hpp"
#include "opencv2/opencv.hpp"
#include "opencv2/core/core.hpp"
#include "opencv2/ximgproc.hpp"
using namespace std;
using namespace cv;
#define IMAGE_PATH "../ImageSet/葵花.png"
void GuideFilter(Mat img)
{
Mat dstImage;
cv::ximgproc::guidedFilter(img,img, dstImage, 5, 0.06f*255*255, -1);
imshow("guide filter", dstImage);
}
int main()
{
Mat img = imread(IMAGE_PATH);
if(img.empty())
{
cout<<"Read image failed"<<endl;
return 0;
}
Mat img_resize;
resize(img, img_resize, cv::Size(img.cols/2, img.rows/2),
0.0,0.0, cv::INTER_LINEAR);
imshow("Raw Image", img_resize);
GuideFilter(img_resize);
waitKey(0);
}
效果对比:
