こんにちは、Yuiです。
引き続き週イチ発信をしていきます。
今週はAWSとOpenCVを使って、画像処理を行ったのでそのことについて書きます。
以下の件です。
ポケモンAPIとOpenCVを使って、画像をポケモンだけで表すAPI作った pic.twitter.com/36GBqvqf82
— yui ☁ Yuiko Ito (@yui_active) July 4, 2021
このままだとちょっとあら過ぎるかなと思ったので、修正後がこちら。
心の目で見るとちゃんとワニに見えてきますね?笑
(なんか下腹部にピカチュウがあるのがちょっと悪意のある変換な感じがしますが笑)
サイズを調整して少し見やすくした pic.twitter.com/5h2CRRv7mG
— yui ☁ Yuiko Ito (@yui_active) July 4, 2021
webアプリ化は来週するとして、今回はこのAPIの作り方に関して書きます。
過去の週イチ発信は以下
- 【React + Typescriptで顔認識】tensorflowを使って画像にマスクをかけるアプリを作った
- 【React + Typescript】ボタン一つでコンポーネントのscssをコピーできるサイトを作った
- 【アップデート】ui-componentsに18個のコンポーネントを追加した
- 【Nuxt.js × Tailwind CSS】ボタン一つで有名絵画風の画像にできるサービスをリリースした!
- 【GASでLINE Bot作成】現在地の近くのおすすめのごはん屋さんを教えてくれるLINE Botを作った
- 【動的OGP】Next.js + TypeScript + Vercelデプロイで動的OGPを実現する
どんなものを作ったか
まずは色々変換してみた結果です。
なかなか面影があります。笑
集合体が苦手な人にはなかなかきつい画像かもしれません。
構成
今回画像を分割して、それぞれをポケモンの画像と比較して色相が似ているものでリプレイスするという感じで組んでいます。
Lambdaではpip installができないので、OpenCVを使うためにDocker imageを利用しました。
というわけで、今回の構成は以下になっています。
やったことは以下です。
- PokeAPIから必要なポケモンの画像をダウンロードする
- 画像処理用のプログラムを書く
- Dockerfileを書く
- Docker imageを作成してECRにpushする
- Lambda関数を作成する(ECRからDocker imageをpull)
- API GatewayでAPI化する
必要なポケモンの画像をダウンロードする
まずはPokeAPIから画像をダウンロードします。
画像を確認すると、余白が結構あるので、余白を削除して、サイズを15px*15pxの状態でダウンロードすることにします。
今回、この処理のために、Google Colaboratoryを利用しました。
ただ、この辺はお好みで要はダウンロードした画像が用意できれていればOKです。
import requests
import cv2
import numpy as np
import tempfile
import urllib.request
from google.colab import files
# 画像のトリミング
def crop(image):
# 画像の読み込み
img = cv2.imread(image)
# 元画像をグレースケールに変換
gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
# グレースケール画像を白黒に変換
ret_, bw_img = cv2.threshold(gray_img, 1, 255, cv2.THRESH_BINARY)
# 白黒画像の輪郭を抽出
contours, h_ = cv2.findContours(bw_img, cv2.RETR_EXTERNAL, method=cv2.CHAIN_APPROX_NONE)
# 輪郭からバンディングボックスを計算
rect = cv2.boundingRect(contours[-1])
# バウンディングボックスの座標で元画像をクロップ
crop_img = img[rect[1]:rect[1]+rect[3]-1, rect[0]:rect[0]+rect[2]-1]
return crop_img
# lists = [1,4,7,10,16,24,25,26]のように、好きなポケモンの番号を指定します。
# 一気に多くの画像をダウンロードしようとすると、途中で抜けが発生するので、10個ずつぐらいでダウンロードします。
for i in lists:
url = "https://pokeapi.co/api/v2/pokemon/{}/".format(i)
tmp_input = '{}.png'.format(i)
r = requests.get(url, timeout=5)
r = r.json()
image =r['sprites']['front_default']
urllib.request.urlretrieve(image, tmp_input)
img = crop(tmp_input)
img2 = cv2.resize(img , (15, 15))
cv2.imwrite(tmp_input , img2)
files.download(tmp_input)
今回は処理速度の関係で、30種類の画像を用意しました。
私はこれらの画像をpokemonsという名前のフォルダで格納しました。
画像処理用のプログラムを書く
やっていることは、以下です。
- 入力された画像(base64形式を想定)をOpenCVで使える形式にデコードする
- デコードされた画像を15px*15pxに分割する
- 分割された画像それぞれとポケモンの画像の色相のヒストグラムを比較する
- 類似度が高いポケモンの画像と入れ替える
コード全文は以下です。
import json
import cv2
import base64
import numpy as np
import glob
def calc_hue_hist(img):
# H(色相)S(彩度)V(明度)形式に変換する。
hsv = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
#hsv = img
# 成分ごとに分離する。
h, s, v = cv2.split(hsv)
# 画像の画素値を計算したもの(ヒストグラム)
h_hist = calc_hist(h)
return h_hist
def calc_hist(img):
# ヒストグラムを計算する。
hist = cv2.calcHist([img], channels=[0], mask=None, histSize=[256], ranges=[0, 256])
# ヒストグラムを正規化する。
hist = cv2.normalize(hist, hist, 0, 255, cv2.NORM_MINMAX)
# (n_bins, 1) -> (n_bins,)
hist = hist.squeeze(axis=-1)
return hist
def base64_to_cv2(image_base64):
# base64 image to cv2
image_bytes = base64.b64decode(image_base64)
np_array = np.fromstring(image_bytes, np.uint8)
image_cv2 = cv2.imdecode(np_array, cv2.IMREAD_COLOR)
return image_cv2
def cv2_to_base64(image_cv2):
# cv2 image to base64
image_bytes = cv2.imencode('.jpg', image_cv2)[1].tostring()
image_base64 = base64.b64encode(image_bytes).decode()
return image_base64
# eventでjsonを受け取る { 'img': base64img }
def handler(event, context):
input_img = event['img'] #画つ目の像を読み出しオブジェクトtrimed_imgに代入
input_img = base64_to_cv2(input_img)
height, width, channels = input_img.shape[:3]
max_ruizido = 0
tmp_num = 5
crop_size = 15
ch_names = {0: 'Hue', 1: 'Saturation', 2: 'Brightness'}
poke_imgs = glob.glob('pokemons/*.png')
for i in range(height//crop_size):
for j in range(width//crop_size):
# 画像をcrop_size*crop_sizeに分割する
trimed_img = input_img[crop_size*i : crop_size*(i+1), crop_size*j: crop_size*(j+1)]
max_ruizido = -1
tmp_num = 5
h_hist_2 = calc_hue_hist(trimed_img)
for poke_name in poke_imgs:
poke_img = cv2.imread(poke_name) #画像を読み出しオブジェクトpoke_imgに代入
# 画像の Hue 成分のヒストグラムを取得する
h_hist_1 = calc_hue_hist(poke_img)
h_comp_hist = cv2.compareHist(h_hist_1, h_hist_2, cv2.HISTCMP_CORREL)
if max_ruizido < h_comp_hist:
max_ruizido = h_comp_hist
tmp_num = poke_name
if j == 0:
# 一番左の画像
tmp_img = cv2.imread(tmp_num)
else:
# 連結したい画像
tmp_img2 = cv2.imread(tmp_num)
# 連結された画像
tmp_img = cv2.hconcat([tmp_img, tmp_img2])
if i == 0:
# 最初の1行が完成したので、次の行に移る
output_img = tmp_img
else:
output_img = cv2.vconcat([output_img, tmp_img])
return {'data': cv2_to_base64(output_img)}
Dockerfileを作る
上記で書いたプログラムを動かすためのDockerfileを作成します。
Lambdaでpythonを使うための基本となるイメージを用意します。
FROM public.ecr.aws/lambda/python:3.7
次にLambdaではpip installができないので、ここでpip install opencv-python
やpip install numpy
を行って、OpenCVや必要なライブラリを使えるようにします。
RUN pip install opencv-python
RUN pip install numpy
今回作成したapp.pyやポケモン画像(pokemonsフォルダ)をイメージ内にコピーします。
COPY app.py /var/task/
COPY pokemons/ /var/task/pokemons/
ここで、公式では、COPY app.py ./
と書いてありますが、これだとOpenCVがうまく動かないので、必ず/var/task/を指定してコピーします。
全体はこちら。
FROM public.ecr.aws/lambda/python:3.7
RUN pip install opencv-python
RUN pip install numpy
COPY app.py /var/task/
COPY pokemons/ /var/task/pokemons/
CMD [ "app.handler" ]
Docker imageを作成してECRにpushする
ECSのページの左のタブからECRをクリック。
Create repositoryをクリックしてリポジトリの名前だけ入れて、create repository
リポジトリの中に入ってView push commandをクリックすると、使い方が載っているので、それに従います。
- コンソールからawsログイン→Login Succeededを確認する。
- docker build -t <リポジトリネーム> .
- AWS用にタグ付け
- ECRにpush
Lambda関数を作成する(ECRからDocker imageをpull)
次にECRからDocker imageをpullするためのLambda関数を作ります。
create functionをクリックして、関数の作成方法はコンテナーイメージから作成を選びます。
関数の名前とイメージのURLを指定します。
Container image URIの部分に、pushしたDockerImageのURLを登録します。
URLに関してはECRのリポジトリ画面からコピペできます。
IAMのroleがない人はLambda用に作っておきます。
初期設定が3秒なので今回はGeneral configurationのEditから30秒に伸ばしました。
メモリはお好みで。(私は128MBにしました。)
API GatewayでAPI化する
REST APIのprivateではない方でBuildをクリックします。
Actionsボタンを押し、Create Resourceを選択。
リソース名などはお好みで。
CORSの許可を忘れないようにします。
ActionsからCreate Methodsを選択、POSTを選びます。
ここで先程作ったLambda関数を選択します。
テストをして通れば、ActionsからDeploy APIを選択して、デプロイします。
これは必須かどうかはわからないのですが、実際に外部からこのAPIを叩こうとしたら、API GatewayからLambdaへのアクセス権限で弾かれてしまったので、私はAPI GatewayからLambdaにアクセスするためのロールを作って付与しました。
これで完成です!
あとがき
最初はOpenCV.jsを使おうとしていたのですが、OpenCV.jsって今の所CDNでしか使えないらしく、Next.jsでOpenCV.jsを使えるようにするのがめんどくさすぎて、今回バックエンドはpythonでやることにしました。
なんのために作ったかよくわからないAPIとなりましたが、なかなか勉強になって楽しかったです。