Orin 上 USB / CSI 相机采集踩坑

文章目录

    • [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-ctlGStreamer 或下文脚本把帧落到磁盘。

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 主要看载板和 dmesglsusb 帮不了多少。


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 组、被占用 groupsfuser
能开但全黑 曝光、格式、开错节点 换节点、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 里同一摄像头下挂 video0video1 时,开错节点会出现:能 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、传感器驱动是否在当前镜像、排线方向与供电/复位。顺序大致是:

  1. 载板文档是否写明支持该传感器
  2. 当前 JetPack / 镜像有没有对应驱动
  3. 物理连接和 CSI 路数(丝印 CAM0/CAM1)
  4. 再用厂商推荐的 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 为准:


13. 小结

USB 侧优先查权限、占用、格式、带宽、供电和设备号;CSI 侧优先查载板文档、设备树、驱动和排线。采集能单独稳住,再接检测和容器,问题才不会全部堆到模型上。

相关推荐
武子康1 小时前
Pi vs Claude Code vs Codex 正确读法:6 组同模型匹配 + 2.08×/1.46×/1.20×/1.54×/1.22×/1.44× 成
人工智能·ai编程·claude
renhongxia11 小时前
AI安全保卫战:我们如何防止“失控”的智能体?
人工智能·深度学习·安全·机器学习·架构·机器人
江边风声1 小时前
PCB自动化收放板机 在薄板制程中的特殊取放策略——0.05mm芯板怎么稳抓不掉
人工智能·科技·自动化·制造·pcb工艺
lupai1 小时前
汽车维保记录精准版 API 快速接入指南
大数据·人工智能·汽车
橙臣程1 小时前
深度学习4——损失计算
人工智能
王同学的AI学习日记1 小时前
Qwen-Image-3.0技术实测:文字渲染、长上下文与多场景能力深度评测
大数据·人工智能·ai·开源
wuhanzhanhui1 小时前
质变的枢纽:2026 武汉国际储能产业博览会开启绿色能源的新周期
人工智能
后端小肥肠2 小时前
Doubao-Seed-Evolving实测:我做了个情绪价值拉满的星座记账 Skill
人工智能·aigc·agent
Molesidy2 小时前
【AI】【OpenCode】基于AI-Agent的工作方式的初步测试
人工智能