AWS Lambdaがコンテナイメージをサポートしたので、Detectron2 を使って画像認識(Object Detection)を行うAPI を作る

この記事は[Fusic Advent Calender](https://qiita.com/advent-calendar/2020/fusic)の5日目の記事です。
昨日は [@ayasamind](https://twitter.com/ayasamind) さんの [PHP8+ Laravel8 + laravel-generatorで簡単CRUD作成からユニットテストまで書く](https://qiita.com/ayasamind/items/a5bfa65ab0b8effb26a8) でした。
[@ayasamind](https://twitter.com/ayasamind) さんといえば、先日大いにバズった [Geeks Bar Tenjin](https://twitter.com/Mr_1484/status/1332262150581075968) を企画した偉大なお方です。
月イチで開催されるらしいので、福岡にお立ち寄りの際は、ぜひ[開催日を確認](https://bar.geekstudio.work/)してみてください！

----
# やったこと
AWS Lambda 上でDetectron2 を実行し、画像にバウンディングボックスを付けます。
![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115634/337ede4d-5104-ed46-7b4f-49cd07e41d0b.jpeg)

# はじめに

現在絶賛開催中の、[AWS Re:Invent](https://aws.amazon.com/jp/about-aws/events/2020/reinvent/) にて、AWS Lambda が[コンテナイメージをサポートする](https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/) という発表がありました。今までは様々な制約があり、AWS Lambda では動かせない処理がありましたが、今回の発表により実質実行環境の制約はなくなったと考えることができそうです(GPU や Amazon Elastic Inference が使えないなどの制約はありますが)。

また、コンテナイメージは最大10GB までとなり、今までの[Lambda Layer](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/configuration-layers.html)の制約(解凍後のデプロイパッケージのサイズ制限250MB) に比べて、大幅に制約が緩和されました。

ということで、今まではなかなか難しかったと思われる、AWS Lambda でDetectron2 を動かしてObject Detection API を作るというチャレンジをしてみようと思います。

# コンテナの作成

Python でコンテナを作成するためのツールがAWS によって作成されているので、参考にしながらDockerfile を作成します。
https://github.com/aws/aws-lambda-python-runtime-interface-client

ここのREADMEどおりに進めれば、ローカル環境でLambda を実行するコンテナの動作を確認することができます。

注意点としては、`https://github.com/aws/aws-lambda-python-runtime-interface-client#local-testing` をよく読むことです。自分は、[くだらない質問](https://github.com/aws/aws-lambda-python-runtime-interface-client/issues/6) をして、作成者のお手を煩わせてしまいました。 動作確認をするためには、[AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) を使用する必要があります。インストール方法も含めてREADME にかかれているので、ご確認ください。

基本はREADME 通りに進めれば実行できるコンテナが作成できますが、今回はDetectron2 を動かすことが目的なので、以下のようにDockerfile を少し修正しました。

README からの変更点は、

- ベースイメージを、python:3.8.6に変更
- libopencv-dev や、torch、torchvision、Detectron2 などをインストール
- Model Zoo からダウンロードすべき重みを予めダウンロードする

などです。

また、開発環境では、 S3 にアクセスするために AWS のアクセスキー等をENV に仕込んでいました。安全上の理由から、現在は削除しています。

```Dockerfile 
ARG FUNCTION_DIR="/function/"
FROM python:3.8.6
ARG FUNCTION_DIR

# Install aws-lambda-cpp build dependencies
RUN apt-get update && \
  apt-get install -y \
  g++ \
  make \
  cmake \
  unzip \
  libcurl4-openssl-dev \
  libopencv-dev && \
  apt-get autoremove -y

RUN pip install torch torchvision opencv-python
RUN pip install 'git+https://github.com/facebookresearch/detectron2.git'
RUN mkdir -p ${FUNCTION_DIR}
COPY src/* ${FUNCTION_DIR}
# モデルのダウンロードを毎回していたら重いので、Docker Image にしておく。
COPY model_final_721ade.pkl /root/.torch/fvcore_cache/detectron2/COCO-Detection/faster_rcnn_R_50_C4_1x/137257644/model_final_721ade.pkl
RUN pip install \
    --target ${FUNCTION_DIR} \
        awslambdaric

WORKDIR ${FUNCTION_DIR}
RUN pip install boto3
# Copy in the built dependencies
# COPY --from=build-image ${FUNCTION_DIR} ${FUNCTION_DIR}
ENTRYPOINT [ "/usr/local/bin/python", "-m", "awslambdaric" ]
CMD [ "app.handler" ]
```



# 実装

上記のDockerfile で `COPY src/* ${FUNCTION_DIR}` という箇所で、`./src` 以下のファイルを `${FUNCTION_DIR}` にコピーしています。また、 `CMD ["app.handler"] ` という箇所がありますが、これは、 app.py の handler メソッドを叩くという意味です。
この辺の雛形も、README に準備してあります。

今回は、Detectron2 を動かしたいので、Detectron2 の demo を参考に、コードを実装しました。
https://github.com/facebookresearch/detectron2/tree/master/demo

predictor.py は、上記のレポジトリからコピーしてきたものをベースに不要な箇所を削りました。
demo.pyは、以下のように書き換えました。

主に行ったのは、

- arg の依存を外す
- video 等の不要な箇所を削除
- 画像をダウンロードしてくるように変更

などです。

get_result メソッドが、 `app.handler` から叩かれる想定です。
あ。get していないのにメソッド名が get になっていますね。ご愛嬌です。

```python:src/demo.py
import argparse
import glob
import os
import time
import cv2
import tqdm
import boto3

from detectron2.config import get_cfg
from detectron2.data.detection_utils import read_image
from detectron2.utils.logger import setup_logger

from predictor import VisualizationDemo

def setup_cfg():
    # load config from file and command-line arguments
    cfg = get_cfg()
    # Lambda は CPU しか使えない (と思う)
    cfg.MODEL.DEVICE = 'cpu'
    config_file = '/function/faster_rcnn_R_50_C4_1x.yaml'
    cfg.MODEL.WEIGHTS = 'https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_C4_1x/137257644/model_final_721ade.pkl'

    cfg.MODEL.RETINANET.SCORE_THRESH_TEST = 0.5
    cfg.MODEL.ROI_HEADS.SCORE_THRESH_TEST = 0.5
    cfg.MODEL.PANOPTIC_FPN.COMBINE.INSTANCES_CONFIDENCE_THRESH = 0.5
    cfg.freeze()
    return cfg

def from_s3_to_tmp(event):
    bucket = event['bucket']
    path = event['s3_path']
    s3 = boto3.resource('s3')

    bucket = s3.Bucket(bucket)
    bucket.download_file(path, '/tmp/input.jpg')

def get_result(event):
    setup_logger(name="fvcore")
    logger = setup_logger()
    cfg = setup_cfg()
    demo = VisualizationDemo(cfg)

    from_s3_to_tmp(event)
    # use PIL, to be consistent with evaluation
    img = read_image('/tmp/input.jpg', format="BGR")
    predictions, visualized_output = demo.run_on_image(img)
    out_filename = '/tmp/output.jpg'
    visualized_output.save(out_filename)

```



また、設定ファイルである、 `faster_rcnn_R_50_C4_1x.yaml` や `Base-RCNN-C4.yaml` は、src 以下に配置しました。

src/app.py は、こんな感じにしました。

```python:src/app.py
from datetime import datetime as dt
import json
from demo import get_result
import base64

def handler(event, context):
    if 's3_path' not in event or 'bucket' not in event:
        return {'result': False}

    get_result(event)
    with open("/tmp/output.jpg", 'rb') as f:
        img = base64.b64encode(f.read())
    base64_encoded_binary_data = img.decode("utf-8")
    return {
        'result': True,
        'isBase64Encoded'   : True,
        'statusCode'        : 200,
        'headers'           : { 'Content-Type': 'image/jpeg' },
        'body'              : base64_encoded_binary_data}

```

ディレクトリ / ファイル の構成は以下のようになっています。

```
├── Dockerfile # 上述のDockerfile
├── access.py # デバック用のコード
├── src
│   ├── Base-RCNN-C4.yaml # Detectron2 の設定ファイル
│   ├── app.py # ENTRY POINT。 handler が入っている。
│   ├── demo.py # Detectron2 の実行を行うファイル
│   ├── faster_rcnn_R_50_C4_1x.yaml # Detectron2 の設定ファイル
│   └── predictor.py # Detectron2 の実行を行うファイル
├── build_and_run.sh # コンテナのビルドと実行をするシェル。後述。
└── model_final_721ade.pkl # 重みファイル。wget https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_C4_1x/137257644/model_final_721ade.pkl でとってきた。
```

ここまでで、コードの準備は完了です。

まず、ローカルで動作検証をします。

# ローカルで動かす

README に従い、 [AWS Lambda Runtime Interface Emulator](https://github.com/aws/aws-lambda-runtime-interface-emulator) をインストールします。

```
mkdir -p ~/.aws-lambda-rie && \
    curl -Lo ~/.aws-lambda-rie/aws-lambda-rie https://github.com/aws/aws-lambda-runtime-interface-emulator/releases/latest/download/aws-lambda-rie && \
    chmod +x ~/.aws-lambda-rie/aws-lambda-rie
```



デバッグ時は何度も build & run を繰り返すことが想定されるので、`build_and_run.sh` を準備します。

といっても、書くのはこれだけですがｗ

```shell:build_and_run.sh
docker build -t gorogoroyasu/lambda_docker .
docker run --name lambda --rm -p 9000:8080 \
          -v ~/.aws-lambda-rie:/aws-lambda \
            --entrypoint /aws-lambda/aws-lambda-rie \
              gorogoroyasu/lambda_docker \
                /usr/local/bin/python -m awslambdaric app.handler
```

実行前に、 `wget https://dl.fbaipublicfiles.com/detectron2/COCO-Detection/faster_rcnn_R_50_C4_1x/137257644/model_final_721ade.pkl` を叩いて、重みファイルをダウンロードしておいてください。

ここまで準備すると、 `sh build_and_run.sh `を実行するだけでデバックができます。便利です。
実行ログや実行結果と一緒に、関数の実行時間や使用したメモリ量も表示してくれるので、実際にデプロイするときに便利です。

また、作ったコンテナに入りたい場合は、

```sh
$ docker run --name lambda --rm -p 9000:8080 \
          -v ~/.aws-lambda-rie:/aws-lambda \
            --entrypoint /bin/bash \
              gorogoroyasu/lambda_docker
```

を実行してあげると良いです。



build_and_run.sh を実行し、コンテナが run されたら、 

```shell
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"bucket": "YOUR_BUCKET_NAME", "s3_path": "path/to/jpg"}'
```

を実行します。 (後述するGithub Repository の access.py を修正して使っても良いです。そのほうが便利。)

思うような結果が返ってきたら、実際にAWS Lambda で実行してみます。



# デプロイ

## ECR

まず、ECR (Elastic Container Registry) に 作ったコンテナイメージをプッシュします。
はじめに、AWS のコンソールからECR のページに行き、レポジトリを作成します。
レポジトリを作成すると、コンソール上でイメージのプッシュの仕方を教えてくれます。

たぶん、こんな感じになります。

```shell
$ export YOUR_REGION=YOUR_REGION
$ export YOUR_PROFILE=YOUR_PROFILE
$ export ACCOUNT_ID=ACCOUNT_ID
$ export REPOSITORY_NAME=REPOSITORY_NAME
$ aws ecr get-login-password --region ${YOUR_REGION} --profile ${YOUR_PROFILE} | docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${YOUR_REGION}.amazonaws.com
$ docker build -t ${REPOSITORY_NAME} .
$ docker tag ${REPOSITORY_NAME}:latest ${ACCOUNT_ID}.dkr.ecr.${YOUR_REGION}.amazonaws.com/${REPOSITORY_NAME}:latest
$ docker push ${ACCOUNT_ID}.dkr.ecr.${YOUR_REGION}.amazonaws.com/${REPOSITORY_NAME}:latest
```

## Lambda 関数の作成
コンソールで AWS Lambda の作成画面に行き、作成ボタンをクリックします。
その後、関数の作成のところで、コンテナイメージを選択し、関数名と、先程プッシュしたイメージの URI を貼ります。
イメージのURI は、ECR のレポジトリの画面から確認することができます。

今回は、画像をS3 から取得するので、Lambda 関数に S3 へのアクセスロールを付与する必要があることに注意してください。
また、デフォルトの設定ではリソースが足りないので、タイムアウト時間やメモリは適宜変更してください。



## Lambda 関数の実行

access.py の中身は、以下のようになっています。

```python
import json
import base64
import boto3

l = boto3.Session(profile_name='YOUR_PROFILE_NAME').client('lambda').invoke(
    FunctionName='YOUR_FUNCTION_NAME',
    InvocationType='RequestResponse', # Event or RequestResponse
    Payload=json.dumps({'bucket': 'YOUR_BUCKET_NAME', 's3_path': 'path/to/jpg'})
)
j = json.loads(l['Payload'].read())
decoded = base64.b64decode(j['body'])
with open("output.jpg", 'wb') as f:
    f.write(decoded)

```

これを実行すると、無事処理が行われます。

今回は、作成した画像をS3 に上げるのではなく、Base64 Encode して、呼び出し元に返却することにしました。(確認するところを作るのがめんどくさかったからｗ)
うまくいくと、結果は output.jpg というファイルに保存されます。

あくまで一例ですが、インプットの画像とアウトプットの画像はそれぞれ以下のようになります。

## 実行例
### input

![baseball.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115634/dfe635e8-db3e-8e62-e17b-27a738354031.jpeg)

### output

![output.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/115634/337ede4d-5104-ed46-7b4f-49cd07e41d0b.jpeg)

バウンディングボックスがついていることがわかります。

# 感想
実行に20~40秒弱かかるので、現実的な推論時間ではありませんが、今までできなかったことができるようになるのはワクワクしますね。
ちなみに、ビルドしたコンテナは、 約`565MB`  でした。
せっかくだから10GB 使いたかったけど、遠く及ばずでした。残念。
でも、おそらく Lambda Layer だけでは実行できなかったと思うので、満足です。

コードは、[Github](https://github.com/gorogoroyasu/lambda-docker-detectron2) にあげておきます。
参考になれば幸いです。

# 明日のFusic Advent Calendar は？

[@masayuki031](https://qiita.com/masayuki031) の記事です。
たぶん、水陸両用の何かを作ったという話になるんだろうなと思っています。
お楽しみに！

# 参考文献

様々な記事を参考にさせていただきました。以下、抜けがあるかもしれませんが、列挙させていただきます。

- https://aws.amazon.com/jp/blogs/news/new-for-aws-lambda-container-image-support/
- https://github.com/aws/aws-lambda-python-runtime-interface-client

- https://github.com/aws/aws-lambda-runtime-interface-emulator
- https://qiita.com/ume1126/items/8170a10fad6b21f0f54a
- https://qiita.com/danishi/items/7f1569151a766b678b02
- https://qiita.com/is_ryo/items/e16527db5800854cd95f
- https://github.com/facebookresearch/detectron2
- https://detectron2.readthedocs.io/tutorials/getting_started.html#inference-demo-with-pre-trained-models
- https://qiita.com/niusounds/items/9be50e9d8538db052275
- https://stackoverflow.com/questions/44860486/how-to-return-binary-data-from-lambda-function-in-aws-in-python

大変勉強になる記事ばかりでした。
ありがとうございました。