Lambda + OpenCVで画像処理 (グレー画像作成)

# Lambda + OpenCVで画像処理 (グレー画像作成)

この記事は[サーバーレスWebアプリ Mosaic](https://mosaic.w2or3w.com "Mosaic")を開発して得た知見を振り返り定着させるための[ハンズオン記事](https://qiita.com/w2or3w/items/87b57dfdbcf218de91e2)の1つです。

以下を見てからこの記事をみるといい感じです。

* [AWS Amplifyを利用してS3へファイルアップロードとLambda呼び出し](https://qiita.com/w2or3w/items/fb82544f001296087bd7)

##イントロダクション
S3への画像アップロードをトリガーに、その画像に対して画像処理をさせるLambdaのファンクションをPython3.6で実装します。
画像処理にはOpenCVを利用します。

##コンテンツ

### GithubにLambdaファンクションプロジェクト用のリポジトリを作成
GitHubにプロジェクト用のリポジトリを作成しましょう。
リポジトリ名は `sample_lambda_py_project` とします。

### Cloud9にリポジトリをクローン

```
$ git clone https://github.com/{YourGithubID}/sample_lambda_py_project.git
```

### シンプルなプログラムの作成
sample_vue_projectの中に適当なフォルダ(ここでは`source`としました)を作成し、その中に`lambda_function.py`ファイルを作成してください。内容はとりあえず、[以前の記事](https://qiita.com/w2or3w/items/fb82544f001296087bd7)でインラインに実装したコードとします。(ログメッセージの内容は少し変えておきます。)

```lambda_function.py
from urllib.parse import unquote_plus
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))
```
Cloud9の環境はこんな感じになっていると思います。
![Screenshot 2019-12-31 at 15.04.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/5444ba84-cd80-56ca-781f-6a83b0748d2f.png)

### 関数コードをzipファイル圧縮し、S3経由でLambdaにデプロイする
現在のようなシンプルなコードであれば必要ないのですが、OpenCVのような標準ライブラリ以外のライブラリを使う場合はライブラリもzipファイル内にまとめてパッケージとしてアップロードする必要があります。

そこでまずは、ライブラリを利用しない状態でzip圧縮してS3経由でLambdaにアップロードしてみましょう。

#### zip圧縮
`lambda_function.py`があるディレクトリ以下を`lambda-package.zip`という名前で圧縮します。
`lambda_function.py`があるディレクトリで、以下のコマンドを実行します。

```
$ zip -r ../lambda-package.zip *
```

#### S3へアップロード
作成した`lambda-package.zip`をS3にアップロードします。
`lambda_function.py`があるディレクトリのまま、以下のコマンドを実行します。

```
$ aws s3 cp ../lambda-package.zip s3://sample-vue-project-bucket-work/deploy/lambda-package.zip
```
#### Lambdaのコンソールでデプロイ
AWSコンソール > Lambda > 関数 > S3Trigger********-work
にアクセスします。
関数コードを以下のように設定してください。
コード エントリ タイプ : Amazon S3からのファイルアップロード
ランタイム : Python 3.6
ハンドラ : lambda_function.lambda_handler
Amazon S3 のリンクURL : 先程アップロードしたlambda-package.zipのリンクURL
![Screenshot 2019-12-31 at 15.20.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/eedd8329-5478-26c4-8762-3a12058cacd1.png)
画面右上の保存ボタンを押下してください。

#### 動作確認
sample_vue_projectのWebアプリからファイルをアップロードしたら、
AWSコンソール > CloudWatch > ロググループ > aws/lambda/S3Trigger********-work
にアクセスし、最新のログストリームを参照してみてください。
Function Start (deploy from S3) : Bucket=sample-vue-project-bucket-work, Key=public/191231063457-16f5aaa24b1/faceA.jpeg
みたいなINFOログが出力されていればOKです。
![Screenshot 2019-12-31 at 15.35.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/8d5ea815-d3a2-c518-faa2-895da78a111b.png)

### OpenCVのインストール
`lambda_function.py`がある`source`ディレクトリで、以下のコマンドを実行します。
※pipが参照するPythonが3.6になっていることを確認してください。

```
$ pip --version
pip 9.0.3 from /usr/lib/python3.6/dist-packages (python 3.6)
$ pip install opencv-python -t .
```
Lambdaファンクションはアップロードするzipパッケージの中にライブラリを含める必要がありますので、`-t .`このオプション指定をしてカレントディレクトリの中にインストールします。

### グレー画像変換プログラム
`lambda_function.py`を更新します。

```lambda_function.py
# coding: UTF-8
import boto3
import os
from urllib.parse import unquote_plus
import numpy as np
import cv2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client("s3")

def lambda_handler(event, context):
    bucket = event['Records'][0]['s3']['bucket']['name']
    key = unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
    logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))

    fileName = os.path.basename(key)
    orgFilePath = u'/tmp/' + fileName
    processedFilePath = u'/tmp/processed-' + fileName

    if (not key.startswith("public")):
        logger.info("not start with public")
        return

    keyOut = key.replace("public", "processed", 1)
    logger.info("Output local path = {0}".format(processedFilePath))

    try:
        s3.download_file(Bucket=bucket, Key=key, Filename=orgFilePath)

        orgImage = cv2.imread(orgFilePath)
        grayImage = cv2.cvtColor(orgImage, cv2.COLOR_RGB2GRAY)
        cv2.imwrite(processedFilePath, grayImage)

        s3.upload_file(Filename=processedFilePath, Bucket=bucket, Key=keyOut)
        logger.info("Function Completed : processed key = {0}".format(keyOut))

    except Exception as e:
        logger.exception(e)
        raise e
        
    finally:
        if os.path.exists(orgFilePath):
            os.remove(orgFilePath)
        if os.path.exists(processedFilePath):
            os.remove(processedFilePath)
```
(※Pythonの基本構文についてはここでは触れません。必要な場合はどこかで学んできてください。)

アップロードされた
public/191231112635-16f5bb5247f/lenna.png のようなファイルをグレー画像に変換して、
processed/191231112635-16f5bb5247f/lenna.png のようなパスへアップロードしています。

#### Lambdaの基本設定
Lambdaの基本設定でメモリやタイムアウトの指定が可能です。デフォルトでは、メモリ＝128MB、タイムアウト25秒、となっています。
最近のスマホで撮影した写真などはサイズが大きいので、デフォルト設定のままだとメモリが足りなくて例外が発生してしまうかもしれません。今後Lambdaで行う処理を増やしていった際に時間が足りなくてタイムアウトしてしまうかもしれません。必要に応じてLambdaの基本設定を調整してください。
(※画像のサイズについては、クライアント側で圧縮してからアップロードするのが良いと思いますけどね。)


### デプロイと動作確認
zip圧縮して、S3にアップロードして、Lambdaにデプロイした上で、Webアプリから画像をアップロードしてみてください。
S3バケットの以下にモノクロ画像がアップロードされていればOKです。
CloudWatchのログも確認しておきましょう。
![lenna.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/c660571c-3c35-5766-f1ef-3eece6b5c7ea.png)
![lenna (1).png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/394775/21fb56dc-6185-8044-1591-282fec2f532a.png)
(レナおうつくしいね)


## あとがき
今回のサンプルプログラムのように、トリガー元となるS3に対してLambdaファクションから操作した場合、その操作によりLambdaファンクションが呼び出されます。気をつけないとLambdaの無限ループになってしまいますので注意が必要です。

うっかりLambda無限ループに陥ってしまった場合、すぐに気づけばいいですが、すぐに気づかなかった場合、、、考えたくもないですね。自動請求加算マシーンは遠慮なく仕事を遂行し続けるでしょう。

無限ループLambdaを止める方法ですが、関数を削除するか、もしくは、例外が発生する関数コードをアップロードするか、どちらかになるでしょう。
関数を削除してしまうとまた作成したり設定するのが面倒なので、例外が発生する関数コードをアップロードするのが良いと思っているのですが、他にもっと良い方法はないでしょうか。誰か教えて下さい。

いずれにしても、震える手で操作することになるでしょうね。変な汗をかきながら。


あと、ワタシはまだLambdaから呼ばれるPythonのコードをデバッグしたことがありません。
Cloud9でデバッグできるようなので、習得したらより効率的に開発を進められるでしょうね。
習得したら、記事を書くようにします。