Jetson NanoでYOLOをPython起動!Dockerでライブ表示 → TensorRT化 → 検出結果を別Pythonへ渡す(ROS前段)

Jetson NanoでYOLOをPython起動!Dockerでライブ表示 → TensorRT化 → 検出結果を別Pythonへ渡す(ROS前段)

色々苦労したので、誰かの助けになればと思いここに手順を残します。

Jetson Nanoで物体検出をロボットに組み込むとき、

  • 推論はDockerで回したい(環境を壊したくない)
  • 推論中はJetson本体の画面に映像を出したい(開発で必須)
  • 検出結果は最終的にROSへ渡したい
  • でも今はROS環境が無いので、まずは 別Pythonへ結果が渡ればOK

という流れになります。

この記事では、最短で

  1. Docker上のPythonでYOLOライブ表示(Jetson画面に表示)
  2. 同じ仕組みのままTensorRT(.engine)へ変換して高速推論
  3. Pythonで検出結果(boxes)を取得
  4. 検出結果を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

生成されたら /workspaceyolo12n.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 を自動でダウンロードします
  • 完了すると /workspaceyolo12m.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

次の記事では独自の学習データを使えるところまでを紹介したいと思います。

それではまた。

About The Author

Hideki
東京大学発AIスタートアップ企業でロボット開発室室長、画像解析室室長、動画解析室室長を務め、AIエンジニアとしても画像認識関連の特許を在籍中に3つ取得。その後、KDDIグループ内でプロダクトリーダーとして自然言語処理パッケージの自社開発を経て、現在はAGRISTのテックリードとして農業の人手不足の解決に向けた収穫ロボットの開発にチャレンジしている。ロボットは技術の総合格闘技との考え方から、AIだけでなく、ハードやエレキ、通信からクラウド、IOTまで幅広く手掛けることができる。最近では人とロボットの共存を目指すべく、性能だけを追い求める開発から「感動やワクワク体験」をデザインできるロボットの研究を進めており、人とロボットがうまく共存できる世界を作り出したいと日々行動している。

LEAVE A REPLY

*
*
* (公開されません)