15
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【LambdaでOpenCVを利用】AWSとOpenCVを利用してポケモン画像でアスキーアート風に変換するAPIを作った

Last updated at Posted at 2021-07-04

こんにちは、Yuiです。

引き続き週イチ発信をしていきます。
今週はAWSとOpenCVを使って、画像処理を行ったのでそのことについて書きます。

以下の件です。

このままだとちょっとあら過ぎるかなと思ったので、修正後がこちら。
心の目で見るとちゃんとワニに見えてきますね?笑
(なんか下腹部にピカチュウがあるのがちょっと悪意のある変換な感じがしますが笑)

webアプリ化は来週するとして、今回はこのAPIの作り方に関して書きます。

過去の週イチ発信は以下

どんなものを作ったか

まずは色々変換してみた結果です。

1.jpg

2.jpg

3.jpg

なかなか面影があります。笑
集合体が苦手な人にはなかなかきつい画像かもしれません。

構成

今回画像を分割して、それぞれをポケモンの画像と比較して色相が似ているものでリプレイスするという感じで組んでいます。

Lambdaではpip installができないので、OpenCVを使うためにDocker imageを利用しました。

というわけで、今回の構成は以下になっています。

image.png

やったことは以下です。

  • 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に分割する
  • 分割された画像それぞれとポケモンの画像の色相のヒストグラムを比較する
  • 類似度が高いポケモンの画像と入れ替える

コード全文は以下です。

app.py
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-pythonpip 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をクリック。

image.png

Create repositoryをクリックしてリポジトリの名前だけ入れて、create repository

image.png

リポジトリの中に入ってView push commandをクリックすると、使い方が載っているので、それに従います。

スクリーンショット 2021-07-04 2.39.57.png

  • コンソールからawsログイン→Login Succeededを確認する。
  • docker build -t <リポジトリネーム> .
  • AWS用にタグ付け
  • ECRにpush

Lambda関数を作成する(ECRからDocker imageをpull)

次にECRからDocker imageをpullするためのLambda関数を作ります。

image.png

create functionをクリックして、関数の作成方法はコンテナーイメージから作成を選びます。

image.png

関数の名前とイメージのURLを指定します。
Container image URIの部分に、pushしたDockerImageのURLを登録します。

URLに関してはECRのリポジトリ画面からコピペできます。

image.png

IAMのroleがない人はLambda用に作っておきます。

image.png

初期設定が3秒なので今回はGeneral configurationのEditから30秒に伸ばしました。

image.png

メモリはお好みで。(私は128MBにしました。)

API GatewayでAPI化する

REST APIのprivateではない方でBuildをクリックします。

image.png

Actionsボタンを押し、Create Resourceを選択。

image.png

リソース名などはお好みで。

image.png

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となりましたが、なかなか勉強になって楽しかったです。

15
17
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
17

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?