以前の記事で、複数フレームにわたる連写画像を使ってノイズ除去のコードを書いてみましたが、OpenCVにもその機能が備わっているようです。今回はその効果を確認するとともに、以前書いた記事との比較をしてみます。
複数画像によるNon-Local Means
画像のノイズ除去手法として、Non-local Means Filterというものが知られています。これは画像の中からちょっと遠いところも含めて、画像の類似性を探して、ノイズを除去する手法です。小さなウィンドウをちょいと遠くからずらして、相関が高いところの画像とそのウィンドウとのフィルタ合成をするものです。
動画のような複数フレームモノであれば、隣接するフレーム間では同一座標付近に似たような特徴が出るだろう。ということで、時間軸方向に似たテクスチャを探して、ノイズ除去することが可能です。以前の記事
これでは、その特徴を使ってノイズ除去を大真面目にやってみました。iPhoneのような、小さいセンサを使ったノイズが乗りまくった画像をソースにしても、それなりに拡大にも耐えられるような画像ができました。
今回はこれとOpenCVのそれを比べてみようと思います。
fastNlMeansDenoisingColoredMulti
Non-Local MeansはOpenCVにも実装されています。簡単に調べたところ、
- cv2.fastNlMeansDenoising() – 一枚のグレースケール画像に対する関数
- cv2.fastNlMeansDenoisingColored() – 一枚のカラー画像に対する関数
- cv2.fastNlMeansDenoisingMulti() – 短時間に撮影されたグレースケール画像列に対する関数
- cv2.fastNlMeansDenoisingColoredMulti() – 短時間に撮影されたカラー画像列に対する関数
と複数用意されています。今回の場合には、複数枚のカラー画像なので、fastNlMeansDenoisingColoredMultiを試すことになります。
基本的な原理は以前の記事でやっていることと同じなのだと思います。たぶん。チュートリアルには明確に、Labに色変換した後に、Lとabに対して独立にノイズ除去を行う。とあるので、このあたりのアプローチは違います。が、隣接画像間で近いところを見てノイズを取っていると思います。その関数の引数ですが、
1.画像列(リスト)
2.ベースとなる画像(フレーム番号)
3.ノイズ除去に使用するフレーム数(なぜか奇数である必要があります)
4.h : フィルタ強度パラメータ。値が大きいとノイズをより消せるが、画像のティテールも失う。(デフォルト3)
5.hForColorComponents : カラー用のフィルタ強度パラメータ
6.templateWindowSize : テンプレートウィンドウの大きさ。奇数(7が推奨)
7.searchWindowSize : 探索ウィンドウの大きさ。奇数(21が推奨)
だそうです。
ココの引用です。ココでは、Cのコードを説明していますが、Pythonでもおそらく同じだろうと思います。これもまたおそらくですが、hがLabのL(明るさ)に対するパラメータで、hForColorComponentsがab(色)に対するパラメータと予想しています。
6のtemplateWindowSizeは、探索する窓の大きさかなと思います。7のsearchWindowSizeは探す範囲かな?推奨値で用いる場合、21×21の範囲を7×7の窓で探すということなのではないかと予想します。この場合、上下左右に7画素ぶれていてもよいよ。と言ってくれているのかな。
こんな感じかな?今回は推奨値(7, 21)で試してみることとします。コードとしては、こんな感じ、多分使い方あってると思うんだけどなぁ。
flist = glob.glob(dirName + "/*.jpg") # ソースの写真群がある場所 length = len(flist) noisy = [cv2.imread(fname) for fname in flist] dst = cv2.fastNlMeansDenoisingColoredMulti(noisy, length//2, length, 3, 3, 7, 21)
処理結果と比較
めちゃくちゃ処理に時間がかかります。どっかでハングったかな?と思うくらいでした。おそらく真面目に7×7のウィンドウ単位で隣接フレームのマッチングを取っているのではないかと予想されます。前回の記事で試した手法は、32×32のブロック単位のマッチングなので、計算量はかなり有利です。このマッチング、回転は考慮されているのかな?そのあたりは不明です。
そしてその結果ですが、ちょっとイマイチな気がします。完全にテクスチャが消えている箇所があるかと思えば、ノイズがイマイチ取り切れてない箇所があったりします。
左がノイズ除去前、右が今回のfastNlMeansDenoisingColoredMultiの結果、中央が以前の記事で試した手法。右の結果では、洋服のスジや縫い目、表面のけば立った感じが完全になまされています。しかしエッジ付近でのノイズ除去がイマイチ効いていません。真ん中のサンプルがその両立ができていて、一番きれいに見えます。
もういっちょ拡大してみます。被写体は隅っこに写っていた子供のおもちゃの人形の家です。一番右は、左にある窓枠のノイズが取り切れていない上に、屋根がつるつるになっています。パラメータがイマイチなのかもしれませんが、うまく両立できていないように見えます。
まとめ
ちょっと気になったので、OpenCVのNlMeansによるノイズ除去と、自前のNlMeans的なノイズ除去を比べてみました。 サンプルが限定的なので一概に有利不利は言えませんが、連写した7枚の静止画をソースとして試した場合、処理時間、画質ともに自前の方が結果がよかったです。パラメータを最適化するには処理がかかりすぎて、あまり真面目に試せていませんが、少しノイズ除去の処理が強すぎる感じがします。何か使い方を間違っているかもしれませんね…。
いったん自前の処理の方が今回の目的には適している。ということにして満足して寝ることにします。
連写写真でいろいろ遊んできましたが、スマホのような画質の悪いカメラで、「はい、チーズ」で止まった状態を撮る場合には、10枚程度の連写がいいかな。
コード
なくさないように、今回のコードを載せておきます。コードはさっぱりしてますね。
import cv2 from PIL import Image import glob import tkinter as tk import tkinter.filedialog import threading dirName = "" dstName = "" nowprocess = False #################################### ## 処理本体 スレッドで呼ばれることを考慮 def process(): global dirName global nowprocess global dstName flist = glob.glob(dirName + "/*.jpg") # ソースの写真群がある場所 nowprocess = True fname = flist[-1] print("base file:", fname) image = Image.open(fname) exif = image.info["exif"] # exif情報取得 length = len(flist) noisy = [cv2.imread(fname) for fname in flist] dst = cv2.fastNlMeansDenoisingColoredMulti(noisy, length//2, length, 3, 3, 7, 21) Image.fromarray(dst[:,:,[2,1,0]]).save(dstName, "JPEG", exif=exif, quality=95) button2["text"] = "実行" nowprocess = False #################################### ## フォルダ選択ボタン def button1_clicked(): if nowprocess: return global dirName dirName = tk.filedialog.askdirectory() #################################### ## 実行ボタン def button2_clicked(): if nowprocess: return global dstName dstName = tk.filedialog.asksaveasfilename(filetypes=[("JPEG file", ".jpg")]) if dstName != "": button2["text"] = "実行中..." thread = threading.Thread(target=process) thread.start() #################################### ## main if __name__ == '__main__': root = tk.Tk() root.geometry('200x100') button1 = tk.Button(text="選択", command=button1_clicked, width=20) button1.place(x=30, y=20) button2 = tk.Button(text="実行", command=button2_clicked, width=20) button2.place(x=30, y=60) root.mainloop()
コメント