前回の第3回では、認識結果を種類ごとに数えてCSVに集計しました。ここまでは、プログラムを人が手で実行する形でした。今回は、画像認識をAPI(ほかのシステムから呼び出せる窓口)にして、既存の業務システムやスマホアプリから使えるようにします。
使うのは、PythonのWebフレームワーク「FastAPI」です。画像を送ると、認識結果がJSONで返ってくる仕組みを作ります。
この回で作るもの
- 画像をアップロードすると認識結果のJSONを返す
POST /detect - 動作確認用の
GET /health - 大きすぎる画像・壊れた画像・範囲外のしきい値を、分かるエラーで断る処理
仕組み:モデルは「起動時に1回だけ」読み込む
AIモデルの読み込みには時間がかかります。リクエストのたびに読み込むと遅くなるため、サーバーの起動時に1回だけ読み込み、以降のリクエストで使い回します。
[起動時] モデルを読み込む(数秒)
[リクエスト] 画像を受け取る → 認識する → JSONを返す(このあいだ、モデルは読み込み済み)
実装
画像のバイト列から認識できるようにする
APIは、画像を「ファイル」ではなく「送られてきたデータ(バイト列)」として受け取ります。Detector に、バイト列から認識するメソッドを足します。
# app/detector.py(追加部分)
def detect_bytes(self, data: bytes, confidence: float | None = None) -> list[Detection]:
"""アップロードされた画像のバイト列から検出する。画像として読めなければ ValueError。"""
try:
image = Image.open(io.BytesIO(data)).convert("RGB")
except (UnidentifiedImageError, OSError) as exc:
raise ValueError("画像として読み込めませんでした") from exc
return self._predict(image, confidence)
しきい値(confidence)を、リクエストごとに変えられるようにもしました。
APIを作る app/api.py
# app/api.py(抜粋)
MAX_UPLOAD_BYTES = 10 * 1024 * 1024 # 10MB。これを超える画像は受け付けない
def create_app(detector: DetectorLike | None = None) -> FastAPI:
"""detector を渡すとそれを使う(テスト用)。渡さなければ起動時にモデルを1回だけ読み込む。"""
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncIterator[None]:
app.state.detector = detector or Detector()
yield
app = FastAPI(title="果物の検品API", lifespan=lifespan)
@app.get("/health")
def health() -> dict[str, str]:
return {"status": "ok"}
@app.post("/detect")
async def detect(
request: Request,
file: Annotated[UploadFile, File(description="認識する画像(jpg/png など)")],
conf: Annotated[float, Query(ge=0.0, le=1.0, description="信頼度のしきい値")] = DEFAULT_CONFIDENCE,
) -> dict[str, object]:
data = await file.read(MAX_UPLOAD_BYTES + 1)
if len(data) > MAX_UPLOAD_BYTES:
raise HTTPException(status_code=413, detail="画像が大きすぎます(上限 10MB)")
try:
detections = request.app.state.detector.detect_bytes(data, conf)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
return to_dict(file.filename or "upload", conf, detections)
return app
app = create_app()
押さえたい点は次のとおりです。
lifespan:起動時にモデルを読み込む場所です。create_appに偽の検出器を渡せるため、テストではモデルなしで動かせますconfの範囲チェック:ge=0.0, le=1.0と書くだけで、範囲外の値は自動的にエラー(422)になります- サイズの上限:10MBを超える画像は受け付けません。巨大なデータを送り付けられて、サーバーが動かなくなるのを防ぎます
- エラーの種類を分ける:壊れた画像は400、大きすぎる画像は413と、原因が伝わる番号で返します
動かして確かめる
起動する
pip install -r requirements-dev.txt --extra-index-url https://download.pytorch.org/whl/cpu
uvicorn app.api:app --port 8000
起動すると、http://127.0.0.1:8000/docs で、FastAPIが自動で作る操作画面(Swagger UI)も使えます。ブラウザから画像を送って試せます。
画像を送る
curl -F "file=@samples/fruits.jpg" "http://127.0.0.1:8000/detect?conf=0.4"
開発環境では、しきい値0.4で3件(すべてorange、信頼度0.867・0.718・0.469)がJSONで返りました。2件目以降の応答は0.1秒程度でした(環境により異なります)。
エラーの確認
| 送ったもの | 結果 |
|---|---|
| 画像ではないファイル | 400(画像として読み込めませんでした) |
conf=5(範囲外) | 422(0〜1の範囲で指定するよう自動で案内) |
| 10MB超の画像 | 413(画像が大きすぎます) |
テスト
pytest -q # 20件成功
ruff check . # 問題なし
テストでは、偽の検出器を使い、正常な応答・しきい値の受け渡し・3種類のエラーを確認しています。
つまずきやすい点
python-multipartが必要:ファイルのアップロードを受けるには、この部品が必要です(requirements.txtに含めています)- テスト時の警告:テストを実行すると、httpx(テスト用の通信部品)に関する「非推奨」の警告が表示されます。動作には影響しませんが、依存部品の版が変わると挙動が変わりうるため、この連載では版を固定しています
- モデルを毎回読み込んでしまう:起動時に1回だけ読み込む作りにしないと、応答が極端に遅くなります
- 認証がない:この回のAPIは、誰でも呼び出せます。社内ネットワークの外に公開するときは、認証(誰が使えるかの確認)が必須です。連載では省略しています
補足:利用統計の送信を切っておく
動作確認中、実行環境の通信の監視で、外部のGoogle Analyticsへの接続の試みが1件検出されました。Ultralyticsの設定に、利用統計などを送る sync という項目が既定で有効(true)になっていたため、これが原因と考えられます。画像そのものが送られるわけではありませんが、社内の画像を扱う前提では、意図しない外部通信は止めておくのが安全です。そのため、コードの先頭でこの設定を切っています。
# app/detector.py(追加部分)
# Ultralytics は既定で利用統計・クラッシュ情報の送信(sync)が有効。社内の画像を扱う前提なので切っておく。
settings.update({"sync": False})
発注者向けメモ:APIにすると決めることが増える
画像認識をAPIにすると、「誰が・どこから・どれくらい使うか」で、必要な構成と費用が変わります。
発注者がやること チェックリスト
- ☐ 呼び出す側のシステム(既存の業務システム・アプリ)と、送る画像の大きさ・枚数を洗い出した
- ☐ 1日・1時間あたりの最大枚数と、許容できる応答時間(何秒以内か)を決めた
- ☐ APIを使える人・システムの範囲(社内のみか、社外にも公開か)を決めた
- ☐ 画像が外部に送られるかどうか、利用統計などの外部通信の有無を確認した
- ☐ 障害時の扱い(APIが止まったとき、業務をどう続けるか)を決めた
開発会社への質問例
- 「想定する最大の枚数と応答時間で、どのくらいのサーバー構成が必要で、月額はいくらですか」
- 「APIには認証を付けますか。誰がアクセスできるかは、どう管理しますか」
- 「使うAIモデルやライブラリが、外部に利用統計や画像を送ることはありますか。あれば止められますか」
- 「モデルを更新したとき、APIの利用側に影響が出ないようにできますか」
まとめと次回予告
第4回では、次のことを行いました。
- 画像を送ると認識結果のJSONを返すAPI(FastAPI)を作った
- モデルを起動時に1回だけ読み込み、応答を速くした
- 大きすぎる画像・壊れた画像・範囲外の値を、分かるエラーで断るようにした
- 利用統計の外部送信を切り、意図しない通信を防いだ
次回の第5回では、このAPIをブラウザから使える画面にして、画像をアップロードして結果をその場で見られるようにします。
この連載の記事一覧
この記事は連載「YOLOで作る画像認識ツール」の1回です。連載のほかの回は次のとおりです(連載の一覧ページ)。
- 【YOLOで作る画像認識ツール 第0回】全体像と環境づくり:YOLOで最初の1枚を認識する
- 【YOLOで作る画像認識ツール 第1回】信頼度のしきい値とJSON出力:認識結果を「使える形」にする
- 【YOLOで作る画像認識ツール 第2回】認識結果を画像に描いて確認する:枠とラベルで「間違い」を見つけやすくする
- 【YOLOで作る画像認識ツール 第3回】種類ごとに何個あるか数えて、CSVに集計する
- 【YOLOで作る画像認識ツール 第4回】FastAPIで画像認識を「API」にして、ほかのシステムから使えるようにする(この記事)
- 【YOLOで作る画像認識ツール 第5回】ブラウザから写真をアップロードして、認識結果をその場で見る
- 【YOLOで作る画像認識ツール 第6回】精度を数字で測る:正解データと、適合率・再現率
- 【YOLOで作る画像認識ツール 第7回】学習済みモデルにない対象を覚えさせる:追加学習の手順と、結果の正しい読み方
- 【YOLOで作る画像認識ツール 第8回】自信の低い結果だけ人が確認する:AIと人の役割分担を設計する
- 【YOLOで作る画像認識ツール 第9回】速く・軽くする:ONNXに変換してCPUで動かし、変換前後を比べる
- 【YOLOで作る画像認識ツール 第10回】Dockerで動かす:設定・ログ・ヘルスチェックを運用に近い形にする
- 【YOLOで作る画像認識ツール 第11回(最終回)】運用の注意点とライセンス(AGPL-3.0):精度の低下に気づく仕組みと、連載のまとめ

