Unitree Go2とG1を活用したロボット目視検査エージェント:処理速度の最適化とG1カメラ互換性検証によるPoCの完結

Unitree Go2とG1を活用したロボット目視検査エージェント:処理速度の最適化とG1カメラ互換性検証によるPoCの完結

Final progress of the Robot Visual Inspection Agent project!
2026.07.30

はじめに

前回までに、Unitree Go2のフロントカメラ画像を入力として、Grounding DINO Tinyによる物体検出、進路ROIとの幾何計算、決定的なPythonポリシー、Streamlitダッシュボードを組み合わせた目視検査エージェントを構築しました。

現在の基本構成は以下です。

Go2フロントカメラ
  ↓
read-only camera capture
  ↓
latest.jpg mailbox
  ↓
Grounding DINO Tiny
  ↓
bounding box
  ↓
前方進路ROIとの幾何計算
  ↓
deterministic risk policy
  ↓
JSON / overlay
  ↓
Streamlit dashboard

このシステムは、手動操作中のオペレーターが前方の状況を確認するためのadvisory-only prototypeです。

Go2やG1の移動、停止、腕、手、graspなどのrobot controlには一切接続していません。

今回の記事では、以下の2点を中心にまとめます。

  1. Inspection pipelineの処理速度をどのように分析・最適化したか
  2. G1でもread-only camera imageを取得し、既存のGrounding DINO analyzerへ入力できることを確認したこと

最終的に、Go2では最新画像のmailbox更新を約3〜4秒ごとから約1秒ごとへ改善できました。また、G1では同じVideoClient互換経路からJPEG画像を取得し、既存のanalyzerで画像を処理できることを確認しました。

Go2デモ動画

以下にGo2の最終デモ動画を掲載します。

https://youtube.com/shorts/GM6hwroVuu0


今週できたこと

今週は主に以下を実装・検証しました。

  • analyzer処理をstage単位で計測できるように改善
  • full-resolution PNG overlay出力が大きなボトルネックであることを特定
  • dashboard表示用overlayを最大幅1280pxへ縮小し、PNG出力コストを最適化
  • persistent analyzerにおける処理時間を計測
  • Go2 camera captureを、単発process起動方式からpersistent VideoClient capture loopへ変更
  • Go2のlatest.jpg更新間隔を約3〜4秒から約1秒へ改善
  • latest.jpg mailbox、CUDA analyzer、dashboardを含む実機Go2 pipelineを再検証
  • G1 Jetson上でcamera sourceを調査
  • G1の/frontvideostream topicと既存camera関連実装を確認
  • G1でGo2 VideoClient互換経路からJPEG画像をread-onlyで取得
  • G1 camera imageを既存のGrounding DINO analyzerへ入力し、detector / overlayが動作することを確認

今回も、Go2およびG1の移動、腕、手、grasp、sport APIなどのmovement commandは使用していません。


analyzerの処理時間を分解する

最初は、Grounding DINO Tinyのmodel forwardが最も時間を使っていると考えていました。

しかし、実際にanalyzerの処理を以下のstageへ分解して計測すると、別のボトルネックが見えてきました。

image load
preprocessing
model forward
post-process
object conversion
geometry policy
overlay write
JSON output

CUDA処理は非同期で実行されるため、model forwardだけをCPU側の時計で測ると、実際より短く見える可能性があります。

そのため、model forward前後でCUDA synchronizationを行い、より実態に近い時間を記録するようにしました。

torch.cuda.synchronize()

forward_start = time.perf_counter()

with torch.inference_mode():
    outputs = model(**inputs)

torch.cuda.synchronize()

model_forward_sec = time.perf_counter() - forward_start

この計測により、推論だけでなく、画像読み込み、前処理、post-process、overlay出力などを比較できるようになりました。


想定外のボトルネック:full-resolution PNG overlay

初期のoffline benchmarkでは、以下の結果になりました。

model_forward_sec:
  1.8079 sec

overlay_write_sec:
  1.9481 sec

total_analyze_sec:
  3.9458 sec

Grounding DINO Tinyのmodel forwardよりも、1920×1080の画像にbboxとROIを描画し、PNGとして保存する処理の方が時間を使っていました。

この結果から、モデルを変更する前に、まず表示用outputを最適化する方が効果的だと判断しました。

重要なのは、risk判定に使う画像・bbox・ROI・threshold・geometry policyは変更しないことです。

変更対象は、あくまでdashboardに表示するoverlay imageだけです。


dashboard overlayの最適化

最適化では、以下の方針を採用しました。

判定用画像:
  元解像度を維持

Grounding DINO input:
  変更なし

bounding box:
  変更なし

ROI geometry:
  変更なし

risk policy:
  変更なし

dashboard表示用overlay:
  bboxとROIを元画像に描画後、
  最大幅1280pxへ縮小してPNG保存

つまり、物理的なリスク判断に関わる部分には触れず、operator-facing display outputだけを軽量化しました。

最適化後のsingle-image benchmarkでは以下の結果になりました。

model_forward_sec:
  1.2296 sec

overlay_write_sec:
  0.1480 sec

total_analyze_sec:
  1.5777 sec

overlay writeは約1.95秒から約0.15秒まで削減されました。

1.9481 sec
  ↓
0.1480 sec

これは約92%の削減です!

ただし、このsingle-image benchmarkは、新しく起動したanalyzerが最初の画像を処理した結果です。

CUDA context initialization、GPU memory allocation、kernel selectionなどのwarm-upコストが含まれるため、通常のpersistent runtimeの代表値としては扱いません。


persistent analyzerのwarm performance

同じanalyzer processを維持した状態で複数画像を処理するwarm benchmarkでは、以下の値を確認しました。

model forward:
  approximately 0.3356 sec

image load:
  approximately 0.0646 sec

preprocessing:
  approximately 0.0285 sec

post-processing:
  approximately 0.0606 sec

overlay write:
  approximately 0.1393 sec

analyzer processing before result publication:
  approximately 0.6507 sec

この結果から、persistent analyzerの内部処理は約0.65秒まで改善できていました。

一方、保存済みの実機Go2 dashboard結果では、以下の値も確認できています。

Inference:
  0.235 sec

Capture to Analysis:
  0.775 sec

Capture to Analysisには、mailbox更新後のanalyzer検知、画像decode、前処理、model forward、geometry、overlay、JSON出力が含まれます。

browser renderingやオペレーターの反応時間は含まれません。

ここで重要なのは、analyzerの処理を高速化しても、camera imageそのものが数秒ごとにしか供給されなければ、dashboardは滑らかには見えないということです。


次のボトルネック:Go2 camera image supply

analyzerの処理時間を改善した後、Go2実機pipelineでdashboardを確認すると、結果は早く出るものの、画面はまだ数秒ごとに更新されるように見えました。

原因を調べると、従来のcapture pathは以下のような構造でした。

Python process起動
  ↓
ChannelFactoryInitialize
  ↓
VideoClient.Init
  ↓
GetImageSampleを1回実行
  ↓
JPEG出力
  ↓
process終了
  ↓
次の周期で再起動

capture intervalを1秒へ変更しても、実際のlatest.jpg更新は約3〜4秒ごとでした。

これは、設定したsleep intervalだけでなく、毎回のPython process起動、DDS初期化、VideoClient初期化、単発camera requestの時間が含まれるためです。


persistent VideoClient capture loop

そこで、Go2 camera captureをpersistent processへ変更しました。

新しい構成は以下です。

ChannelFactoryInitialize
  ↓
VideoClient.Init
  ↓
while loop
  ↓
GetImageSample
  ↓
JPEG validation
  ↓
.latest.jpg.tmp
  ↓
atomic replace
  ↓
latest.jpg

初期化を1回だけ行い、その後は同じVideoClientを使って繰り返しGetImageSample()を実行します。

概念的には以下のような構成です。

ChannelFactoryInitialize(0, interface)

client = VideoClient()
client.SetTimeout(3.0)
client.Init()

while True:
    code, data = client.GetImageSample()

    if code == 0:
        write_latest_jpeg_atomically(data)

このcapture loopは、Go2のmovement APIを呼び出しません。

実行するのはfront camera image requestと、host上のlatest.jpg書き込みだけです。

また、入力ディレクトリにはtimestamp付きraw imageを蓄積せず、常に最新の1枚だけを保持します。


Go2の画像更新間隔を約1秒へ改善

persistent VideoClient capture loopを実機Go2で検証した結果、latest.jpgの更新は__約1秒__ごとになりました。

former repeated one-shot capture:
  approximately every 3–4 sec

persistent VideoClient capture:
  approximately every 1 sec

初回取得時には、以下のような軽い処理時間も確認できました。

GetImageSample request:
  approximately 0.009 sec

mailbox publish:
  approximately 0.0125 sec

もちろん、実際のcamera frame availabilityやDDS通信状態によって変動する可能性はあります。

しかし、以前のように毎回Python processとVideoClientを立ち上げ直す方式と比べ、dashboardの更新感は明らかに改善しました。

現在のGo2 pipelineは以下のようになります。

Go2 front camera
  ↓
persistent read-only VideoClient capture loop
  ↓
latest.jpg mailbox
  ↓
Grounding DINO Tiny on CUDA
  ↓
deterministic ROI geometry
  ↓
JSON / overlay
  ↓
Streamlit dashboard

本記事でいう約1秒の更新は、periodic still-image mailboxの更新間隔です。

連続video streamを直接解析しているわけではありません。


G1 camera compatibility smoke test

Go2の処理速度改善に加えて、G1 camera compatibilityも確認しました。

最初にG1 Jetson上でcamera関連の既存実装を調査しました。

確認できた前方camera topicは以下です。

ROS 2 topic:
  /frontvideostream

message type:
  unitree_go/msg/Go2FrontVideoData

既存projectのdocumentationでは、このtopicにはH.264 encoded video streamが含まれることが記録されていました。

一方で、現在のG1環境では、Go2 SDKのVideoClient互換経路も実際に動作することを確認できました。

ChannelFactoryInitialize(0, "eth0")
  ↓
VideoClient.Init
  ↓
GetImageSample
  ↓
valid JPEG bytes

取得したJPEGはPillowでdecodeでき、既存のGrounding DINO analyzerへそのまま入力できました。

G1デモ画像

G1では今回、camera compatibilityとanalyzer inputを確認するread-only smoke testを実施しました。

G1のmovementやmanipulationは今回のscopeに含めていないため、Go2のような移動デモ動画は作成していません。

代わりに、G1 camera imageに対するdetector / overlay結果を掲載します。

デモ画像
G1

今回確認できたことは以下です。

G1 front camera
  ↓
VideoClient compatibility path
  ↓
valid JPEG
  ↓
Grounding DINO analyzer input
  ↓
detection / overlay output

G1 camera imageは既存analyzerで概ね問題なく処理できました。

ただし、Go2用の前方進路ROIは、G1のnavigationまたはmanipulation safety policyとして検証したものではありません。

G1専用のg1_near_field_v0 ROIを追加するかは、将来のnear-field inspection taskに必要になった時点で、controlled imageと明確な評価目的に基づいて検討します。

今回は、G1 camera image compatibilityを確認できたことを成果とし、無理に未校正ROIや新しいrisk policyを追加しない判断としました。


dashboardとoperator advisory

Streamlit dashboardには以下の情報を表示しています。

latest analyzed image
bounding box overlay
forward-route ROI
ADVISORY RISK
PATH BLOCKED
PRIMARY OBJECT
INFERENCE
CAPTURE TO ANALYSIS
ROUTE OCCUPANCY
OBJECTS USED
CAPTURED
ANALYZED
analyzer log

operatorはSTART / STOPを通じてanalyzer processを管理できます。

ただし、STOPはanalyzerのみを停止します。

STOP analyzer
  ≠ Go2 stop
  ≠ camera capture stop
  ≠ robot movement control
  ≠ emergency stop

capture loopは最新画像をlatest.jpgへ書き続けます。

analyzerを再びSTARTした際には、過去画像のbacklogではなく、現在の最新画像だけを処理します。

この設計により、operator advisory dashboardとして現在の場面に戻りやすくなります。


試行錯誤から得られた学び

今回の最適化では、モデルを変更する前に、実際のpipeline全体を分解して計測する重要性を学びました。

最初はmodel inferenceが最も遅いと考えていました。

しかし、実測では以下のような順番で課題が見つかりました。

1. full-resolution PNG overlay出力
2. analyzer warm-up
3. camera image supply cadence
4. dashboard port conflict
5. process lifecycle / cleanup

特に重要だったのは、Grounding DINO Tinyの推論速度だけを最適化しても、camera imageが数秒ごとにしか更新されなければ、operatorが見るdashboardの更新感は改善しないことです。

そのため、以下のようにボトルネックを段階的に解消しました。

full-resolution overlay
  ↓
1280px display overlay + PNG encoding optimization

one-shot capture process
  ↓
persistent VideoClient capture loop

このように、robotics systemではAI modelだけでなく、camera I/O、DDS、process lifecycle、file mailbox、container、browser UIを含めた全体設計が重要だと感じました。


まとめ

今回のロボット目視検査エージェントでは、Go2とG1を対象として、visual inspection pipelineを構築・検証しました。

主な成果は以下です。

Go2 front cameraのread-only JPEG取得
Grounding DINO Tinyによるobject detection
bounding boxとROIを用いたdeterministic geometry policy
multi-object union occupancy
Streamlit operator dashboard
analyzer-side overlay output optimization
persistent VideoClient capture loop
Go2 latest.jpg更新間隔の約3〜4秒から約1秒への改善
G1 camera JPEG compatibilityの確認
G1 imageのGrounding DINO analyzer入力確認

今回の設計では、VLMによる意味理解と、進路に対する幾何的判定を分離しました。

Grounding DINO:
  object localization

Python geometry:
  ROI overlap and occupancy

deterministic policy:
  risk level and path_blocked

VLM / LLM:
  future semantic explanation and reporting

この役割分担により、進路リスクadvisoryに必要な速度、再現性、説明可能性を改善できました。

本システムは、手動操作中のoperatorが最新のvisual risk evidenceを確認するためのadvisory-only prototypeです。

Go2またはG1の移動、停止、腕、手、grasping、emergency stopなどのrobot controlには接続していません。

今回のinspection projectはここでfeature freezeとし、今後は別projectとして、Isaac Labを用いたG1 stationary bottle manipulation reinforcement learningへ進む予定です。

Go2とG1で構築したcamera、detector、dashboard、evaluationの知見は、次のPhysical AI projectにおける観察・検証基盤として活用していく予定です!

この記事をシェアする

関連記事