1. 程式人生 > >OpenCV4Android開發實錄(5):影象邊緣處理與非線性濾波(中值、雙邊)

OpenCV4Android開發實錄(5):影象邊緣處理與非線性濾波(中值、雙邊)

OpenCV4Android開發實錄(4):影象去噪與線性濾波(均值、方框、高斯)文章中,我們較為詳細地介紹了OpenCV中幾種常用的線性濾波方法原理和相關API的使用,本文將在此基礎上繼續講解OpenCV中中值和雙邊兩種非線性濾波,以及在影象濾波過程中OpenCV對影象邊緣的處理方法。

1. 非線性濾波

 由上一篇文章可知,線性濾波器考慮的是每個畫素的輸出值為其鄰域畫素的加權和,這種濾波器易於構造,且易於從頻率響應角度來進行分析。然而,在很多情況下,比如影象中偶爾出現很大的值時(散粒噪聲),如果使用高斯濾波器對影象進行模糊時,噪聲畫素並沒有被去除,它們只是變得柔和但仍然可見散粒,同時也帶來了影象細節模糊,這時候應該使用相關的非線性濾波器替代線性濾波器來處理。

1.1 中值濾波

1. 基本理論
中值濾波是基於排序統計理論的一種能夠有效抑制噪聲的非線性訊號處理的非線性濾波技術,它的基本原理是用畫素點鄰域灰度值的中值來替代該畫素點的灰度值,以便讓周圍的畫素值接近真實值,從而消除孤立點的噪聲點和保留影象細節資訊。由於中值濾波不依賴於鄰域內與要處理畫素真實值相差很大的值(噪聲畫素),中值濾波對處理斑點噪聲和椒鹽噪聲非常有效。中值濾波器在處理連續影象窗函式時與線性濾波器的工作方式類似,但濾波過程不再是鄰域畫素的加權運算。
 假設取核大小為3x3,取源影象一塊區域進行中值濾波所得結果,即先對該區域目標畫素值排序,然後將目標畫素(原始值為33)使用該區域畫素值中間值代替,即52。示意圖如下:
這裡寫圖片描述


2. 原始碼解析
(1)  medianBlu函式原型

// 中值濾波
// src:輸入源影象,Mat型別
// dst:輸出影象,Mat型別
// ksize:核大小
void medianBlur( InputArray src, OutputArray dst, int ksize );

(2) 原始碼解析
 medianBlur函式原始碼位於…\openCV3.3.0\opencv\sources\modules\imgproc\src的smooth.cpp中,具體內容如下:

void cv::medianBlur( InputArray _src0, OutputArray _dst, int ksize )
{
    CV_INSTRUMENT_REGION()
    CV_Assert( (ksize %
2 == 1) && (_src0.dims() <= 2 )); // ksize小於1,或源影象無資料直接返回 if( ksize <= 1 || _src0.empty() ) { _src0.copyTo(_dst); return; } CV_OCL_RUN(_dst.isUMat(), ocl_medianFilter(_src0,_dst, ksize)) // 目標Mat與輸入影象大小、畫素型別一致 Mat src0 = _src0.getMat(); _dst.create( src0.size(), src0.type() ); Mat dst = _dst.getMat(); CV_OVX_RUN(true, openvx_medianFilter(_src0, _dst, ksize)) CV_IPP_RUN_FAST(ipp_medianFilter(src0, dst, ksize)); #ifdef HAVE_TEGRA_OPTIMIZATION if (tegra::useTegra() && tegra::medianBlur(src0, dst, ksize)) return; #endif // 當核大小為3x3,或者5x5且源影象位深大於CV_8U或通道數量為2時 // useSortNet=true bool useSortNet = ksize == 3 || (ksize == 5 #if !(CV_SSE2 || CV_NEON) && ( src0.depth() > CV_8U || src0.channels() == 2 || src0.channels() > 4 ) #endif ); Mat src; // 以下程式碼開始進行中值濾波處理 // 當useSortNet=true,根據源影象的深度呼叫medianBlur_SortNet函式使用不同 // 的模板(核)大小進行中值濾波操作 if( useSortNet ) { if( dst.data != src0.data ) src = src0; else src0.copyTo(src); if( src.depth() == CV_8U ) medianBlur_SortNet<MinMax8u, MinMaxVec8u>( src, dst, ksize ); else if( src.depth() == CV_16U ) medianBlur_SortNet<MinMax16u, MinMaxVec16u>( src, dst, ksize ); else if( src.depth() == CV_16S ) medianBlur_SortNet<MinMax16s, MinMaxVec16s>( src, dst, ksize ); else if( src.depth() == CV_32F ) medianBlur_SortNet<MinMax32f, MinMaxVec32f>( src, dst, ksize ); else CV_Error(CV_StsUnsupportedFormat, ""); return; } // 當useSortNet=false情況 else { // 使用BORDER_REPLICATE方法處理影象邊緣 cv::copyMakeBorder( src0, src, 0, 0, ksize/2, ksize/2, BORDER_REPLICATE ); int cn = src0.channels(); CV_Assert( src.depth() == CV_8U && (cn == 1 || cn == 3 || cn == 4) ); double img_size_mp = (double)(src0.total())/(1 << 20); // 根據條件呼叫合適方法進行中值濾波,這些方法實現的是具體的中值濾波演算法 // 這裡我們就不繼續看進去了 if( ksize <= 3 + (img_size_mp < 1 ? 12 : img_size_mp < 4 ? 6 : 2)* (MEDIAN_HAVE_SIMD && (checkHardwareSupport(CV_CPU_SSE2) || checkHardwareSupport(CV_CPU_NEON)) ? 1 : 3)) medianBlur_8u_Om( src, dst, ksize ); else medianBlur_8u_O1( src, dst, ksize ); } }

3. 實戰演練

void ImageSmoothing::mediaBlurImage(Mat srcImage, Mat &dstImage, int ksize) {
    if (ksize <= 0) {
        printf("ksize should be > 0, and the best to be odd number like 1,3,5,7...\n");
        return;
    }
    if (!srcImage.data) {
        printf("open source image error!\n");
        return;
    }
    medianBlur(srcImage, dstImage, ksize);
}

// 新增椒鹽噪聲,n為噪聲點數量
void addSaltNoise(Mat &image, int n){
    // 新增白點
    for (int k = 0; k<n; k++)
    {
        int i = rand() % image.cols;
        int j = rand() % image.rows;
        if (image.channels() == 1){
            image.at<uchar>(j, i) = 255;
        }
        else if (image.channels() == 3){
            image.at<Vec3b>(j, i)[0] = 255;
            image.at<Vec3b>(j, i)[1] = 255;
            image.at<Vec3b>(j, i)[2] = 255;
        }
    }
    // 新增黑點
    for (int k = 0; k<n; k++)
    {
        int i = rand() % image.cols;
        int j = rand() % image.rows;
        if (image.channels() == 1) {
            image.at<uchar>(j, i) = 0;
        }
        else if (image.channels() == 3) {
            image.at<Vec3b>(j, i)[0] = 0;
            image.at<Vec3b>(j, i)[1] = 0;
            image.at<Vec3b>(j, i)[2] = 0;
        }
    }
}

效果演示:

  • 高斯濾波處理效果
    這裡寫圖片描述

  • 中值濾波處理效果
    這裡寫圖片描述

 從兩個結果來看,中值濾波對椒鹽噪聲的處理效果遠好於高斯濾波(其他線性濾波器效果一樣的),它不僅消除了影象中的椒鹽噪聲,同時能夠有效地保護影象細節(包括影象邊緣資訊)。雖然中值濾波相較線性濾在處理這類噪聲優勢非常明顯,但不可否認的是中值濾波花費的時間要高於線性濾波,這是因為中值濾波在處理每個畫素時需要對包含該畫素在內的鄰域畫素的灰度值進行排序,得到所有畫素點灰度值的中值。
 總而言之,中值濾波在一定條件下能夠克服線性濾波器所帶來的影象細節模糊,尤其對椒鹽類似噪聲非常有效,但是對一些影象細節很多的影象仍然不合適。

1.2 雙邊濾波

1. 基本理論
雙邊濾波(Bilateral filter)是一種非線性的濾波方法,是結合影象的空間鄰近度和畫素值相似度的一種折衷處理,同時考慮空域資訊和灰度相似性,達到保邊去噪的目的。具有簡單、非迭代、區域性的特點。雙邊濾波器的好處是可以做邊緣儲存(edge preserving),一般過去用的維納濾波或者高斯濾波去降噪,都會較明顯地模糊邊緣,對於高頻細節的保護效果並不明顯。雙邊濾波器顧名思義比高斯濾波多了一個高斯方差sigma-d,它是基於空間分佈的高斯濾波函式,所以在邊緣附近,離的較遠的畫素不會太多影響到邊緣上的畫素值,這樣就保證了邊緣附近畫素值的儲存。但是由於儲存了過多的高頻資訊,對於彩色影象裡的高頻噪聲,雙邊濾波器不能夠乾淨的濾掉,只能夠對於低頻資訊進行較好的濾波。
 在雙邊濾波器中,輸出畫素的值依賴於鄰域畫素值的加權值組合,公式如下:
這裡寫圖片描述
其中,w(i,j,k,l)是濾波器的加權係數,即核,取決於定義域核和值域核的乘積。
(1) 定義核域
這裡寫圖片描述
(2)值域核
這裡寫圖片描述
 雙邊濾波處理示意圖:
這裡寫圖片描述
2. 原始碼解析
(1)  bilateralFilter函式原型

// 雙邊濾波
// _src:源影象,只支援畫素型別為CV_8U和CV_32F 
// _dst:目標影象,影象尺寸和型別與源影象一致
// d:濾波核直徑
// sigmaColor:雙邊濾波核高斯係數標準差
// sigmaSpace:/雙邊濾波核空間域係數標準差
// borderType:指定影象邊緣方法,預設為BORDER_DEFAULT
void cv::bilateralFilter( InputArray _src, OutputArray _dst, int d,
                      double sigmaColor, double sigmaSpace,
                      int borderType )

(2) 原始碼解析
 bilateralFilter函式原始碼位於…\openCV3.3.0\opencv\sources\modules\imgproc\src的smooth.cpp中,具體內容如下:

void cv::bilateralFilter( InputArray _src, OutputArray _dst, int d,
                      double sigmaColor, double sigmaSpace,
                      int borderType )
{
    CV_INSTRUMENT_REGION()
    // 初始化dst Mat
    _dst.create( _src.size(), _src.type() );

    CV_OCL_RUN(_src.dims() <= 2 && _dst.isUMat(),
               ocl_bilateralFilter_8u(_src, _dst, d, sigmaColor, sigmaSpace, borderType))

    Mat src = _src.getMat(), dst = _dst.getMat();

    CV_IPP_RUN_FAST(ipp_bilateralFilter(src, dst, d, sigmaColor, sigmaSpace, borderType));
    // 雙邊濾波只支援CV_8U和CV_32F 兩種型別影象
    // 通過呼叫演算法實現函式bilateralFilter_8u和bilateralFilter_32f實現
    if( src.depth() == CV_8U )
        bilateralFilter_8u( src, dst, d, sigmaColor, sigmaSpace, borderType );
    else if( src.depth() == CV_32F )
        bilateralFilter_32f( src, dst, d, sigmaColor, sigmaSpace, borderType );
    else
        CV_Error( CV_StsUnsupportedFormat,
        "Bilateral filtering is only implemented for 8u and 32f images" );
}

3. 實戰演練

void ImageSmoothing::bilateralFilterImage(Mat srcImage, Mat &dstImage, int d, double sigmaColor, double sigmaSpace) {
    if (!srcImage.data) {
        printf("open source image error!\n");
        return;
    }
    bilateralFilter(srcImage, dstImage, d, sigmaColor, sigmaSpace);
}

效果圖
這裡寫圖片描述

2. 影象邊緣處理

 經過了解,無論是線性濾波還是非線性濾波,它們均有一個特點,即利用視窗函式(或稱核、模板)依次掃描影象上的畫素,然後根據相應的線性濾波或非線性濾波的規則計算得出被平滑處理畫素的值,直到掃描完整副影象為止。掃描的過程如下圖所示:
這裡寫圖片描述
 上圖演示的是核大小為3x3、錨點為核中心位置時影象濾波處理的過程,可以看出原始影象的邊緣有一個畫素沒有被處理,且這個未處理的邊緣畫素數量是隨核增大而變大的,而未被處理的影象邊緣畫素資訊將被丟失。下圖為核大小為5x5影象濾波演示圖,可以發現影象邊緣有兩個畫素未被處理。
這裡寫圖片描述

 OpenCV影象邊緣處理方法

 不知大家在呼叫OpenCV線性和非線性相關濾波函式API時,是否發現它們的引數列表中均需傳入一個int型別的borderType引數,且預設值為BORDER_DEFAULT。事實上,這個引數就是OpenCV提供用於指定如何去處理影象邊緣的方法,除了BORDER_DEFAULT型別,還包括BORDER_CONSTANT、BORDER_REPLICATE以及BORDER_WRAP,它們處理的原理是在卷積開始之前增加邊緣畫素,填充的畫素值為0或者RGB黑色,比如3x3在四周各填充1個畫素的邊緣,這樣就確保影象的邊緣被處理,在卷積處理之後再去掉這些邊緣。
1. copyMakeBorder函式講解

// 擴充src的邊緣將影象變大,然後以各種外插方式自動填充影象邊界
//  src:源影象,Mat型別
//  dst:輸出影象,Mat型別
//  top、bottom、left、right:影象邊緣增加的畫素數量
//  borderType:指定邊緣處理的方法,一共有四種
//  value:顏色值。當borderType=BORDER_CONSTANT設定邊緣畫素的顏色值
void copyMakeBorder(InputArray src, OutputArray dst,
             int top, int bottom, int left, int right,
             int borderType, const Scalar& value = Scalar() );

 影象邊緣處理常見方法
 (1) BORDER_DEFAULT:即BORDER_REFLECT_101,以最邊緣畫素為軸對稱;
 (2) BORDER_CONSTANT:常量法,使用固定顏色value來填充;
 (3) BORDER_REPLICATE :複製法,複製最邊緣畫素來填充
 (4) BORDER_WRAP:對稱法,用另外一邊的畫素來補償填充;
2. 程式碼實現

void testCopyMakeBorder(int type) {
    Mat src, dst;
    src = imread("hashiqi.jpg");
    if (!src.data) {
        printf("載入源影象失敗");
    }
    namedWindow("源影象");
    imshow("源影象", src);
    // 要處理邊緣畫素數量(測試用,可自定義)
    int top = (int)(0.05*src.rows);
    int bottom = (int)(0.05*src.rows);
    int left = (int)(0.05*src.cols);
    int right = (int)(0.05*src.cols);
    // 邊緣處理,固定填充顏色為藍色(OpenCV預設為BGR)
    copyMakeBorder(src, dst, top, bottom, left, right, type, 
            Scalar(255,0,0));
    imshow("BORDER_DEFAULT效果",dst);
}

 (1) BORDER_DEFAULT效果:
這裡寫圖片描述
 (2) BORDER_CONSTANT效果:
這裡寫圖片描述
 (3) BORDER_REPLICATE效果:
這裡寫圖片描述
 (4) BORDER_WRAP效果:
這裡寫圖片描述