opencv-10-影像濾波-雜訊添加與均值濾波-含opencv C++ 程式碼實現

開始之前

再說上一篇文章中, 我們想按照雜訊產生, 然後將降噪的, 但是限於篇幅, 我就放在這一篇裡面了,
說起影像的雜訊問題就又回到了我們上一章的內容, 把雜訊當作訊號處理, 實際上數字影像處理實際上也是在進行數字訊號的處理過程, 我們這一章就是將濾除訊號的過程,

根據上一章的方式, 我們對影像添加雜訊, 然後計算 PSNR 與 SSIM 參數, 然後通過降噪, 再從新計算參數值, 比較我們演算法的效果 對比我們的演算法效果, 看正文吧

目錄

正文

我們在上一章給出了兩種雜訊的添加方式, 可以根據我們的需求添加椒鹽雜訊和高斯雜訊, 但是由於我們的雜訊添加使用了隨機數 , 導致我們在每次進行的結果可能不一致, 所以我們提前設計好雜訊情況, 將圖片存儲起來, 後面我們進行濾波的時候, 都使用一樣的照片, 這樣我們能夠保證每次的結果是一致的, 這樣就能進行演算法的對比了,

生成雜訊影像

我們在上一章給出了不同雜訊情況下影像結果, 也給出了一個鏈接, 對比了更多情況下的影像雜訊情況, 可以參考, 所以我們考慮五種情況吧 分別是低椒鹽雜訊, 高椒鹽雜訊,低高斯雜訊, 高高斯雜訊,低椒鹽混合低高斯雜訊, 高椒鹽混合高高斯雜訊. 我們分別將圖片進行存儲便能夠得到結果

說明一下: 之前的演算法使用的 lena 影像 忘記從那搞來的了, 今天對比了一下, 發現影像不太對, 我現在找到opencv的一個標準影像Lena.jpg, 我下載下來了, 轉換成了 lena.png 的影像, 可以見lena.png 這幅圖, 可以直接訪問鏈接 //gitee.com/schen00/BlogImage/raw/master/小書匠/1588298950276.png 直接下載即可..

這裡的處理演算法比較簡單, 我們來看程式碼

void MainWindow::testFunc1(void)
{
    // 用於生成 測試影像 一共6幅影像
    std::vector<cv::Mat> noise_img(6);
    // 初始化為原始影像
    for(auto &m: noise_img)
        m = gSrcImg.clone();

    // 分別添加 低, 高, 低混合, 高混合 共6幅影像
    addSaltNoise(noise_img[0],1000);
    addSaltNoise(noise_img[1],10000);

    addGaussianNoise(noise_img[2],0,1);
    addGaussianNoise(noise_img[3],100,10);

    addSaltNoise(noise_img[4],1000);
    addGaussianNoise(noise_img[4],0,1);

    addSaltNoise(noise_img[5],10000);
    addGaussianNoise(noise_img[5],100,10);


    // 計算 6幅影像的  psnr 和 ssim 然後存儲結果值
    std::vector<double> psnr(6);
    std::vector<cv::Scalar> mssim(6);

    QString res_temp = "image-%1: psnr:%2, mssim: B:%3 G:%4 R:%5 ";
    QString res_str;

    // 計算每個影像的 參數值, 然後存儲起來
    for(int i=0;i<6;i++)
    {
        psnr[i] = getPSNR(gSrcImg, noise_img[i]);
        mssim[i] = getMSSIM(gSrcImg, noise_img[i]);
        res_str = res_temp.arg(i+1)
                            .arg(psnr[i])
                            .arg(mssim[i].val[0])
                            .arg(mssim[i].val[1])
                            .arg(mssim[i].val[2]);

        ui->pt_log->appendPlainText(res_str);
        cv::imwrite("../testimages/noise/lena-" + std::to_string(i+1) + ".png", noise_img[i]);
    }
}

我們將圖片輸出, 然後輸出了每幅圖的參數, 同時將結果圖存儲下來, 由於我們在實際進行影像處理的時候會有很多

image-1: psnr:29.4922, mssim: B:0.880587 G:0.888243 R:0.944992 
image-2: psnr:19.4727, mssim: B:0.353134 G:0.383638 R:0.629353 
image-3: psnr:46.8705, mssim: B:0.991138 G:0.991732 R:0.991185 
image-4: psnr:9.15966, mssim: B:0.492354 G:0.482311 R:0.680167 
image-5: psnr:29.2807, mssim: B:0.874794 G:0.881488 R:0.935624 
image-6: psnr:8.92587, mssim: B:0.392531 G:0.393254 R:0.655795 

我這裡使用之前提到的 影像”拼接” 的方式將影像拼接起來, 這樣我們可以更為直觀的比較, 影像尺寸都是 \(512*512\), 如果需要可以裁剪出來,

CSDN 上傳會自己轉存圖片, 上傳資源比較麻煩, 懶得搞, 我這邊的圖片都存在了 gitee 的圖床上, 原圖存儲的 , 有需要的可以自己取用

雜訊影像"拼接"圖

傳統影像降噪演算法及其對比

之前提到過的【技術綜述】一文道盡傳統影像降噪方法
這篇文章講的還比較詳細, 大概的給我們講了一下傳統的降噪的方法,
這裡我想將各種濾波分開進行實現, 但是比較麻煩, 我就直接在一篇文章中寫了吧..

目前常用的降噪的方法主要可以分為空域降噪與頻域降噪, 空域濾波也是我們常用的使用空間處理的方式,計算量小, 簡單易用. 頻域比較難理解,計算量也比較大, 但是在很多情況結果比較有效..

所以我們主要的部分也是空間域處理的方式, 也比較直觀. opencv 的常式中Smoothing Images 章節大概講了一下目前使用的模糊方式, 其實模糊是相對的, 也是進行降噪的一個有利手段, 在處理掉雜訊的同時, 會導致原始影像的細節模糊, 進而丟失一部分影像資訊,我們之後看下影像測試結果. 同時在常式中還提到了一本書Computer Vision: Algorithms and Applications, 1st ed. 有中譯版本, 內容還不錯, 可以學習

opencv 核表示的演算法操作

在之前的內容中, 我們介紹了 opencv 核操作的方式, 對於影像的每個像素點的領域操作都可以使用 opencv 提供的 filter2D 方式進行指定核的運算, 我們能夠很容易核的操作, 也就是說我們將影像的演算法操作都可以轉換成影像的矩陣相乘的運算, 可以表示成

\[g(x,y) = M \cdot f(x,y)
\]

\(g(x,y)\) 用來表示結果影像, \(f(x,y)\) 表示原始影像, (x,y) 表示 列行座標, M 就是我們的影像運算矩陣,
我們後續都不再重複這些默認的操作, 希望能夠明白

一般來說, 我們進行矩陣運算的時候都會選擇方陣, 這樣不會由於矩陣的方向性導致的處理結果不同, 所以我們在一般情況下都會選擇
方陣, 比如上面進行的濾波 採用的就是 \(3×3\) 尺寸的影像, 而且由於我們的影像都是離散的, 所以 實際山採用的濾波的窗口邊長也是奇數值, 類似於 \(3,5,7,9…(2k+1)\) 的形式

均值濾波及C++ 程式碼實現

算術均值濾波

均值濾波(Mean Filter)的演算法就是對於每一個像素點, 將其設定為取其鄰域窗口內的所有像素的平均值
我們考慮一般形式的均值濾波器

\[g(x,y) = \frac{1}{mn} \sum_{(i,j) \in S_{xy}} f(i,j)
\]

那我們開始轉換一下, 則可以得到下相應的 均值濾波的矩陣

\[M = \frac{1}{9} \left [
\begin{array}{c}
1 & 1 & 1 \\ 1& 1 & 1 \\ 1 & 1 & 1
\end{array}
\right ]
\]

加權均值濾波

上面給出的均值濾波讓人容易的就會想一個問題, 對於不同的像素位置, 應該要賦予不同的權重值, 靠近中間的位置我們必須要考慮權重的問題, 這就是我們使用加權的均值濾波了, 一般來說我們最常用的矩陣為

\[M = \frac{1}{16} \left [
\begin{array}{c}
1 & 2 & 1 \\ 2& 4 & 2 \\ 1 & 2 & 1
\end{array}
\right ]
\]

這種矩陣對於中心元素的權重更高, 邊緣的較弱, 符合人的感覺, 具體的參數值可以自己調整, 前面的係數為矩陣內各個元素的總和, 是為了保證係數的歸一.

其實均值濾波器還有很多, 有興趣的推薦看影像處理基礎(3):均值濾波器及其變種 這篇文章, 寫的很好,介紹的很詳細,

C++手動實現均值濾波

我們這裡還是使用基礎的 算術均值濾波, 實現起來簡單一點, 邊界問題也不考慮, 這樣的話,我們處理的影像區域就稍微內縮小一個像素(1,1)- (m-2,n-2), 至於邊界問題, 處理起來還是要看
看起來還是比較簡單的, 我們按照給出的方法寫一下

// 默認 尺寸為3的  均值濾波 // 自定義實現 暫時不考慮參數異常等 處理
cv::Mat meanFilter(const cv::Mat src, int ksize = 3)
{
    // 邊界不處理, 直接忽略掉 使用原始圖, 拷貝, 避免直接修改
    cv::Mat dst = src.clone();

    // 直接出, 強制向下取整, // 暴力計算每一個 鄰域區間的值
    int k0 = ksize/2;
    int sum[3] = {0,0,0};
    for(int i=k0;i<dst.rows-k0-1;i++)
    {
        for(int j=k0;j<dst.cols-k0-1;j++)
        {
            // 清空 和數組
            memset(sum,0, sizeof(sum));

            // 計算三個通道的結果 和值 並計算 均值寫入目標影像
            for(int c = 0;c<3;c++)
            {
                for(int m = 0;m<ksize;m++)
                {
                    for (int n=0;n<ksize;n++)
                    {
                        sum[c] += src.at<cv::Vec3b>(i-k0+m,j-k0+n)[c];
                    }
                }
                // 計算均值寫入
                dst.at<cv::Vec3b>(i,j)[c] = cv::saturate_cast<uchar>((float)sum[c] /(ksize*ksize));
            }
        }
    }
    return dst;
}

中間部分寫的比較暴力,直接計算的窗口的和值, 然後進行均值得到的結果, 其實這裡如果要考慮窗口的和值, 我們沒必要重複計算一次, 每次計我們移動窗口後變化的兩個邊界差值即可, 這樣計算上的一點點速度優化, 我們這裡實現的只是一個 小小的demo , 有一定的效果即可

opencv 實現均值濾波

我們在之前的章節提到了 使用 filter2D 代替普通操作的方法,在這裡自然而然的想到怎麼去實現, 我們還是一樣的構造一個核, 然後計算結果即可, 這裡使用的核還是 上面提到的 算術均值濾波的核

// filter2D 實現 meanfilter
cv::Mat meanFilterByFilter2D(const cv::Mat src, int ksize = 3)
{
    cv::Mat kernel = (cv::Mat_<float>(ksize,ksize) << 1,1,1,1,1,1,1,1,1);
    kernel = kernel / 9.0f;
    cv::Mat dst;
    cv::filter2D(src,dst,src.depth(),kernel);
    return dst;
}

這裡實現起來真的很簡單, 這裡的 Mat 可以直接進行矩陣的操作, 每個元素都除以了9,這樣就簡單很多了,

接下來呢, opencv 對於這種基礎且常見的演算法肯定自己去在做了實現呀, 在我們上面也提到了opencv 的常式Smoothing Images, 提到了 一個模糊的函數, cv::blur, 這個函數可以調用盒式濾波器, 其實也就是均值濾波的通用形式, 前面的係數不一定而已, 我們先實現一下看下效果, 這裡跟上面寫成一樣的形式, 看起來好看一點, 其實只需要一句話便可以實現了 沒什麼難度, 至於效果, 我們馬上來對比

// 使用 blur 均值濾波
cv::Mat meanFilterByBlur(const cv::Mat src, int ksize = 3)
{
    cv::Mat dst;
    cv::blur(src,dst,cv::Size(ksize,ksize));
    return dst;
}

均值濾波演算法對比

我們上面提到了構造雜訊影像, 然後我們存儲了起來, 這裡我們選擇了一副影像進行直接給結果, 這裡我們選擇 高椒鹽雜訊的影像進行測試, 然後先看結果, 第一行表示雜訊影像與原始影像的參數值, 後面的三行依次是我們進行上面提到的三種實現出來的濾波方式得到的影像與原始影像進行的對比分析, 這裡還是能看到比較明顯的結果的,

image-noise: psnr:19.4727, mssim: B:0.353134 G:0.383638 R:0.629353 
image-1: psnr:26.505, mssim: B:0.603292 G:0.63888 R:0.806963 
image-2: psnr:26.7208, mssim: B:0.605704 G:0.641344 R:0.809115 
image-3: psnr:26.7208, mssim: B:0.605704 G:0.641344 R:0.809115 

我們看一下測試的程式碼, 還是之前的介面裡面的第二個按鈕執行的函數, 這裡我們第一個按鈕是去讀取我們之前存儲的雜訊影像, 按名稱讀取,
然後結果的時候, 我們是按照每幅影像進行的, 這裡暫時 高椒鹽雜訊的影像, 可以在上面給出的圖中看到

// 全局 雜訊影像數組, psnr 數組 mssim 數組
const std::string IMAGE_DIR ="../testimages/noise/";
std::vector<cv::Mat> gNoiseImg(6);
double psnr[6];
cv::Scalar mssim[6];
void MainWindow::testFunc1(void)
{
    // 用於讀取 測試圖片
    for(int i=0;i<6;i++)
    {
        gNoiseImg[i] = cv::imread(IMAGE_DIR + "lena-" + std::to_string(i+1) + ".png");
    }

    qDebug("ReadOK");
}
void MainWindow::testFunc2(void)
{
    QString res_temp = "image-%1: psnr:%2, mssim: B:%3 G:%4 R:%5 ";
    QString res_str;

    // 測試 均值濾波 三種方式的不同
    const int TEST = 1; // 使用統一的圖進行測試 暫時使用 高 椒鹽雜訊影像
    psnr[TEST] = getPSNR(gSrcImg, gNoiseImg[TEST]);
    mssim[TEST] = getMSSIM(gSrcImg,gNoiseImg[TEST]);

    res_str = res_temp.arg("noise")
            .arg(psnr[TEST])
            .arg(mssim[TEST].val[0])
            .arg(mssim[TEST].val[1])
            .arg(mssim[TEST].val[2]);

    // 雜訊的參數值
    ui->pt_log->appendPlainText(res_str);

    cv::Mat dst[3];

    dst[0] = meanFilter(gNoiseImg[TEST]);
    dst[1] = meanFilterByFilter2D(gNoiseImg[TEST]);
    dst[2] = meanFilterByBlur(gNoiseImg[TEST]);

    // 分別計算三種方式得到的濾波的效果 (結果圖與 原始圖比較)
    for(int i=0;i<3;i++)
    {
        psnr[TEST] = getPSNR(gSrcImg, dst[i]);
        mssim[TEST] = getMSSIM(gSrcImg,dst[i]);

        res_str = res_temp.arg(i+1)
                .arg(psnr[TEST])
                .arg(mssim[TEST].val[0])
                .arg(mssim[TEST].val[1])
                .arg(mssim[TEST].val[2]);

        // 雜訊的參數值
        ui->pt_log->appendPlainText(res_str);

        cv::imwrite(IMAGE_DIR + "dst_" + std::to_string(i+1)+".png",dst[i]);
    }
}

從上面的參數也能看出來, 後面兩種方法得到的結果影像是一模一樣的, 我們就不再進行展示, 先看 我們實現的均值濾波與自帶的均值濾波的影像區別

自己實現的均值濾波與自帶的均值濾波區別

其實吧, 總體的結果上是看不出來區別的, 主要是我們的演算法上沒有進行邊界的處理部分, 能在圖的中間部分看到稍微的幾個雜訊點沒有處理掉, 這可能也是我們的結果參數要稍微小一點的原因, 總體來說, 我們的演算法還是能夠進行均值濾波的, 而且跟自帶的處理結果也是一致的.

我這裡就有了一個疑問, 為什麼我們後面的結果就一模一樣了呢,blur 去調用了 filter2D? 然後我去看兩個函數的調用圖, 感覺問題應該是出在 cv::FilterEngine::apply 函數上,在後面就沒去研究了

其實這裡看了好久, 看得不是很懂, 就不再敘述了, 給出這兩張函數的調用圖 有興趣的自己去看

filter2D 調用圖

blur 函數調用圖

總結

其實這裡原本是計劃一起寫完的, 但是真的太傷了, 慢慢來吧, 中間容易跑偏, 因為這邊還要做比較多的東西, 所以寫的越來越慢, 不過至少目前興緻還是很高的 , 昨天發在部落格園的文章還被 ImageShop 大佬點贊, 還是很開心的,

其實越寫感覺自己越虛, 很多深入的東西自己都不能說摸透了, 還是要深入去研究了 但是寫的深入了看得就少了一點, 其實我盡量寫的淺一點, 因為很多人最開始就是搜索 blog 找答案的, 能看懂就行,
我會在後面將常用的都給寫完的, 盡量更新的快, 現在每天要花大量的時間去查, 去看還要寫, 希望我還能堅持下去, 加油.

廣告

就是我這裡還有之前的 函數調用圖都是自己使用 doxygen 和graphiz 參考繪製函數調用圖(call graph)(4):doxygen + graphviz 自己重新生成的opencv 的文檔圖, 至少在用起來還是比較簡單的 這個就是一個靜態的網頁, 我把它放在了我的 伺服器上, 這樣別人也能訪問有需要的可以看下, 基於 opencv 4.3.0 版本的文檔圖 //schen.xyz:89/opencv

沒有備案, 也沒有做防護, 別搞我

參考

  1. 《高斯雜訊_百度百科》. 見於 2020年4月30日. //baike.baidu.com/item/高斯雜訊.
  2. 《繪製函數調用圖(call graph)(4):doxygen + graphviz_運維_許振坪的專欄-CSDN部落格》. 見於 2020年5月2日. //blog.csdn.net/benkaoya/article/details/79763668.
  3. 知乎專欄. 《【技術綜述】一文道盡傳統影像降噪方法》. 見於 2020年4月29日. //zhuanlan.zhihu.com/p/51403693.
  4. 知乎專欄. 《可復現的影像降噪演算法總結》. 見於 2020年4月29日. //zhuanlan.zhihu.com/p/32502816.
  5. 《影像雜訊的成因分類與常見影像去噪演算法簡介_Java_qq_27606639的部落格-CSDN部落格》. 見於 2020年4月30日. //blog.csdn.net/qq_27606639/article/details/80912071.
  6. 《最小均方濾波器》. 收入 維基百科,自由的百科全書, 2018年3月9日. //zh.wikipedia.org/w/index.php?title=最小均方濾波器&oldid=48602322.
  7. 《Computer Vision: Algorithms and Applications, 1st ed.》 見於 2020年5月1日. //szeliski.org/Book/.
  8. 《OpenCV: Smoothing Images》. 見於 2020年5月1日. //docs.opencv.org/4.3.0/dc/dd3/tutorial_gausian_median_blur_bilateral_filter.html.
  9. 《openCV之中值濾波&均值濾波(及程式碼實現)_人工智慧_林小默-CSDN部落格》. 見於 2020年5月1日. //blog.csdn.net/weixin_37720172/article/details/72627543.