2022年第一篇,来总结一下图像处理中常用的几种滤波算法,包括中值、均值、高斯、双边、引导滤波这五种,主要用于图像平滑去噪方面。
滤波算法的基本思路,就是采用周边像素,加权平均计算一个新的像素,来缓减噪声对当前像素的影响。
均值滤波是典型的线性滤波算法,它是指在图像上对目标像素给一个模板,该模板包括了其周围的临近像素(以目标像素为中心的周围8个像素,构成一个滤波模板,即包括目标像素本身),再用模板中的全体像素的平均值来代替原来像素值。
opencv提供均值滤波函数blur():
CV_EXPORTS_W void blur( InputArray src, OutputArray dst,
Size ksize, Point anchor = Point(-1,-1),
int borderType = BORDER_DEFAULT );
C++实现:
#include
#include
#include
using namespace std;
using namespace cv;
int main()
{
Mat imgOri = imread("原图.jpg");
namedWindow("原图");
imshow("原图", imgOri);
Mat imgOut;
blur(imgOri, imgOut, Size(20, 20));
namedWindow("效果图");
imshow("效果", imgOut);
waitKey(0);
return true;
}
中值滤波法是一种非线性平滑技术,它将每一像素点的灰度值设置为该点某邻域窗口内的所有像素点灰度值的中值。采用窗口内中值的方法,有效剔除了异常高亮或过暗的噪声,对椒盐噪声的去除效果比较好,但实际的图像会伴随着边缘纹理,由于只考虑中值,也会将图像细节去除,只是比均值滤波稍微好一点而已。
公式如下:
opencv提供高斯滤波函数medianBlur():
CV_EXPORTS_W void medianBlur(InputArray src,OutputArray dst,int Ksize //只能3,5,7,9后的奇数)
C++实现:
#include
#include
#include
using namespace cv;
int main()
{
//载入图像
Mat image = imread("1.jpg");
//创建窗口
namedWindow("中值滤波原图");
namedWindow("中值滤波效果图");
//进行滤波
Mat out;
medianBlur(image, out, 7);
imshow("中值滤波原图", image);
imshow("中值滤波效果图", out);
waitKey(0);
return 0;
}
高斯滤波是一种线性平滑滤波,适用于消除高斯噪声,广泛应用于图像处理的减噪过程。 [1] 通俗的讲,高斯滤波就是对整幅图像进行加权平均的过程,每一个像素点的值,都由其本身和邻域内的其他像素值经过加权平均后得到。高斯滤波的具体操作是:用一个模板(或称卷积、掩模)扫描图像中的每一个像素,用模板确定的邻域内像素的加权平均灰度值去替代模板中心像素点的值。
二维高斯函数:
5*5卷积核:
二维高斯函数公式如下:
opencv提供高斯滤波函数GaussianBlur():
CV_EXPORTS_W void GaussianBlur( InputArray src, OutputArray dst, Size ksize,
double sigmaX, double sigmaY = 0,
int borderType = BORDER_DEFAULT );
C++实现:
#include
#include
#include
#define GAUSS_NOISE "GaussNoise_127"
#define FILE_TYPE ".png"
#define GAUSS_NOISE_FILE string(GAUSS_NOISE)+string(FILE_TYPE)
#define MEANS 127
#define STANDARD_DAV 10
#define MAX_KERNEL_SIZE 500
#define MAX_SIGMA 255
using namespace std;
using namespace cv;
static void showImgPara(Mat &img);
static void showImgMinMaxMeansStdev(Mat &img);
static void kernelSizeCallBack(int, void*);
static void xSigmaCallBack(int, void*);
static void ySigmaCallBack(int, void*);
Mat gImgOri, gImgGauss, gImgGaussFilter;
int gKernelSize = 11;
int gXSigma = 10;
int gYSigma = 10;
int main()
{
/* Original Image */
gImgOri = imread("original.png", IMREAD_GRAYSCALE);
if (gImgOri.empty())
{
cout << "Cannot find original picture!!" << endl;
return false;
}
imshow("imgOri", gImgOri);
//showImgPara(gImgOri);
showImgMinMaxMeansStdev(gImgOri);
/* Gauss Noise Picture Create */
gImgGauss = imread(GAUSS_NOISE_FILE, IMREAD_GRAYSCALE);
if (gImgGauss.empty())
{
cout << "Cannot find noise picture!!" << endl;
return false;
}
imshow("imgGauss", gImgGauss);
//showImgPara(gImgGauss);
showImgMinMaxMeansStdev(gImgGauss);
/* Gauss filter */
//GaussianBlur(gImgGauss, gImgGaussFilter, Size(7, 7), 10, 10);
GaussianBlur(gImgGauss, gImgGaussFilter, Size(gKernelSize, gKernelSize), gXSigma, gYSigma);
imshow("Gauss Filter Out", gImgGaussFilter);
showImgMinMaxMeansStdev(gImgGaussFilter);
/* Add Bar */
cv::createTrackbar("Kernel Size", "Gauss Filter Out", &gKernelSize, MAX_KERNEL_SIZE, kernelSizeCallBack);
cv::createTrackbar("X Sigma", "Gauss Filter Out", &gXSigma, MAX_KERNEL_SIZE, xSigmaCallBack);
cv::createTrackbar("Y Sigma", "Gauss Filter Out", &gYSigma, MAX_KERNEL_SIZE, ySigmaCallBack);
waitKey(0);
return true;
}
static void showImgPara(Mat &img)
{
cout << "sizeof(img) is: " << sizeof(img) << ", img size is: " << img.size << endl;
cout << "rows x cols: (" << img.rows << " x " << img.cols << ")" << endl;
cout << "dims: " << img.dims << endl;
cout << "channels: " << img.channels() << endl;
cout << "type: " << img.type() << endl;
cout << "depth:" << img.depth() << endl;
cout << "elemSize:" << img.elemSize() << " (Bytes per element)" << endl;
cout << "elemSize1:" << img.elemSize1() << "(Bytes per channel)" << endl;
cout << "step[0]: " << img.step[0] << " (Bytes per cows only when 2 dims)" << endl;
cout << "step[1]: " << img.step[1] << " (Bytes per element only when 2 dims)" << endl;
cout << "step1(0): " << img.step1(0) << ", step1(1): " << img.step1(1) << " (step / elemSize1)" << endl;
cout << "----showImgPara End----" << endl;
}
static void showImgMinMaxMeansStdev(Mat &img)
{
/* Max and Min value and location */
double minValue, maxValue;
Point minLdx, maxLdx;
minMaxLoc(img, &minValue, &maxValue, &minLdx, &maxLdx, Mat());
cout << "Min: " << minValue << "[" << minLdx << "]" << endl;
cout << "Max: " << maxValue << "[" << maxLdx << "]" << endl;
/* means and stdev*/
Mat means, stdev;
meanStdDev(img, means, stdev);
cout << "Means: " << means.at<double>(0, 0) << endl;
cout << "Standard Deviationst: " << stdev.at<double>(0, 0) << endl;
cout << "----showImgMinMaxMeansStdev End----\n" << endl;
}
static void kernelSizeCallBack(int, void*)
{
if (gKernelSize % 2 == 0)
{
gKernelSize = gKernelSize + 1;
}
GaussianBlur(gImgGauss, gImgGaussFilter, Size(gKernelSize, gKernelSize), gXSigma, gYSigma);
imshow("Gauss Filter Out", gImgGaussFilter);
showImgMinMaxMeansStdev(gImgGaussFilter);
}
static void xSigmaCallBack(int, void*)
{
GaussianBlur(gImgGauss, gImgGaussFilter, Size(gKernelSize, gKernelSize), gXSigma, gYSigma);
imshow("Gauss Filter Out", gImgGaussFilter);
showImgMinMaxMeansStdev(gImgGaussFilter);
}
static void ySigmaCallBack(int, void*)
{
GaussianBlur(gImgGauss, gImgGaussFilter, Size(gKernelSize, gKernelSize), gXSigma, gYSigma);
imshow("Gauss Filter Out", gImgGaussFilter);
showImgMinMaxMeansStdev(gImgGaussFilter);
}
双边滤波(Bilateral filter)是一种非线性的滤波方法,是结合图像的空间邻近度和像素值相似度的一种折中处理,同时考虑空域信息和灰度相似性,达到保边去噪的目的。具有简单、非迭代、局部的特点。双边滤波器的好处是可以做边缘保存(edge preserving),一般过去用的维纳滤波或者高斯滤波去降噪,都会较明显地模糊边缘,对于高频细节的保护效果并不明显。
灰度距离:指的是当前点灰度与中心点灰度的差的绝对值。
OpenCV提供bilateralFilter()函数,API的介绍如下:
CV_EXPORTS_W void bilateralFilter( InputArray src, OutputArray dst, int d,
double sigmaColor, double sigmaSpace,
int borderType = BORDER_DEFAULT );
C++实现:
#include
#include
using namespace std;
using namespace cv;
Mat addSaltNoise(const Mat src, int n); // 添加椒盐噪声
int main(){
// 读取图像
Mat src = imread("/home/chen/dataset/lena.jpg");
if (src.empty()){
cout << "cloud not load image." << endl;
return -1;
}
// 增加椒盐噪声
Mat srcSaltPepper = addSaltNoise(src, 100);
// 中值滤波
Mat dstMedian;
medianBlur(srcSaltPepper, dstMedian, 3);
Mat dstGaussian;
GaussianBlur(srcSaltPepper, dstGaussian, Size(3, 3), 3, 3);
// 双边滤波
Mat dstBilateralFilter;
bilateralFilter(srcSaltPepper, dstBilateralFilter, 25, 25*2, 25/2);
namedWindow("src", WINDOW_AUTOSIZE);
imshow("src", src);
namedWindow("srcSaltPepper", WINDOW_AUTOSIZE);
imshow("srcSaltPepper", srcSaltPepper);
namedWindow("medianBlur", WINDOW_AUTOSIZE);
imshow("medianBlur", dstMedian);
namedWindow("GaussianBlur", WINDOW_AUTOSIZE);
imshow("GaussianBlur", dstGaussian);
namedWindow("bilateralFilter", WINDOW_AUTOSIZE);
imshow("bilateralFilter", dstBilateralFilter);
waitKey(0);
return 0;
}
// 添加椒盐噪声
Mat addSaltNoise(const Mat src, int n){
Mat dst = src.clone();
for (int k = 0; k < n; k++){
// 随机选择行列
int i = rand() % dst.rows;
int j = rand() % dst.cols;
if (dst.channels() == 1){
dst.at<uchar>(i, j) = 255; // 盐噪声
} else{
dst.at<Vec3b>(i, j)[0] = 255;
dst.at<Vec3b>(i, j)[1] = 255;
dst.at<Vec3b>(i, j)[2] = 255;
}
}
for (int k = 0; k < n; k++)
{
//随机取值行列
int i = rand() % dst.rows;
int j = rand() % dst.cols;
//图像通道判定
if (dst.channels() == 1)
{
dst.at<uchar>(i, j) = 0; // 椒噪声
} else
{
dst.at<Vec3b>(i, j)[0] = 0;
dst.at<Vec3b>(i, j)[1] = 0;
dst.at<Vec3b>(i, j)[2] = 0;
}
}
return dst;
}
导向图滤波(Guided Filter)是一种能使视频平滑化的非线性滤波器,通过一张引导图G,对目标图像P(输入图像)进行滤波处理,使得最后的输出图像大体上与目标图像P相似,但是纹理部分与引导图G相似。应用有两个:保边图像平滑,抠图。
引导滤波是由何凯明等人于2010年发表在ECCV的文章《Guided Image Filtering》中提出的,后续于2013年发表了改进算法快速引导滤波的实现。它与双边滤波最大的相似之处,就是同样具有保持边缘特性。该模型认为,某函数上一点与其邻近部分的点成线性关系,一个复杂的函数就可以用很多局部的线性函数来表示,当需要求该函数上某一点的值时,只需计算所有包含该点的线性函数的值并做平均即可。这种模型,在表示非解析函数上,非常有用。
在滤波效果上,引导滤波和双边滤波差不多,在一些细节上,引导滤波较好。引导滤波最大的优势在于,可以写出时间复杂度与窗口大小无关的算法,因此在使用大窗口处理图片时,其效率更高。
引导图像I,原图P,公式如下:
引导滤波算法,目前opencv没有提供API,伪代码如下:
实现这种算法的关键思想是盒式滤波(box filter),而且必须是通过积分图来实现的盒式滤波,否则不可能与窗口大小无关,好在OpenCV的boxFilter函数满足这个要求。与均值滤波不同的是,方框滤波不会计算像素的均值。在均值滤波中,滤波结果的像素值是任意一个点的邻域平均值,等于各邻域像素值之和除以邻域面积。而在方框滤波中,可以自由选择是否对均值滤波的结果进行归一化,即可以自由选择滤波结果是邻域像素值之和的平均值,还是邻域像素值之和。
CV_EXPORTS_W void boxFilter(InputArray src,OutputArray dst, int ddepth,
Size ksize, Point anchor=Point(-1,-1), boolnormalize=true,
int borderType=BORDER_DEFAULT)
#include
#include
#include
#include
//
// GUIDEDFILTER O(1) time implementation of guided filter.
// -guidance image : I(should be a gray - scale / single channel image)
// -filtering input image : p(should be a gray - scale / single channel image)
// -local window radius : r
// -regularization parameter : eps
/
cv::Mat GuidedFilter(cv::Mat& I, cv::Mat& p, int r, double eps){
int wsize = 2 * r + 1;
//数据类型转换
I.convertTo(I, CV_64F, 1.0 / 255.0);
p.convertTo(p, CV_64F, 1.0 / 255.0);
//meanI=fmean(I)
cv::Mat mean_I;
cv::boxFilter(I, mean_I, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
//meanP=fmean(P)
cv::Mat mean_p;
cv::boxFilter(p, mean_p, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
//corrI=fmean(I.*I)
cv::Mat mean_II;
mean_II = I.mul(I);
cv::boxFilter(mean_II, mean_II, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
//corrIp=fmean(I.*p)
cv::Mat mean_Ip;
mean_Ip = I.mul(p);
cv::boxFilter(mean_Ip, mean_Ip, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
//varI=corrI-meanI.*meanI
cv::Mat var_I, mean_mul_I;
mean_mul_I=mean_I.mul(mean_I);
cv::subtract(mean_II, mean_mul_I, var_I);
//covIp=corrIp-meanI.*meanp
cv::Mat cov_Ip;
cv::subtract(mean_Ip, mean_I.mul(mean_p), cov_Ip);
//a=conIp./(varI+eps)
//b=meanp-a.*meanI
cv::Mat a, b;
cv::divide(cov_Ip, (var_I+eps),a);
cv::subtract(mean_p, a.mul(mean_I), b);
//meana=fmean(a)
//meanb=fmean(b)
cv::Mat mean_a, mean_b;
cv::boxFilter(a, mean_a, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
cv::boxFilter(b, mean_b, -1, cv::Size(wsize, wsize), cv::Point(-1, -1), true, cv::BORDER_REFLECT);//盒子滤波
//q=meana.*I+meanb
cv::Mat q;
q = mean_a.mul(I) + mean_b;
//数据类型转换
I.convertTo(I, CV_8U, 255);
p.convertTo(p, CV_8U, 255);
q.convertTo(q, CV_8U, 255);
return q;
}
int main(){
cv::Mat src = cv::imread("I:\\Learning-and-Practice\\2019Change\\Image process algorithm\\Img\\woman.jpg");
if (src.empty()){
return -1;
}
//if (src.channels() > 1)
// cv::cvtColor(src, src, CV_RGB2GRAY);
//自编GuidedFilter测试
double t2 = (double)cv::getTickCount(); //测时间
cv::Mat dst1, src_input, I;
src.copyTo(src_input);
if (src.channels() > 1)
cv::cvtColor(src, I, CV_RGB2GRAY); //若引导图为彩色图,则转为灰度图
std::vector<cv::Mat> p,q;
if (src.channels() > 1){ //输入为彩色图
cv::split(src_input, p);
for (int i = 0; i < src.channels(); ++i){
dst1 = GuidedFilter(I, p[i], 9, 0.1*0.1);
q.push_back(dst1);
}
cv::merge(q, dst1);
}
else{ //输入为灰度图
src.copyTo(I);
dst1 = GuidedFilter(I, src_input, 9, 0.1*0.1);
}
t2 = (double)cv::getTickCount() - t2;
double time2 = (t2 *1000.) / ((double)cv::getTickFrequency());
std::cout << "MyGuidedFilter_process=" << time2 << " ms. " << std::endl << std::endl;
cv::namedWindow("GuidedImg", CV_WINDOW_NORMAL);
cv::imshow("GuidedImg", I);
cv::namedWindow("src", CV_WINDOW_NORMAL);
cv::imshow("src", src);
cv::namedWindow("GuidedFilter_box", CV_WINDOW_NORMAL);
cv::imshow("GuidedFilter_box", dst1);
cv::waitKey(0);
}