先日のAWS Dev Day 2022 Day0にて
こんにちは、kzk_maedaです。
みなさん、2022/11/08-10の3日間開催されていたAWS Dev Day 2022 Japanには参加されましたか?
そのDay0のセッションである、 「【スペシャルプログラム】ついに決着!! 犬派 vs 猫派 LT 大会」 に登壇させていただき、そこでAWSアーキテクチャ図を犬猫画像に寄せて作成してAmazon Rekognitionに判定させる、という発表を中川翔子さんの前でさせていただきました。
何を言っているのかわからないと思いますが登壇資料かYoutube(年内は公開されているらしい)を見て察してください。
このときに作成したアプリを中川翔子さんに絶賛していただいたと勝手に自負しているので、それを紹介したいと思います。
作成したアプリ
入力画像を与えたら、それをLambdaベースのサーバーレスアーキテクチャ図に変換するアプリを作成しました。
こういう感じです。
何の役に立つのかと問われたら何も返せません。紛うことなき「クソアプリ」です。
どうやっているのか
完成形
まずはソースを共有します。登壇のために突貫で作ったので、汎用性とか全無視です。万が一拡張したい方がいたらお好きに使ってください。
なお、今回のクソアプリ作成のために始めてOpenCVに触ったくらいのスキル感なので、画像処理に慣れている方から見ると怪しい点が多々あるかと思いますが、ご了承ください。
使用技術スタック
PythonとOpenCVを用いて作成しています。
Python 3.9.11
numpy==1.23.4
opencv-contrib-python==4.6.0.66
ipython==8.5.0
大まかなステップ
大きくは次のステップで処理を実行しています。
細かい部分は省略しますが、要点となる処理をいくつか解説します。
各処理の要点
モザイク処理
入力画像をそのまま扱っても良いのですが、後からタイルと重ね合わせるときに、コントラストをはっきりさせておきたいのと、あまり細かい線の描画などに向いていないだろうということで、前処理の中で薄くモザイク処理をかけています。
モザイク処理をかける方法について調べていたのですが、「一度縮小してから再度拡大する」ことで実現するという天才的なアイデアを発見したので、そのように実現しています。
cv2.resize()
を2回かけるだけでモザイク画像が生成できます。
def _mosaic(self, ratio: float=0.5) -> None:
small_image = cv2.resize(self.src_image, None, fx=ratio, fy=ratio, interpolation=cv2.INTER_NEAREST) # type: ignore
self.src_image = cv2.resize(small_image, self.src_image.shape[:2][::-1], interpolation=cv2.INTER_NEAREST)
cv2.imwrite('./img/tmp/mosaic.png', self.src_image)
参照したのはこちらのサイトです。
https://note.nkmk.me/python-opencv-mosaic/
二値化
まず、二値化についてです。
二値化とは、画像の明度に対して閾値を設けて、「明度が閾値以上なら黒、閾値以下なら白」というように白黒つける処理のことを指します。
こちらの記事などご参照ください。
https://www.learning-nao.com/?p=1866
その上で、今回行いたいのは単純に閾値を一つ設けての二値化ではなく、「二つの明度の閾値の間に入るピクセルを黒として残す」という処理を閾値を動かしながら実施し、各閾値間に収まる画像を生成することです。
明度は0~255の範囲で表されますが、あまりにも極値(255)に近い要素は除いて、今回は0~230の間で閾値を移動させ、それぞれの間に収まる画像を生成しています。
以下のようなイメージです。
このように、入力画像に対して明度が所定の範囲に入るピクセルだけを抽出してレイヤー画像を生成し、それぞれ一時領域に保管して後の重ね合わせ処理で利用するようにします。
コード上では次のように実現しています。
def _binarization(self) -> None:
for i in range(self.step):
# 各ステップでの二値化範囲を指定
lower_thresh = int(230 / self.step * i)
upper_thresh = int(230 / self.step * (i+1))
lower = np.array([lower_thresh])
upper = np.array([upper_thresh])
binary_img = cv2.inRange(self.src_image, lower, upper)
binary_img = cv2.bitwise_not(binary_img)
cv2.imwrite(f'./img/tmp/input/input_{i}.png', binary_img)
cv2.inRange()
は指定した範囲の画素を 255、それ以外の画素を0として2値化を行う関数です。ここに下限と上限の閾値を渡します。
しかし、これでは指定範囲が255、つまり白で、それ以外が黒となってしまい、欲しいレイヤー画像とは白黒が反転してしまった状態になります。
そのため、 cv2.bitwise_not()
を用いて白黒反転をしてから保管しています。
入力画像とタイル画像の重ね合わせ
タイル画像生成については省略します。
ここまででレイヤーごとの入力画像とタイル画像が生成されている状態です。ここで、二値化された入力画像の中で黒で残っているピクセルをタイル画像の要素に置き換える処理を実行します。
OpenCVでは画像の各要素は行列で表現されるので、入力画像とタイル画像のそれぞれに対してOR演算をしてあげれば、入力画像で残されたピクセル部分がタイル画像の要素で置き換わります。
コード上では以下のように実現します。レイヤーごとに実行するため、ループの中で cv2.bitwise_or()
を呼び出しています。
for i in range(self.step):
input_img = cv2.imread(self.input_image_paths[i])
tile_img = cv2.imread(self.tile_image_paths[i])
masked_img = cv2.bitwise_or(input_img, tile_img)
各レイヤー画像の重ね合わせ
各レイヤーの画像が準備できたら、最後にそれらを重ね合わせれば完成です。
ただし、そのまま画像を重ね合わせても、一番最後に重ねたレイヤーの画像しか表示されません。背景色が透明でないからです。
そこで、一時領域に保管していたレイヤー画像を背景色を透明にしておく必要があります。
def _convert_to_transparent(filename: str) -> cv2.Mat:
img = cv2.imread(filename)
mask = np.all(img[:,:,:] == [255, 255, 255], axis=-1)
dst = cv2.cvtColor(img, cv2.COLOR_BGR2BGRA)
dst[mask,3] = 0
return dst
画像を img
に格納した後、 np.all()
で白色ピクセルの部分だけ True
を格納します。
次に、元の画像はRGBの情報しかなく、透明ピクセルを扱えないため、アルファチャンネル込みの画像に cv2.cvtColor()
で変換します。
※アルファチャンネル:各ピクセルの透明度を表すデータ。0~255の値を取り、0のときに完全に透明となる。
そして白色ピクセルが True
になっていたところのアルファチャンネルの値を0にすると、背景が透明な画像が生成されます。
最後に、背景を透明にした各レイヤー画像を順番に重ね合わせていきます。
def _overlap(base_img_path: str, over_img_path: str) -> None:
baseImg = cv2.imread(base_img_path)
overImg = cv2.imread(over_img_path, cv2.IMREAD_UNCHANGED)
width, height = baseImg.shape[:2]
baseImg[0:width, 0:height] = (
baseImg[0:width, 0:height] * (1 - overImg[:, :, 3:] / 255)
+ overImg[:, :, :3] * (overImg[:, :, 3:] / 255)
)
cv2.imwrite(over_img_path, baseImg)
画像データに対して行列演算を行い、重ね合わせを実施します。
baseImg[0:width, 0:height] * (1 - overImg[:, :, 3:] / 255)
の部分で、baseImgの各セルに対してoverImgのアルファチャンネルの数値を正規化した値を掛けています。overImgのアルファチャンネルが0、すなわち透明な箇所ならbaseImgの値がそのまま残り、overImgのアルファチャンネルが1、すなわち値を持つ箇所ならbaseImgの値が0になります。
そこにoverImgの各セルの値を同様にアルファチャンネルの比率で正規化して加算してbaseImgを上書きする方式です。
こちらの記事を参考にして実装しました。
これらの処理を各レイヤに対してループ実行し、最終的に全ての画像を重ね合わせると目的の画像が生成されます。
(忘れていました、目的の画像はサーバーレスアプリケーションのアーキテクチャ図である、という立て付けでした。)
やりきれなかった部分
最終出力された画像を見ると、白飛びしている箇所が散見されるかと思います。
ちゃんと時間を取って検証していないのですが、二値化の際に指定する閾値での重複があることが原因なのか、画像の重ね合わせの際に境界値データの重複計算を行なってどちらかのデータを優先するみたいな処理が必要なのか、改善する余地はありそうですが、そんな真面目に作るものでもなかった&登壇まで時間がなかったためこのクオリティで妥協しました。
完璧を求めてアウトプットしないのが一番ダメですからね!(言い訳)
結び
そもそもこのアプリを作ろうと思ったモチベーションは、セッションの司会が中川翔子さんになるというツイートを見たときに、「しょこたんに会えるならワンチャン登壇してみたいな」くらいのノリでCfP出して通ってしまったことがきっかけでした。
(原因のツイート)
イベントの登壇って身構えて考えてしまう方も多いかと思いますが、とりあえず何も考えずにCfPさえ出してしまえば、あとは通った後の自分が泣きながらなんとかしてくれるので、ガンガン応募しましょう!
僕もAWS Startup Communityというコミュニティでコアメンバーをやっているので、なんかAWS系のイベントに出てみたいなぁという方はお気軽にお声がけください!