AI を使った画像解析システムなどでは、画像をリクエストで投げて推論結果をレスポンスでもらう、というのをよくやるかと思います。
そういった API で、最初は画像を Base64 に変換して JSON で渡していたのですが、エンコード/デコードの処理時間があるため、バイナリでデータを送りたいと考えました。
また、1度のリクエストで複数枚の画像を送る必要があるのと、画像の対する付加情報(カメラのIDや画像の取得日時など)も一緒に渡す必要があるということで、multipart/form-data
を使って送ってみることにしました。
multipart/form-data
についての詳細は割愛しますが、バウンダリ文字列で区切って複数のデータを送信するやつで、HTML の form 文とかで使われるやつです。
今まで WebAPI では json と yaml しか扱ったことありませんでしたが、調べた感じ、WebAPI でも multipart/form-data
を使ってテキストとバイナリを一緒に送るというのはよくある手法っぽいです。
リクエスト内容
例として、POST で以下のような内容を送ります。
ヘッダー(抜粋)
Content-Type multipart/form-data; boundary=cc874efaa4e6a1e85c14280c96a7b237
サンプルコードを後に載せていますが、バウンダリ文字列は自動生成でリクエスト毎に異なります。
基本的に意識しなくても大丈夫でした。
ペイロード
テキストについては一つずつ text/plain
で送ろうかとも思いましたが、今後データが増えた時にサーバー側の処理が面倒かなと思い、JSON にまとめることにしました。
画像データ以外は JSON でまとめて送り、画像データはバイナリで送ります。
これをカメラ台数分送ります。
カメラ毎に JSON と画像をマッピングする必要がありますが、試行錯誤した結果、name
にカメラを識別するためのIDを設定することにしました。
つまり、name
の値を見てどのカメラの JSON データ、画像データかを識別する想定です。
後述するサンプルコードで試した結果、ペイロードは以下のような形になりました。
--cc874efaa4e6a1e85c14280c96a7b237
Content-Disposition: form-data; name="カメラのID"
{
"camera_id": "1",
"datetime": "2021-08-05T17:07:01.388437",
}
--cc874efaa4e6a1e85c14280c96a7b237
Content-Disposition: form-data; name="カメラのID"; filename="ファイル名"
Content-Type: image/jpeg
<ここに画像のバイナリデータが入る>
--cc874efaa4e6a1e85c14280c96a7b237--
複数カメラ分のデータを送信した場合、並びは以下ようになりました。
- 1台目のカメラの JSON データ
- 2台目のカメラの JSON データ
- 1台目のカメラの画像データ
- 2台目のカメラの画像データ
name
でどのカメラかを判別できるので、基本的にこの並びは意識しなくても大丈夫でした。
サンプルコード
Python での実装例です。
クライアント側
上記のリクエストを requests
モジュールを使って送信するサンプルです。
import requests
import json
from datetime import datetime
if __name__ == '__main__':
data = {}
files = {}
# 複数カメラを想定して送信
for camera_id in ['hoge', 'piyo']: # 各カメラのID
# 適当なテキストデータ
json_data = {
'camera_id': camera_id,
'datetime': datetime.now().isoformat()
}
# 適当な画像ファイルを読み込む
image = None
filename = 'send_image.jpg'
with open(filename, 'rb') as f:
image = f.read()
# 送信
# data,files は dict 型でなければならないので、カメラIDをキーとして渡すことにした。このキーが name に設定される
data[camera_id] = json.dumps(json_data), # dict -> str への変換
# filename や Content-Type を指定して送りたい場合
files[camera_id] = (filename, image, 'image/jpeg')
# 画像のみで送りたい場合。filename は name と同じになり、Content-Type は付与されなかった
#files[camera_id] = image
res = requests.post(f'http://localhost:8000', files=files, data=data)
print(res)
サーバー側
上記のリクエストを Flask で受信するサンプルです。
import json
from flask import Flask, request
if __name__ == '__main__':
app = Flask(__name__)
@app.route('/', methods=['POST'])
def index():
print(request.headers)
#print(request.get_data()) # 生データ
# JSON データの受け取り
for key, value in request.form.items():
json_data = json.loads(value) # str から dict への変換
print(f'json, key={key}:, value={json_data}, type={type(json_data)}')
# 画像データの受け取り
for key, value in request.files.items():
print(f'image, key={key}, type={type(value)}') # FileStorage 型
#print(value.headers)
# ファイルに保存して確認
with open(f'{key}.jpg', 'wb') as f:
f.write(value.read())
# ファイルの保存だけなら以下でも可
#value.save(f'{key}.jpg')
return {}, 200
app.run(port=8000, debug=True)
OpenAPI で multipart/form-data を記述する
OpenAPI で API の仕様書を作成しているのですが、multipart/form-data
は以下のように記述するようです。
ただ、上述したような name
をカメラのIDとするような記述はできないようです。
ここでは json_data
image
という値で記述しています。
また、Content-Type
を記述したい場合は encoding
を使うそうですが、Swagger で試しに送信しても付与されていませんでした。
あと、Redoc ではリクエストボディの内容が表示されませんでした。
``multipart/form-data` 自体に対応していないのかもしれません。
/endpoint:
post:
summary: サンプル
description: 説明
requestBody:
content:
multipart/form-data:
schema:
type: object
required:
- json_data
- image
properties:
data:
type: object
required:
- camera_id
- datetime
description: 画像以外のデータ(JSON)
properties:
camera_id:
type: string
description: カメラの識別子
example: 'hoge'
datetime:
type: string
format: date-time
description: 画像の取得日時(ISO8601)
example: '2021-08-05T17:07:01.388437'
image:
type: string
format: binary
description: 画像データ(バイナリ)
encoding:
image:
contentType: image/jpeg;
参考 URL
- https://raahii.github.io/posts/files-upload/
- https://swagger.io/docs/specification/describing-request-body/multipart-requests/
- https://www.yoheim.net/blog.php?q=20171201
- https://github.com/Redocly/redoc/issues/707
- https://github.com/springdoc/springdoc-openapi/issues/396
- https://max999blog.com/python-how-to-post-json-and-file-same-time/
- https://cpoint-lab.co.jp/article/201902/7860/