[備忘] OpenCVでマスク付きの画像にフィルタを適用する

やりたいこと

画像内に無効値が格納されているデータに対して、平滑化フィルタなどを適用したい。

どういうことかというと、下の画像の中央のように意味を持たない値が格納されているような画像に対して、ぼかし処理をしたいといった場合です。使うのは特殊なときかもしれませんが、必要に迫られると意外と情報がなかったので、メモしておきます。

【元画像】中央に意味のない値(赤) が格納されている
【処理後画像】中央のマスク範囲以外にフィルタを適用

下準備

色々と処理をする前に、ライブラリのインポートと画像の読み込み、フィルタの設定をしておきます。このあとのコードは全て、この設定が終わったあとという前提で記載します。

import numpy as np
import cv2

# 画像の読み込み
img = cv2.imread('masked_img.png')

# フィルタサイズの設定
kernel_size = 21

何も考えずにフィルタを適用

まずはそのまま画像にガウシアンフィルタを適用した結果です。

無効値もフィルタに使っているため、マスクの周辺がかなりぼやけています。

img_simple_filter = cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)

元々の無効値の範囲を黒で重ねてみると、平滑化の影響で正しい値が格納されている範囲にも赤の領域が広がってしまっていることがわかります。

無効値をフィルタ計算から除外

マスクの準備

このようなことを防ぐために、無効値はフィルタ計算から除外するようにします。ここでは、無効値のピクセルを示すマスク (下図) がすでにあることとして進めます。

無効にしたい値がわかっている場合には、こんな感じでマスクを作ります。(今回もこうやってマスク画像を作成しました)

mask = (img[:,:,0]!=0) | (img[:,:,1]!=0) | (img[:,:,2]!=255)
mask = mask.astype(np.uint8)
mask = cv2.cvtColor(mask*255, cv2.COLOR_GRAY2RGB)

処理コード

以下のコードで処理をします。考え方としては次のような感じです。フィルタの計算に使ったピクセルに無効値が含まれていなければ、フィルタ後のマスクの合計が1になるはずですので、1にならない (ピクセルの除外の影響がある) 箇所に対して、1になるように値を調整するわけです。

  • 無効値のピクセルはフィルタ計算から除外
    (ゼロを格納しているため、計算結果には影響しない)
  • 除外によりピクセル数が足りない(カーネルの和が1にならない)分を補正

途中、ゼロ割が出てくるのがちょっと気持ち悪いですが、その後のnp.where()で影響は除外されますので、結果に影響はありません(ないはずです)。

# マスクの読み込み
mask = cv2.imread('mask.png') == 255  # 0: マスク、255: 非マスクの場合に、0/1のバイナリ値に変換 
mask = mask.astype(np.float32)

# 無効値のピクセルには全て0を格納
img_masked = img * mask

# 無効値=0にした画像と、マスクそれぞれにフィルタを適用
# maskはfloat64にしないと1.0のときの値が適切にならない
img_masked_filter = cv2.GaussianBlur(img_masked, (kernel_size, kernel_size), 0)
mask_f = cv2.GaussianBlur(mask.astype(np.float64), (kernel_size, kernel_size), 0)

# フィルタ後の画像をマスクの重みで割る
img_weighted_filter = (img_masked_filter/mask_f).astype(np.uint8)

# マスク=0 (無効値)のピクセルには元々の無効値を格納
img_weighted_filter = np.where(mask==[1., 1., 1.], img_weighted_filter, img)

できました。無効値の矩形もくっきりはっきりです。

単純適用バージョンと同様に、元々の無効値の範囲を黒で重ねてみました。
無効値の影響を受けずにフィルタが適用できていることがわかります。

やり方もうひとつ

このやり方はあくまでも、「無効値の範囲を補正」というアプローチです。明るさの差が大きい画像だったり、ガウシアンフィルタのような対称的なフィルタでない場合は、期待通りの結果にならない可能性があります。(調べられていませんが)

マスクの領域を広くしてでも、確実に適切な結果を取りたいという場合には、フィルタの影響の及ぶ範囲全てを無効にしてしまうというやり方もあるかなと思います。

以下のように、Erosionを適用することでフィルタの影響範囲までマスクを膨張させます。
そして、膨張させたマスク範囲には全て無効値を埋め込むことで、有効な値のみを使って、補正などもせずにフィルタをかけた結果を得られます。

# 読み込んだ画像に単純にフィルタを適用
img_simple_filter = cv2.GaussianBlur(img, (kernel_size, kernel_size), 0)

# マスクの読み込み
mask = cv2.imread('mask.png') == 255 
mask = mask.astype(np.float32)

invalid_val = [0, 0, 255]  # 無効値

# Erosionの適用
kernel_erosion = np.ones((kernel_size, kernel_size), np.uint8) 
mask_erosion = cv2.erode(mask, kernel_erosion, iterations=1)

# 拡大後のマスクの範囲に無効値を格納
img_simple_filter_erosion = np.where(mask_erosion==0, invalid_val, img_simple_filter)

膨張処理により拡大されたマスクを可視化しました(緑部分)。無効値の縁部分で、フィルタの影響を受けている箇所が新たにマスクになっていることがわかります。

コメントを残す

メールアドレスが公開されることはありません。