【Python・OpenCV】動画・連続画像のノイズを効果的に除去するには(cv2.fastNlMeansDenoisingMulti, cv2.fastNlMeansDenoisingColoredMulti)

※当サイトではアフィリエイト広告を利用しています

Python プログラミング 画像処理

【Python・OpenCV】動画・連続画像のノイズを効果的に除去するには(cv2.fastNlMeansDenoisingMulti, cv2.fastNlMeansDenoisingColoredMulti)

はじめに

過去の記事で非局所平均法アルゴリズムを実装した関数を紹介しましたが、本記事では同アルゴリズムを用いた動画や連続画像(バースト画像)に適したノイズ除去関数であるcv2.fastNlMeansDenoisingMulti関数、cv2.fastNlMeansDenoisingColoredMulti関数を紹介します。
効率的に高い効果のノイズ除去を行えるアルゴリズムとなっています。

(広告) OpenCV関連書籍をAmazonで探す

動画・連続画像のノイズ除去

cv2.fastNlMeansDenoisingMulti関数、cv2.fastNlMeansDenoisingColoredMulti関数は、cv2.fastNlMeansDenoising関数、cv2.fastNlMeansDenoisingColored関数の拡張版で、複数のフレーム(画像)を使用してノイズ除去を行います。
主に動画やバースト撮影された連続画像のノイズ除去に適しています。

主な違いを下に示します。

項目cv2.fastNlMeansDenoisingMulti関数、cv2.fastNlMeansDenoisingColoredMulti関数cv2.fastNlMeansDenoising関数、cv2.fastNlMeansDenoisingColored関数
入力画像の数複数の画像(通常は連続したフレーム)を処理単一の画像を処理
ノイズ除去の方法複数フレーム間で類似パッチを探索し、時間的な情報も活用してノイズ除去単一画像内の類似パッチを探索してノイズ除去
性能と品質複数フレームの情報を使用するため、一般的により高品質なノイズ除去が可能
適用範囲動画や連続撮影された画像シーケンスのノイズ除去に適している単一の静止画像のノイズ除去に適している
動きへの対応フレーム間の動きが小さい場合に特に効果的。
大きな動きがある場合、適切なフレーム選択や動き補償が必要になる場合がある。
メモリ使用量複数フレームを同時に処理するため、より多くのメモリを必要とする単一の静止画像なのでそれほどメモリを必要としない

cv2.fastNlMeansDenoisingMulti関数

この関数は、cv2.fastNlMeansDenoising関数の動画・連続画像対応版です。
cv2.fastNlMeansDenoising関数と同様に、グレースケールの動画・連続画像に適しています。
引数など、使い方も類似しています。

cv2.fastNlMeansDenoisingMulti(srcImgs, imgToDenoiseIndex, temporalWindowSize[, dst[, h[, templateWindowSize[, searchWindowSize[, normType]]]]])

引数

名称説明
srcImgs(必須)・入力画像のリスト。各画像は8ビット、または16 ビット (NORM_L1 のみ) の 1 チャネル、2 チャネル、3 チャネル、または 4 チャネル画像である必要があります。
・すべての画像は同じタイプとサイズである必要があります。
List[numpy.ndarray]オブジェクト。
imgToDenoiseIndex(必須)・ノイズ除去を行う中心フレームのインデックス。
・0 ≤ imgToDenoiseIndex < len(srcImgs) を満たす必要があります。
・int型
temporalWindowSize(必須)・時間軸方向の検索窓サイズ。
・奇数である必要があります。
・推奨値は 7
・int型
dst(オプション)結果がこの変数に格納されます。指定しない場合は、入力画像と同じサイズ・型で新しい配列が作成されて結果が格納され、戻り値として取得できます。
h(オプション)・フィルタリングの強さを決定するパラメータ。(デフォルト:3)
・大きい値ほど強くノイズ除去されます。
・float型
templateWindowSize(オプション)・テンプレートパッチのサイズ(デフォルト:7)
・奇数である必要があります。
・int型
searchWindowSize(オプション)・各ピクセル周辺の検索窓サイズ(デフォルト:21)
・奇数である必要があります。
・int型
normType(オプション)・重み計算に使用されるノルムのタイプ。
NormTypesで定義されるNORM_L2 、または NORM_L1 のいずれか。(デフォルト:NORM_L2)
・int型

ポイント

  • この関数は、連続したフレーム間で類似したパターンを探すことで、より効果的にノイズを除去します。
  • temporalWindowSizeは、処理するフレーム数を決定します。例えば、temporalWindowSize=3の場合、中心フレームの前後1フレームずつ、合計3フレームが使用されます。
  • imgToDenoiseIndextemporalWindowSizeの設定には注意が必要です。例えば、5フレームの入力で中心フレームを処理する場合、imgToDenoiseIndex=2, temporalWindowSize=5とするのが適切です。
  • 処理時間とメモリ使用量は、入力フレーム数とtemporalWindowSizeに大きく依存します。
  • フレーム間の差異の大きい映像では、効果が限定的になる可能性があります。静止した背景やゆっくりとした動きの映像で最も効果的です。

戻り値

imgToDenoiseIndexで指定されたフレームに対応する、ノイズ除去が適用された画像データをnumpy.ndarrayオブジェクトで返します。

使い方

以下に使い方を示します。

ノイズ除去を行うのは下の動画サイトの動画をモノクロに変換した動画を使用しました。
サイズ 960 × 540、フレーム数 122、再生時間8.1秒の動画となっています。

カラー動画をモノクロに変換する例は、この記事で紹介しています。
サンプルコードもありますので、参考にしてください。

下の画像は、処理後の動画の同じフレームを切り取った画像です。
cv2.fastNlMeansDenoisingMulti関数と同様にノイズ除去の効果は十分です。
引数hの値が小さくてもcv2.fastNlMeansDenoising関数より強いノイズ除去効果が得られている様に思えます。
大きなノイズより、細かなノイズ除去に大きな効果を感じます。
一方で、処理時間は必要となります。
実測の結果、筆者の環境(M1 MacBook Pro)では1フレームあたり約0.66秒の処理時間でした。
CUDAなどで高速化した場合はどうなのか、気になるところです。

import cv2

def denoise_video(input_path, output_path, h=6, templateWindowSize=7, searchWindowSize=21, temporalWindowSize=3):
    # 入力動画を開く
    cap = cv2.VideoCapture(input_path)
    if not cap.isOpened():
        print("Could not open the video file.")
        exit()

    # 動画の情報を取得
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = int(cap.get(cv2.CAP_PROP_FPS))

    # 動画の長さを計算する(sec)
    video_len_sec = total_frames / fps
    print(f"Total time {video_len_sec}sec")

    # 出力動画のwriter設定
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

    # フレームバッファ初期化
    frame_buffer = []

    # 時間計測開始時間 取得
    start_time = cv2.getTickCount()

    for i in range(total_frames):
        ret, frame = cap.read()
        if not ret:
            break

        # フレームバッファにフレームを追加
        frame_buffer.append(frame)

        # バッファが必要なサイズになったらノイズ除去を適用
        if len(frame_buffer) == temporalWindowSize:
            # 中心フレームのインデックス
            center_index = temporalWindowSize // 2

            # ノイズ除去を適用
            denoised_frame = cv2.fastNlMeansDenoisingMulti(
                srcImgs=frame_buffer, 
                imgToDenoiseIndex=center_index, 
                temporalWindowSize=temporalWindowSize, 
                dst=None, 
                h=h, 
                templateWindowSize=templateWindowSize, 
                searchWindowSize=searchWindowSize
            )   

            # ノイズ除去されたフレームを書き出し
            out.write(denoised_frame)

            # 最も古いフレームをバッファから削除
            frame_buffer.pop(0)

        print(f"Processing frame {i+1}/{total_frames}")

    # 時間計測終了
    end_time = cv2.getTickCount()
    elapsed_time = (end_time - start_time) / cv2.getTickFrequency()
    print("Elapsed time:", elapsed_time, "sec")

    # リソースの解放
    cap.release()
    out.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    # 使用例
    input_video = "input_movie.mp4"
    output_video = "denoised_movie.mp4"
    denoise_video(input_video, output_video)

cv2.fastNlMeansDenoisingColoredMulti関数

cv2.fastNlMeansDenoisingColoredMulti関数はcv2.fastNlMeansDenoisingMulti関数のカラー画像に対応した関数です。
カラーの動画・連続画像に適しています。
引数など、使い方も類似しています。

cv2.fastNlMeansDenoisingColoredMulti(srcImgs, imgToDenoiseIndex, temporalWindowSize[, dst[, h[, templateWindowSize[, searchWindowSize[, normType]]]]])

引数

名称説明
srcImgs(必須)・入力画像のリスト。各画像は8ビット、または16 ビット (NORM_L1 のみ) の 1 チャネル、2 チャネル、3 チャネル、または 4 チャネル画像である必要があります。
・すべての画像は同じタイプとサイズである必要があります。
List[numpy.ndarray]オブジェクト。
imgToDenoiseIndex(必須)・ノイズ除去を行う中心フレームのインデックス。
・0 ≤ imgToDenoiseIndex < len(srcImgs) を満たす必要があります。
・int型
temporalWindowSize(必須)・時間軸方向の検索窓サイズ。
・奇数である必要があります。
・推奨値は 7
・int型
dst(オプション)・結果がこの変数に格納されます。指定しない場合は、入力画像と同じサイズ・型で新しい配列が作成されて結果が格納され、戻り値として取得できます。
numpy.ndarrayオブジェクト
・デフォルト値: None(新しい配列が作成されます)
h(オプション)・フィルタリングの強さを決定するパラメータ。(デフォルト:3)
・大きい値ほど強くノイズ除去されます。
・float型
hColor(オプション)・色チャンネルに対するフィルタ強度(デフォルト:3)
・float型
templateWindowSize(オプション)・テンプレートパッチのサイズ(デフォルト:7)
・奇数である必要があります。
・int型
searchWindowSize(オプション)・各ピクセル周辺の検索窓サイズ(デフォルト:21)
・奇数である必要があります。
・int型
normType(オプション)・重み計算に使用されるノルムのタイプ。
NormTypesで定義されるNORM_L2 、または NORM_L1 のいずれか。(デフォルト:NORM_L2)
・int型

ポイント

  • この関数は、カラー画像の連続フレームを処理します。輝度と色彩の情報を考慮してノイズ除去を行います。
  • hhColorパラメータを個別に設定することで、輝度ノイズと色ノイズに対して異なる強度でフィルタリングできます。
  • temporalWindowSizeは、処理するフレーム数を決定します。例えば、temporalWindowSize=3の場合、中心フレームの前後1フレームずつ、合計3フレームが使用されます。
  • imgToDenoiseIndextemporalWindowSizeの設定には注意が必要です。例えば、5フレームの入力で中心フレームを処理する場合、imgToDenoiseIndex=2, temporalWindowSize=5とするのが適切です。
  • 処理時間とメモリ使用量は、入力フレーム数とtemporalWindowSizeに大きく依存します。
  • カラー画像を扱うため、グレースケール版よりも計算コストが高くなります。
  • フレーム間の差異の大きい映像では、効果が限定的になる可能性があります。静止した背景やゆっくりとした動きの映像で最も効果的です。

この関数は、特に低照度環境で撮影されたカラー動画や、高ISO感度で撮影された連続カラー画像のノイズ除去に有効です。
色情報を保持しながら効果的にノイズを除去できるため、画質の向上が期待できます。

戻り値

imgToDenoiseIndexで指定されたフレームに対応する、ノイズ除去が適用された画像データをnumpy.ndarrayオブジェクトで返します。

使い方

以下にサンプルコードを示します。

入力動画はcv2.fastNlMeansDenoisingMulti関数のサンプルコードで使用した動画のオリジナル(グレースケール化する前)のものを使用しました。

下の画像は、処理後の動画の同じフレームを切り取った画像です。
カラー画像でもcv2.fastNlMeansDenoisingMulti関数と同様に十分なノイズ除去がされている様です。
実測の結果、筆者の環境(M1 MacBook Pro)では1フレームあたり約0.86秒の処理時間でした。
cv2.fastNlMeansDenoisingMulti関数による処理時間の増分は筆者の環境では約0.2秒なので、カラー動画の3チャンネル分のノイズ除去の処理ということを考慮するとかなり効率の高い処理がされていることがわかります。

import cv2

def denoise_video(input_path, output_path, h=6, hColor=4, templateWindowSize=7, searchWindowSize=21, temporalWindowSize=3):
    # 入力動画を開く
    cap = cv2.VideoCapture(input_path)
    if not cap.isOpened():
        print("動画ファイルを開けませんでした。")
        exit()

    # 動画の情報を取得
    width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
    height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
    total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
    fps = int(cap.get(cv2.CAP_PROP_FPS))

    # 動画の長さを計算する(sec)
    video_len_sec = total_frames / fps
    print(f"Total time {video_len_sec}sec")

    # 出力動画のwriter設定
    fourcc = cv2.VideoWriter_fourcc(*'mp4v')
    out = cv2.VideoWriter(output_path, fourcc, fps, (width, height))

    # フレームバッファ初期化
    frame_buffer = []

    # 時間計測開始時間 取得
    start_time = cv2.getTickCount()

    for i in range(total_frames):
        ret, frame = cap.read()
        if not ret:
            break

        # フレームバッファにフレームを追加
        frame_buffer.append(frame)

        # バッファが必要なサイズになったらノイズ除去を適用
        if len(frame_buffer) == temporalWindowSize:
            # 中心フレームのインデックス
            center_index = temporalWindowSize // 2

            # ノイズ除去を適用
            denoised_frame = cv2.fastNlMeansDenoisingColoredMulti(
                srcImgs=frame_buffer, 
                imgToDenoiseIndex=center_index, 
                temporalWindowSize=temporalWindowSize, 
                dst=None, 
                h=h, 
                hColor=hColor, 
                templateWindowSize=templateWindowSize, 
                searchWindowSize=searchWindowSize
            )   

            # ノイズ除去されたフレームを書き出し
            out.write(denoised_frame)

            # 最も古いフレームをバッファから削除
            frame_buffer.pop(0)

        print(f"Processing frame {i+1}/{total_frames}")

    # 時間計測終了
    end_time = cv2.getTickCount()
    elapsed_time = (end_time - start_time) / cv2.getTickFrequency()
    print("Elapsed time:", elapsed_time, "sec")

    # リソースの解放
    cap.release()
    out.release()
    cv2.destroyAllWindows()

if __name__ == "__main__":
    # 使用例
    input_video = "input_movie.mp4"
    output_video = "denoised_movie.mp4"
    denoise_video(input_video, output_video)

おわりに

動画や連続画像(バースト画像)に適したノイズ除去関数であるcv2.fastNlMeansDenoisingMulti関数、cv2.fastNlMeansDenoisingColoredMulti関数を紹介しました。
ノイズ除去効果としては、特に低照度環境で撮影された動画や、高ISO感度で撮影された連続画像のノイズ除去に有効のようです。
また、カラー画像の処理効率はとても高いことも特徴の一つと言えるでしょう。
一方で、ノイズ除去効果が強いので、画像の細部の表現が削られてしまうこともあるかもしれませんが、高品質なノイズ除去結果を得るためには適切なパラメータ調整が必要です。

ご質問や取り上げて欲しい内容などがありましたら、コメントをお願いします。
最後までご覧いただきありがとうございました。

参考リンク

■(広告)OpenCVの参考書としてどうぞ!■

-Python, プログラミング, 画像処理
-