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源碼已經分析結束了,格式排版太累了!歡迎交流!

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