RON-V 連載リンク
← 前の記事:#22補足 — Kaggleでの学習・保存手順
→ 次の記事:#24 — 実機構成・UPS・バックアップ
実機確認の記録(2026-10-20)
#22で構築した道路・災害画像AIをRaspberry Pi 5へ載せた記録です。撮影と推論に加え、AI結果をL3、根拠画像をL4へ対応付け、Google Drive転送とSheetsへの記録まで2台の代表イベントで確認しました。Grafana用のダッシュボードも配置しました。ここでは、実機で確認した範囲と再確認の手順を説明します。
はじめに
AtomS3Rのボタン操作や衝撃検知をきっかけに画像を保存し、道路・冠水・煙・視界の4モデルで解析します。その結果を、根拠画像と結び付けて保存・転送できるようになりました。
#22はAIの構築方針、#22補足はKaggleでの学習・変換・保存を説明します。この記事は、その後のONNXをPiへ置く → 設定する → BUTTON / SHOCKで自動推論する → 結果と画像を観測データとして扱う流れを扱います。10節以降に、L3/L4への登録、Grafana、Drive、Sheetsの確認をまとめます。
前提は、RON-Vのコード、Pythonの仮想環境、AtomS3Rからのイベント受信、RingCamが既に動いているPiです。4つのONNXは、自分で保存したモデルや利用可能なバックアップから用意します。GitHubを取得するだけではモデル本体はそろいません。リポジトリは記事作成時点で非公開なので、読む権限も必要です。
| 順番 | 作業 | 確認するもの |
|---|---|---|
| 1 | ONNXをPiへ配置する | 4モデルのファイルが models/ 以下にある |
| 2 | 同じ仮想環境でONNX Runtimeを読み込む |
onnxruntime / numpy / Pillow をimportできる |
| 3 |
config/ronv.toml の [vision] を設定する |
有効化、モデルのパス、confidence |
| 4 | 保存画像で手動実行する |
vision_result.json と4モデルそれぞれの status
|
| 5 | BUTTON / SHOCKで自動実行する | 新しいイベントの画像・撮影記録・推論結果 |
| 6 | もう1台にも同じ構成を配置する | コードの版、モデルの照合値、設定、各機の動作 |
| 7 | AI結果を観測・表示・転送へつなぐ | L3の結果とL4の画像の対応、Drive転送、SheetsのINDEX、Grafanaの表示用データ |
設定例は6節、保存画像での再確認は4節、自動実行は5節で説明します。1〜9節には最初のAI展開時の記録も残し、10〜13節で10月19〜20日の更新結果を説明します。以下は既存のRON-V環境への追加手順です。OSや各観測機能をゼロから構築する手順ではありません。
RON-VではRTL-SDR、LTE、GNSS、CAN、カメラなど複数の処理を同時に扱うため、
画像AIを常時推論させるのではなく、イベントが発生したときだけ実行する構成にしました。
今回の最終構成は次です。
| profile | task | threshold |
|---|---|---|
| road-v02 | fallen_tree / road_debris / pothole | 0.20 |
| flood-v05 | flood | 0.50 |
| smoke-v01 | smoke | 0.20 |
| visibility-v02 | clear / low_visibility | top-1 |
1. イベント駆動にする
RON-Vでは AtomS3R が車両側イベントを Raspberry Pi へ送ります。
| 処理 | 入力 | 保存・次の処理 |
|---|---|---|
| RingCamが画像を蓄える | USBカメラの画像 | data/ringcam/buffer/ |
| AtomS3Rからイベントを受信 | BUTTON / SHOCKのJSON |
data/camera/triggers/ に撮影要求 |
| 撮影要求を処理 | 要求とRingCamの画像 |
data/ringcam/events/<event_uid>/ に画像と撮影記録 |
| 4モデルで推論 | 代表画像 event.jpg
|
同じイベントフォルダーへ vision_result.json
|
BUTTONでは代表画像1枚、SHOCKではイベント時刻に対する−3、−2、−1、0、+1、+2秒の計6枚を保存します。AIへ渡すのは0秒に対応する event.jpg です。狙った時刻の画像がなければ、実装は近い画像を選び、実際に選んだ時刻を capture_manifest.json に記録します。
RingCamによる画像の蓄積と、AIの実行タイミングは別です。画像を蓄えておき、撮影要求の処理で代表画像を保存した後、AIがその1枚を解析します。
2. モデル本体はGitHubへ入れない
ONNXモデルは数MB〜十数MBあり、学習を繰り返すと履歴も急速に大きくなります。
そのため、保存物を次のように分けました。
| 保存先 | 保存するもの |
|---|---|
| GitHub | 推論コード、設定例、学習・変換スクリプト |
| 別のバックアップ |
best.pt、ONNX、モデルをまとめたZIP |
公開可否の判断は#22の説明を参照します。
.gitignore では models/ をGit管理対象外にしています。
実機では、/home/ronv/ronv/ を基準に次の場所へ配置します。
| モデル | ONNXの配置先 |
|---|---|
| 道路 | models/road-v02/road-v02.onnx |
| 冠水 | models/flood-v05/flood-v05.onnx |
| 煙 | models/smoke-v01/smoke-v01.onnx |
| 視界 | models/visibility-v02/visibility-v02.onnx |
#22補足のモデルZIPは、選択したモデルだけを含みます。道路だけのZIPでは4モデル構成はそろいません。ZIPをPCなどで展開し、表のONNXをPiへコピーします。models/models/... のように二重の階層へ入れないよう確認します。
Piのターミナルで、配置後に確認する例です。
cd /home/ronv/ronv
ls -lh models/road-v02/road-v02.onnx models/flood-v05/flood-v05.onnx models/smoke-v01/smoke-v01.onnx models/visibility-v02/visibility-v02.onnx
sha256sum models/road-v02/road-v02.onnx models/flood-v05/flood-v05.onnx models/smoke-v01/smoke-v01.onnx models/visibility-v02/visibility-v02.onnx
照合値は保存元のONNXのSHA256と見比べます。2台へ置く場合も同じ値かを確認します。
3. Raspberry Pi側はONNX Runtime
RONV1 / RONV2 では Python venv 内に ONNX Runtime を入れています。
確認した構成:
numpy 2.5.3
onnxruntime 1.31.0
Pillow 12.3.0
入力 / 出力 shape は次の通りです。
road-v02
input [1, 3, 640, 640]
output [1, 7, 8400]
flood-v05
input [1, 3, 640, 640]
output [1, 5, 8400]
smoke-v01
input [1, 3, 640, 640]
output [1, 5, 8400]
visibility-v02
input [1, 3, 224, 224]
output [1, 2]
3本の検出モデルと、1本の分類モデルを同じイベント処理へまとめています。
自動実行のserviceが使用するPythonは /home/ronv/.venvs/ronv/bin/python です。別のPythonへインストールしても、このserviceでは使われません。現在の環境を確認する例です。
/home/ronv/.venvs/ronv/bin/python -c "import numpy, onnxruntime, PIL; print('numpy', numpy.__version__); print('onnxruntime', onnxruntime.__version__); print('Pillow', PIL.__version__)"
このimport確認だけではモデルの推論まで確認したことにならないため、次の保存画像での実行へ進みます。
4. event_infer.py で4モデルを順番に実行
実装の中心は次のファイルです。
app/vision/event_infer.py
app/vision/multi_onnx.py
app/vision/road_onnx.py
イベントディレクトリの同じ event.jpg を4モデルへ渡し、
結果を vision_result.json に保存します。
出力例:
{
"schema": "ronv.vision.event.v2",
"status": "ok",
"hazard": false,
"detection_count": 0,
"models": {
"road-v02": {
"status": "ok"
},
"flood-v05": {
"status": "ok",
"confidence_threshold": 0.5
},
"smoke-v01": {
"status": "ok"
},
"visibility-v02": {
"status": "ok",
"top1": "clear"
}
}
}
同時に capture_manifest.json へも推論の要約を追記します。上のJSONは主要項目を抜粋した例で、実際の出力には画像パス、推論時間、検出枠、confidenceなども入ります。
保存画像で手動確認する(再確認用の追記)
まず6節の [vision] を設定します。既存の config/ronv.toml の該当部分を編集し、車両・カメラなど他の設定は残します。設定例全体への置き換えや、[vision] の二重追加はしません。
以下はPiのターミナルで実行します。環境変数 RONV_EVENT_DIR の値だけ、event.jpg がある実際のイベントフォルダーへ変更します。この再実行は、同じフォルダーの推論結果と撮影記録の推論要約を更新するため、以前の結果を残す場合は先にコピーします。
cd /home/ronv/ronv
export RONV_CONFIG=/home/ronv/ronv/config/ronv.toml
export RONV_EVENT_DIR='/home/ronv/ronv/data/ringcam/events/ここを実際のevent_uidへ変更'
/home/ronv/.venvs/ronv/bin/python -m app.vision.event_infer --event-dir "$RONV_EVENT_DIR"
/home/ronv/.venvs/ronv/bin/python -m json.tool "$RONV_EVENT_DIR/vision_result.json"
| 結果 | 意味・次に確認するもの |
|---|---|
status=ok |
4モデルすべての推論が成功。各モデルの結果も確認する |
status=partial |
一部だけ成功。models 内の error を確認する |
status=error |
全モデルで失敗。モデルのパス、画像、依存ソフトを確認する |
VISION: disabled |
読み込んだ設定でAIが無効。[vision].enabled と設定ファイルを確認する |
hazard=false |
成功した検出モデルが閾値以上の候補を返さなかった。道路の安全を保証する値ではない |
現在のCLIはモデルの一部が失敗しても終了コード0で終わる場合があります。コマンド終了だけで成功とせず、JSONの status と各モデルの結果を確認します。
5. systemd path unitで撮影要求を拾う
RON-Vでは次を使っています。
| unit | 役割 |
|---|---|
ronv-ringcam.service |
USBカメラの画像をバッファへ蓄える |
ronv-camera-trigger.path |
/home/ronv/ronv/data/camera/triggers に撮影要求が入ったことを検知する |
ronv-camera-trigger.service |
要求を処理して画像と撮影記録を保存し、その後AIを実行する |
AIが専用の常駐serviceとして動く構成ではありません。カメラのイベント処理の中から呼び出します。
確認例:
systemctl is-active ronv-ringcam.service
systemctl is-active ronv-camera-trigger.path
正常時:
active
active
RONV1ではAI展開後、ronv-camera-trigger.path が inactive だったため、
sudo systemctl enable --now ronv-camera-trigger.path
で有効化しました。
手動確認が成功した後、AtomS3RのBUTTON操作で新しいイベントを作り、data/ringcam/events/ に新しいフォルダーができるか確認します。その中の event.jpg / capture_manifest.json / vision_result.json を確認し、続いてSHOCKイベントも確認します。
journalctl -u ronv-camera-trigger.service -n 80 --no-pager
撮影が成功した RINGCAM EVENT OK と、4モデルが成功した VISION EVENT: status=ok は別の確認です。AIで失敗しても撮影画像が残るようにしているため、画像が保存されたことだけで4モデルの成功とは判断しません。
6. RONV2で先に展開
最初に RONV2 へ4モデルを展開しました。
Floodは v02 から v05 へ置き換え、
[vision]
enabled = true
road_model = "/home/ronv/ronv/models/road-v02/road-v02.onnx"
road_confidence = 0.20
flood_model = "/home/ronv/ronv/models/flood-v05/flood-v05.onnx"
flood_confidence = 0.50
smoke_model = "/home/ronv/ronv/models/smoke-v01/smoke-v01.onnx"
smoke_confidence = 0.20
visibility_model = "/home/ronv/ronv/models/visibility-v02/visibility-v02.onnx"
としました。
4モデルの実イベント試験:
VISION EVENT:
status=ok
hazard=False
detections=0
visibility=clear
個別の推論時間は、おおむね detection 1本あたり 160〜200ms 程度でした。
確認時の4モデルの推論処理は約1秒以内でした。これは画像の保存後に測った推論時間です。SHOCKではイベント後の画像がそろうまで待つため、衝撃発生から全ファイルが完成する時間とは分けて考えます。
7. RONV1へ同じ構成を展開
RONV2で確認後、AI展開時点のマイルストーンとしてGitHubへ次のcommitを反映しました。
4ca5b56
feat: deploy flood-v05 hard-negative model profile
これはAI展開時点のcommitであり、現在のGitHub main はその後も更新されています。運用時は固定SHAではなく最新の main とnode auditを基準にします。
RONV1では、
git pull --ff-only origin main
を行い、
4ca5b56 (HEAD -> main, origin/main, origin/HEAD)
Already up to date.
まで確認しました。
モデル本体は runtime ZIP から別途配置しています。
RONV1での実イベント推論:
status = ok
hazard = False
detections = 0
inference_ms = 761.5
ALL FOUR MODELS: OK
8. RONV1 / RONV2を同じbaselineへ
AI展開時点では2台とも同じGitHub baselineへ揃え、その後も共通 main を追従する運用にしました。モデルprofileは現在も次の4本です。
road-v02
flood-v05
smoke-v01
visibility-v02
BUTTON / SHOCK イベントも両機で発生させ、
一晩放置して長時間安定性を確認する段階へ進みました。
9. この構成の狙い
RON-VではAIそのものを主目的にしていません。
主役は、
- RF
- LTE
- GNSS
- CAN / OBD-II
- Camera
- Mesh
などの観測です。
AIは、記録した画像へ「道路障害物らしい」「冠水らしい」「煙らしい」「視界不良らしい」
という追加情報を与える役割です。
通信が使えるときは外部へ送れる。
通信が使えなくても Pi の中には観測結果が残る。
この設計を崩さず、AIもローカルで完結するようにしました。
10. AI結果をL3、根拠画像をL4へ登録する
vision_result.json は推論の出力です。RON-Vでは、そのファイルを作っただけで完了にせず、「どのイベントの、どの画像を、どのモデルで解析したか」を保管・参照できるようにします。
2026年10月19〜20日に使ったコードは cd0a94e です。この版では app/integration/vision_evidence.py がAI結果を読み込み、既存の画像保存・転送処理へ接続しています。
| 保存・参照先 | 入るもの | 確認する目的 |
|---|---|---|
| イベントフォルダー |
event.jpg、capture_manifest.json、vision_result.json
|
撮影と推論が実行されたか |
L3:/var/lib/ronv/l3/vehicle/
|
AI結果を含む、版を付けたJSON | 検出内容・モデル・画像との対応を残す |
L4:/var/lib/ronv/l4/vehicle/
|
保存した根拠画像 | 実際に何が写っているか確認する |
SQLite:vision_latest
|
最新のAI要約と画像・結果の転送状態 | 履歴表示と転送確認に使う |
| Google Drive | L3のJSONとL4の画像ファイル | Piの外へ保管する |
| Google Sheets |
L3_INDEX / L4_INDEX と転送の記録 |
保存先と転送結果を探す |
L3のAIファイル名には、イベントIDと内容のSHA256を含めます。再解析で結果が変わった場合は別のファイルにし、既に転送した内容を上書きしません。L4は既存の画像を参照し、入力画像の照合値と一致するか確認します。
新しく実行した推論には、入力画像とモデルファイルのSHA256も記録します。過去のJSONにモデルの照合値がなければ、後から現在のモデルの値で埋めません。過去にどのモデルを使ったか分からない状態を、そのまま区別して残します。
更新時に確認したこと
2台とも設定とSQLiteを退避してからGitHubのコードを取得しました。Python依存パッケージは既に導入済みで、今回の更新では pip による入れ直しは必要ありませんでした。Wi-Fiやノード別の設定をサンプル設定で上書きせずに進めています。
推論結果の来歴とL3/L4登録に関する16件のテストが通り、登録前の確認では20件が対象、エラーは0件でした。実際の登録も両機で20件、エラー0件を確認しました。これは登録処理の結果であり、全件のDrive転送完了を表す数字ではありません。
既存のイベントを登録する
以下は、モデル・撮影・推論が既に動き、上記の連携コードを取得済みのPiで使う例です。最初のコマンドは、ファイルと転送要求を作らずに対象を確認します。 表示用のテーブル・ビューの初期化は行われます。
(
set -e
cd /home/ronv/ronv
export RONV_CONFIG=/home/ronv/ronv/config/ronv.toml
/home/ronv/.venvs/ronv/bin/python - <<'PY'
import json
from app.common.config import load_config
from app.integration.vision_evidence import produce_vision
report = produce_vision(load_config(), max_events=20, dry_run=True)
print(json.dumps(report, ensure_ascii=False, indent=2))
raise SystemExit(1 if report['errors'] else 0)
PY
)
エラーがなければ、同じコードの dry_run=True を dry_run=False に変更して登録します。登録後は、既存の ronv-vehicle-evidence.timer により新しい撮影イベントも処理します。
sudo systemctl enable --now ronv-vehicle-evidence.timer
systemctl is-active ronv-vehicle-evidence.timer
繰り返しの登録で同じ結果の転送要求を増やさない設計です。ただし、破損した画像やJSONなどは errors を確認して対応します。
11. GrafanaでAI結果と根拠画像を参照する
grafana/dashboards/ronv-vision.json に「RON-V 画像AI・根拠画像・転送状況」のダッシュボードを追加しました。vision_latest などを使い、イベント時刻、AIの状態、検出件数、視界分類、AI結果と画像の転送状態を表示する構成です。
既存のGrafana環境へ、JSONを /var/lib/grafana/dashboards/ronv-vision.json として配置しました。画像へのリンクはRON-V Webを参照します。ダッシュボード変数 web_url の既定値はRONV1向けなので、RONV2では http://ronv2.local へ変更したコピーを配置しました。ノード名だけ変えても、Grafanaのデータソースやダッシュボード自動読み込み設定が未導入なら表示できません。
今回確認できたのは、SQLを使うテストと実機へのダッシュボード配置までです。 ブラウザー上での各パネル・画像リンクの最終確認は別に残っています。AI専用の地図や警報まで完成した、という意味ではありません。
挿絵を追加するなら、この位置に「イベント一覧・AI状態・転送状態が同時に見えるGrafana画面」を入れます。未確認の表示を完成画面として掲載しないようにします。
12. Google DriveとSheetsまで確認する
ファイルの登録後、ronv-rclone-delivery が転送し、その結果を ronv-google-sync がSheetsへ記録します。ネットワークが使えない間はPi内に残し、通信の回復後に送る構成です。
今回はDrive認証の更新が必要だった
2台ともDriveへの転送が invalid_grant で止まっていました。AI推論の失敗ではなく、rcloneの認証エラーでした。既存の認証設定を退避し、転送処理を止めてから、各ノードで rclone config reconnect を実行しました。
SSH接続先にブラウザーがないため、WindowsからSSHのポート転送で認証画面を開きました。Googleからのコード受領後、通常の個人用DriveではShared Driveの質問へ n と答え、フォルダーを一覧できることを確認しました。remote名はノードの設定に合わせます。RONV1のremote名をRONV2へそのまま流用しません。
認証手順は rclone公式のリモート認証 と reconnect を参照できます。トークンの実際の値や rclone.conf の内容は記事へ貼りません。
代表イベントで確認した結果
| 実機・確認日 | イベントID | 画像の転送 | AI結果の転送 | Sheetsへの記録時刻(JST) |
|---|---|---|---|---|
| RONV1・10月19日 | 171 | acked |
acked |
2026-10-19 21:29:58 |
| RONV2・10月20日 | 131 | acked |
acked |
2026-10-20 19:34:31 |
イベントIDは各Pi内の番号で、同じ番号の画像を新しく作る必要はありません。手元では自分の最新イベントを確認します。
(
set -e
cd /home/ronv/ronv
export RONV_CONFIG=/home/ronv/ronv/config/ronv.toml
/home/ronv/.venvs/ronv/bin/python - <<'PY'
from app.common.config import load_config
from app.common.db import connect_db
con = connect_db(load_config())
try:
for row in con.execute('''
SELECT v.event_id, v.status, v.upload_state,
v.image_upload_state,
gx.exported_at_jst AS sheets_recorded_at
FROM vision_latest v
JOIN sync_objects o ON o.object_uid=v.object_uid
LEFT JOIN google_sync_exports gx
ON gx.stream='L3_INDEX' AND gx.source_id=o.rowid
ORDER BY v.event_time_jst DESC LIMIT 5
'''):
print(dict(row))
finally:
con.close()
PY
)
| 表示 | 読み方 |
|---|---|
status=ok |
AIの処理が成功した |
upload_state=acked |
AI結果ファイルのDrive転送が完了した |
image_upload_state=acked |
根拠画像のDrive転送が完了した |
sheets_recorded_at に日時がある |
AI結果のL3_INDEXをSheetsへ記録した |
pending / retry |
未転送または再試行待ち。AI成功とは別に確認する |
新しいイベントが次々に作られると、最新の行はまだ pending の場合があります。表全体が一瞬で acked になることを求めず、同じイベントの状態が進むかを追います。また、SheetsのINDEXはファイルと転送の一覧で、全モデルのconfidenceを並べるAI専用シートではありません。
13. 今回できたことと、残る確認
10月20日のRONV2では、新しいBUTTON撮影の入力画像と、道路・冠水・煙・視界の4モデルすべてに照合値が記録され、全モデルの status=ok を確認しました。RONV1でも前日に同じ項目を確認しています。
実機への接続確認と、道路で正しく検出できるかの評価は分けます。今回の机上試験だけで、道路冠水や障害物の検出精度、走行中の安定性まで確認済みとはしません。残る作業は、Grafana画面の最終確認、未転送データの経過確認、実車・道路画像での評価です。
次の#24では、この処理を動かす実機の構成と、UPSの通信不良への対応、別媒体へ残した復旧用バックアップを扱います。