计算机视觉
图像处理

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

这一节来真正进入Opencv的源码分析中,本次分析的函数是GaussianBlur(),即高斯滤波函数。在前前面博文《opencv源码解析之滤波前言》中已经阐述了这个函数的用法,即:

     其函数声明为:

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

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

接着分析函数createGaussianFilter。

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

 

其源码及注释如下:

cv::Ptr<cv::FilterEngine> 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<FilterEngine> f = createGaussianFilter( src.type(), ksize, sigma1, sigma2, borderType );
    f->apply( src, dst );//调用引擎函数,完成将输入矩阵src高斯滤波为输出矩阵dst
}

     至此,函数GaussianBlur源码已经分析结束了!

转载注明来源:CV视觉网 » opencv源码解析之(4):GaussianBlur()

分享到:更多 ()
扫描二维码,给作者 打赏
pay_weixinpay_weixin

请选择你看完该文章的感受:

1不错 0超赞 0无聊 0扯淡 0不解 0路过

评论 3

评论前必须登录!