【畳込みニューラルネット No.1】Conv2Dで単純な画像フィルタを実現

シェアする

深層学習を勉強してますが,もうとにかく無茶苦茶簡単なものから少しずつ理解していきたいと思ってます.研究の世界ではPyTorchの方が主流らしいですが,私はTensorflow/Kerasの方が簡単でわかりやすいので好きですね.

ま,いずれPyTorchも取り組まなきゃいけないのかもしれませんが,先ずはTensorFlow/Kerasでやっていきます.ひとまず私の研究レベルだとこれで十分なので.

そこで,最初は畳み込み層であるConv2Dの使い方から勉強したので,その備忘録を残しておきます.

と言っても,先ずは畳み込みニューラルネットワークではなく,単純な画像フィルタをConv2Dのみで実装します.なので,AIでもないし深層学習でもないです.

インポートと画像読み込み

これはサクッとコードを紹介して終わりです.あ,ちなみに使用しているのはGoogle Colaboratoryです.

import cv2
import numpy as np
from keras.layers import Input, Conv2D, Dense
from keras.models import Sequential
from google.colab.patches import cv2_imshow

続いて画像読み込みはこちら

koushi = cv2.imread("./drive/MyDrive/Colab Notebooks/Data/koushi.jpg")
koushi = koushi[:,:,0] # RGBの3チャネルあるので,R成分のみ取り出す
print(koushi.shape)

画像の読み込みパスは各自の環境に合わせて書き換えください.

print(koushi.shape)でkouchi.jpgの画像サイズを出力してます.結果は(400, 400)になったので,400×400ピクセルでした.で,このkoushi.jpegという画像ですが,こういうものです.

なんでこんな画像にしたのかというのは後でわかります.

画像フィルタのモデルの構築

Kerasのモデル作成方法は2通りあるようですが,私はこのようにしました.
model = Sequential()
model.add(Input((400,400,1))) #チャネルは1
model.add(Conv2D(2, (3,3), padding='same')) #フィルタ数は2
model.summary()
model.summary()を実行した結果,以下のように表示されました.

Input(400,400,1)は,400×400ピクセルの画像を1チャネル(つまり,白黒画像)

Conv2D(2,(3,3),padding=’same’)は,3×3の画像フィルタの係数を2種類定義し,padding=’same’なので,画像の周囲に1ピクセル分余分に加え,フィルタ後も画像サイズが変わらないようにします.

さて,モデルはこれだけ.

モデルの出力は,(None, 400, 400, 2)ですが,この中で2は,2種類の画像フィルタを適用した結果を表します.Noneは入力画像の枚数を表しますが,モデル作成時点で何枚の画像を入力するのか不明なのでNoneとなってます.

画像フィルタの係数を設定

CNNではフィルタ係数は自動的に学習で値が決まっていきますが,ここでは値を決め打ちします.よく知られた画像フィルタであるSobelフィルタを実現します.

それには,Conv2Dレイヤーの重みの値をこちらが指定する値に設定しないといけません.

w21 = np.array([[1,0,-1],[2,0,-2],[1,0,-1]]) # w21.shape = (3,3)
w22 = np.array([[-1,-2,-1],[0,0,0],[1,2,1]]) # w22.shape = (3,3)
w2 = np.array([w21, w22]) # w2.shape = (2,3,3)
w2 = w2[:,:,:,None] # w2.shape = (2,3,3,1)
w2 = np.transpose(w2, (1,2,3,0)) # w2,shape = (3,3,1,2)
ww2 = [w2, np.array([0,0])] # 通常リスト[ ].フィルタが2つあるので,それぞれにバイアス
model.layers[0].set_weights(ww2) # 重みの値をモデルに設定

1,2行目:w21はSobelフィルタの横方向.w22はSobelフィルタの縦方向です.

3行目: w2 = np.array([w21, w22])はこれらを結合してます.結果,w2は(2,3,3)のテンソルとなります.w[0,:,:]はw21,w[1,:,:]がw22の値がそれぞれ入ってます.

4行目:w2にチャネル分の次元を追加しないといけないので,w2 = w2[:,:,:,None]とします.これで,(2,3,3,1)のテンソルになります.

5行目:modelの中のフィルタ係数のフォーマットは,(フィルタの横幅, フィルタの縦幅, チャネル数, フィルタ数)のテンソルですが,w2は,(フィルタ数, フィルタの横幅, フィルタの縦幅, チャネル数).なので,np.transposeで入れ替えます.

np.transpose(w2, (1,2,3,0)) は w2が(フィルタ数, フィルタの横幅, フィルタの縦幅, チャネル数)であり,それぞれが順番に0〜3番目.それを入れ替えて,1番目(フィルタの横幅),2番目(フィルタの縦幅),3番目(チャネル数),0番目(フィルタ数)という(1,2,3,0)という順番に並び替えます.

6行目:Conv2Dの重みとバイアスは通常のリストで連結される.np.array([0,0])はフィルタ1のバイアスとフィルタ2のバイアス.それぞれ0.

7行目:Conv2D層に値を2つの画像フィルタの値を設定する.

フィルタリング処理

読み込んだ画像をそのまま与えてもダメなので,フォーマットを合わせます.koushiは,

(画像の横幅, 画像の縦幅)

の2次元テンソル(2次元配列)ですが,modelの入力画像は,

(画像数, 画像の横幅, 画像の縦幅, チャネル)

のフォーマットなので,それに合わせます.

koushi = koushi[None,:,:,None]
f_koushi = model.predict(koushi)

1行目:最初と最後にNoneを加えて次元を増やしてます.これで(1,400,400,1)となります.画像数が1,チャネル数が1.

2行目:画像フィルタリングして,その結果をf_koushiに代入.

結果を表示

f_koushiを表示します.1番目のフィルタを用いた結果は,

cv2_imshow(f_koushi[0,:,:,0])

を実行すると,以下のようになります.横方向のラインは消えてます.

同様に,2番目のフィルタを用いた結果は,

cv2_imshow(f_koushi[0,:,:,1])

を実行すると,以下のようになります.今度は縦方向が消えてます.

Sobelフィルタの縦方向と横方向の検出がうまくいきました!

この効果がはっきり分かるように,わざと入力画像を格子模様にしました.もちろん,一般的な画像も可能です.

画像処理でよく使用されるLenaを用いた場合,最初のフィルタの結果は以下のようになります.

プログラムリスト一覧

プログラムリストは下記のようになります.

モデル内部を流れるデータが(1,400,400,2)などのテンソルなので,まさにテンソルフロー(TensorFlow)ですね.

import cv2
import numpy as np
from keras.layers import Input, Activation, Conv2D, Dense, MaxPooling2D
from keras.models import Sequential
from google.colab.patches import cv2_imshow  #colab上で画像を表示するため

koushi = cv2.imread("./drive/MyDrive/Colab Notebooks/Data/koushi.jpeg")
koushi = koushi[:,:,0] # RGBのR成分のみを使用する(グレースケール)

# モデル構築
model = Sequential()
model.add(Input((400,400,1))) #入力は400×400ピクセルのグレースケール
model.add(Conv2D(2, (3,3), padding='same')) #3×3のフィルタが2種類

#Sobelフィルタの係数の値を設定
w21 = np.array([[1,0,-1],[2,0,-2],[1,0,-1]]) # 横方向Sobelフィルタの係数
w22 = np.array([[1,2,1],[0,0,0],[-1,-2,-1]]) # 縦方向Sobelフィルタの係数
w2 = np.array([w21, w22]) 
w2 = w2[:,:,:,None]  # (2,3,3,1)の形にする
w2 = np.transpose(w2, (1,2,3,0)) # (3,3,1,2)の形にする
ww2 = [w2, np.array([0,0])] # 2つのフィルタのバイアスを0にしリストで連結
model.layers[0].set_weights(ww2) # Conv2Dレイヤーに設定

# 入力画像のフォーマットを変更する
koushi = koushi[None,:,:,None] #(400,400)から(1,400,400,1)にする

# フィルタリング
f_koushi = model.predict(koushi)

# フィルタリングした後の画像を表示
cv2_imshow(f_koushi[0,:,:,0]) #フィルタ1を通した結果
cv2_imshow(f_koushi[0,:,:,1]) #フィルタ2を通した結果