2
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.

【AWS-Chalice】multipart/form-data の形式でアップロードされたバイナリファイルの扱い方 (有名人識別サービスをChaliceで作るハンズオン付き!)

Last updated at Posted at 2021-03-28

はじめに

AWS Chalice を用いてAPIを開発した際、multipart/form-data の形式でアップロードされた画像ファイルの取り扱いにつまずきました。
先駆者様の記事を見つけたものの、2021年3月現在ではうまく動きません:cry:
他の解決策を探した結果、GitHubの issue を参考に解決できたため、備忘も兼ねて手法を記載します。

コード例

Chalice の基本は理解しているものとして、主要部分のみを記載します。
Chalice については、こちらのAWS公式ハンズオン記事 がファーストステップとしておすすめです。

▼Chalice - app.py

app.py
import cgi
from chalice import Chalice
from io import BytesIO

# Chalice設定
app = Chalice(app_name='APP_NAME')

# API Gateway に multipart/form-data をバイナリとして扱うように指示する設定 (ここがポイント!!)
app.api.binary_types.append('multipart/form-data')

@app.route('/upload', methods=['POST'], content_types=['multipart/form-data'], cors=True)
def upload():
    # 受け取った情報をバイナリデータとして保存
    body_binary = BytesIO(app.current_request.raw_body)

    # cgi.FieldStorageクラスを用いて、フォームの内容を解析
    environ = {'REQUEST_METHOD': 'POST'}
    headers = {'content-type': app.current_request.headers['content-type']}
    form = cgi.FieldStorage(fp=body_binary, environ=environ, headers=headers)
    
    # 指定したファイルをバイナリ形式で取得('uploadfile'は送信時に設定した当該ファイルのnameの値)
    file_binary = form.getvalue('uploadfile')
    
    # 以降、任意の処理を記述

▼index.html (アップロード元の例)
コード内、** API Gateway URL をここに貼り付け! **に作成した API の URL を上書きしてください。
「/」以下のパスまで忘れずに記載ください。
(例:https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/api/upload) ← 「/upload」まで忘れずに!

index.html
<!DOCTYPE html>
<html lang="ja">
    <head>
        <meta charset="UTF-8">
        <title>TITLE</title>
    </head>
    <body>
        <form action="** API Gateway URL をここに貼り付け! **"
                enctype="multipart/form-data"
                method="POST">
            <input type="file" name="uploadfile" />
            <input type="submit"/>
        </form>
    </body>
</html>

コードのポイント

2021年3月現在、先駆者様の記事を参考にしても、Chaliceでは画像ファイルなどのバイナリファイルをうまく扱えません。
これは、API Gateway のデフォルト設定が、 multipart/formdata 形式をバイナリとして扱わない設定になっているためです。この設定のため、API Gateway は、http の body の部分を UTF-8 で再エンコーディングしてしまい、アップロードされたファイルを壊してしまいます。
参照:multipart/form-data support in AWS Chalice · Issue #796 · aws/chalice

解決方法としては、上記 issue に記載の通り、app.api.binary_types.append()メソッドを使います。
これにより、 multipart/form-data 形式で送られる body 部分をバイナリとして扱うように API Gateway に明示的に設定することができます。
参照:Chalice — Python Serverless Microframework for AWS 1.15.1 documentation
上記の app.py の中では以下の箇所でその設定をしています。

app.py(edited)
# API Gateway に multipart/form-data をバイナリとして扱うように指示する設定 (ここがポイント!!)
app.api.binary_types.append('multipart/form-data')

おまけ: 【ハンズオン】有名人識別サービスを Chalice で作ろう!

以前、【AWSハンズオン】サーバレスアーキテクチャで、有名人識別サービスを作ろう! - Qiita という記事を投稿させていただきました。
せっかくなので、今回の記事の内容を活用して、Chalice版で上記サービスを作り直してみましょう。
フロントには Vue.js も使用していますが、わからなくても動かせる内容になっています。ご安心ください。
私の GitHub リポジトリ からクローンする方法もあります。この場合の手順はリポジトリの README を参照してください。

作るもの(動作イメージ)

有名人の画像(例えばガッキー)の画像をアップロードすると、Yui Aragaki 100%と表示されるシンプルな有名人の識別サービスです。
gakkii_rekognition_long2.gif

作るもの(アーキテクチャ)

  1. クライアントPCから API Gateway 経由で有名人の画像を Lambda にアップロードします。
  2. 画像ファイルの受信をトリガーに、Lambda 関数が起動します。
  3. Lambda 関数内で Amazon Rekognition の recognize_celebrities 機能に画像ファイルを送り、画像内の有名人を識別します。
  4. 取得した有名人の情報を出力用に整形して、API Gateway を通して返します。
  5. 呼び出し元のブラウザ上で、識別結果が表示されます。
    image.png

ソースコード

$ chalice new-project rekognition-handson-chalice-v2 をした状態を仮定します。
以下、主要ファイルについてのみ記載します。 詳細なコードは私の GitHub リポジトリ を参照してください、

▼Lambda側:Chalice - app.py

app.py
import logging
import traceback
import boto3
import cgi
from chalice import Chalice, BadRequestError
from io import BytesIO

# logger設定
logger = logging.getLogger()
logger.setLevel(logging.INFO)

# Chalice設定
app = Chalice(app_name='rekognition-handson-chalice-v2')

# API Gateway に multipart/form-data をバイナリとして扱うように指示する設定
app.api.binary_types.append('multipart/form-data')

# rekognition インスタンスの作成
rekognition = boto3.client('rekognition')

@app.route('/upload', methods=['POST'], content_types=['multipart/form-data'], cors=True)
def upload():
    # 受け取った情報をバイナリデータとして保存
    body_binary = BytesIO(app.current_request.raw_body)

    # cgi.FieldStorageクラスを用いて、フォームの内容を解析
    environ = {'REQUEST_METHOD': 'POST'}
    headers = {'content-type': app.current_request.headers['content-type']}
    form = cgi.FieldStorage(fp=body_binary, environ=environ, headers=headers)
    
    # ファイルをバイナリ形式で取得('uploadfile'は送信時に設定した当該ファイルのnameの値)
    file_binary = form.getvalue('uploadfile')

    # 取得した画像ファイルを、Rekognitionに渡して有名人の識別をする
    response = rekognition.recognize_celebrities(
        Image={'Bytes': file_binary}
    )
    logger.info(f'Rekognition response = {response}')

    try:
        # Rekognition のレスポンスから有名人の名前と確度を取り出し、APIのコール元へレスポンスする。
        label   = response['CelebrityFaces'][0]
        name    = label['Name']
        conf    = round(label['Face']['Confidence'])
        output  = { 'name': name, 'confidence': conf }
        logger.info(f'API response = {output}')
        return output

    except IndexError as e:
        # Rekognition のレスポンスから有名人情報を取得出来なかった場合、他の写真で試すように伝える。
        logger.warning(f"Coudn't detect celebrities in the Photo. Exception = {e}")
        logger.warning(traceback.format_exc())
        raise BadRequestError("Couldn't detect celebrities in the uploaded photo. Please upload another photo.")

▼フロント側:index.html

index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <title>Rekognition-Handson</title>
</head>
<body>
    <div id="app">
        <h2>有名人識別ハンズオン</h2>
        <input type="file" @change="onChange">
        <div v-if="imagePreview">
            <hr/>
            <button @click="onSubmit">画像解析を実行</button><br/><br/>
            <img :src="imagePreview" width="300"/>
        </div>
        <div v-if="isLoading">
            <p>画像を送信中...</p>
        </div>
        <div v-if="celebrityInfo.name">
            <p>名前:{{ celebrityInfo.name }}</p>
            <p>確度:{{ celebrityInfo.confidence }} %</p>
        </div>
        <div v-if="celebrityInfo.errorMessage">
            <p>{{ celebrityInfo.errorMessage }}</p>
        </div>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.6.12/dist/vue.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/axios@0.21.1/dist/axios.min.js"></script>
    <script src="main.js"></script>
</body>
</html>

▼フロント側:main.js
コード内、** API Gateway URL をここに貼り付け! **に作成した API の URL を上書きしてください。
「/」以下のパスまで忘れずに記載ください。
(例:https://XXXXXXX.execute-api.ap-northeast-1.amazonaws.com/api/upload) ← 「/upload」まで忘れずに!

main.js
let app = new Vue({
    el: '#app',
    data: {
        imageFile: '',
        imagePreview: '',
        celebrityInfo: '',
        isLoading: false,
    },
    methods: {
        onChange: function(event) {
            this.celebrityInfo = '';
            this.imageFile = event.target.files[0];
            if (this.imageFile && this.imageFile.type.match(/^image\/(png|jpeg)$/)) {
                this.imagePreview = window.URL.createObjectURL(this.imageFile);
            }
        },
        onSubmit: function(event) {
            this.isLoading = true;
            this.celebrityInfo = '';
            const targetUrl = '** API Gateway URL をここに貼り付け! **';
            const formData = new FormData();
            formData.append('uploadfile', this.imageFile);
            var config = {
                headers: {
                    'content-type': 'multipart/form-data',
                }
            };
            const that = this;
            axios
                .post(targetUrl, formData, config)
                .then(response => {
                    that.celebrityInfo = response.data;
                })
                .catch(error => {
                    console.error(error);
                    that.celebrityInfo = {errorMessage: '画像から有名人を認識できませんでした。他の画像に変更してください。'}
                })
                .finally(() => {
                    that.isLoading = false;
                });
        }
    }
})

HTMLファイルの開き方

今回、JavaScript を利用していることもあり、index.html の開き方について注意点が3つあります。

  1. index.html と main.js は同じディレクトリに配置してください。
  2. Internet Explorer では動作しません。(ES2018の文法を一部使用しているため、Chrome などを推奨します。)
  3. index.html は http スキーマでブラウザから読み込んでください。 ← 重要!

3点目について説明をします。
index.html ファイルをブラウザにドラッグ&ドロップして開いた場合、アップロード処理が正常に動作しません
これは、index.html ファイルを file スキーマの形式でブラウザから読み込んだ場合、Same Origin Policy のセキュリティー制限によって、画像アップロードの JavaScript 処理が正常に動かないためです。
そのため、以下の方法でローカルサーバーを立て、http スキーマで index.html ファイルにアクセスする必要があります。
Pythonを用いて簡単にサーバーを起動できます。index.html と main.js が存在するディレクトリで、ターミナルから以下のコマンドを実行してください。

TERMINAL
$ ls #ファイルの確認
index.html  main.js
$ python -m http.server 8080

サーバー起動後に、ブラウザからhttp://0.0.0.0:8080/へアクセスしてください。作成したHTMLファイルが開けるはずです。
サーバーを停止する場合は Ctrl + C で止めてください。
なお、index.html や main.js を更新した際、サーバーを再起動しても更新後のファイルがロードされないことがあります。
その際はターミナルを一度停止し、開き直してサーバーも再起動することで解決します。

動作デモ

YouTube に動作画面をアップロードしました。
思い当たる有名人の画像を数枚集めてテストしてみたところ、一通り認識できているようです。(さすがAmazon Rekognition!)
ただし、キムタクは認識できませんでした。 個人的にはガッキーよりも有名な気がしますが...(注:諸説あります)
kimutaku_rekognition_long.gif

まとめ

以上、AWS Chalice にて、 multipart/form-data 形式でアップロードされたバイナリファイルを扱う方法についてでした。
おまけのハンズオンが本編よりも長く脱線してしまいましたが、お役に立てれば幸いです!

2
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
2
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?