Bilateral Filters(双边滤波算法)原理及实现(二)

第一篇是让读者对双边滤波拥有直观的概念,本篇是对参数进行具体讲解,以及opencv中实现的过程。

双边滤波(Bilateral Filter)是非线性滤波中的一种。这是一种结合图像的空间邻近度与像素值相似度的处理办法。在滤波时,该滤波方法同时考虑空间临近信息与颜色相似信息,在滤除噪声、平滑图像的同时,又做到边缘保存。
双边滤波采用了两个高斯滤波的结合。一个负责计算空间邻近度的权值,也就是常用的高斯滤波器原理。而另一个负责计算像素值相似度的权值。在两个高斯滤波的同时作用下,就是双边滤波。

如果对于高斯滤波(空间邻近)原理不清晰的,建议先阅读这两篇文章
OpenCV高斯滤波器详解及代码实现
快速高斯滤波、高斯模糊、高斯平滑(二维卷积分步为一维卷积)

看到这里,博主认为你对高斯滤波(空间临近)已经理解并掌握了,那么我们开始对双边滤波进行讲解。
在上面两篇文章中提到,高斯滤波(空间临近)是将二维高斯正态分布放在图像矩阵上做卷积运算。考虑的是邻域内像素值的空间距离关系。通过在核大小范围内,各个点到中心点的空间临近度计算出对应的权值,并将计算好的核与图像矩阵作卷积。最后,图像经过滤波后达到平滑的效果,而图像上的边缘也会有一定程度的平滑,使得整个图像变得模糊,边缘得不到保存。

双边滤波的基本思想是:将高斯滤波(空间临近)的原理中,通过各个点到中心点的空间临近度计算的各个权值进行优化,将其优化为空间临近度计算的权值 和 像素值相似度计算的权值的乘积,优化后的权值再与图像作卷积运算。从而达到保边去噪的效果。

一、双边滤波的公式

g(i, j)代表输出点;
S(i, j)的是指以(i,j)为中心的(2N+1)(2N+1)的大小的范围;
f(k, l)代表(多个)输入点;
w(i, j, k, l)代表经过两个高斯函数计算出的值(这里还不是权值)

上述公式我们进行转化,假设公式中w(i,j,k,l)为m,则有


设 m1+m2+m3 … +mn = M,则有

此时可以看到,这明显是图像矩阵与核的卷积运算了。其中m1/M代表的第一个点(或最后一个点,看后面如何实现)的权值,而图像矩阵与核通过卷积算子作加权和,最终得到输出值。

接下来我们来讨论最关键的w(i, j, k, l)
ws为空间临近高斯函数,wr为像素值相似度高斯函数

可以看到,w是ws和wr的乘积。对于ws来说,这就是普通的高斯滤波函数,其代入的坐标,sigmas是程序输入值,该函数是在空间临近度上计算的。
而wr是计算像素值相似度(颜色空间),注意,这就是高斯函数代入坐标值,2sigmar^2的上方是范数,在这里的值为 |f(i,j)-f(k,l)|^2。也就是两个点像素值差值的绝对值的平方。其中,彩色图片计算差值时应将(i,j)点的RGB三通道值之和减去(k,l)点的RGB三通道值之和。这里是颜色空间计算,不能当成单通道,但是在最后矩阵卷积时,是单通道与权值相乘而不是三个通道之和。

二、双边滤波权值分布(图片来源于网络)

(1)当图像在变化程度平缓的区域时,邻域中的像素值(RGB值)差距相差不大。此时wr无限接近于1,因此此时的双边就是普通的高斯滤波,达到对图像平滑的效果。

(2) 当图像在变化程度剧烈的区域,比如在边缘区域时,邻域中的像素值(RGB值)差距相差很大。此时wr朝0值趋近,颜色差值越大,wr越逼近0,最终整个式子的值逼近于0。最终的结果是权值为0。因此在最终计算时,该处将不影响输出值。


通过此种方式,其既能平滑图像,又保持了图像的边缘。

三、双边滤波实现(图片来源于网络)
首先来看下双边滤波的效果

该函数有三个参数,滤波板(核)半径N、参数sigmas和sigmar.
(1) main函数实现

int main(void)
{
	// [1] src读入图片
	cv::Mat src = cv::imread("pic3.jpg");
	// [2] dst目标图片
	cv::Mat dst;
	// [3] 滤波 N越大越平越模糊(2*N+1) sigmas空间越大越模糊sigmar相似因子
	myBialteralFilter(&src, &dst, 25, 12.5, 50);
	// [4] 窗体显示
	cv::imshow("src 1006534767", src);
	cv::imshow("dst 1006534767", dst);
	cv::waitKey(0);
	cv::destroyAllWindows();
	
	return 0;
}

(2)双边滤波主函数,注意,现在的权值并没有进行归一化,归一化需要在计算时,根据第一大点中讲解的公式进行计算。

/* 双边滤波函数 */
void myBialteralFilter(cv::Mat *src, cv::Mat *dst, int N, double sigmas, double sigmar)
{
	// [1] 初始化
	*dst = (*src).clone();
	int _size = 2 * N + 1;
	// [2] 分别计算空间权值和相似度权值
	int channels = (*dst).channels();
	double *_colorArray = NULL;
	double **_spaceArray = NULL;
	_colorArray = get_color_Array( _size, channels, sigmar);
	_spaceArray = get_space_Array(_size, channels, sigmas);
	// [3] 滤波
	doBialteral(dst, N, _colorArray, _spaceArray);

	return;
}

(3)高斯函数带入坐标(空间临近)

/* 计算空间权值 */
double **get_space_Array( int _size, int channels, double sigmas)
{
	// [1] 空间权值
	int i, j;
	// [1-1] 初始化数组
	double **_spaceArray = new double*[_size+1];   //多一行,最后一行的第一个数据放总值
	for (i = 0; i < _size+1; i++) {
		_spaceArray[i] = new double[_size+1];
	}
	// [1-2] 高斯分布计算
	int center_i, center_j;
	center_i = center_j = _size / 2;
	_spaceArray[_size][0] = 0.0f;
	// [1-3] 高斯函数
	for (i = 0; i < _size; i++) {
		for (j = 0; j < _size; j++) {
			_spaceArray[i][j] =
				exp(-(1.0f)* (((i - center_i)*(i - center_i) + (j - center_j)*(j - center_j)) /
				(2.0f*sigmas*sigmas)));
			_spaceArray[_size][0] += _spaceArray[i][j];
		}
	}
	return _spaceArray;
}

(4)高斯函数代入坐标值(颜色空间)

/* 计算相似度权值 */
double *get_color_Array(int _size, int channels, double sigmar)
{
	// [2] 相似度权值
	int n;
	double *_colorArray = new double[255 * channels + 2];   //最后一位放总值
	double wr = 0.0f;
	_colorArray[255 * channels + 1] = 0.0f;
	for (n = 0; n < 255 * channels + 1; n++) {
		_colorArray[n] = exp((-1.0f*(n*n)) / (2.0f*sigmar*sigmar));
		_colorArray[255 * channels + 1] += _colorArray[n];
	}
	return _colorArray;
}

(5)滤波

/* 双边 扫描计算 */
void doBialteral(cv::Mat *_src, int N, double *_colorArray, double **_spaceArray)
{
	int _size = (2 * N + 1);
	cv::Mat temp = (*_src).clone();
	// [1] 扫描
	for (int i = 0; i < (*_src).rows; i++) {
		for (int j = 0; j < (*_src).cols; j++) {
			// [2] 忽略边缘
			if (i > (_size / 2) - 1 && j > (_size / 2) - 1 &&
				i < (*_src).rows - (_size / 2) && j < (*_src).cols - (_size / 2)) {

				// [3] 找到图像输入点,以输入点为中心与核中心对齐
				//     核心为中心参考点 卷积算子=>高斯矩阵180度转向计算
				//     x y 代表卷积核的权值坐标   i j 代表图像输入点坐标
				//     卷积算子     (f*g)(i,j) = f(i-k,j-l)g(k,l)          f代表图像输入 g代表核
				//     带入核参考点 (f*g)(i,j) = f(i-(k-ai), j-(l-aj))g(k,l)   ai,aj 核参考点
				//     加权求和  注意:核的坐标以左上0,0起点 
				double sum[3] = { 0.0,0.0,0.0 };
				int x, y, values;
				double space_color_sum = 0.0f;
				// 注意: 公式后面的点都在核大小的范围里
				// 双边公式 g(ij) =  (f1*m1 + f2*m2 + ... + fn*mn) / (m1 + m2 + ... + mn)
                // space_color_sum = (m1 + m12 + ... + mn)
				for (int k = 0; k < _size; k++) {
					for (int l = 0; l < _size; l++) {
						x = i - k + (_size / 2);   // 原图x  (x,y)是输入点
						y = j - l + (_size / 2);   // 原图y  (i,j)是当前输出点 
						values = abs((*_src).at<cv::Vec3b>(i, j)[0] + (*_src).at<cv::Vec3b>(i, j)[1] + (*_src).at<cv::Vec3b>(i, j)[2]
							- (*_src).at<cv::Vec3b>(x, y)[0] - (*_src).at<cv::Vec3b>(x, y)[1] - (*_src).at<cv::Vec3b>(x, y)[2]);
						space_color_sum += (_colorArray[values] * _spaceArray[k][l]);
					}
				}
				// 计算过程
				for (int k = 0; k < _size; k++) {
					for (int l = 0; l < _size; l++) {
						x = i - k + (_size / 2);   // 原图x  (x,y)是输入点
						y = j - l + (_size / 2);   // 原图y  (i,j)是当前输出点 
						values = abs((*_src).at<cv::Vec3b>(i, j)[0] + (*_src).at<cv::Vec3b>(i, j)[1] + (*_src).at<cv::Vec3b>(i, j)[2]
							- (*_src).at<cv::Vec3b>(x, y)[0] - (*_src).at<cv::Vec3b>(x, y)[1] - (*_src).at<cv::Vec3b>(x, y)[2]);
						for (int c = 0; c < 3; c++) {
							sum[c] += ((*_src).at<cv::Vec3b>(x, y)[c]
								* _colorArray[values]
								* _spaceArray[k][l])
								/ space_color_sum;
						}
					}
				}
				for (int c = 0; c < 3; c++) {
					temp.at<cv::Vec3b>(i, j)[c] = sum[c];	
				}
			}
		}
	}
	// 放入原图
	(*_src) = temp.clone();

	return ;
}

转自:https://blog.csdn.net/qq_36359022/article/details/80198890

猜你喜欢

转载自blog.csdn.net/u013066730/article/details/87919412