2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

バイクの写真を送るだけで車種を判定するAIをLaravelで作った

2
Last updated at Posted at 2026-03-15

バイクの写真を送るだけで車種を判定するAIをLaravelで作った

はじめに

「このバイクなに?」と思ったことはありませんか?

街で見かけたバイク、友人が乗っているバイク、昔のカタログに載っていたバイク。名前がわからなくてモヤモヤした経験は、バイク好きなら誰でもあるはず。

そこで、バイクの中古・新車一括検索プラットフォーム「MotoHub」に写真を送るだけでAIが車種を判定する機能を実装しました。

判定後はそのままMotoHubの在庫検索・中古相場ページに遷移できます。

この記事では、実装の流れと詰まったポイントをリアルに書いていきます。


完成イメージ

  1. バイクの写真をアップロード(またはカメラで直接撮影)
  2. AIが車種を瞬時に判定(メーカー・排気量・カテゴリ・確度も表示)
  3. 候補一覧から「MotoHubで検索」ボタンで在庫・相場ページへ遷移

実際の画面はこんな感じです👇

スクリーンショット (119).png
:アップロード画面 → 分析中 → ジャイロキャノピー判定結果 → 検索結果)


技術スタック

  • バックエンド: Laravel 12 / PHP 8.3
  • AI: OpenAI GPT-4o(画像認識)
  • インフラ: Docker / さくらVPS / Cloudflare
  • フロントエンド: Blade / Tailwind CSS

実装の流れ

1. コントローラーの作成

// app/Http/Controllers/Bike/BikeIdentifierController.php

public function identify(Request $request): JsonResponse
{
    $request->validate(['image' => 'required|image|max:20480']);

    // 画像をサーバー側で圧縮(重要!後述)
    $base64 = $this->compressImage($request->file('image'));

    // GPT-4o APIを叩く
    $response = Http::timeout(60)->withHeaders([
        'Authorization' => 'Bearer ' . config('services.openai.key'),
        'Content-Type' => 'application/json',
    ])->post('https://api.openai.com/v1/chat/completions', [
        'model' => 'gpt-4o',
        'max_tokens' => 1000,
        'messages' => [[
            'role' => 'user',
            'content' => [
                [
                    'type' => 'image_url',
                    'image_url' => ['url' => "data:image/jpeg;base64,{$base64}"]
                ],
                ['type' => 'text', 'text' => $this->buildPrompt()]
            ]
        ]]
    ]);

    $result = json_decode(
        $response->json('choices.0.message.content'), true
    );

    return response()->json($result);
}

2. プロンプト設計

GPT-4oへの指示はJSON形式で返すよう設計しました。

private function buildPrompt(): string
{
    return <<<PROMPT
あなたは日本のバイク専門家AIです。画像を見てバイクの車種を特定してください。

以下のJSON形式のみで返してください(説明文不要):
{
  "maker": "メーカー英語名",
  "maker_jp": "メーカー日本語名(カタカナ)",
  "model": "車種名(カタカナ)",
  "year": "推定年式",
  "category": "カテゴリ",
  "displacement": "排気量",
  "confidence": "高 or 中 or 低",
  "features": ["特徴1", "特徴2", "特徴3"],
  "comment": "一言コメント",
  "candidates": [
    {"maker": "メーカー", "model": "車種名", "probability": "高"},
    {"maker": "メーカー", "model": "車種名", "probability": "中"},
    {"maker": "メーカー", "model": "車種名", "probability": "低"}
  ]
}

重要:
・車種名・メーカー名は必ず日本語カタカナで返すこと
・日本市場で販売されたモデル名を優先すること
・自信がない場合はconfidenceを「低」にすること
PROMPT;
}

3. ルーティング

// routes/web.php
Route::get('/bikes/identify', [BikeIdentifierController::class, 'index'])
    ->name('bikes.identify');
Route::post('/bikes/identify', [BikeIdentifierController::class, 'identify'])
    ->name('bikes.identify.post');

詰まったポイント3選

① iPhoneの写真が5MB制限に引っかかる

GPT-4oのAPIには画像サイズ5MB制限があります。iPhoneで撮影した写真は4〜8MBあるため、そのまま送ると image exceeds 5 MB maximum エラーになります。

解決策:サーバー側でGDライブラリを使って圧縮

private function compressImage(UploadedFile $file): string
{
    $imageData = file_get_contents($file->path());
    $image = imagecreatefromstring($imageData);

    // 長辺を1600pxに縮小
    $width = imagesx($image);
    $height = imagesy($image);
    $maxSize = 1600;

    if ($width > $height && $width > $maxSize) {
        $newWidth = $maxSize;
        $newHeight = (int)($height * $maxSize / $width);
    } elseif ($height > $maxSize) {
        $newHeight = $maxSize;
        $newWidth = (int)($width * $maxSize / $height);
    } else {
        $newWidth = $width;
        $newHeight = $height;
    }

    $resized = imagecreatetruecolor($newWidth, $newHeight);
    imagecopyresampled($resized, $image, 0, 0, 0, 0,
        $newWidth, $newHeight, $width, $height);

    ob_start();
    imagejpeg($resized, null, 75); // JPEG品質75
    $compressed = ob_get_clean();

    return base64_encode($compressed);
}

これで5〜8MBの写真が 200〜400KB に圧縮されます。

iPhoneのHEIC形式にも対応するため、DockerfileでGDにWebPサポートを追加しました:

RUN apt-get install -y libwebp-dev
RUN docker-php-ext-configure gd --with-webp --with-jpeg --with-png
RUN docker-php-ext-install gd

② モバイル回線だけ通信エラーになる

WiFiでは問題なく動作するのに、モバイル回線(5G)だと即座に通信エラーになる謎の現象が発生しました。

Cloudflareのアクセスログを確認すると、原因は**HTTP/3(QUIC)**でした。

モバイル回線はHTTP/3を使いますが、NginxがHTTP/3に未対応だったため、リクエストがVPSに届いていませんでした。

解決策:CloudflareでHTTP/3をオフにする

Cloudflare → Speed → Settings → Protocol Optimization タブ
↓ このスクショの通り、HTTP/3 (with QUIC) をオフにする
スクリーンショット (125).png
This setting was last changed 3 hours ago と表示されているのが今回の修正です。

これだけで解決しました。同じ症状で詰まっている方はぜひ試してみてください。

③ Claude vs Gemini vs GPT-4oの精度比較

最初はClaude Sonnetで実装しましたが、バイクの画像認識精度がイマイチでした。

モデル 精度 コスト 備考
Claude Sonnet 安い テキスト処理は優秀
Gemini 2.0 Flash - 無料枠あり 無料枠のTPM制限が厳しく実用困難
GPT-4o 中程度 画像認識が優秀

最終的にGPT-4oに落ち着きました。1回あたり約$0.01なので、個人開発でも十分許容範囲です。


実際の精度

得意なケース:

  • カタログ写真や横から撮影した写真
  • ジャイロキャノピー、グロムなど特徴的な形のバイク

苦手なケース:

  • 極端な角度からの撮影
  • バイクが部分的にしか写っていない写真
  • 背景が複雑な写真

現状1〜3位の候補一覧を表示する形にして、ユーザーが選べるようにしています。


まとめ

LaravelでGPT-4oを使った画像認識AIを実装しました。

主な詰まりポイントは以下の3つでした:

  • iPhoneの写真サイズ制限 → サーバー側圧縮で解決
  • モバイル回線のみ通信エラー → HTTP/3オフで解決
  • AI精度の違い → GPT-4oが最も優秀

「このバイクなに?」と思ったらぜひ試してみてください👇
https://motohub.jp/bikes/identify


次回は「GPT-4o vs Claude Sonnet vs Gemini、バイク画像認識で徹底比較した話」(https://qiita.com/auchida1982/items/dc13b84b61d390bced49)を書く予定です。

🏍 MotoHub: https://motohub.jp
🐦 X: https://x.com/motohub_jp
💻 GitHub: https://github.com/ausssxi/MotoHub

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

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?