この記事はからあげ帝国 Advent Calendar 2025 20日目の記事です。
はじめに
「一日一万回、感謝のアノテーション!」
を繰り返す日々により、2025年のAIカーレースのシーズンは満足のいく結果で終了しましたが、はっきり言ってアノテーションがきつい。クリックの速度が認識を超える前に発狂しそうです。
Transformerが進化して完全ゼロショットができるようになるまでにはまだまだ時間がかかりそうなので、 「面倒なことはChatGPTにやらせよう」 と皇帝陛下も著書に表しておられますので、からあげ帝国臣民らしく、LLMにアノテーションを丸投げすることにしました。
もともとローカルLLMを使ってあれやこれやをやる試みは続けていたので、そのプログラムを流用して、JetRacerのアノテーションを作ることとしました。
やりたいこと
- ローカルLLMに画像を見せてアノテーション作業を自動化
- MCP(Model Context Protocol) でツール連携
- VLM(Vision Language Model) でカメラ画像を直接理解
- 8GBメモリのJetson上で動作(一部PCにオフロード)
クラウドAPI不要、完全ローカルで動くアノテーション支援システムを目指します。
「澄ヶ瀬やな」反乱ス
よっぱらいの友人が付けた私のローカルLLMシステム「澄ヶ瀬やな」。
LLMを組み合わせてプロンプトを書き換えて自己カスタムするが、たまに変なプロンプトに書き換えてしまう。
今回は、JetRacer向けのキャラクタになるようにプロンプトをいじくったり、適当な機能名の整理を自動実装させている間に、Your Autonomous Navigation Assistant - YANA と名乗り始めました。
バツとして私のノートPCから追放し、Jetson Orin Nanoに島流しです。
そっちも8GBあるからそれほど過酷じゃないはずよ。
システムアーキテクチャ
ハードウェア構成
┌─────────────────────────────────────────────────────────────┐
│ Jetson Orin Nano Super (8GB) │
│ ├─ カメラ制御・センサー処理 │
│ ├─ 走行制御(ステアリング・スロットル) │
│ └─ 軽量推論(セグメンテーション) │
├─────────────────────────────────────────────────────────────┤
│ PC (RTX 1660 Ti) ← WiFi経由で連携 │
│ ├─ LLM推論(Gemma3-4B) │
│ ├─ VLM画像解析 │
│ └─ 複雑な意思決定 │
└─────────────────────────────────────────────────────────────┘
当初はJetson単体で完結させる予定でしたが、LLMの応答速度がボトルネックに。Qwen2.5-1.5Bでも約57秒かかっていたのが、RTX PCにオフロードしてGemma3-4Bを使うことで6.7秒まで短縮できました。
ソフトウェアスタック
| レイヤー | 技術 |
|---|---|
| UI | NiceGUI(Pythonベースの非同期WebUI) |
| LLM | Gemma3-4B(Ollama経由) |
| ツール連携 | MCP(Model Context Protocol) |
| 画像処理 | OpenCV, OneFormer, YOLO |
| 通信 | FastAPI + MJPEG Streaming |
技術的ハイライト
1. MCP(Model Context Protocol)で機能をモジュール化
AnthropicがリリースしたMCPを活用し、LLMが使えるツール群を定義しています。
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("JetRacer Tools")
@mcp.tool()
def analyze_road_surface(image_path: str) -> dict:
"""カメラ画像から走行可能領域を分析"""
# セグメンテーション実行
mask = segmentation_model.infer(image_path)
road_ratio = (mask == ROAD_CLASS).sum() / mask.size
return {
"road_percentage": round(road_ratio * 100, 2),
"is_safe": road_ratio > 0.3,
"recommendation": "直進可能" if road_ratio > 0.5 else "要注意"
}
@mcp.tool()
def get_distance_to_obstacle() -> dict:
"""前方障害物までの距離を推定"""
# カメラキャリブレーションを使った距離推定
...
LLMは状況に応じてこれらのツールを呼び出し、結果を総合して走行判断を下します。
なぜMCPが良いのか?
- ツールの追加・削除が容易
- ツールの説明文がそのままLLMへのプロンプトに
- LLMを差し替えてもツール側は変更不要
各種の画像処理をMCPにして呼び出せるようにします。
一番使い勝手がいいのがセグメンテーションです。
2. VLMで「見て理解する」(失敗編)
Gemma3-4BはVLM機能を持っているため、カメラ画像を直接入力して状況を理解させることができます。
# VLMへの問い合わせ例
response = llm.chat(
model="gemma3:4b",
messages=[{
"role": "user",
"content": "この画像を見て、走行可能な領域を教えてください",
"images": [camera_frame_base64]
}]
)
セグメンテーションだけでは判断しにくい状況(例:水たまり、影、未知の障害物)でも、VLMなら文脈を理解して判断できるはず・・・
と、思いましたが、さすがにモデルサイズが小さかったか、あまりまともな判断はしてくれませんでした。
現状はセグメンテーション結果をもとに整理して結論を出すというシステムに落ち着きました。VLMをやるなら豪華なPCが必要です。
3. AIデシジョングリッドの可視化
LLMの判断をカラーコードのグリッドで表示しています。
これにより「AIが何を見て判断しているか」が一目瞭然になり、デバッグが格段に楽になりました。
が、はっきり言って遅すぎるので、リアルタイム処理というわけにはいきません。
セグメンテーションオンリーの判定でも十分です。
4. YANA帰還!
8GBでは十分なサイズの私がよく使っているGemma3は安定稼働には程遠く、かといってサイズの小さいモデルでは性能がいまいちです。
泣く泣くYANAをPCに呼び戻します。
(追放期間、わずか2日)
結局、現在のアーキテクチャはこうなりました:
- Jetson Orin Nano: カメラ制御、センサー処理、軽量セグメンテーション
- PC (RTX 1660 Ti): LLM推論(Gemma3-4B)、VLM画像解析
WiFi経由でJetsonとPCが連携し、重い処理はPCにオフロード。Jetsonは移動しながら情報を収集して母艦に送る装置となりました。
機嫌を損ねられては困るので、新しいGUIウィンドウと専用アイコンを用意してご機嫌を取ります。
よしよし。ちゃんと働いてるな。
- センサー統合
Jetson側にはカメラ以外のセンサーも追加しました。
マトリクス距離センサー(8x8グリッド)
DFRobotのVL53L7CXを使って、前方の距離を8x8のグリッドで取得しています。
壁が近づいたら減速・停止する安全装置として機能します。
(8x8グリッド可視化のスクリーンショット)
IMU(BNO055)
車体の傾きや加速度を監視。転倒しそうになったり、何かにぶつかって急減速したら停止します。
これらのセンサー情報もMCPツールとして公開しているので、YANAが「壁が近いから止まって」と判断することもできます。
・・・が、現状はセンサー値で直接制御した方が速くて確実です。
FaboのJetRacer基板のI2Cがなぜか5Vになってしまっているので、3.3Vに変換し、I2C Hubにつなぎ、距離センサーとIMUに接続します。
これは3Dプリンタで部品を作って、フロントに固定したいところ。
はまりポイント
タイムアウト地獄
分散システムあるあるですが、各コンポーネント間のタイムアウト設定に苦労しました。
- LLM推論: 6-10秒
- MCP通信: 往復1-2秒
- カメラストリーミング: 数十ms
これらが連鎖すると、デフォルトのタイムアウト設定では簡単に切断されます。最終的に全コンポーネントのタイムアウトを系統的に見直し、余裕を持った設定に統一しました。
小型LLMの限界
当初試したQwen2.5-1.5Bでは、関数呼び出しの安定性に課題がありました。
Jetson単独では無理でも、ローカルにLLMを残してFanction callingに使おうと思ったのですが、小型モデルでは安定しません。
やることがシンプルなので、PCからプロンプトを書き換えながら運用するという手はあまり効果が無いと思われました。
# 期待する出力
{"tool": "analyze_road", "args": {"image": "frame_001.jpg"}}
# 実際の出力(不安定)
道路を分析します。analyze_roadを呼び出し...
13-14Bパラメータ未満のモデルでは、構造化出力の安定性が低い傾向があります。対策として:
- ReActパターンの採用(思考→行動→観察のループ)
- テキストパースによる柔軟な解釈
- より大きなモデル(Gemma3-4B)への移行
Jetson側にLLMを残しておくメリットがないため、今回は撤去し、PC側のYANAのためにデータを収集する目的に特化させることとします。
カメラキャリブレーション
距離推定の精度はカメラの取り付け角度に大きく依存します。
# キャリブレーションパラメータ
CAMERA_HEIGHT = 0.27 # 地面からの高さ [m]
CAMERA_PITCH = -15.0 # 俯角 [度]
FOCAL_LENGTH = 3.04 # 焦点距離 [mm]
これらを正確に測定し、歪み補正と組み合わせることで、画像上の位置から実際の距離を推定しています。
また、レンズの歪みも補正します。視界を確保するため、かなり広角のレンズを使っているので、チェッカーボードを使って補正しました。
計算したグリッド線と地面の角度が一致するまで補正します。
デモ
今後の展望
YANAの最終目標は 「めんどくさいJetRacerのアノテーションをやってくれるAI」 です。
コースに置いたら、壁や白線を検出しながらゆっくり走って画像を収集し、アノテーションまでしてくれ、最後にMoEのResNetを学習してレースで使えるレベルの推論器を作ってくれる・・・といいな。
まだまだ先が長い。
まとめ
JetRacerにローカルLLMを載せてアノテーションを自動化する「YANA」プロジェクトを紹介しました。
ぜひ皆さんも「面倒なことはAIにやらせる」精神で、楽しいAIライフを!
謝辞
本プロジェクトのUI部分は avatar-ui-core を使わせていただいています。クラシックなターミナル風UIとピクセルアートのアバターが、AIとのインタラクションにやる気を与えてくれます。素敵なプロジェクトに感謝!












