0
0

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.

Google Cloud Platformを使って超解像度化APIを公開してみる

Posted at

# はじめに
 この記事は、こちらのブログの記事をqiita用に書き直したものです。ぜひこちらの方でもご覧になってください。
 
 今回はGoogle Cloud Platform 通称GCPを用いてAPIの公開をやっていきたいと思います。ちょっと前にGCPを使うことがあったので、今回はそれを忘れないように記録することと、自分でサービスをデプロイする経験をすることの二つを目的としたいと思います。ちなみにAPIにするのは以前実装したESPCNを用いた超解像度化モデルです。
 今回触れたいと思っているのは、GCPの中でも、Cloud Runという機能です。

GCPへの登録

GCPへの登録はGoogleのガイドか、そのほかの記事を参考にすればだいたい大丈夫です。特にスタートガイド通りにしておけば問題ないです。↓の記事が参考になります。
 これから始めるGCP(GCE) 安全に無料枠を使い倒せ
 基本的以下の順番で登録を行います。

 1. GCPにログイン
 image.png
 2. 上の画面の「無料トライアルに登録」ボタンを押す
 image.png
 3. 表示される指示に従って情報を記入
 image.png
 ここでは、クレジットカードなどの情報を入力することになりますが、有料アカウントに設定しない限りは勝手に引き落とされてしまうような事はないので安心してください。

登録完了

これで、登録完了です。GCPの無料枠では、f1-microのGCEインスタンスが無料で建てられるので、ぜひ使ってみてください。ちなみに無料トライアルでは12ヶ月の間300$相当のサービスが無料で使えるので、色々試して見ると良いかもしれません。

Cloud Runを使う

 Cloud RunはDockerのコンテナをデプロイすることのできるサービスで、基本的にContainer RegistryにアップロードされたDocker Imageを使ってサービスをデプロイします。この際に、Githubへのpullをトリガーにコンテナのビルドとデプロイを行うようにする方法があるそうですが、ちょっと難しそうなので今回はローカルでビルドしたイメージをアップロードする形をとりたいと思います。

gcloud コマンドのインストール

 私はmacを使っていますので以下の説明はmac以外の方には参考になりません。
 まずは以下のアーカイブをダウンロードします。

 google-cloud-sdk-245.0.0-darwin-x86_64.tar.gz

 その後以下のコマンドを実行します。

$ ./google-cloud-sdk/install.sh 

 次にSDKの初期化を行います。

$ gcloud init

 その後ログインしますか?という旨のメッセージが出てくるのでYを入力します。その後接続するプロジェクトを選択しますがプロジェクトが一つだけの時は勝手に選ばれるそうなので安心してください。この後も適当に入力していけばコマンドが使えるようになります。↓は公式のセットアップ方法です。

macOS 用のクイックスタート
Windows 用のクイックスタート
Linux 用のクイックスタート

Dockerイメージを作成

 ここでは前回実装したESPCNのモデルを用いて解像度を倍にして出力するコードをAPIとして使えるようにしようと思います。使うのはFlaskとgunicornです。

 全体の動作の流れとしては

  1. リクエストのjsonのから入力となる画像のbase64をデコード
  2. デコードした画像を超解像の関数に入力
  3. base64にエンコードしてレスポンス

 という簡単なものです。まずはEspcn-API用のGitリポジトリを作成します(リポジトリはこちら)。ディレクトリ構成は以下のような感じです。

ESPCN-API
 ├model
 │ └pre_trained_mode.pth
 ├api.py
 ├requirements.txt
 ├Dockerfile
 └networks.py
test
 ├test.py
 └test.png

テストコード

 まずはテスト用のコードから書いていきます。テストで確認する事は以下の二つです。

 ・レスポンスが返ってくる(status_codeが200)
 ・画像のサイズが2倍になっている事
 
 これらを確認するための動作を実装していきます。

import sys
import requests
import json
from io import BytesIO
from PIL import Image
import base64

def image_file_to_base64(file_path):
    with open(file_path, "rb") as image_file:
        data = base64.b64encode(image_file.read())
    return 'data:image/png;base64,' + data.decode('utf-8')

def base64_to_img(img_formdata):
    img_base64 = img_formdata.split(',')[1]
    input_bin = base64.b64decode(img_base64)
    with BytesIO(input_bin) as b:
        img = Image.open(b).copy().convert("RGB")
    return img

if __name__ == "__main__":
    source_image_path = "test.png"
    source_image = Image.open(source_image_path)
    source_width, source_height = source_image.size
    print("source_width :", source_width)
    print("source_height :", source_height)
    host_url = "http://0.0.0.0:8000"
    data = {"srcImage":image_file_to_base64(source_image_path)}
    json_data = json.dumps(data)

    response = requests.post(host_url, json_data, headers={'Content-Type': 'application/json'})

    assert response.status_code == 200, "validation error status code should be 200"

    res_json = response.json()
    
    res_image = base64_to_img(res_json["sresoImage"])
    sreso_width, sreso_height = res_image.size
    print("sreso_width :", sreso_width)
    print("sreso_height :", sreso_height)
    assert sreso_width == source_width * 2 and sreso_height == source_height * 2 , \
        "validation error image size should be 2 times of input image"
    res_image.show()
    print("OK")

メインのコード

 次はメインとなるapi.pyを実装していきます。はじめに全体の実装の骨格を作成しました。関数の中身については全く実装していません。

from flask import Flask, request, jsonify
from networks import Espcn
import os
from io import BytesIO
import base64
from torchvision import transforms
from torch import load
import torch
import json
from PIL import Image

device = "cpu"
net = Espcn(upscale=2)
net.load_state_dict(torch.load(opt.model_path, map_location="cpu"))
net.to(device)
net.eval()

def b64_to_PILImage(b64_string):
    """
    process convert base64 string to PIL Image
    input: b64_string: base64 string : data:image/png;base64,{base64 string}
    output: pil_img: PIL Image
    """
    pass

def PILImage_to_b64(pil_img):
    """
    process convert PIL Image to base64
    input: pil_img: PIL Image
    output: b64_string: base64 string : data:image/png;base64,{base64 string}
    """
    pass

def expand(src_image, model=net, device=device):
    pass

@app.route("/", methods=["POST"])
def superResolution():
    pass

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=os.environ.get("PORT", 8000))

 expand関数がメインの超解像度化を行い、それをリクエストに対して稼働するsuperResolution内で実行する構造を考えています。また、超解像度化の際に使うモデルは、関数外で定義しておく事で、workerが複数のリクエストを処理する際にモデルを読み込む手間を省く事ができると思っています。

 では、まずは入出力となる~_to_~関数二つを実装します。

def b64_to_PILImage(b64_string):
    """
    process convert base64 string to PIL Image
    input: b64_string: base64 string : data:image/png;base64,{base64 string}
    output: pil_img: PIL Image
    """
    b64_split = b64_string.split(",")[1]
    b64_bin = base64.b64decode(b64_split)
    with BytesIO(b64_bin) as b:
        pil_img = Image.open(b).copy().convert('RGB')
    return pil_img

def PILImage_to_b64(pil_img):
    """
    process convert PIL Image to base64
    input: pil_img: PIL Image
    output: b64_string: base64 string : data:image/png;base64,{base64 string}
    """
    with BytesIO() as b:
        pil_img.save(b, format='png')
        b.seek(0)
        img_base64 = base64.b64encode(b.read()).decode('utf-8')
    img_base64 = 'data:image/png;base64,' + img_base64
    return img_base64

 BytesIOの使い方が意外と難しくて苦戦しました。次は、expand関数とメインとなるsuperResolution関数です。

def tensor_to_pil(src_tensor):
    src_tensor = src_tensor.mul(255)
    src_tensor = src_tensor.add_(0.5)
    src_tensor = src_tensor.clamp_(0, 255)
    src_tensor = src_tensor.permute(1, 2, 0)
    src_tensor = src_tensor.to("cpu", torch.uint8).numpy()
    return Image.fromarray(src_tensor)


def expand(src_image, model=net, device=device):
    src_tensor = transforms.ToTensor()(src_image).to(device)
    if src_tensor.dim() == 3:
        src_tensor = src_tensor.unsqueeze(0)
    
    srezo_tensor = model(src_tensor).squeeze()
    srezo_img = tensor_to_pil(srezo_tensor)
    return srezo_img

@app.route("/", methods=["POST"])
def superResolution():
    req_json = json.loads(request.data)
    src_b64 = req_json["srcImage"]

    # main process
    src_img = b64_to_PILImage(src_b64)
    srezo_img = expand(src_img)
    srezo_b64 = PILImage_to_b64(srezo_img)

    results = {"sresoImage":srezo_b64}

    return jsonify(results)


if __name__ == "__main__":
    app.run(host="0.0.0.0", port=os.environ.get("PORT", 8000))

 当初は二つの関数に収めようと思ったのですが、torch.tensorからPIL Imageへの変換が、transforms.ToPILImageを使うと色が変な感じになってしまったので、別で定義することにしました。また、最後のapp.runの時の引数ですが、cloud runの仕様上docker containerごとに別のポートが自動で割り振られるため、環境変数PORTを読み取る設定にしておかないといけないらしいためです。

 ではローカルで動作チェックしたこれらをdocker imageとしてビルドします。

$ docker build -t gcr.io/[project id]/espcn-api:0 .

 次にイメージをgcloud コマンドを用いてcontainer registryにアップロードします。

$ gcloud docker -- push gcr.io/[project id]/espcn-api:0

 実際にGCP上のContainer Registry上でイメージが確認できると思います。
image.png
 次にアップロードされたイメージを用いてデプロイします。

$ gcloud beta run deploy SR-API --image=gcr.io/[project id]/espcn-api:0 

 deployの後のSR-APIと入力している部分はサービスの名前なので各自好きなようにつけて問題ないです。また初めて実行する際にはbetaコンポーネントをインストールするらしいです。この時(--platform managed)を入れなければコンソール上で

 [1] Cloud Run (fully managed)
 [2] Cloud Run for Anthos deployed on Google Cloud
 [3] Cloud Run for Anthos deployed on VMware
 [4] cancel
Please enter your numeric choice: _

 と入力を求められるので1を入力しておきましょう。fully managed でなければ無料で運用する事は難しくなります。次にリージョンを聞かれます。

 [1] asia-east1
 [2] asia-northeast1
 [3] europe-north1
 [4] europe-west1
 [5] europe-west4
 [6] us-central1
 [7] us-east1
 [8] us-east4
 [9] us-west1
 [10] cancel
Please enter your numeric choice: _

 ここでは、「us-*」を選びましょう。Cloud Run では北米内での下りのネットワーキングは1GBまで無料なので、これをCloud Functionsから呼び出すようにすれば(Cloud Functionsでは下りのネットワーキングはどこに対しても5GBまで無料なので)ほとんど無料で使用できます。

 この後認証されていないアクセスを許可するか聞いてくるのですが、今はとりあえず動かしてみることが目的なのでyesにしておきましょう。以上のように入力していくとサービスがデプロイされ以下のようなメッセージが出力されます。

Deploying container to Cloud Run service [espcn-api] in project [studied-brace-261908] region [us-central1]
✓ Deploying new service... Done.                                                                                                                       
  ✓ Creating Revision...                                                                                                                               
  ✓ Routing traffic...                                                                                                                                 
  ✓ Setting IAM Policy...                                                                                                                              
Done.                                                                                                                                                  
Service [espcn-api] revision [espcn-api-00001-guj] has been deployed and is serving 100 percent of traffic at https://espcn-api-~~~-uc.a.run.app

 最終的にどこどこのurlでデプロイされたといったメッセージが出てくるので、ここにtest.pyを使ってリクエストを投げてみるときちんとレスポンスが返ってくるのが確認できました。

 この時一つ詰まったことがあったのですが、はじめにリクエストを投げた時にはレスポンスが返ってきませんでした。そこでCloud Runのログの方を確認してみると、

Memory limit of 244M exceeded with 272M used. Consider increasing the memory limit, see https://cloud.google.com/run/docs/configuring/memory-limits

 メモリーが足りないという旨のメッセージが届きました。cloud runでは使用するメモリが決まっているらしく、指定できるメモリのサイズは128MB~2GBでデフォルトでは256MBが割り当てられているそうです。512 × 512のイメージでこのエラーが発生してしまったので、512MBなどを割り当てることで解決できそうですが、あまり大きいメモリを割り当ててしまうとすぐに無料枠分を超えてしまいそうです。
 ちなみにアプリケーションに割り当てるメモリを変える場合には次のコマンドを使ってください。

$ gcloud beta run services update [service name] --memory 512M

終わりに

 以上APIを作ってCloud Runで公開するところまで行いました。今の状態では、このAPIは何処からでもアクセスできる状態で、セキュリティ的にも財布にも優しくないのでので、次はCloud Runに対するアクセスをGCP内からに制限した上で、Cloud Functionsを使ってこれにリクエストを送る構造に変更していきたいと思います。継続的デプロイについてもいつか触れたいと思います。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?