openCV 中的高斯滤波GaussianBlur函数

在上次的opencv源码解析之滤波前言1中,按照opencv_tutorials.pdf中的滤波部分试了下常用的4种滤波器的使用方法。在opencv的C++中,这4个函数分别为:blur,GaussianBlur,meidaBlur,bilateralFilter.下面就这几个函数在opencv中的功能,以及参数做个介绍:

  1. 均值滤波:其函数声明为:void blur(InputArray src, OutputArray dst, Size ksize, Point anchor=Point(-1,-1), int borderType=BORDER_DEFAULT )。

这个函数在上一节中介绍过了,这里简单些一下。

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

参数:src和dst当然分别是输入图像和输出图像。size为均值滤波器模板大小。Anchor为锚点(具体什么没看源码不懂),如果为Point(-1,-1),则锚点是滤波器的中心点。borderType为边缘点插值类型。

理解:以原图对应像素为中心的与模板中心重叠,将模板覆盖领域内全部像素求均值就是滤波后像素的值了。

  1. 高斯滤波:其函数声明为: 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为边缘点插值类型。

理解:数字图像的滤波可以简单的这么理解,就是对原图像的每一个像素滤波,那么对应这个像素滤波后的值是根据其相邻像素(包括自己那个点)与一个滤波模板进行相乘即可。所以具体到高斯滤波,我们只要知道这个高斯滤波的模板即可。

那怎么确定这个模板呢?首先这个模板的大小为ksize,其每个数字的计算是这样的:

 

其中 是归一化系数,因为其和要为1.

为了简化,一般在二维图像处理中,ui和uj取0,sigma1和sigma2取相等。所以公式就简化为 :

因此很容易就计算出模板每个位置的数字了,简单吧!

但是要注意2点,第一点就是ksize的宽和高必须是奇数;第二点就是如果参数sigmaX=sigmaY=0,则实际用的是公式sigma = 0.3*((ksize-1)*0.5 - 1) + 0.8 .

  1. 中值滤波:其函数声明为void medianBlur(InputArray src, OutputArray dst, int ksize)。

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

参数:src和dst当然分别是输入图像和输出图像。ksize为均值滤波器模板大小,因为模板为正方形,所以只有一个参数。

理解:以原图对应像素为中心的与模板中心重叠,将模板覆盖领域内全部像素排序后的中间值就是滤波后像素的值了,所以模板长度必须为奇数。

  1. 双向滤波:其函数声明为:void bilateralFilter(InputArray src, OutputArray dst, int d, double sigmaColor, double sigmaSpace, int borderType=BORDER_DEFAULT )

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

参数:src和dst当然分别是输入图像和输出图像。d为每个像素领域的直径,sigmaColor为颜色空间的标准偏差,sigmaSpace为座标空间的标准偏差。borderType为边缘点插值类型。

理解:暂时不明白双向滤波的工作原理,以后有时间弄懂再补上吧,也欢迎大家补上。


函数声明为:

     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源码已经分析结束了,格式排版太累了!欢迎交流!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章