文章目录
-
- [1. 范围](#1. 范围)
- [2. USB 和 CSI](#2. USB 和 CSI)
- [3. 工具、权限、板况](#3. 工具、权限、板况)
- [4. USB:枚举、格式、抓帧](#4. USB:枚举、格式、抓帧)
- [5. USB 采集脚本](#5. USB 采集脚本)
- [6. USB 常见坑](#6. USB 常见坑)
-
- [6.1 设备号漂移](#6.1 设备号漂移)
- [6.2 带宽和格式](#6.2 带宽和格式)
- [6.3 曝光和自动档](#6.3 曝光和自动档)
- [6.4 多个 video 节点](#6.4 多个 video 节点)
- [6.5 供电和口](#6.5 供电和口)
- [7. CSI:和 USB 不是同一套排障](#7. CSI:和 USB 不是同一套排障)
- [8. 接到检测和容器](#8. 接到检测和容器)
- [9. 掉线重连](#9. 掉线重连)
- [10. 采集和推理分开验收](#10. 采集和推理分开验收)
- [11. 术语对照](#11. 术语对照)
- [12. 延伸阅读](#12. 延伸阅读)
- [13. 小结](#13. 小结)
摘要 :检测通了之后,经常卡在相机:OpenCV 打不开、能开却黑屏、CSI 换载板就没图。本文写 Jetson Orin 上 USB / CSI 的确认步骤、采集脚本、格式与带宽、设备号漂移、掉线重连、容器透传,以及 CSI 和 USB 不同的排障路径。适合已经能用文件跑检测、要把实时画面接进链路的人。命令和插件名随 JetPack / 载板会变,以你板上实际为准。
1. 范围
目标:板上能稳定拿到相机帧。
先不扯 RTSP 调度、DeepStream 拓扑、模型精度。相机挂了和 TensorRT 挂了混在一起查,半天都在猜。顺序上,文件链路通了再上 USB;USB 能连续存图了,再碰 CSI。
现场三种高频症状,后文都会拆开写:
| 症状 | 常见落点 |
|---|---|
| 打不开 | 权限、占用、节点选错、容器没映射设备 |
| 能开但黑/花 | 曝光、像素格式、开到 metadata 节点 |
| 跑一会儿挂 | 供电、线材、带宽、USB 控制器复位 |

图1. 采集和推理分开验收,排障才分得清层。
2. USB 和 CSI
| 类型 | 常见接口 | 优点 | 麻烦点 |
|---|---|---|---|
| USB 相机 | USB-A / USB-C | 好买、好换、排障相对直观 | 带宽、供电、UVC 兼容、节点编号会漂 |
| CSI 相机 | 载板 MIPI CSI | 量产常见、延迟和集成更好 | 绑载板、设备树、驱动、排线方向 |
| 场景 | 更合适 | 原因 |
|---|---|---|
| 预研、演示、临时验证 | USB | 换相机成本低,问题多半在软件侧 |
| 结构件固定、产线安装 | CSI | 机械固定、线束短、延迟更可控 |
| 多路高分辨率同时采 | 看载板和带宽预算 | USB 口抢带宽;CSI 看 lane 和驱动 |
Orin 模组有 CSI 能力,不等于你那块载板开了哪一路、要不要改 device tree。宣传页上的"支持多路相机",落到手上永远是:载板丝印 + BSP + 传感器型号 三件套。USB(UVC)和走 ISP 的 CSI,在 NVIDIA 文档里对应的用户态路径也不一样,可对照 Camera Software Development Solution。
3. 工具、权限、板况
bash
sudo apt-get update
sudo apt-get install -y v4l-utils
v4l-utils 提供 v4l2-ctl。有桌面可用 guvcview;没桌面就用 v4l2-ctl、GStreamer 或下文脚本把帧落到磁盘。
bash
groups | rg video
id
ls -l /dev/video0
不在 video 组就加,然后重新登录(或重开 SSH 会话):
bash
sudo usermod -aG video $USER
ls -l /dev/video0 一般能看到属组是 video。当前会话还是旧 groups 的话,表现就是"同一块板,root 能开、普通用户不能开"。
记板况,后面换镜像、换同事接手都用得上:
bash
cat /etc/nv_tegra_release
uname -a
lsusb
ls -l /dev/video*
sudo nvpmodel -q
| 信息 | 干什么用 |
|---|---|
| L4T / JetPack | CSI 驱动、Argus、插件是否对版 |
lsusb |
USB 相机有没有被枚举 |
/dev/video*(V4L2) |
节点有几个、会不会漂 |
| 功耗档 | 采集+推理一起跑时别拿默认档对标 |
USB 先看 lsusb;CSI 主要看载板和 dmesg,lsusb 帮不了多少。
4. USB:枚举、格式、抓帧
bash
ls /dev/video*
v4l2-ctl --list-devices
一个物理摄像头常对应多个 /dev/videoN。有的出图,有的是 metadata。别默认 video0。
bash
v4l2-ctl -d /dev/video0 --all
v4l2-ctl -d /dev/video0 --list-formats-ext
--all 能看到当前格式、分辨率、部分控件;--list-formats-ext 看支持的格式组合。重点看:
- 有没有
MJPG/YUYV(有的还有H264) - 每个格式下的宽高和帧率档位
- 你后面推理要用的尺寸,相机能不能直接出,还是必须软件缩放
bash
v4l2-ctl -d /dev/video0 \
--set-fmt-video=width=640,height=480,pixelformat=MJPG \
--stream-mmap \
--stream-count=1 \
--stream-to=frame.jpg
file frame.jpg
file 看是不是真 JPEG。这一步失败,先别写 OpenCV VideoCapture 业务代码------问题还在驱动、权限、格式或供电。
也可以用 GStreamer 做对照(不经过 OpenCV):
bash
gst-launch-1.0 v4l2src device=/dev/video0 ! \
image/jpeg,width=640,height=480,framerate=30/1 ! \
jpegdec ! videoconvert ! jpegenc ! filesink location=gst_frame.jpg
OpenCV 失败、GStreamer 成功时,优先查 OpenCV 后端(要不要强制 CAP_V4L2)、FOURCC 设置顺序,而不是先重装系统。
5. USB 采集脚本
工作目录:
bash
mkdir -p ~/orin-cam/{cam_out,cam_live}
cd ~/orin-cam
# 保存下文 usb_cam_probe.py / usb_cam_reconnect.py
| 文件 | 作用 |
|---|---|
usb_cam_probe.py |
打开相机、设分辨率、连续存帧 |
usb_cam_reconnect.py |
读帧失败后自动重开 |
保存为 usb_cam_probe.py:
python
#!/usr/bin/env python3
"""Orin 上 USB 相机最小探针:打开、设分辨率、保存若干帧。
用法:
python3 usb_cam_probe.py --device 0 --width 640 --height 480 --frames 30
python3 usb_cam_probe.py --device /dev/video0 --save-dir ./cam_out --show
"""
from __future__ import annotations
import argparse
import time
from pathlib import Path
import cv2
def open_cam(device: str, width: int, height: int, fps: int):
if device.isdigit():
cap = cv2.VideoCapture(int(device), cv2.CAP_V4L2)
else:
cap = cv2.VideoCapture(device, cv2.CAP_V4L2)
if not cap.isOpened():
raise RuntimeError(f"打不开: {device}")
# 先设后端和格式,再设分辨率;顺序反了有的相机会 silently ignore
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
if fps > 0:
cap.set(cv2.CAP_PROP_FPS, fps)
actual_w = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
actual_h = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
actual_fps = cap.get(cv2.CAP_PROP_FPS)
print(f"opened {device}: ask={width}x{height}@{fps}, got={actual_w}x{actual_h}@{actual_fps:.1f}")
return cap
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--device", default="0")
ap.add_argument("--width", type=int, default=640)
ap.add_argument("--height", type=int, default=480)
ap.add_argument("--fps", type=int, default=30)
ap.add_argument("--frames", type=int, default=30)
ap.add_argument("--save-dir", default="cam_out")
ap.add_argument("--show", action="store_true")
args = ap.parse_args()
out_dir = Path(args.save_dir)
out_dir.mkdir(parents=True, exist_ok=True)
cap = open_cam(args.device, args.width, args.height, args.fps)
ok_count = 0
t0 = time.perf_counter()
for i in range(args.frames):
ok, frame = cap.read()
if not ok:
print(f"frame {i}: read failed")
continue
ok_count += 1
cv2.imwrite(str(out_dir / f"frame_{i:04d}.jpg"), frame)
if args.show:
cv2.imshow("usb-cam", frame)
if cv2.waitKey(1) & 0xFF == ord("q"):
break
dt = time.perf_counter() - t0
cap.release()
if args.show:
cv2.destroyAllWindows()
print(f"saved {ok_count}/{args.frames} frames to {out_dir}, elapsed {dt:.2f}s")
if ok_count == 0:
raise SystemExit(1)
if __name__ == "__main__":
main()
bash
python3 usb_cam_probe.py --device 0 --width 640 --height 480 --frames 30
ls cam_out | head
打印里的 ask= 和 got= 要对一下:很多相机对不支持的分辨率会静默回落到别的档,脚本以为设成功了,其实一直在采 640×480 或更奇怪的尺寸。
有桌面加 --show;纯 SSH 只存文件。cam_out/ 里应是正常画面,不是全黑、绿屏、条纹。
想粗测采集侧帧率,可以把 --frames 提到 300,看脚本打印的 elapsed,再和相机标称帧率比。差很多时,先查 USB 口是 2.0 还是 3.x、是不是 YUYV 把带宽打满。
6. USB 常见坑
| 现象 | 多见原因 | 先查什么 |
|---|---|---|
ls /dev/video* 没有 |
没枚举、线/口、供电 | lsusb、换口换线、供电 |
| 有节点但 OpenCV 打不开 | 不在 video 组、被占用 |
groups、fuser |
| 能开但全黑 | 曝光、格式、开错节点 | 换节点、MJPG/YUYV、曝光控件 |
| 分辨率设了不生效 | 不支持、设置顺序不对 | --list-formats-ext、看 got= |
| 跑一会儿掉线 | 供电不稳、线差、口供电不足 | 短线、带供电的 Hub、换口 |
| 重启后设备号变了 | 多个 UVC | udev 或按名称选 |
| 容器里打不开 | 没映射设备 | --device、--group-add video |
| 画面卡、实际 FPS 很低 | YUYV+高分辨率、USB2 | 改 MJPG、降分辨率 |
bash
sudo fuser /dev/video0
被占用就停掉。远程桌面、浏览器、之前没杀掉的 guvcview / Python 进程都很常见。
6.1 设备号漂移
今天 video0,多插一个 UVC 就变成 video2。业务脚本别写死 0。
bash
v4l2-ctl --list-devices
量产可以用 udev 固定名。先查相机的 idVendor / idProduct:
bash
lsusb
udevadm info -a -n /dev/video0 | rg -i 'idVendor|idProduct|serial|ATTRS'
示例规则(按你的 vid/pid 改),保存为 /etc/udev/rules.d/99-orin-usbcam.rules:
bash
# 示例:把指定 UVC 的捕获节点链成 /dev/orin_cam0
SUBSYSTEM=="video4linux", ATTRS{idVendor}=="046d", ATTRS{idProduct}=="0825", \
ATTR{index}=="0", SYMLINK+="orin_cam0", GROUP="video", MODE="0660"
bash
sudo udevadm control --reload-rules
sudo udevadm trigger
ls -l /dev/orin_cam0
脚本和 Docker 都用 /dev/orin_cam0,少受插拔顺序影响。ATTR{index}=="0" 用来避开 metadata 节点,具体以 udevadm info 为准。
6.2 带宽和格式
同一分辨率下,YUYV 未压缩,MJPG 压缩后更省 USB 带宽。1080p@30 走 YUYV,在 USB2 口上很容易卡或直接设失败。
| 格式 | 特点 | 什么时候用 |
|---|---|---|
| MJPG | 省带宽,解码吃一点 CPU | USB 口紧张、分辨率较高时优先试 |
| YUYV | 无压缩,带宽大 | 短距离 USB3、分辨率不高、要省解码时 |
| H264(若支持) | 相机端编码 | 要传较远或录流时,链路更复杂 |
FPS 低时先把采集格式和口速查清,别一上来怪 TensorRT。
6.3 曝光和自动档
有的模组默认曝光极低,室内像黑屏;自动曝光狂抖时,后面检测框也会晃。
bash
v4l2-ctl -d /dev/video0 --list-ctrls
bash
# 控件名因相机而异
v4l2-ctl -d /dev/video0 --set-ctrl=auto_exposure=1
v4l2-ctl -d /dev/video0 --set-ctrl=exposure_time_absolute=300
黑屏时先存一张原图:直方图贴边、几乎全 0,多半是曝光;花屏、颜色通道错乱,再怀疑格式和转换。
6.4 多个 video 节点
v4l2-ctl --list-devices 里同一摄像头下挂 video0、video1 时,开错节点会出现:能 open 但一直 read 失败,或帧内容异常。两个节点各跑一遍探针,看谁出正常图。
6.5 供电和口
Orin DevKit / 载板上的 USB 口供电能力不一样。工业相机、长线、劣质 Hub 叠在一起时,表现是:刚插上能枚举,采几分钟 lsusb 设备消失,或 dmesg 里出现 reset。
bash
dmesg | rg -i 'usb|uvcvideo|disconnect|reset' | tail -n 40
优先换短线、换口、去掉无供电 Hub。板子供电本身不稳时,还会连带文件系统和随机重启,那时先查电源适配器,别只盯相机驱动。
7. CSI:和 USB 不是同一套排障
CSI 依赖:载板接口定义、设备树/BSP、传感器驱动是否在当前镜像、排线方向与供电/复位。顺序大致是:
- 载板文档是否写明支持该传感器
- 当前 JetPack / 镜像有没有对应驱动
- 物理连接和 CSI 路数(丝印 CAM0/CAM1)
- 再用厂商推荐的 GStreamer / Argus 路径验证
插件名和 pipeline 随 JetPack 会变,以载板文档为准。CSI + ISP 场景优先看 Libargus 与文档里的 nvarguscamerasrc 路径,不要默认等同于 USB 那套 V4L2 打开方式。
bash
ls /dev/video*
dmesg | rg -i 'imx|ov56|camera|vi|nvcsi|argus|i2c' | tail -n 80
dmesg 里 I2C NACK、probe fail,先查排线、座子、供电和设备树,别先改 Python。
有 Argus 时可以这样试(示意,sensor-id / 分辨率按文档改):
bash
gst-launch-1.0 nvarguscamerasrc sensor-id=0 ! \
'video/x-raw(memory:NVMM),width=1280,height=720,framerate=30/1' ! \
nvvidconv ! 'video/x-raw,format=BGRx' ! \
videoconvert ! jpegenc ! filesink location=csi_test.jpg
| 现象 | 多见原因 | 先查什么 |
|---|---|---|
| Argus 直接报错退出 | 驱动/设备树不匹配、sensor-id 错 | 载板文档、dmesg |
有 /dev/video* 但 OpenCV 打不开 |
CSI 应用路径本就不走这个节点 | 改用文档里的 GStreamer |
| 一路有图一路没有 | 线序、座子、只开了一路电源 | 换 CAM 口、查供电 |
| 换 JetPack 后挂了 | BSP 未跟上 | 回退或找载厂对应镜像 |
别把 USB 经验硬套成"换个 /dev/video0"。不少 CSI 方案稳定路径是 nvarguscamerasrc 一类插件,不是任意 V4L 节点。
换载板或传感器时记一页,比收藏一条网上命令有用:
| 记录项 | 例子 |
|---|---|
| 载板型号 | 某 Orin NX 载板 |
| 传感器型号 | IMX 某型号 |
| CSI 路数 / 丝印 | CAM0 |
| JetPack / L4T | 某版本 |
| 验证 pipeline | nvarguscamerasrc ... |
| 稳定分辨率 / 帧率 | 1280×720@30 |
| 已知限制 | 某模式花屏、某线材不稳 |

图2. CSI:传感器 → 载板 → 设备树/驱动 → 用户态取流,缺一层都会"没图"。
8. 接到检测和容器
USB 出图稳定后,再改检测入口。例如沿用检测闭环里的脚本:
bash
python3 detect_camera.py --engine yolov8n_fp16.engine --device 0
# 或
python3 detect_camera.py --engine yolov8n_fp16.engine --device /dev/orin_cam0
| 点 | 说明 |
|---|---|
| 分辨率 | 相机 1080p、模型 640,中间必有缩放;letterbox 要和训练一致 |
| 预览 | imshow 吃 CPU;SSH 无桌面会直接失败,先存帧 |
| 计时 | 端到端 FPS 含采集,和纯推理不要混着报 |
| 缓冲区 | OpenCV 默认可能积压旧帧,实时场景要考虑丢中间帧 |
容器映射示例(--device):
bash
docker run --rm \
--runtime nvidia \
--device /dev/orin_cam0:/dev/orin_cam0 \
--group-add video \
-v $PWD/models:/models \
-v $PWD/output:/output \
your-image ...
--privileged 能省事,量产别当默认。映射后先在容器里确认节点存在:
bash
docker run --rm \
--device /dev/orin_cam0:/dev/orin_cam0 \
--group-add video \
ubuntu:22.04 ls -l /dev/orin_cam0
宿主机能采、容器不能采,按这个查:--device 是否映射对、容器内有没有节点、组权限、重启后设备号是否又漂了。用 udev 固定名再映射,比写死 video0 稳。
9. 掉线重连
USB 常见:拔插、供电抖、跑久了 read 失败。进程直接退出的话,现场就得人上去重启。
保存为 usb_cam_reconnect.py:
python
#!/usr/bin/env python3
"""USB 相机读帧失败时自动重开。
用法:
python3 usb_cam_reconnect.py --device 0 --save-dir ./cam_live --max-fail 30
"""
from __future__ import annotations
import argparse
import time
from pathlib import Path
import cv2
def open_cam(device: str, width: int, height: int):
cap = cv2.VideoCapture(int(device) if device.isdigit() else device, cv2.CAP_V4L2)
if not cap.isOpened():
return None
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(*"MJPG"))
cap.set(cv2.CAP_PROP_FRAME_WIDTH, width)
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, height)
return cap
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--device", default="0")
ap.add_argument("--width", type=int, default=640)
ap.add_argument("--height", type=int, default=480)
ap.add_argument("--save-dir", default="cam_live")
ap.add_argument("--every", type=int, default=30, help="每 N 帧存一张")
ap.add_argument("--max-fail", type=int, default=30)
args = ap.parse_args()
out_dir = Path(args.save_dir)
out_dir.mkdir(parents=True, exist_ok=True)
fail = 0
idx = 0
cap = open_cam(args.device, args.width, args.height)
if cap is None:
raise RuntimeError("首次打开失败")
print("running, Ctrl+C to stop")
try:
while True:
if cap is None:
time.sleep(1.0)
cap = open_cam(args.device, args.width, args.height)
if cap is None:
print("reopen failed, retry...")
continue
fail = 0
print("reopened camera")
ok, frame = cap.read()
if not ok:
fail += 1
print(f"read fail {fail}/{args.max_fail}")
if fail >= args.max_fail:
cap.release()
cap = None
time.sleep(0.05)
continue
fail = 0
idx += 1
if idx % args.every == 0:
path = out_dir / f"live_{idx:06d}.jpg"
cv2.imwrite(str(path), frame)
print(f"saved {path.name}")
except KeyboardInterrupt:
print("stop")
finally:
if cap is not None:
cap.release()
if __name__ == "__main__":
main()
bash
python3 usb_cam_reconnect.py --device /dev/orin_cam0 --save-dir ./cam_live --every 30
量产再加:最大重连次数、告警、systemd / 看门狗把进程拉起。重连成功后最好再读几帧丢弃,避免拿到半截缓冲。
10. 采集和推理分开验收
| 阶段 | 通过标准 | 失败时先别动 |
|---|---|---|
| 设备枚举 | lsusb / v4l2-ctl 能看到 |
模型、DeepStream |
| 单帧保存 | 磁盘上有正常图片 | 推理代码 |
| 连续读帧 | 约 30~60 秒不掉线 | 后处理、画框 |
| 冷启动 | 重启后再采一次仍正常 | 临时热插拔状态 |
| 接入检测 | 框大致合理 | 相机驱动 |
| 容器 | 映射和权限正确 | 模型结构 |
不少"昨天还好"其实是:占用进程、加组后没重登、设备号变了、或昨天用的是 root。验收时按上表走,比上来就改模型快。
11. 术语对照
| 说法 | 含义 |
|---|---|
| UVC | USB Video Class,多数 USB 摄像头走这套 |
| V4L2 | Linux 视频子系统;/dev/video*、v4l2-ctl |
| FOURCC | 像素/压缩格式四字符码,如 MJPG、YUYV |
| Argus / Libargus | Jetson 上常见的相机用户态栈之一 |
nvarguscamerasrc |
GStreamer 取 CSI/Argus 流的常见插件 |
| metadata 节点 | 不出预览图的附属 /dev/video* |
| 设备树 / DT | 描述板上有哪些传感器、接在哪路 CSI |
12. 延伸阅读
排障时官方文档比零散博客稳,版本要以你板上的 L4T / JetPack 为准:
- Jetson Orin 产品页
- JetPack 下载与版本说明
- Camera Software Development Solution(USB/V4L2 与 CSI/Argus 路径怎么选)
- Libargus Camera API
- V4L2 用户态文档
- v4l-utils
- GStreamer 文档
- OpenCV VideoCapture
- udev
- Docker
run --device - 后续若上多路视频链,可再看 DeepStream
13. 小结
USB 侧优先查权限、占用、格式、带宽、供电和设备号;CSI 侧优先查载板文档、设备树、驱动和排线。采集能单独稳住,再接检测和容器,问题才不会全部堆到模型上。