當前位置: 妍妍網 > 碼農

OpenCV降噪演算法之非局部均值濾波

2024-05-31碼農

1. 非局部均值濾波

非局部均值濾波(Non-Local Means,NL-Means)是一種 非線性 的影像去噪演算法。它基於影像中的像素具有相似結構這一假設,利用影像的全域資訊來對影像進行去噪。

1.1 全域演算法 VS 局部演算法

非局部均值濾波在計算每個像素點的估計值時,會考慮影像中所有與該像素點具有 相似 鄰域結構的像素點。因此,非局部均值濾波是一種 全域演算法

那麽相對於全域演算法的 局部演算法 是什麽呢? 局部演算法 是指僅利用影像局部資訊進行處理的演算法。例如其鄰域視窗內的資訊,來計算該像素點的估計值。常用的局部演算法包括:

  • 均值濾波

  • 中值濾波

  • 高斯濾波

  • 雙邊濾波

  • 局部演算法的優點是計算量小,速度快。但其缺點是去噪效果有限,容易造成影像細節遺失。

    1.2 均值濾波和非局部均值濾波的區別

    均值濾波:對於影像中的每個像素點,其濾波值是其鄰域視窗內所有像素點的平均值。

    非局部均值濾波:該演算法需要計算影像中 所有像素 與當前像素之間的相似性。首先需要定義一個 大的搜尋視窗 一個小的鄰域視窗 。搜尋視窗用於搜尋與當前像素點具有相似鄰域結構的像素點,鄰域視窗用於計算像素點之間的相似度。鄰域視窗在搜尋視窗中滑動,對於搜尋視窗內的每個像素點,計算其與當前像素點的鄰域視窗的相似度,並將其作為 權重 。相似度越大則權重越大。

    非局部均值濾波的基本原理與均值濾波類似都要取平均值,但是非局部均值濾波在計算中加入了每一個點的權重。

    非局部均值濾波是一種比均值濾波更先進的影像去噪方法,但其計算量也更大。

    1.3 非局部均值濾波的原理

    非局部均值濾波的公式如下:

    其中,w(x,y) 是一個權重,表示在原始影像 v 中,像素 x 和像素 y 的相似度。是像素 x 的搜尋視窗。是濾波後的影像。

    非局部均值濾波的示意.png

    常用的 相似度度量 方法包括:歐式距離、高斯相似度、L1 範數、L2 範數、MSE 等等。

    衡量兩個影像塊的相似度最常用的方法是計算他們之間的歐氏距離:

    其中:

  • n(x) 是一個 歸一化 的因子,是所有權重的和。對每個權重除以該因子後,使得權重滿足和為1的條件。

  • a 是高斯核的標準差。在求歐式距離的時候,不同位置的像素的權重是不一樣的,距離塊的中心越近,權重越大,距離中心越遠,權重越小,權重服從高斯分布。實際計算中常常采用均勻分布的權重。

  • h 是濾波系數。控制指數函式的衰減從而改變歐氏距離的權重,h >0 。

  • 非局部均值濾波的復雜度跟 影像的大小、搜尋視窗的大小、相似度計算方法、權重計算方法 密切相關。

    2. 非局部均值濾波的實作

    下面的例子,是在影像中添加斑點雜訊,然後用非局部均值濾波消除雜訊。

    #include<opencv2/opencv.hpp>
    #include<opencv2/core.hpp>
    #include<opencv2/highgui.hpp>
    #include<random>
    usingnamespacestd;
    usingnamespace cv;
    voidaddSpeckleNoise(Mat& image, double scale, Mat &dst){
    dst = image.clone();
    RNG rng;
    dst.forEach<Pixel>([&](Pixel &p, constint * position) -> void {
    int row = position[0];
    int col = position[1];
    double random_value = rng.uniform(0.01.0);
    double noise_intensity = random_value * scale;
    dst.at<Vec3b>(row, col) = dst.at<Vec3b>(row, col) + Vec3b(noise_intensity * 255, noise_intensity * 255, noise_intensity * 255);
    });
    }
    //NL-means 演算法的實作
    voidnonlocalMeansFilter(Mat& src, Mat& dst, int templeteWindowSize, int searchWindowSize, double h, double sigma = 0.0){
    //鄰域的大小不能超過搜尋視窗的大小
    if (templeteWindowSize > searchWindowSize){
    cout << "searchWindowSize should be larger than templeteWindowSize" << endl;
    return;
    }
    if (dst.empty())
    dst = Mat::zeros(src.size(), src.type());
    constint tr = templeteWindowSize >> 1;//tr為鄰域的中心位置
    constint sr = searchWindowSize >> 1; //sr為搜尋域的中心位置
    constint bb = sr + tr;//需增加的邊界寬度
    constint D = searchWindowSize*searchWindowSize;//搜尋域中的元素個數
    constint H = D / 2 + 1;//搜尋域中的中心點位置
    constdouble div = 1.0 / (double)D;//均勻分布時,搜尋域中的每個點的權重大小
    constint tD = templeteWindowSize*templeteWindowSize;//鄰域中的元素個數
    constdouble tdiv = 1.0 / (double)(tD);//均勻分布時,搜尋域中的每個點的權重大小
    //擴充邊界
    Mat boardSrc;
    copyMakeBorder(src, boardSrc, bb, bb, bb, bb, cv::BORDER_DEFAULT);
    //weight computation;
    vector<doubleweight(256 * 256 * src.channels());
    double* w = &weight[0];
    constdouble gauss_sd = (sigma == 0.0) ? h : sigma;//高斯標準差
    double gauss_color_coeff = -(1.0 / (double)(src.channels())) * (1.0 / (h*h));//高斯顏色系數
    int emax=0;
    //w[i]保存變異數,即鄰域平均歐氏距離對應的高斯加權權重,供後面計算出歐式距離後呼叫
    for (int i = 0; i < weight.size(); i++){
    double v = std::exp(max(i - 2.0*gauss_sd*gauss_sd, 0.0)*gauss_color_coeff);
    w[i] = v;
    if (v<0.001){
    emax = i;
    break;
    }
    }
    for (int i = emax; i < weight.size(); i++)
    w[i] = 0.0;
    int height = src.rows;
    int width = src.cols;
    if (src.channels() == 3){
    constint cstep = (int)boardSrc.step - templeteWindowSize * 3;
    constint csstep = (int)boardSrc.step - searchWindowSize * 3;
    #pragma omp parallel for
    for (int j = 0; j<height; j++){
    uchar* d = dst.ptr(j);
    int* ww = newint[D];//D 為搜尋域中的元質數量,ww用於記錄搜尋域每個點的鄰域變異數
    double* nw = newdouble[D];//根據變異數大小高斯加權歸一化後的權重
    for (int i = 0; i<width; i++){
    double tweight = 0.0;
    //search loop
    uchar* tprt = boardSrc.data + boardSrc.step * (sr + j) + 3 * (sr + i);
    uchar* sptr2 = boardSrc.data + boardSrc.step * j + 3 * i;
    for (int l = searchWindowSize, count = D - 1; l--;){
    uchar* sptr = sptr2 + boardSrc.step * (l);
    for (int k = searchWindowSize; k--;){
    //templete loop
    int e = 0;
    uchar* t = tprt;
    uchar* s = sptr + 3 * k;
    for (int n = templeteWindowSize; n--;){
    for (int m = templeteWindowSize; m--;){
    // computing color L2 norm
    e += (s[0] - t[0])*(s[0] - t[0]) + (s[1] - t[1])*(s[1] - t[1]) + (s[2] - t[2])*(s[2] - t[2]);//L2 norm
    s += 3;
    t += 3;
    }
    t += cstep;
    s += cstep;
    }
    constint ediv = e*tdiv;
    ww[count--] = ediv;
    //get weighted Euclidean distance
    tweight += w[ediv];
    }
    }
    //weight normalization
    if (tweight == 0.0){
    for (int z = 0; z<D; z++) nw[z] = 0;
    nw[H] = 1;
    }else{
    double itweight = 1.0 / (double)tweight;
    for (int z = 0; z<D; z++) nw[z] = w[ww[z]] * itweight;
    }
    double r = 0.0, g = 0.0, b = 0.0;
    uchar* s = boardSrc.ptr(j + tr); s += 3 * (tr + i);
    for (int l = searchWindowSize, count = 0; l--;){
    for (int k = searchWindowSize; k--;)
    {
    r += s[0] * nw[count];
    g += s[1] * nw[count];
    b += s[2] * nw[count++];
    s += 3;
    }
    s += csstep;
    }
    d[0] = saturate_cast<uchar>(r);
    d[1] = saturate_cast<uchar>(g);
    d[2] = saturate_cast<uchar>(b);
    d += 3;
    }//i
    delete[] ww;
    delete[] nw;
    }//j
    elseif (src.channels() == 1){
    constint cstep = (int)boardSrc.step - templeteWindowSize;//在鄰域比較時,從鄰域的上一行末尾跳至下一行開頭
    constint csstep = (int)boardSrc.step - searchWindowSize;//搜尋域迴圈中,從搜尋域的上一行末尾跳至下一行開頭
    #pragma omp parallel for
    for (int j = 0; j<height; j++){
    uchar* d = dst.ptr(j);
    int* ww = newint[D];
    double* nw = newdouble[D];
    for (int i = 0; i<width; i++){
    double tweight = 0.0;
    uchar* tprt = boardSrc.data + boardSrc.step * (sr + j) + (sr + i);
    uchar* sptr2 = boardSrc.data + boardSrc.step * j + i;
    for (int l = searchWindowSize, count = D - 1; l--;){
    uchar* sptr = sptr2 + boardSrc.step * (l);
    for (int k = searchWindowSize; k--;){
    int e = 0;
    uchar* t = tprt;
    uchar* s = sptr + k;
    for (int n = templeteWindowSize; n--;){
    for (int m = templeteWindowSize; m--;){
    e += (*s - *t)*(*s - *t);
    s++;
    t++;
    }
    t += cstep;
    s += cstep;
    }
    constint ediv = e*tdiv;
    ww[count--] = ediv;
    tweight += w[ediv];
    }
    }
    if (tweight == 0.0){
    for (int z = 0; z<D; z++) nw[z] = 0;
    nw[H] = 1;
    }else{
    double itweight = 1.0 / (double)tweight;
    for (int z = 0; z<D; z++) nw[z] = w[ww[z]] * itweight;
    }
    double v = 0.0;
    uchar* s = boardSrc.ptr(j + tr); s += (tr + i);
    for (int l = searchWindowSize, count = 0; l--;){
    for (int k = searchWindowSize; k--;){
    v += *(s++)*nw[count++];
    }
    s += csstep;
    }
    *(d++) = saturate_cast<uchar>(v);
    }//i
    delete[] ww;
    delete[] nw;
    }//j
    }
    }
    intmain(){
    Mat src = imread(".../girl.jpg");
    imshow("src", src);
    Mat result;
    Mat dst4;
    addSpeckleNoise(src,0.5,dst4);
    imshow("addSpeckleNoise", dst4);
    nonlocalMeansFilter(dst4, result, 31540,40);
    imshow("removeSpeckleNoise", result);
    waitKey(0);
    return0;
    }



















    斑點雜訊和使用非局部均值濾波後的效果.png

    OpenCV 提供了非局部均值濾波演算法,並對其進行了加速。

  • fastNlMeansDenoising() :對單個灰度影像進行去噪。

  • fastNlMeansDenoisingColored() :對彩色影像進行去噪。

  • fastNlMeansDenoisingMulti() :用於連續相關灰度影像的快速去噪(例如視訊中的連續灰度幀)。

  • fastNlMeansDenoisingColoredMulti() :用於連續相關彩色影像的快速去噪(例如視訊中的連續彩色幀)。

  • intmain(){
    Mat src = imread(.../girl.jpg");
    imshow("
    src", src);
    Mat result;
    Mat dst4;
    addSpeckleNoise(src,0.5,dst4);
    imshow("

    addSpeckleNoise", dst4);
    fastNlMeansDenoisingColored(dst4,result,40,40);
    imshow("
    removeSpeckleNoise2", result);
    waitKey(0);
    return 0;
    }

    3. 總結

    非局部均值濾波能夠有效地去除影像中的各種雜訊,包括高斯雜訊、椒鹽雜訊、紋理雜訊等。非局部均值濾波能夠較好地保留影像的細節,對雜訊的型別和分布不敏感,具有較強的魯棒性。

    當然,非局部均值濾波的缺點也很明顯:計算量大,容易受到影像邊緣的影響等等。

    非局部均值濾波的計算量大、速度慢是可以透過減少搜尋視窗大小、采用快速相似度計算方法、利用影像的稀疏性、並列化計算、利用硬體加速等方法來加速。

    推薦閱讀