MENU

問い合わせ


    【YOLOで作る画像認識ツール 第4回】FastAPIで画像認識を「API」にして、ほかのシステムから使えるようにする

    前回の第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回です。連載のほかの回は次のとおりです(連載の一覧ページ)。

    システム制作・運用・保守のお問い合わせはこちら


      よかったらシェアしてね!
      • URLをコピーしました!
      • URLをコピーしました!

      この記事を書いた人

      株式会社THIRD HERO代表取締役 朝野貴朗
      Webシステム開発を中心に、toC向けサービスサイトの運営、ツール開発などを行ってまいりました。

      目次