モルフォロジー変換は実はMaxPoolingだったという話(TensorFlowでの実装)
画像処理の重要な変換に膨張(Dilation)や収縮(Erosion)といったモルフォロジー変換があります。実はこれはディープラーニングでよく使われるMaxPoolingフィルターで置き換えることができます。TensorFlowの実装で見ていきます。
目次
モルフォロジー変換
OpenCVでのモルフォロジー変換の解説がとてもわかりやすいのでこちらを参照してください。
膨張(Dilation)
OpenCVではある適当なパッチサイズに対して、「パッチ内で1つでも1が含まれれば1を返す処理」と紹介されています。簡単に3×3フィルターで考えると
このように1ピクセルでも1はあったら1を返すような処理です。
一見論理和のように見えますが、より一般的にはパッチ内の最大値を取ればいいです。例えば、TensorFlowのtf.image.dilation2dという関数ではMaxと書かれています。
収縮(Erosion)
膨張とは逆で、「パッチ内で1つでも0が含まれていれば0を返す処理」です。同様に3×3フィルターで考えると
論理積のような処理ですね。すべてのピクセルが1の場合のみ1を返します。
より一般的にはパッチ内の最小値を取ればいいです。例えば、TensorFlowのtf.image.erosion2dという関数ではMinと書かれています。
最大値を取るのはMaxPooling、最小値は?
「パッチ内の最大値を取る」というのは実はニューラルネットワークでは頻繁に使われています。CNNでよくあるMaxPoolingがそうです。
ただしCNNでのMaxPoolingはおもにダウンサンプリング(解像度を小さくするの)を目的するのに対して、モルフォロジー変換ではダウンサンプリングは行いません。これはMaxPoolingのstrideの値を変えることで再現できます。
通常、ダウンサンプリングでPooling層を使う場合は、カーネルサイズとストライドの大きさを同じにしますが、モルフォロジー変換の場合はStrideを1にします。Inceptionモデル内のモジュールでも同じ使い方をしています。
では、最小値を取る処理はどうすればいいのかというと、TensorFlowにはMinPoolingという関数はありません。しかし、MaxPoolingでMinPoolingを再現することができます。マイナスを取ってMaxPoolingを取り、更に符号を反転させればMinPoolingとなります。例えば、「-1, 3, 5」という配列のMaxは5ですが、マイナスを取ってMaxを取ると1(元が-1)が出力されます。この値の符号を反転させれば最小値の-1が出てくるわけです。
膨張(Dilation)の実装
5×5のカーネルでモルフォロジー変換をします。膨張処理から。
import tensorflow as tf
# 膨張
def dilation(tensor):
# 5x5のMaxフィルター
return tf.nn.max_pool2d(tensor, 5, strides=1, padding="SAME") # stride=1がポイント
カーネルサイズを5、Strideを1でMaxPoolingすればいいです。解像度が変わらないようにPaddingを入れています。OpenCVの解説同じ画像でやってみましょう。画像はOpenCVのページのものを使っています。
また、読み込み用、表示用に以下の2つの関数を定義しておきます。
import matplotlib.pyplot as plt
def load_tensor(filename):
x = tf.io.decode_png(tf.io.read_file(filename))[:,:,:3] # RGB
x = tf.image.rgb_to_grayscale(x)
x = tf.expand_dims(x, axis=0) # 3rank -> 4rank
return tf.image.convert_image_dtype(x, tf.float32)
def show_tensor(tensors):
for i, tensor in enumerate(tensors):
ax = plt.subplot(1, len(tensors), i + 1)
ax.imshow(tensor[0,:,:,0].numpy(), cmap="gray")
plt.show()
膨張のモルフォロジー変換を行ってみましょう。
if __name__ == "__main__":
base = load_tensor("j.png")
x = dilation(base)
show_tensor([base, x])
いい感じに太くなりました。
収縮(Erosion)の実装
逆に収縮のTensorFlowでの実装は次のようになります。符号を反転してMapPoolingを取るとMinPoolingになるのがポイント。
# 収縮
def erosion(tensor):
# 5x5のMinフィルター
return -tf.nn.max_pool2d(-tensor, 5, strides=1, padding="SAME") # Min->マイナスを取ってMax
if __name__ == "__main__":
base = load_tensor("j.png")
x = erosion(base)
show_tensor([base, x])
今度は線が細くなりました。
オープニング(Opening)
収縮→膨張としたものは、オープニング処理と呼ばれます。以下のような外側のノイズを取るのに適しています。
def opening(tensor):
x = erosion(tensor)
return dilation(x)
if __name__ == "__main__":
base = load_tensor("outer_noise.png")
x = opening(base)
show_tensor([base, x])
クロージング(Closing)
オープニングと逆で、膨張→収縮としたものをクロージング処理と言います。内側のノイズを取ってみましょう。
def closing(tensor):
x = dilation(tensor)
return erosion(x)
if __name__ == "__main__":
base = load_tensor("inner_noise.png")
x = closing(base)
show_tensor([base, x])
モルフォロジー勾配
膨張と収縮の差を取るもの。輪郭線の抽出の一方法としては便利だと思います(アニメから線画を自動で作成する一例にも似たような手法を使っています)。
def morphology_gradient(tensor):
return dilation(tensor) - erosion(tensor)
if __name__ == "__main__":
base = load_tensor("j.png")
x = morphology_gradient(base)
show_tensor([base, x])
まとめ
モルフォロジー変換は実はMaxPooling。符号を反転することでMinPoolingを再現可能で、Pooling1つでかなり多くの処理を再現できるということでした。ディープラーニングの関数がこんな使い方できるというのも面白いですね。
あとなぜtf.image.dilation2dではなく、MaxPoolingを使っているかというと、MaxPoolingなら大抵のデバイス(特にTPU)や型で実行できるからです。
Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内
技術書コーナー
北海道の駅巡りコーナー