高斯平滑 高斯模糊 高斯滤波器 ( Gaussian Smoothing, Gaussian Blur, Gaussian Filter ) C++ 实现

发展到现在这个平滑算法的时候, 我已经完全不知道如何去命名这篇文章了, 只好罗列出一些关键字来方便搜索了.

在之前我们提到过了均值滤波器, 就是说某像素的颜色, 由以其为中心的九宫格的像素平均值来决定. 在这个基础上又发展成了带权的平均滤波器, 这里的高斯平滑或者说滤波器就是这样一种带权的平均滤波器. 那么这些权重如何分布呢? 我们先来看几个经典的模板例子:

尝试了使用这些滤波器对我们原来的图进行操作, 得到了这样的一组结果:

原图:

raw

3x3 高斯:

3x3

5x5 高斯:

5x5

 

单纯从效果来看, 两个模板都起到了平滑的作用, 只是程度有深浅的区分. 那么从理论上来说为什么能起到平滑的作用呢? 很显然, 像素的颜色不仅由自身决定了, 同时有其周围的像素加权决定, 客观上减小了和周围像素的差异. 同时这些权重的设定满足了越近权重越大的规律. 从理论来讲, 这些权重的分布满足了著名的所谓高斯分布:

   这就是1维的计算公式

 这就是2维的计算公式

x, y表示的就是当前点到对应点的距离, 而那些具体的模板就是由这里公式中的一些特例计算而来. 需要说明的是不只有这么一些特例, 从wikipedia可以方便地找到那些复杂的模板比如像:

Sample Gaussian matrix

This is a sample matrix, produced by sampling the Gaussian filter kernel (with σ = 0.84089642) at the midpoints of each pixel and then normalising. Note that the center element (at [4, 4]) has the largest value, decreasing symmetrically as distance from the center increases.

0.00000067 0.00002292 0.00019117 0.00038771 0.00019117 0.00002292 0.00000067
0.00002292 0.00078633 0.00655965 0.01330373 0.00655965 0.00078633 0.00002292
0.00019117 0.00655965 0.05472157 0.11098164 0.05472157 0.00655965 0.00019117
0.00038771 0.01330373 0.11098164 0.22508352 0.11098164 0.01330373 0.00038771
0.00019117 0.00655965 0.05472157 0.11098164 0.05472157 0.00655965 0.00019117
0.00002292 0.00078633 0.00655965 0.01330373 0.00655965 0.00078633 0.00002292
0.00000067 0.00002292 0.00019117 0.00038771 0.00019117 0.00002292 0.00000067

是不是看到就头大了:) 不过没关系, 对于一般的应用来说, 前面的例子已经可以完成任务了.  代码的话我们还是给一份5x5的example:

[cpp]  view plain copy
  1. /** 
  2. ** method to remove noise from the corrupted image by gaussian filter value 
  3. * @param corrupted input grayscale binary array with corrupted info 
  4. * @param smooth output data for smooth result, the memory need to be allocated outside of the function 
  5. * @param width width of the input grayscale image 
  6. * @param height height of the input grayscale image 
  7. */  
  8. void gaussianFilter2 (unsigned char* corrupted, unsigned char* smooth, int width, int height)  
  9. {  
  10.     int templates[25] = { 1, 4, 7, 4, 1,   
  11.                           4, 16, 26, 16, 4,   
  12.                           7, 26, 41, 26, 7,  
  13.                           4, 16, 26, 16, 4,   
  14.                           1, 4, 7, 4, 1 };        
  15.       
  16.     memcpy ( smooth, corrupted, width*height*sizeof(unsigned char) );  
  17.     for (int j=2;j
  18.     {  
  19.         for (int i=2;i
  20.         {  
  21.             int sum = 0;  
  22.             int index = 0;  
  23.             for ( int m=j-2; m
  24.             {  
  25.                 for (int n=i-2; n
  26.                 {  
  27.                     sum += corrupted [ m*width + n] * templates[index++] ;  
  28.                 }  
  29.             }  
  30.             sum /= 273;  
  31.             if (sum > 255)  
  32.                 sum = 255;  
  33.             smooth [ j*width+i ] = sum;  
  34.         }  
  35.     }  
  36. }  

附带说一些,很明显,和均值滤波器类似, 这个滤波器没有消除校验噪声的作用


opencv源码解析之(4):GaussianBlur()

     这一节来真正进入opencv的源码分析中,本次分析的函数是GaussianBlur(),即高斯滤波函数。在前前面博文《opencv源码解析之滤波前言2》:http://www.cnblogs.com/tornadomeet/archive/2012/03/05/2379921.html 中已经阐述了这个函数的用法,即:

     其函数声明为:

     void GaussianBlur(InputArray src, OutputArray dst, Size ksize, double sigmaX, double sigmaY=0, int borderType=BORDER_DEFAULT ) ;

     功能:对输入的图像src进行高斯滤波后用dst输出。

     参数:src和dst当然分别是输入图像和输出图像。Ksize为高斯滤波器模板大小,sigmaX和sigmaY分别为高斯滤波在横线和竖向的滤波系数。borderType为边缘扩展点插值类型。

 

     接下来的工作就是进入GaussianBlur函数内部,跟踪其函数代码,经过分析,在该函数内部调用了很多其他的函数,其调用的函数层次结构如下图所示:

     这里我们分析源代码不需要深入到最底层,我们只需分析到函数createSeparableLinearFilter和getGaussianKernel这一层。

 

     那就开始我们的源码分析工作吧!

     从函数调用层次结构图可以看出,要分析函数GaussianBlur,必须先分析其调用过的内部函数。

     因此首先分析函数getGaussianKernel。

     功能:返回一个ksize*1的数组,数组元素满足高斯公式:

 

     其中只有系数alpha和参数sigma未知,sigma的求法为:

     如果输入sigma为非正,则计算公式为:sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8 .

     如果输入sigma为正,则就用该输入参数sigma。

     最后alpha为归一化系数,即计算出的ksize个数之和必须为1,所以后面只需求ksize个数,计算其和并求倒即可。

其源码及注释如下:

复制代码
cv::Mat cv::getGaussianKernel( int n, double sigma, int ktype )
{
    const int SMALL_GAUSSIAN_SIZE = 7;
    static const float small_gaussian_tab[][SMALL_GAUSSIAN_SIZE] =
    {
        {1.f},
        {0.25f, 0.5f, 0.25f},
        {0.0625f, 0.25f, 0.375f, 0.25f, 0.0625f},
        {0.03125f, 0.109375f, 0.21875f, 0.28125f, 0.21875f, 0.109375f, 0.03125f}
    };
    
      /*如果sigma小于0,且n为不大于7的奇整数,则核的滤波系数固定了,其固定在数组

        small_gaussian_tab中,根据其n的长度来选择具体的值 ,如果不满足上面的,则固定核为0
        固定核为0表示自己计算其核*/ 
        
    const float* fixed_kernel = n % 2 == 1 && n <= SMALL_GAUSSIAN_SIZE && sigma <= 0 ?
        small_gaussian_tab[n>>1] : 0;

    CV_Assert( ktype == CV_32F || ktype == CV_64F );//确保核元素为32位浮点数或者64位浮点数
    Mat kernel(n, 1, ktype);//建立一个n*1的数组kernel,一个Mat矩阵包括一个矩阵头和一个指向矩阵元素的指针
    float* cf = (float*)kernel.data;//定义指针cf指向kernel单精度浮点型数据
    double* cd = (double*)kernel.data;//定义指针cd指向kernerl双精度浮点型数据

    double sigmaX = sigma > 0 ? sigma : ((n-1)*0.5 - 1)*0.3 + 0.8;//当sigma小于0时,采用公式得到sigma(只与n有关)
    double scale2X = -0.5/(sigmaX*sigmaX);//高斯表达式后面要用到
    double sum = 0;

    int i;
    for( i = 0; i < n; i++ )
    {
        double x = i - (n-1)*0.5;
        //如果自己算其核的话,就常用公式exp(scale2X*x*x)计算,否则就用固定系数的核
        double t = fixed_kernel ? (double)fixed_kernel[i] : std::exp(scale2X*x*x);
        if( ktype == CV_32F )
        {
            cf[i] = (float)t;//单精度要求时存入cf数组中
            sum += cf[i];//进行归一化时要用到
        }
        else
        {
            cd[i] = t;//双精度时存入cd数组中
            sum += cd[i];
        }
    }

    sum = 1./sum;//归一化时核中各元素之和为1
    for( i = 0; i < n; i++ )
    {
        if( ktype == CV_32F )
            cf[i] = (float)(cf[i]*sum);//归一化后的单精度核元素
        else
            cd[i] *= sum;//归一化后的双精度核元素
    }

    return kernel;//返回n*1的数组,其元素或是单精度或是双精度,且符合高斯分布
}
复制代码

    下面该分析函数createSeparableLinearFilter了。

    功能为:创建一个图像滤波其引擎类,其主要处理的是原图像和目标图像数据格式的统以及滤波器核的合成。

其源码及注释如下:

复制代码
cv::Ptr cv::createSeparableLinearFilter(
    int _srcType, int _dstType,
    InputArray __rowKernel, InputArray __columnKernel,
    Point _anchor, double _delta,
    int _rowBorderType, int _columnBorderType,
    const Scalar& _borderValue )//InputArray是Mat类型,表示的是输入数组
{
    //_rowKernel存储其矩阵头,_columnKernel类似
    Mat _rowKernel = __rowKernel.getMat(), _columnKernel = __columnKernel.getMat();
    _srcType = CV_MAT_TYPE(_srcType);//求矩阵的数组类型,数据类型包过通道数,深度,和数据类型3种
    _dstType = CV_MAT_TYPE(_dstType);//类似
    int sdepth = CV_MAT_DEPTH(_srcType), ddepth = CV_MAT_DEPTH(_dstType);//求矩阵元素深度
    int cn = CV_MAT_CN(_srcType);//求矩阵元素通道
    CV_Assert( cn == CV_MAT_CN(_dstType) );//源数组和目标数组的通道数必须相等
    int rsize = _rowKernel.rows + _rowKernel.cols - 1;//求行长
    int csize = _columnKernel.rows + _columnKernel.cols - 1;//求列长
    if( _anchor.x < 0 )//求被滤波点的位置
        _anchor.x = rsize/2;
    if( _anchor.y < 0 )
        _anchor.y = csize/2;
    
    /*getKernelType()这个函数内部就不分析了,宏观上分析一下,其函数声明为:
    int getKernelType(InputArray kernel, Point anchor)
    功能:根据输入核系数矩阵kernel和被平滑点anchor来分析该核的类型,其类型主要有以下5种。
    1.普通核,没什么特点的
    2.对称核,anchor点在中心,且中心点2边的系数对称相等
    3.反对称核,anchor点也在中心,但中心点2边的系数对称相反
    4.平滑核,即每个数都是非负,且所有数相加为1
    5.整数核,即核内每个系数都是整数
    */    
    int rtype = getKernelType(_rowKernel,
        _rowKernel.rows == 1 ? Point(_anchor.x, 0) : Point(0, _anchor.x));//返回行矩阵核类型
    int ctype = getKernelType(_columnKernel,
        _columnKernel.rows == 1 ? Point(_anchor.y, 0) : Point(0, _anchor.y));//返回列矩阵核类型
    Mat rowKernel, columnKernel;

    /*在源代码types_c.h中有
    #define CV_8U   0
    #define CV_8S   1
    #define CV_16U  2
    #define CV_16S  3
    #define CV_32S  4
    #define CV_32F  5
    #define CV_64F  6
    */
    
    int bdepth = std::max(CV_32F,std::max(sdepth, ddepth));//在sdepth,ddepth,CV_32F(即5)中选出一个最大的数
    int bits = 0;

    if( sdepth == CV_8U &&
        ((rtype == KERNEL_SMOOTH+KERNEL_SYMMETRICAL &&//行列都是平滑对称核,且类型为8位无符号整型
          ctype == KERNEL_SMOOTH+KERNEL_SYMMETRICAL &&
          ddepth == CV_8U) ||
         ((rtype & (KERNEL_SYMMETRICAL+KERNEL_ASYMMETRICAL)) &&
          (ctype & (KERNEL_SYMMETRICAL+KERNEL_ASYMMETRICAL)) &&
          (rtype & ctype & KERNEL_INTEGER) &&   //或者行列都是整型对称或反对称核,且目标数组类型为16位有符号型
          ddepth == CV_16S)) )
    {
        bdepth = CV_32S; //重新给bdepth赋值
        bits = ddepth == CV_8U ? 8 : 0;//当目标矩阵类型为CV_8U时,位深就为8,否则为0
        
        /*convertTo()函数是源数组线性变换成目标数组,第二个参数为目标数组的类型*/
        _rowKernel.convertTo( rowKernel, CV_32S, 1 << bits );//将源行数组变换成32s的目标数组
        _columnKernel.convertTo( columnKernel, CV_32S, 1 << bits );//将源列数组变换成32s的目标数组
        bits *= 2;//为0或者为16
        _delta *= (1 << bits);//起放大作用?
    }
    else
    {
        if( _rowKernel.type() != bdepth )
            _rowKernel.convertTo( rowKernel, bdepth );//将源行数组深度转换为目的数组深度
        else
            rowKernel = _rowKernel;  
        if( _columnKernel.type() != bdepth )
            _columnKernel.convertTo( columnKernel, bdepth );//将源列数组深度转换为目的数组深度
        else
            columnKernel = _columnKernel;
    }//到目前这一行为止,也只是做了一个非常简单的工作,即把输入的行列矩阵数据类型统一

    int _bufType = CV_MAKETYPE(bdepth, cn);//创建一个缓冲数组类型,有深度和通道数2方面的信息?

    /*Ptr _rowFilter表示创建一个参数为BaseRowFilter的具体类Ptr*/
    Ptr _rowFilter = getLinearRowFilter(
        _srcType, _bufType, rowKernel, _anchor.x, rtype);
    Ptr _columnFilter = getLinearColumnFilter(
        _bufType, _dstType, columnKernel, _anchor.y, ctype, _delta, bits );//基本上也是完成数据类型的整理

    /*FilterEngine为一个通用的图像滤波类
    */
    
    return Ptr( new FilterEngine(Ptr(0), _rowFilter, _columnFilter,
        _srcType, _dstType, _bufType, _rowBorderType, _columnBorderType, _borderValue ));
    //新创建一个Ptr的模板类并用类FilterEngine的构造函数来初始化它
}
复制代码

     接着分析函数createGaussianFilter。

     功能:给定滤波核大小和类型,以及2个sigma,就可以得出一个二维滤波核。两个sigma允许输入负数等其他不常用的输入。

 

     其源码及注释如下:

复制代码
cv::Ptr cv::createGaussianFilter( int type, Size ksize,
                                        double sigma1, double sigma2,
                                        int borderType )
{
    int depth = CV_MAT_DEPTH(type);//取数组元素的深度
    if( sigma2 <= 0 )
        sigma2 = sigma1;//当第3个参数为非正时,取其与第二个参数相同的值

    // automatic detection of kernel size from sigma
    /*一般情况下满足sigma1>0*/
    if( ksize.width <= 0 && sigma1 > 0 )//当滤波器核的宽非正时,其宽要重新经过计算
    /*根据CV_8U来计算,核宽为接近7*sigma1或者9*sigma1*/
        ksize.width = cvRound(sigma1*(depth == CV_8U ? 3 : 4)*2 + 1)|1;
    if( ksize.height <= 0 && sigma2 > 0 )
        /*同理,核高根据CV_8U来计算,为接近7*sigma2或者9*sigma2*/
        ksize.height = cvRound(sigma2*(depth == CV_8U ? 3 : 4)*2 + 1)|1;

    CV_Assert( ksize.width > 0 && ksize.width % 2 == 1 &&
        ksize.height > 0 && ksize.height % 2 == 1 );//确保核宽和核高为正奇数

    sigma1 = std::max( sigma1, 0. );//sigma最小为0
    sigma2 = std::max( sigma2, 0. );

    Mat kx = getGaussianKernel( ksize.width, sigma1, std::max(depth, CV_32F) );//得到x方向一维高斯核
    Mat ky;
    if( ksize.height == ksize.width && std::abs(sigma1 - sigma2) < DBL_EPSILON )
        ky = kx;//如果核宽和核高相等,且两个sigma相差很小的情况下,y方向的高斯核去与x方向一样,减少计算量
    else
        ky = getGaussianKernel( ksize.height, sigma2, std::max(depth, CV_32F) );//否则计算y方向的高斯核系数

    return createSeparableLinearFilter( type, type, kx, ky, Point(-1,-1), 0, borderType );//返回2维图像滤波引擎
}
复制代码

     最后来看真正的高斯滤波函数GaussianBlur:

    功能:对输入图像_src进行滤波得到输出图像_dst,滤波核大小为ksize,滤波参数由sigma1和sigma2计算出,边缘扩展模式为borderType.

    其源代码和注释如下:

复制代码
void cv::GaussianBlur( InputArray _src, OutputArray _dst, Size ksize,
                   double sigma1, double sigma2,
                   int borderType )
{
    Mat src = _src.getMat();//创建一个矩阵src,利用_src的矩阵头信息
    _dst.create( src.size(), src.type() );//构造与输入矩阵同大小的目标矩阵
    Mat dst = _dst.getMat();//创建一个目标矩阵
    
    if( ksize.width == 1 && ksize.height == 1 )
    {
        src.copyTo(dst);//如果滤波器核的大小为1的话,则说明根本就不用滤波,输出矩阵与输入矩阵完全相同
        return;
    }

    if( borderType != BORDER_CONSTANT )//当边缘扩展不是常数扩展时
    {
        if( src.rows == 1 )
            ksize.height = 1;//如果输入矩阵是一个行向量,则滤波核的高强制为1
        if( src.cols == 1 )
            ksize.width = 1;//如果输入矩阵是一个列向量,则滤波核的宽强制为1
    }

    /*生成一个高斯滤波器引擎f*/
    Ptr f = createGaussianFilter( src.type(), ksize, sigma1, sigma2, borderType );
    f->apply( src, dst );//调用引擎函数,完成将输入矩阵src高斯滤波为输出矩阵dst
}
复制代码

     至此,函数GaussianBlur源码已经分析结束了,格式排版太累了!欢迎交流!


你可能感兴趣的:(opencv,图像处理--预处理)