最近のOpenCVには、以下のような新しいフィルタ処理の関数が追加されています。
- cv::fastNlMeansDenoising (photo)
- cv::adaptiveBilateralFilter (imgproc)
この関数がどれくらい使えるものなのか、サクッと試してみました。あまり学術的に正確でないかもしれないですが、ご容赦ください。間違っていたら教えてください。
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; }
こんな画像が得られます。
ノイズ付加画像(元画像)
Bilateral
NLMeans (Colored)
Adaptive Bilateral
PSNR (dB)
上のコードで、ノイズの強さを指定するNoiseSigmaを変えながらPSNRを計測してみた結果をグラフにまとめてみました。横がnoiseSigma, 縦がPSNR[dB]です。(小さくて見えづらいですね。すみません。)
(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圧縮をかけてもやもやさせてみました。このノイズを取ってみましょう。
コード
もやっとした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; }
元画像
Bilateral
NLMeans
Adaptive Bilateral
Adaptive Bilateralは、見た目では大変良くモスキートノイズを取り除けています。パラメータの兼ね合いはあるかもしれませんが、試している感触では、おそらく優位性ははっきりあるのだろうと思われました。