Jetson Orin Nano 實戰(一):CSI 相機+YOLOv8 即時物件偵測串流,一次就做對

把一台 Jetson Orin Nano 接上 CSI 相機,跑 YOLOv8 物件偵測,再用瀏覽器即時看畫面。聽起來很單純,實際動手時卻很容易卡關:相機抓不到、YOLO 慢得像幻燈片、套件裝完 OpenCV 就壞掉……

這篇是我實際做完一輪後,整理出的正確順序版。照著做,可以避開常見的坑。完成後,在區域網路上任何一台電腦的瀏覽器,都能看到帶有偵測框的即時畫面:推論約 17 ms、畫面約 30 FPS。

下一篇〈實戰(二)〉會再用 TensorRT,把推論時間從 17 ms 降到 3.6 ms。


本文環境

項目 內容
開發板 Jetson Orin Nano 8GB(Super),官方 devkit 載板
系統 JetPack 6.2(L4T R36.4.x,CUDA 12.6,TensorRT 10.3),Python 3.10
相機 IMX219 CSI 相機(樹莓派 Camera V2 同款感光元件)
操作方式 從 Windows 透過 SSH/VS Code Remote-SSH 遠端操作(Jetson 不必接螢幕)

以下指令若沒有特別說明,都是在 Jetson 的終端機執行。


先看重點:四個最容易走錯的地方

  1. 相機設定完沒有生效:jetson-io 會新增一個開機項目,但預設開機項目可能沒有跟著切換。→ 步驟 1
  2. 不要用 pip 直接安裝 PyPI 上的 torch:通用版的 torch 跟 Jetson 的 CUDA 不相容,YOLO 會默默改用 CPU,速度慢 5 倍以上。→ 步驟 2
  3. numpy 要維持 1.x、不要裝 opencv-python:Jetson 內建的 OpenCV 支援 GStreamer,CSI 相機全靠它;pip 版的 OpenCV 不支援 GStreamer,numpy 2 則會讓內建的 OpenCV 無法匯入。→ 步驟 3
  4. 程式碼不要直接貼進終端機:長字串容易被截斷。請用 VS Code Remote-SSH 編輯,或用 scp 傳檔。→ 步驟 4

步驟 1:啟用 CSI 相機,並確認設定真的生效

1-1 用 jetson-io 設定相機

sudo /opt/nvidia/jetson-io/jetson-io.py

依序選擇:Configure Jetson 24pin CSI Connector → Configure for compatible hardware → 你的相機(例如 Camera IMX219 Dual)→ Save pin changes → Save and reboot to reconfigure pins。

Orin Nano devkit 的 CSI 座是 22-pin 窄口。樹莓派相機附的 15-pin 排線插不上,需要 15→22 pin 轉接排線。插拔排線前請先關機。

1-2 關鍵:確認預設開機項目(很多人卡在這裡)

jetson-io 會在 /boot/extlinux/extlinux.conf 新增一個 LABEL JetsonIO 開機項目,相機設定(OVERLAYS)就寫在裡面。但 DEFAULT 有可能還指向原本的 primary,這樣每次開機都不會載入相機驅動。先檢查:

grep -E "^DEFAULT|^LABEL|OVERLAYS|FDT" /boot/extlinux/extlinux.conf

如果看到 DEFAULT primary,而 OVERLAYS 只在 LABEL JetsonIO 底下,就要切換。切換前先確認 JetsonIO 指定的 FDT 檔案存在,而且和你的模組型號一致:

ls -l /boot/dtb/                                   # 找到 FDT 那行指定的檔名
tr '\0' '\n' < /proc/device-tree/compatible        # 模組編號,例如 p3767-0005

兩者一致後,先備份再切換:

sudo cp /boot/extlinux/extlinux.conf /boot/extlinux/extlinux.conf.bak
sudo sed -i 's/^DEFAULT primary/DEFAULT JetsonIO/' /boot/extlinux/extlinux.conf
grep ^DEFAULT /boot/extlinux/extlinux.conf         # 應顯示 DEFAULT JetsonIO
sudo reboot

萬一開不了機:接上螢幕和鍵盤,在開機選單選 primary kernel 進入系統,再把 .bak 複製回去即可還原。

1-3 不經過 Python,直接測試相機

ls /dev/video*                                     # 應出現 /dev/video0
lsmod | grep imx219                                # 應看到 nv_imx219
gst-launch-1.0 nvarguscamerasrc sensor-id=0 num-buffers=30 ! fakesink

最後一行會列出相機支援的解析度,最後出現 Done Success,就代表相機正常。

sensor-id 是系統偵測到的相機順序,不是插座編號。只接一顆相機時就是 0。


步驟 2:安裝 Jetson 專用的 PyTorch(順序很重要)

一定要先裝 Jetson 版的 torch,再裝 ultralytics。 如果先裝 ultralytics,pip 會順手從 PyPI 安裝通用版 torch。那個版本需要較新的 CUDA,在 Jetson 上 torch.cuda.is_available() 會是 False,YOLO 就只能用 CPU 跑。

2-1 安裝 cuDSS(新版 torch 需要的 NVIDIA 函式庫)

cd ~
wget https://developer.download.nvidia.com/compute/cudss/0.7.1/local_installers/cudss-local-tegra-repo-ubuntu2204-0.7.1_0.7.1-1_arm64.deb
sudo dpkg -i cudss-local-tegra-repo-ubuntu2204-0.7.1_0.7.1-1_arm64.deb
sudo cp /var/cudss-local-tegra-repo-ubuntu2204-0.7.1/cudss-*-keyring.gpg /usr/share/keyrings/
sudo apt-get update
sudo apt-get -y install cudss

2-2 安裝 Jetson 版 torch/torchvision

以下 wheel 取自 Ultralytics 官方 Jetson 指南,適用 JetPack 6 與 Python 3.10:

pip3 install https://github.com/ultralytics/assets/releases/download/v0.0.0/torch-2.10.0-cp310-cp310-linux_aarch64.whl
pip3 install https://github.com/ultralytics/assets/releases/download/v0.0.0/torchvision-0.25.0-cp310-cp310-linux_aarch64.whl

版本日後可能更新,請以官方指南為準。

確認 GPU 可用:

python3 -c "import torch; print(torch.__version__, 'CUDA:', torch.cuda.is_available())"
# 2.10.0 CUDA: True

一定要看到 True 才往下做。


步驟 3:安裝 ultralytics 與 Flask,並保護系統的 OpenCV

pip3 install ultralytics flask "numpy<2"

因為 torch 和 torchvision 已經裝好,pip 不會再去動它們。不過 ultralytics 會順便安裝 opencv-python,它會蓋過 JetPack 內建、支援 GStreamer 的 OpenCV,所以要移除:

pip3 uninstall -y opencv-python opencv-python-headless

最後做三項檢查,全部通過才繼續:

python3 -c "import numpy; print('numpy', numpy.__version__)"
python3 -c "import cv2; print('cv2', cv2.__version__, [l.strip() for l in cv2.getBuildInformation().splitlines() if 'GStreamer' in l])"
python3 -c "import torch; print('torch', torch.__version__, torch.cuda.is_available())"
檢查項目 應該看到
numpy 1.26.x(1 開頭)
cv2 4.5.4,而且 GStreamer: YES
torch 2.10.0 True

原則:在這台 Jetson 上用 pip 安裝任何套件時,都一起加上 "numpy<2",避免 numpy 被升級。


步驟 4:建立 server.py

建立專案資料夾:

mkdir -p ~/yolo_project && cd ~/yolo_project

建議用 VS Code 的 Remote-SSH 連進 Jetson,直接在 ~/yolo_project 裡建立 server.py。或者在 Windows 寫好檔案,再用 scp 傳過去:

scp server.py <使用者>@<Jetson-IP>:~/yolo_project/

不要把整段 Python 貼進 SSH 終端機:貼進 bash 會被當成指令執行;貼進 nano 時,長字串可能被截斷,出現 SyntaxError: unterminated string literal。

這支程式做了哪些事

  • 用 GStreamer 的 nvarguscamerasrc 讀取 CSI 相機。
  • 開啟後先實際讀一張畫面。這點很重要:GStreamer 管線失敗時,cap.isOpened() 仍可能回傳 True,只檢查它會被騙。
  • 由一個背景執行緒負責「讀相機 → YOLO 推論 → 畫框 → JPEG 編碼」,所有瀏覽器連線共用最新一張畫面,多人同時觀看也不會互搶相機。
  • 如果資料夾裡有 yolov8n.engine(下一篇的 TensorRT 版)就用它,否則使用 yolov8n.pt。
  • 畫面左下角顯示 FPS 與推論毫秒數;啟動時自動印出 Jetson 的區域網路 IP。

完整程式碼

import os
import socket
import threading
import time

import cv2
from flask import Flask, Response
from ultralytics import YOLO

SENSOR_ID = int(os.environ.get("SENSOR_ID", 0))   # 單顆相機用 0
WIDTH, HEIGHT, FPS = 1280, 720, 30

# 有 TensorRT engine 就用 engine,否則用 .pt(可用環境變數 MODEL 強制指定)
MODEL_PATH = os.environ.get("MODEL") or (
    "yolov8n.engine" if os.path.exists("yolov8n.engine") else "yolov8n.pt"
)

app = Flask(__name__)
model = YOLO(MODEL_PATH, task='detect')

gst_pipeline = (
    f"nvarguscamerasrc sensor-id={SENSOR_ID} ! "
    f"video/x-raw(memory:NVMM), width={WIDTH}, height={HEIGHT}, format=NV12, framerate={FPS}/1 ! "
    "nvvidconv flip-method=0 ! video/x-raw, format=BGRx ! "
    "videoconvert ! video/x-raw, format=BGR ! "
    "appsink drop=true max-buffers=1 sync=false"
)

print(f"正在嘗試開啟相機 (sensor-id={SENSOR_ID}),請稍候...")
cap = cv2.VideoCapture(gst_pipeline, cv2.CAP_GSTREAMER)

# GStreamer 失敗時 isOpened() 仍可能是 True,必須實際讀一張才算成功
ok, _ = cap.read() if cap.isOpened() else (False, None)
if not ok:
    cap.release()
    print("❌ 無法從相機取得畫面。請檢查:\n"
          "   1. ls /dev/video* 是否存在\n"
          "   2. SENSOR_ID 是否正確(單顆相機用 0)\n"
          "   3. sudo systemctl restart nvargus-daemon")
    raise SystemExit(1)

print(f"✅ 相機成功啟動!模型:{MODEL_PATH}")

# 單一背景執行緒負責讀相機 + 推論,所有連線共享最新一張 JPEG
latest_jpeg = None
lock = threading.Lock()


def capture_loop():
    global latest_jpeg
    fps_avg = 0.0
    while True:
        t0 = time.time()
        success, frame = cap.read()
        if not success:
            print("無法讀取相機畫面,0.5 秒後重試")
            time.sleep(0.5)
            continue

        results = model.predict(source=frame, conf=0.4, imgsz=640, half=True, verbose=False)
        r = results[0]
        frame = r.plot()

        # 左下角黑底顯示 FPS 與推論時間,避免被偵測標籤蓋住
        fps_avg = 0.9 * fps_avg + 0.1 * (1.0 / max(time.time() - t0, 1e-6))
        label = f"FPS {fps_avg:.1f} | infer {r.speed['inference']:.1f} ms"
        h = frame.shape[0]
        cv2.rectangle(frame, (0, h - 40), (430, h), (0, 0, 0), -1)
        cv2.putText(frame, label, (10, h - 12), cv2.FONT_HERSHEY_SIMPLEX, 0.9, (0, 255, 0), 2)

        ret, buffer = cv2.imencode('.jpg', frame, [cv2.IMWRITE_JPEG_QUALITY, 80])
        if ret:
            with lock:
                latest_jpeg = buffer.tobytes()


def generate_frames():
    while True:
        with lock:
            jpeg = latest_jpeg
        if jpeg is not None:
            yield (b'--frame\r\n'
                   b'Content-Type: image/jpeg\r\n\r\n' + jpeg + b'\r\n')
        time.sleep(1 / FPS)


def lan_ip():
    """取得 Jetson 在區域網路上的 IP(UDP connect 不會真的送出封包)。"""
    s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
    try:
        s.connect(("8.8.8.8", 80))
        return s.getsockname()[0]
    except OSError:
        return "<Jetson-IP>"
    finally:
        s.close()


@app.route('/')
def video_feed():
    return Response(generate_frames(), mimetype='multipart/x-mixed-replace; boundary=frame')


if __name__ == '__main__':
    threading.Thread(target=capture_loop, daemon=True).start()
    print(f"🚀 伺服器啟動成功!請開啟瀏覽器存取 http://{lan_ip()}:5000")
    app.run(host='0.0.0.0', port=5000, debug=False, threaded=True)

步驟 5:執行並觀看

cd ~/yolo_project
sudo jetson_clocks          # 選用:鎖定最高時脈(重開機後會失效)
python3 server.py

第一次執行時,ultralytics 會自動下載 yolov8n.pt(約 6 MB)。看到以下訊息就代表成功:

✅ 相機成功啟動!模型:yolov8n.pt
🚀 伺服器啟動成功!請開啟瀏覽器存取 http://<Jetson-IP>:5000

在 Windows 的 Chrome 或 Edge 打開這個網址,就能看到即時偵測畫面。

實測結果

項目 數值
YOLOv8n 推論時間 約 17.4 ms
畫面 約 30 FPS(已達相機設定的幀率上限)

如果還是遇到問題

症狀 原因與處理
No cameras available,而且沒有 /dev/video* 相機設定沒有生效 → 回到步驟 1-2,確認 DEFAULT JetsonIO
有 /dev/video0,但還是 No cameras available sensor-id 錯了,或相機被其他程式占用 → 執行 sudo systemctl restart nvargus-daemon
畫面很卡,torch.cuda.is_available() 是 False 裝到通用版 torch → 移除後重做步驟 2
numpy.core.multiarray failed to import numpy 被升到 2.x → pip3 install "numpy<2"
cv2 的 GStreamer: NO 又被裝了 opencv-python → 移除它(步驟 3)
can't open file '.../server.py' 不在專案資料夾 → cd ~/yolo_project
畫面四周偏粉紅 IMX219 在 JetPack 沒有預設的色彩校正檔;不影響偵測,需要時可向相機廠商索取 ISP 校正檔

下一步

目前已經很流暢了,但 GPU 其實還有很多餘裕。下一篇〈Jetson Orin Nano 實戰(二)〉會把模型轉成 TensorRT engine,推論時間從 17.4 ms 降到 3.6 ms(約快 4.8 倍),也會說明為什麼會快這麼多。

留言

這個網誌中的熱門文章

在 VS Code 安裝Opencode

使用VS code開發Python程式 - Matplotlib繪圖

LabVIEW教學: 如何在VI中使用.dll檔