
色々苦労したので、誰かの助けになればと思いここに手順を残します。
Jetson Nanoで物体検出をロボットに組み込むとき、
- 推論はDockerで回したい(環境を壊したくない)
- 推論中はJetson本体の画面に映像を出したい(開発で必須)
- 検出結果は最終的にROSへ渡したい
- でも今はROS環境が無いので、まずは 別Pythonへ結果が渡ればOK
という流れになります。
この記事では、最短で
- Docker上のPythonでYOLOライブ表示(Jetson画面に表示)
- 同じ仕組みのままTensorRT(
.engine)へ変換して高速推論 - Pythonで検出結果(boxes)を取得
- 検出結果をUDPで“別Python(ホスト側)”へ渡す(ROSの前段)
までを一本道で作ります。
この記事のゴール(できること)
- Jetson Nano + USBカメラで 推論映像をJetson本体ディスプレイに表示
- YOLOを TensorRT engine化してDocker内Pythonから実行
- 検出結果を JSONでUDP送信 → ホスト側Pythonで受信
- 将来は受信側をROSノードに置き換えるだけで応用可能
前提(最低限)
- Jetson Nano(JetPack 4系想定)
- Dockerが使える(※この記事では sudo docker で統一)
- USBカメラが接続されている(細かいデバイスチェックはしない)
手順(1コマンドずつ)
1) PCからJetsonへSSH接続
PCのターミナルで:
ssh ai@<JETSONのIPアドレス>
2) Dockerが動くことを確認
Jetson側(SSH)で:
sudo docker ps
3) UltralyticsのJetson用イメージをpull
sudo docker pull ultralytics/ultralytics:latest-jetson-jetpack4
4) 作業フォルダを作る(名前を固定)
フォルダ名は紛らわしくないように yolo_docker で統一します。
mkdir -p ~/yolo_docker && cd ~/yolo_docker
5) “最初から落ちない”GUI対応イメージを作る(重要)
ここが最重要です。
公式イメージのままだと cv2.imshow() が GUI未実装エラーで落ちることがあるため、最初に回避します。
さらにTensorRT実行で出やすい np.bool 問題も、最初から潰します。
5-1) Dockerfile作成
cat > Dockerfile <<'EOF'
FROM ultralytics/ultralytics:latest-jetson-jetpack4
ENV DEBIAN_FRONTEND=noninteractive
# 1) OpenCV GUI (cv2.imshow) を動かすための最低限
RUN apt-get update && apt-get install -y --no-install-recommends \
libgtk-3-0 \
libcanberra-gtk3-module \
libx11-6 libxext6 libxrender1 libsm6 libice6 \
&& rm -rf /var/lib/apt/lists/*
# 2) GUIありOpenCVに入れ替え(ここが肝)
RUN python3 -m pip uninstall -y opencv-python opencv-python-headless opencv-contrib-python opencv-contrib-python-headless || true
RUN python3 -m pip install --no-cache-dir opencv-python==4.8.0.76 numpy==1.23.5
# 3) TensorRTで出る np.bool 問題を恒久回避
RUN python3 -c "from pathlib import Path; Path('/usr/lib/python3.8/sitecustomize.py').write_text('import numpy as np\\nif not hasattr(np,\\\"bool\\\"):\\n np.bool = bool\\n'); print('installed sitecustomize')"
WORKDIR /workspace
EOF
5-2) ビルド
sudo docker build -t yolo12-gui-jp4 .
6) Jetson画面に表示できるようにX11許可
DISPLAY=:0 xhost +SI:localuser:root
7) コンテナ起動(Jetson画面へ表示する設定込み)
sudo docker run --rm -it --name yolo12_gui \
--runtime nvidia --net=host --privileged --device=/dev/video0 \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v $HOME/yolo_docker:/workspace -w /workspace \
yolo12-gui-jp4 bash
以後、プロンプトが root@...:/workspace# なら Docker内です。
8) GUI確認(ユーザーが確認して q で閉じる)
SSHの入力待ち(input)だとEOFになることがあるので、Jetson側で q に統一します。
python3 - <<'PY'
import cv2, numpy as np
img = np.zeros((360, 640, 3), dtype=np.uint8)
cv2.putText(img, 'GUI OK - press q on Jetson to close', (20, 200),
cv2.FONT_HERSHEY_SIMPLEX, 0.9, (255,255,255), 2)
cv2.namedWindow('test', cv2.WINDOW_NORMAL)
while True:
cv2.imshow('test', img)
k = cv2.waitKey(30) & 0xFF
if k == ord('q'):
break
cv2.destroyAllWindows()
PY
ステップA:TensorRTを気にせず「Pythonでライブ表示」まで
9) YOLOライブ表示スクリプトを作成(Docker内)
cat > yolo_live.py <<'PY'
import os
import cv2
from ultralytics import YOLO
MODEL = os.getenv("MODEL", "yolo12n.pt")
DEV = int(os.getenv("DEV", "0"))
IMGSZ = int(os.getenv("IMGSZ", "320"))
def main():
model = YOLO(MODEL)
cap = cv2.VideoCapture(DEV, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
if not cap.isOpened():
raise RuntimeError("camera open failed")
win = "YOLO LIVE (press q to quit)"
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
cv2.resizeWindow(win, 1280, 720)
while True:
ok, frame = cap.read()
if not ok:
break
r = model.predict(frame, imgsz=IMGSZ, device=0, half=True, verbose=False)[0]
vis = r.plot()
cv2.imshow(win, vis)
if (cv2.waitKey(1) & 0xFF) == ord("q"):
break
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
PY
10) 実行(まずは.pt)
python3 yolo_live.py
ステップB:同じ表示のままTensorRT(engine)へ
11) TensorRT engineを作る(Docker内)
ここは時間がかかるので、不安なら別SSHで
tegrastatsを流しておくと「生きてる」が分かります。
yolo export model=yolo12n.pt format=engine imgsz=640 half=True device=0
生成されたら /workspace に yolo12n.engine ができます。
フリーズ不安対策(別SSHで)
tegrastats
12) engineでライブ表示(Docker内)
表示方法は一切変えず、MODELだけ差し替えます。
MODEL=yolo12n.engine IMGSZ=640 python3 yolo_live.py
ステップC:Pythonで検出結果(boxes)を取得
13) 結果取得つきスクリプトを作成(Docker内)
ターミナル出力が重くならないよう、PRINT_EVERY=10 で間引きします。
cat > yolo_live_with_boxes.py <<'PY'
import os, time
import cv2
from ultralytics import YOLO
MODEL = os.getenv("MODEL", "yolo12n.engine")
DEV = int(os.getenv("DEV", "0"))
IMGSZ = int(os.getenv("IMGSZ", "640"))
PRINT_EVERY = int(os.getenv("PRINT_EVERY", "10"))
def main():
model = YOLO(MODEL)
cap = cv2.VideoCapture(DEV, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
if not cap.isOpened():
raise RuntimeError("camera open failed")
win = "YOLO LIVE + BOXES (press q to quit)"
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
cv2.resizeWindow(win, 1280, 720)
f = 0
t0 = time.time()
while True:
ok, frame = cap.read()
if not ok:
break
r = YOLO(MODEL).predict(frame, imgsz=IMGSZ, device=0, half=True, verbose=False)[0]
if (f % PRINT_EVERY) == 0:
boxes = []
if r.boxes is not None and len(r.boxes) > 0:
xyxy = r.boxes.xyxy.cpu().numpy()
conf = r.boxes.conf.cpu().numpy()
cls = r.boxes.cls.cpu().numpy()
for i in range(len(xyxy)):
x1,y1,x2,y2 = xyxy[i]
boxes.append({
"xyxy": [float(x1), float(y1), float(x2), float(y2)],
"conf": float(conf[i]),
"cls": int(cls[i]),
})
fps = (f + 1) / max(1e-6, (time.time() - t0))
print({"frame": f, "fps_avg": round(fps, 2), "n": len(boxes), "boxes": boxes})
vis = r.plot()
cv2.imshow(win, vis)
if (cv2.waitKey(1) & 0xFF) == ord("q"):
break
f += 1
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
PY
14) 実行(Docker内)
MODEL=yolo12n.engine IMGSZ=640 PRINT_EVERY=10 python3 yolo_live_with_boxes.py
ステップD:検出結果を“別Python”へ渡す(ROS前段)
ここが重要です。
送信はDocker内(推論側)、**受信はホスト側(JetsonのOS上)**で行います。
将来ROSはホスト側で動かす想定なので、ここが一番自然です。
用語(初心者向けに明確化)
root@...:/workspace#→ Docker内(推論側)ai@ai:~$→ ホスト側(Jetson本体のOS)
15) 受信スクリプト(udp_receiver.py)を作る(Docker内に置いてOK)
cat > udp_receiver.py <<'PY'
import json
import socket
HOST = "0.0.0.0"
PORT = 5005
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.bind((HOST, PORT))
print(f"listening UDP {HOST}:{PORT} ...")
while True:
data, addr = sock.recvfrom(65535)
msg = json.loads(data.decode("utf-8"))
print(f"from {addr} n={msg.get('n')} frame={msg.get('frame')} fps_avg={msg.get('fps_avg')}")
PY
受信はホスト側で動かす(重要)
Docker内 /workspace はホストの ~/yolo_docker なので、ホスト側へコピーします。
15-1) Dockerを抜ける
exit
15-2) ホスト側でコピー
cp ~/yolo_docker/udp_receiver.py ~/udp_receiver.py
15-3) ホスト側で受信を起動
python3 ~/udp_receiver.py
listening UDP ... が出たらOK。
16) 送信側(Docker内)スクリプトを作る
別ターミナルでDockerに入り直し(または元の推論コンテナで)送信を実行します。
sudo docker run --rm -it --name yolo12_gui_tx \
--runtime nvidia --net=host --privileged --device=/dev/video0 \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v $HOME/yolo_docker:/workspace -w /workspace \
yolo12-gui-jp4 bash
そしてDocker内で送信スクリプトを作成:
cat > yolo_live_send_udp.py <<'PY'
import os, time, json, socket
import cv2
from ultralytics import YOLO
MODEL = os.getenv("MODEL", "yolo12n.engine")
DEV = int(os.getenv("DEV", "0"))
IMGSZ = int(os.getenv("IMGSZ", "640"))
UDP_IP = os.getenv("UDP_IP", "127.0.0.1")
UDP_PORT = int(os.getenv("UDP_PORT", "5005"))
SEND_EVERY = int(os.getenv("SEND_EVERY", "10"))
def main():
model = YOLO(MODEL)
names = model.names if hasattr(model, "names") else {}
cap = cv2.VideoCapture(DEV, cv2.CAP_V4L2)
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
cap.set(cv2.CAP_PROP_FPS, 30)
if not cap.isOpened():
raise RuntimeError("camera open failed")
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
win = "YOLO LIVE + UDP (press q to quit)"
cv2.namedWindow(win, cv2.WINDOW_NORMAL)
cv2.resizeWindow(win, 1280, 720)
f = 0
t0 = time.time()
while True:
ok, frame = cap.read()
if not ok:
break
h, w = frame.shape[:2]
r = model.predict(frame, imgsz=IMGSZ, device=0, half=True, verbose=False)[0]
if (f % SEND_EVERY) == 0:
dets = []
if r.boxes is not None and len(r.boxes) > 0:
xyxy = r.boxes.xyxy.cpu().numpy()
conf = r.boxes.conf.cpu().numpy()
cls = r.boxes.cls.cpu().numpy()
for i in range(len(xyxy)):
x1,y1,x2,y2 = xyxy[i]
c = int(cls[i])
dets.append({
"cls_id": c,
"cls_name": str(names.get(c, c)),
"conf": float(conf[i]),
"xyxy": [int(x1), int(y1), int(x2), int(y2)],
})
fps = (f + 1) / max(1e-6, (time.time() - t0))
payload = {
"seq": f,
"ts_unix": time.time(),
"fps_avg": round(fps, 2),
"img_w": int(w),
"img_h": int(h),
"n": len(dets),
"dets": dets,
}
sock.sendto(json.dumps(payload).encode("utf-8"), (UDP_IP, UDP_PORT))
vis = r.plot()
cv2.imshow(win, vis)
if (cv2.waitKey(1) & 0xFF) == ord("q"):
break
f += 1
cap.release()
cv2.destroyAllWindows()
if __name__ == "__main__":
main()
PY
17) 送信を実行(Docker内)→ ホスト受信へ届く
MODEL=yolo12n.engine IMGSZ=640 SEND_EVERY=10 UDP_IP=127.0.0.1 UDP_PORT=5005 python3 yolo_live_send_udp.py
ホスト側の udp_receiver.py に
from ('127.0.0.1', xxxx) n=... frame=... fps_avg=...
が流れれば成功です。
よくあるエラーと対処(初心者が詰まる罠だけ)
cv2.imshow が「function is not implemented」
→ 公式イメージのOpenCVがGUI未対応。
この記事では 最初からGUI対応イメージ(yolo12-gui-jp4)を作ることで回避しています。
TensorRT実行で np.bool エラー
→ NumPyの互換問題。
この記事では Dockerfileに sitecustomize.py を仕込んで 恒久回避しています。
yolo12n.engine does not exist
→ まだexportしてないだけ。yolo export ... format=engine imgsz=640 ... を実行してください。
exportが長くて不安
→ 別SSHで tegrastats を流すと「生きてる」が分かります。
ファイル保存場所と Docker の構造(初心者向け)
この手順で作ったファイルはどこに保存される?
結論から言うと、この記事で作ったファイルは 基本すべて Jetson 本体(ホスト側)の ~/yolo_docker に保存されます。
なぜなら、コンテナ起動時に次のオプションを指定しているからです。
-v $HOME/yolo_docker:/workspace -w /workspace
これは、
- Jetson本体(ホスト側)の
~/yolo_docker - Docker内の
/workspace
を 同じフォルダとして“接続(マウント)”する指定です。
つまり、Docker内で /workspace に作ったファイルは、Dockerを終了しても消えずに Jetson本体の ~/yolo_docker に残り続けます。
「ホスト側」と「Docker内」の見分け方
初心者が混乱しやすいので、プロンプトの見た目で判断します。
ai@ai:~$→ ホスト側(JetsonのUbuntu)root@ai:/workspace#→ Docker内(コンテナ)
今どちらにいるか不安なら、これだけで判定できます。
whoami
aiならホスト側rootならDocker内
実際に保存されるファイル例
この記事の手順で作った主なファイルは、ホスト側で見ると次の場所にあります。
ls -lh ~/yolo_docker
例(環境により多少違います):
~/yolo_docker/Dockerfile~/yolo_docker/yolo_live.py~/yolo_docker/yolo_live_with_boxes.py~/yolo_docker/yolo_live_send_udp.py~/yolo_docker/udp_receiver.py~/yolo_docker/yolo12n.engine(TensorRT化したengine)
例外:ホスト側のホーム直下にコピーしたファイル
受信スクリプトを「ホスト側で実行しやすくするため」に、この記事では一度だけコピーしています。
cp ~/yolo_docker/udp_receiver.py ~/udp_receiver.py
この結果、
- 元ファイル:
~/yolo_docker/udp_receiver.py(本体) - コピー:
~/udp_receiver.py(実行しやすいショートカット)
という2つが存在します。
なぜこの構造が便利なの?
将来、ROSはホスト側で動かす前提なので、受信側Pythonはホスト側に置くと自然
Docker内で推論環境を完結できる(Jetson本体を汚しにくい)
ファイルはホスト側に残るので、コンテナを消しても成果物が消えない
yolo12n → yolo12m に変更して TensorRT化し、同じ仕組みで動かす
前回は yolo12n(軽量モデル)で、
- Docker内でライブ表示(Jetson画面)
- TensorRT(
.engine)化 - Pythonでboxes取得
- UDPでホスト側Pythonへ送信(ROS前段)
まで確認しました。
今回は 精度を上げるために yolo12m を使う版です。
0. 前提(前回と同じ)
- 作業フォルダは
~/yolo_docker - 推論は Docker(
yolo12-gui-jp4イメージ) - Jetson画面に表示
- engineは
imgsz=640固定で作る
1) Dockerコンテナに入る(前回と同じ)
ホスト側(ai@ai:~$)で:
DISPLAY=:0 xhost +SI:localuser:root
sudo docker run --rm -it --name yolo12_gui \
--runtime nvidia --net=host --privileged --device=/dev/video0 \
-e DISPLAY=:0 \
-v /tmp/.X11-unix:/tmp/.X11-unix:rw \
-v $HOME/yolo_docker:/workspace -w /workspace \
yolo12-gui-jp4 bash
以後、root@...:/workspace# なら Docker内です。
2) yolo12m.pt をTensorRT engine化する(最重要)
Docker内で、次の1行だけです。
yolo export model=yolo12m.pt format=engine imgsz=640 half=True device=0
- 初回は
yolo12m.ptを自動でダウンロードします - 完了すると
/workspaceにyolo12m.engineができます
engine名を分かりやすくする(おすすめ)
今後モデルを増やすと混乱するので、サイズ入りでリネームしておきます。
mv -f yolo12m.engine yolo12m_640.engine
exportが長くて不安なときは、別SSHで
tegrastatsを流すのが一番分かりやすいです。tegrastats
3) yolo12m(TensorRT engine)でライブ表示する
前回作った yolo_live.py をそのまま使い、MODEL= だけ差し替えます。
MODEL=yolo12m_640.engine IMGSZ=640 python3 yolo_live.py
- Jetson画面に映像+bbox表示
- 終了は Jetson側で
q
4) yolo12mで boxes をPythonで取得する
前回作った boxes取得版をそのまま使えます。
MODEL=yolo12m_640.engine IMGSZ=640 PRINT_EVERY=10 python3 yolo_live_with_boxes.py
5) yolo12mでUDP送信(ROS前段)する
前回作ったUDP送信スクリプトをそのまま使えます(MODELだけ変更)。
MODEL=yolo12m_640.engine IMGSZ=640 SEND_EVERY=10 UDP_IP=127.0.0.1 UDP_PORT=5005 python3 yolo_live_send_udp.py
受信側はホスト(Jetson本体)で起動しておきます(前回と同じ):
python3 ~/udp_receiver.py
次の記事では独自の学習データを使えるところまでを紹介したいと思います。
それではまた。






LEAVE A REPLY