双边滤波(bilateral filter)是一种非线性滤波技术,它是由Tomasi于1998年提出。它扩展了高斯平滑滤波技术。高斯滤波是一种常见并且有效的滤波方法,简单地说它是以被处理像素与其周围像素的距离作为权重而进行的一种加权平均过程。
高斯距离权值为:
其中,d(ξ,x)=d(ξ-x)=||ξ-x||表示的是两个像素ξ和x之间的距离。
但该权值仅仅考虑的是距离,而对像素本身的亮度信息没有考虑,因此高斯滤波的结果是使整幅图像都模糊了,即图像的边缘信息(高频部分)被严重削弱了。我们知道当图像中邻域像素亮度与被处理像素的亮度差异很大时,邻域像素与该像素的关系很小的,即两者相似性很差。因此双边滤波还考虑了领域像素的亮度信息,通过计算相似度来赋予领域像素一定的权重,下面就是高斯相似度的权值:
其中,σ(f(ξ),f(x))=||f(ξ)-f(x)||表示两个像素亮度之差。最终的双边滤波的公式为:
(3)
其中,,Σ表示的是邻域范围,及滤波内核大小,f(ξ)表示原图。
双边滤波的原理介绍完了,下面看看opencv是如何实现的。该部分的源码是在/sources/modules/imgproc/scr/smooth.cpp内,
//_src为输入原图像,_dst为双边滤波后的图像,d为滤波内核的大小,即领域的大小 // sigmaColor为相似度权值公式中的方差,即公式2中的σr // sigmaSpace为距离权值公式中的方差,即公式1中的σd // borderType表示用什么方式来处理加宽后的图像四周边界 void cv::bilateralFilter( InputArray _src, OutputArray _dst, int d, double sigmaColor, double sigmaSpace, int borderType ) { Mat src = _src.getMat(); //得到原图矩阵 _dst.create( src.size(), src.type() ); //创建一个与原图大小和类型一致的输出图像 Mat dst = _dst.getMat(); //得到输出图像矩阵 if( src.depth() == CV_8U ) //原图深度为8位二进制 bilateralFilter_8u( src, dst, d, sigmaColor, sigmaSpace, borderType ); else if( src.depth() == CV_32F ) //原图深度为32位浮点型 bilateralFilter_32f( src, dst, d, sigmaColor, sigmaSpace, borderType ); else // bilateralFilter函数只支持深度为CV_8U和CV_32F的图像 CV_Error( CV_StsUnsupportedFormat, "Bilateral filtering is only implemented for 8u and 32f images" ); }在这里我们以处理 8 为二进制图像为例,因此它调用了 bilateralFilter_8u 函数:
static void bilateralFilter_8u( const Mat& src, Mat& dst, int d, double sigma_color, double sigma_space, int borderType ) { int cn = src.channels(); //得到图像的通道数,即是灰度图像还是彩色图像 int i, j, maxk, radius; Size size = src.size(); //得到图像的大小尺寸 //处理之前再次检查图像中的相关信息是否正确 CV_Assert( (src.type() == CV_8UC1 || src.type() == CV_8UC3) && src.type() == dst.type() && src.size() == dst.size() && src.data != dst.data ); //如果在函数调用时给出的高斯公式中的两个σ值小于0,则为1 if( sigma_color <= 0 ) sigma_color = 1; if( sigma_space <= 0 ) sigma_space = 1; //计算两个高斯公式中的系数,即e指数部分的分母 double gauss_color_coeff = -0.5/(sigma_color*sigma_color); double gauss_space_coeff = -0.5/(sigma_space*sigma_space); if( d <= 0 ) //如果调用双边滤波函数时给出的滤波内核大小小于等于0 radius = cvRound(sigma_space*1.5); //根据σd来自动给出内核的半径 else //否则得到内核的半径 radius = d/2; radius = MAX(radius, 1); //保证内核半径大于1 d = radius*2 + 1; //重新得到内核大小尺寸 //为了在图像边界处得到更好的处理效果,需要对图像四周边界做适当的处理 //把原图的四周都加宽为内核半径的宽度,而加宽部分的像素值由borderType值决定 //待处理的图像由src换成了temp Mat temp; copyMakeBorder( src, temp, radius, radius, radius, radius, borderType ); #if defined HAVE_IPP && (IPP_VERSION_MAJOR >= 7) if( cn == 1 ) { bool ok; IPPBilateralFilter_8u_Invoker body(temp, dst, sigma_color * sigma_color, sigma_space * sigma_space, radius, &ok ); parallel_for_(Range(0, dst.rows), body, dst.total()/(double)(1<<16)); if( ok ) return; } #endif //无论是距离权值还是相似度权值都是事先计算后,并保持在相应的矢量中 vector<float> _color_weight(cn*256); //保持相似度权值的矢量 vector<float> _space_weight(d*d); //保持距离权值的矢量 vector<int> _space_ofs(d*d); //距离的偏移量 //得到各个矢量的第一个数据的地址指针 float* color_weight = &_color_weight[0]; float* space_weight = &_space_weight[0]; int* space_ofs = &_space_ofs[0]; // initialize color-related bilateral filter coefficients //计算亮度相似度权值 /*如果是灰度图像,亮度是从0~255,因此两个灰度值的差值也是在0~255之间,事先计算这些值把它们保持在color_weight中,当要用到相似度权值时,只要知道两个灰度值的差值,通过color_weight,就很容易得到权值;而如果是彩色图像,则是把一个像素中的红、绿、蓝三色加在一起后得到一个差值,而不是一个像素分别有红、绿、蓝三个差值,因此彩色图像两个像素的差值范围是在0~256×3之间。*/ for( i = 0; i < 256*cn; i++ ) color_weight[i] = (float)std::exp(i*i*gauss_color_coeff); //高斯公式 // initialize space-related bilateral filter coefficients //计算距离权值 for( i = -radius, maxk = 0; i <= radius; i++ ) { j = -radius; for( ;j <= radius; j++ ) { double r = std::sqrt((double)i*i + (double)j*j); //距离范数 if( r > radius ) //如果距离大于内核半径,则抛弃该值 continue; space_weight[maxk] = (float)std::exp(r*r*gauss_space_coeff); //高斯公式 //得到偏移量,在后面通过该偏移量来找到相应的像素 space_ofs[maxk++] = (int)(i*temp.step + j*cn); } } //通过实例化BilateralFilter_8u_Invoker类计算得到双边滤波的结果 BilateralFilter_8u_Invoker body(dst, temp, radius, maxk, space_ofs, space_weight, color_weight); parallel_for_(Range(0, size.height), body, dst.total()/(double)(1<<16)); }
我们重点介绍BilateralFilter_8u_Invoker类中的operator():
virtual void operator() (const Range& range) const { int i, j, cn = dest->channels(), k; Size size = dest->size(); #if CV_SSE3 ……略…… #endif for( i = range.start; i < range.end; i++ ) { const uchar* sptr = temp->ptr(i+radius) + radius*cn; //得到原图数据指针 uchar* dptr = dest->ptr(i); //输出图像指针 if( cn == 1 ) //灰度图像 { for( j = 0; j < size.width; j++ ) { float sum = 0, wsum = 0; int val0 = sptr[j]; //内核的中心像素,即待处理的像素值 k = 0; #if CV_SSE3 ……略…… #endif //遍历这个内核 for( ; k < maxk; k++ ) { int val = sptr[j + space_ofs[k]]; //得到邻域像素值 //计算公式3中的k中的一个值 float w = space_weight[k]*color_weight[std::abs(val - val0)]; //计算公式3中的分子部分 sum += val*w; //计算公式3中的分母部分 wsum += w; } // overflow is not possible here => there is no need to use CV_CAST_8U //得到处理后的结果 dptr[j] = (uchar)cvRound(sum/wsum); } } else //彩色图像 { assert( cn == 3 ); for( j = 0; j < size.width*3; j += 3 ) { float sum_b = 0, sum_g = 0, sum_r = 0, wsum = 0; int b0 = sptr[j], g0 = sptr[j+1], r0 = sptr[j+2]; k = 0; #if CV_SSE3 ……略…… #endif for( ; k < maxk; k++ ) { const uchar* sptr_k = sptr + j + space_ofs[k]; //得到邻域像素 //分别得到邻域像素中的蓝、绿、红分量 int b = sptr_k[0], g = sptr_k[1], r = sptr_k[2]; //计算公式3中的k值, //其中相似度权值中的像素差值是红、绿、蓝分量差值之和 float w = space_weight[k]*color_weight[std::abs(b - b0) + std::abs(g - g0) + std::abs(r - r0)]; //利用公式3分别得到红、绿、蓝三个分量值 sum_b += b*w; sum_g += g*w; sum_r += r*w; wsum += w; } wsum = 1.f/wsum; b0 = cvRound(sum_b*wsum); g0 = cvRound(sum_g*wsum); r0 = cvRound(sum_r*wsum); dptr[j] = (uchar)b0; dptr[j+1] = (uchar)g0; dptr[j+2] = (uchar)r0; } } } }
Opencv中的双边滤波的源码分析就到这里,另外根据Opencv的文档中介绍,两个高斯公式的σ可以相同,而且如果σ小于10,则滤波效果不明显,如果大于150,则会有强烈的卡通效果。当实时处理时,内核尺寸d推荐为5;如果在非实时处理情况下,而且有较强的噪声时,d为9效果会较好。
下面是滤波的效果,图1为原图,图2为滤波的结果,使用的参数为
bilateralFilter(src,dst,10,75,75);
图1 原图
图2 双边滤波后的图