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 的終端機執行。
先看重點:四個最容易走錯的地方
- 相機設定完沒有生效:jetson-io 會新增一個開機項目,但預設開機項目可能沒有跟著切換。→ 步驟 1
- 不要用 pip 直接安裝 PyPI 上的 torch:通用版的 torch 跟 Jetson 的 CUDA 不相容,YOLO 會默默改用 CPU,速度慢 5 倍以上。→ 步驟 2
- numpy 要維持 1.x、不要裝 opencv-python:Jetson 內建的 OpenCV 支援 GStreamer,CSI 相機全靠它;pip 版的 OpenCV 不支援 GStreamer,numpy 2 則會讓內建的 OpenCV 無法匯入。→ 步驟 3
- 程式碼不要直接貼進終端機:長字串容易被截斷。請用 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 倍),也會說明為什麼會快這麼多。

留言