OpenCV(2.4.8)による新しいノイズ除去 ざっくり精度調べ

最近のOpenCVには、以下のような新しいフィルタ処理の関数が追加されています。

この関数がどれくらい使えるものなのか、サクッと試してみました。あまり学術的に正確でないかもしれないですが、ご容赦ください。間違っていたら教えてください。

cv::fastNlMeansDenoising

下でもう一度話題にしていますが、手法の中身についてはこちらが大変参考になります。
http://opencv.jp/opencv2-x-samples/non-local-means-filter

cv::adaptiveBilateralFilter

以下ドキュメントより。この関数、2.4.5には無く、2.4.8では有ったので、その間のいつごろかに追加されたようです。

A main part of our strategy will be to load each raw pixel once, and reuse it to calculate all pixels in the output (filtered) image that need this pixel value. The math of the filter is that of the usual bilateral filter, except that the sigma color is calculated in the neighborhood, and clamped by the optional input value.

http://docs.opencv.org/modules/imgproc/doc/filtering.html#adaptivebilateralfilter

sigma color is calculated in the neighborhood, and clamped by the optional input value. が肝なのでしょう。ご近所の画素からsigmaColorが決定される、ここがAdaptiveということでしょうか。

カラー画像でのテスト

OpenCV.jpの記事「Non-local Means Filterによるデノイジング」を参考にさせて頂きます。基本的にはそのままです。
http://opencv.jp/opencv2-x-samples/non-local-means-filter

ノイズをレナさんに付加して、各手法によるフィルタをかけ、PSNRを計測します。述べたように、最近のOpenCVではNLMeansが組み込まれたので、本記事ではそれを使うように変えています。ついでに言うとcv::PSNRという関数も増えたので、PSNR計算はそれを使っています。

コード

以下の各手法でノイズ除去を行っています。

  • Gaussian Filter
  • Median Filter
  • Bilateral Filter
  • Non-local Means Filter (NL Means Filter)
  • Non-local Means Filter (Colored)
  • Adaptive Bilateral Filter

カラー画像では fastNlMeansDenoisingColored を使った方が良さげ、らしいのですが、一応Colored無し版も呼び出してみています。

処理時間計測は、新しいC++ラムダ式を使ってみています。便利ですね。

#include <cstdio>
#include <vector>
#include <opencv2/opencv.hpp>

// ノイズ付加 (1ch)
void addNoiseMono(const cv::Mat &src, cv::Mat &dst, double sigma)
{
    cv::Mat s;
    src.convertTo(s, CV_16S);
    cv::Mat n(s.size(), CV_16S);
    cv::randn(n, 0, sigma);
    cv::Mat temp = s + n;
    temp.convertTo(dst, CV_8U);
}
// ノイズ付加 (3ch)
void addNoise(const cv::Mat &src, cv::Mat &dst, double sigma)
{
    std::vector<cv::Mat> s;
    std::vector<cv::Mat> d(src.channels());
    cv::split(src, s);
    for(int i=0; i<src.channels(); i++)
    {
        addNoiseMono(s[i], d[i], sigma);
    }
    cv::merge(d, dst);
}

// 時間計測 (ms)
template <typename TFunc>
double measureTime(TFunc func)
{
    int64 start = cv::getTickCount();
    func();
    int64 elapsed = cv::getTickCount() - start;
    return elapsed / cv::getTickFrequency() * 1000; // ms
}


int main(int argc, char *argv[])
{
    cv::Mat src = cv::imread("C:\\lena.tif", cv::IMREAD_COLOR);

    const int NoiseSigma = 15;
    cv::Mat noise;
    addNoise(src, noise, NoiseSigma);

    cv::Mat gaussian, median, bilateral, nlMeans, 
        nlMeansColored, adaptiveBilateral;

    std::printf("[RAW] PSNR=%.3f\n", cv::PSNR(src, noise));	
    // (1) Gaussian filter
    {
        double elapsed = measureTime(
            [&noise, &gaussian](){
                cv::GaussianBlur(noise, gaussian, cv::Size(7, 7), 5);
            });
        double psnr = cv::PSNR(src, gaussian);
        std::printf("[Gaussian] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }
    // (2) Median filter
    {
        double elapsed = measureTime(
            [&noise, &median](){
                cv::medianBlur(noise, median, 3);
            });
        double psnr = cv::PSNR(src, median);
        std::printf("[Median] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }
    // (3) Bilateral Filter
    {
        double elapsed = measureTime(
            [&noise, &bilateral]() {
                cv::bilateralFilter(noise, bilateral, 7, 35, 5);
            });
        double psnr = cv::PSNR(src, bilateral);
        std::printf("[Bilateral] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }
    // (4) Nonlocal means filter
    {
        double elapsed = measureTime(
            [&noise, &nlMeans, NoiseSigma]() {
                cv::fastNlMeansDenoising(noise, nlMeans, NoiseSigma, 3, 7);
            });
        double psnr = cv::PSNR(src, nlMeans);
        std::printf("[NLMeans] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }
    // (5) Nonlocal means filter (Colored)
    {
        double elapsed = measureTime(
            [&noise, &nlMeansColored, NoiseSigma]() {
                cv::fastNlMeansDenoisingColored(noise, nlMeansColored, 
                    NoiseSigma, NoiseSigma, 3, 7);
            });
        double psnr = cv::PSNR(src, nlMeansColored);
        std::printf("[NLMeans Colored] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }
    // (6) Adaptive bilateral filter
    {
        double elapsed = measureTime(
            [&noise, &adaptiveBilateral](){
                cv::adaptiveBilateralFilter(noise, adaptiveBilateral, 
                    cv::Size(7, 7), 5, 35);
            });
        double psnr = cv::PSNR(src, adaptiveBilateral);
        std::printf("[AdaptiveBilateral] PSNR=%.3f, time=%.3fms\n", psnr, elapsed);	
    }

    cv::imshow("src", src);
    cv::imshow("noise", noise);
    cv::imshow("Gaussian Filter", gaussian);
    cv::imshow("Median Filter", median);
    cv::imshow("Bilateral Filter", bilateral);
    cv::imshow("Non-local Means Filter", nlMeans);
    cv::imshow("Non-local Means Filter (Colored)", nlMeansColored);
    cv::imshow("Adaptive Bilateral Filter", adaptiveBilateral);
    cv::waitKey();

    return 0;
}

こんな画像が得られます。

ノイズ付加画像(元画像)
f:id:Schima:20140218195332p:plain

Bilateral
f:id:Schima:20140218195342p:plain

NLMeans (Colored)
f:id:Schima:20140218195351p:plain

Adaptive Bilateral
f:id:Schima:20140218195513p:plain

PSNR (dB)

上のコードで、ノイズの強さを指定するNoiseSigmaを変えながらPSNRを計測してみた結果をグラフにまとめてみました。横がnoiseSigma, 縦がPSNR[dB]です。(小さくて見えづらいですね。すみません。)
f:id:Schima:20140218194903p:plain

(Gaussianはアレなのでそれ以外を見ると、)NLMeans、優秀ですね。ノイズが強くなるにつれ優位性が強くなります。
Colored有りと無しの差は小さいですが、強いノイズにてわずかに有りが勝っています。といっても次に述べる処理時間が大きく増してくるので、それだけ時間かけてこの程度しか違わない、とも言えます。

Adaptive Bilateralは、このグラフ上では目立って良い点はありません。強いノイズにおいてBilateralより優位にはなっています。

処理時間 (ms)

noiseSigma=15の状態で、10回実行した平均です。

Gaussian Filter
2.74
Median Filter
0.91
Bilateral Filter
14.57
Non-local Means Filter
175.79
Non-local Means Filter (Colored)
278.71
Adaptive Bilateral Filter
190.22

NLMeansはかなり重いです。windowSizeをデフォルト引数のまま(7, 21)にするとさらに時間がかかります。

最速はMedian Filter。ノイズ除去の王道としてPSNRもそこそこ良い結果でした。

グレースケール画像 (文書画像)

ここまで空気のAdaptive Bilateral。さまざまに試してみた結果、結論を言うと、(少なくとも白地に黒の文書画像での)モスキートノイズにはよく効くようです。ここからはその話題です。

白地に "ABC" と書いた画像をペイントで作って、JPEG圧縮をかけてもやもやさせてみました。このノイズを取ってみましょう。
f:id:Schima:20140218201147p:plain

コード

もやっとしたJPEGと、もうひとつPSNR計測用にもやっとしないPNGでも元画像を作っておきます。そしてJPEGの方をもとに、先ほどのレナさん同様に各フィルタをかけていきます。

#include <cstdio>
#include <opencv2/opencv.hpp>

int main(int argc, char *argv[])
{
    cv::Mat org = cv::imread("C:\\abc.png", cv::IMREAD_COLOR);
    cv::Mat noisy = cv::imread("C:\\abc.jpg", cv::IMREAD_COLOR);

    cv::Mat gaussian, median, bilateral, nlMeans, adaptiveBilateral;

    // Raw
    std::printf("RAW: %.3f\n", cv::PSNR(org, noisy));	
    // (1) Gaussian filter
    cv::GaussianBlur(noisy, gaussian, cv::Size(5, 5), 3);
    std::printf("Gaussian: %.3f\n", cv::PSNR(org, gaussian));	
    // (2) Median filter
    cv::medianBlur(noisy, median, 3);
    std::printf("Median: %.3f\n", cv::PSNR(org, median));	
    // (3) Bilateral Filter
    cv::bilateralFilter(noisy, bilateral, 7, 35, 7);
    std::printf("Bilateral: %.3f\n", cv::PSNR(org, bilateral));	
    // (4) Nonlocal means filter
    cv::fastNlMeansDenoising(noisy, nlMeans, 3, 3, 7);
    std::printf("NLMeans: %.3f\n", cv::PSNR(org, nlMeans));		
    // (5) Adaptive bilateral filter
    cv::adaptiveBilateralFilter(noisy, adaptiveBilateral, cv::Size(7, 7), 7, 35);
    std::printf("Adaptive Bilateral: %.3f\n", cv::PSNR(org, adaptiveBilateral));	

    cv::imshow("org", org);
    cv::imshow("noisy", noisy);
    cv::imshow("Gaussian Filter", gaussian);
    cv::imshow("Median Filter", median);
    cv::imshow("Bilateral Filter", bilateral);
    cv::imshow("Non-local Means Filter", nlMeans);
    cv::imshow("Adaptive Bilateral Filter", adaptiveBilateral);
    cv::waitKey();
    cv::destroyAllWindows();

    return 0;
}

元画像
f:id:Schima:20140218201147p:plain
Bilateral
f:id:Schima:20140218201159p:plain
NLMeans
f:id:Schima:20140218201204p:plain
Adaptive Bilateral
f:id:Schima:20140218201207p:plain

Adaptive Bilateralは、見た目では大変良くモスキートノイズを取り除けています。パラメータの兼ね合いはあるかもしれませんが、試している感触では、おそらく優位性ははっきりあるのだろうと思われました。

PSNR (dB)

PNGのきれいな画像との対比です。いちおう見た目に違わず、Adaptive Bilateralが一位。

Gaussian Filter
16.576
Median Filter
22.543
Bilateral Filter
24.573
Non Local Means Filter
24.161
Adaptive Bilateral Filter
26.350


以上、文書画像処理屋としては、1つ良いメソッドが得られたということでうれしく思っております。