双边滤波(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(i, j)[0] + (*_src).at(i, j)[1] + (*_src).at(i, j)[2]
- (*_src).at(x, y)[0] - (*_src).at(x, y)[1] - (*_src).at(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(i, j)[0] + (*_src).at(i, j)[1] + (*_src).at(i, j)[2]
- (*_src).at(x, y)[0] - (*_src).at(x, y)[1] - (*_src).at(x, y)[2]);
for (int c = 0; c < 3; c++) {
sum[c] += ((*_src).at(x, y)[c]
* _colorArray[values]
* _spaceArray[k][l])
/ space_color_sum;
}
}
}
for (int c = 0; c < 3; c++) {
temp.at(i, j)[c] = sum[c];
}
}
}
}
// 放入原图
(*_src) = temp.clone();
return ;
}