1. 从模型跑通到画面输入:这一步到底在解决什么
前面几篇我们把 YOLOv5s 在香橙派 RK3588 上从环境搭建、模型转换一路推到了能跑通推理。但如果你只拿一张固定的测试图反复跑,其实感受不到真实项目里的复杂度——因为实际场景里,输入永远是动态的、不可控的。摄像头这一环接不上,整个链路就只是个"演示",而不是"系统"。
这篇要干的事很具体:在香橙派 RK3588 上,用 OpenCV 打开一路摄像头,抓取一帧图像,喂给已经跑通的 YOLOv5 推理流程,拿到检测结果。听起来就三步,但真正动手你会发现,坑几乎全集中在"摄像头怎么打开"和"抓到的帧怎么和模型输入对齐"这两件事上。
为什么单独拿一篇讲摄像头?因为这是从"模型能跑"到"系统能用"的分水岭。模型推理本身是纯计算,输入是内存里的一块张量;而摄像头涉及设备节点、驱动、像素格式、色彩空间、分辨率协商、帧缓冲管理这一整套东西。很多人在 RK3588 上模型跑得飞起,一接摄像头就卡住,问题往往不在模型,而在采集端。
这篇文章适合两类人:一类是刚把 YOLOv5 在 RK3588 上跑通、准备接真实输入的新手;另一类是接过摄像头但总遇到"打不开""花屏""帧率上不去""检测框位置偏"的老手。我会把 OpenCV 采集、V4L2 设备节点、MIPI 与 USB 摄像头的差异、帧格式转换、以及和 YOLOv5 输入对齐这几块讲透,并且给出可以直接抄的命令和代码。
先明确一个前提:本文假设你已经完成了前面 RK3588 上 YOLOv5s 的部署,模型能对静态图输出正确结果。如果你还没到这一步,建议先回去把推理链路跑通,否则接上摄像头也只是多一个变量,排查起来更乱。
2. 香橙派 RK3588 上摄像头选型:MIPI 还是 USB,先想清楚
2.1 两种接口的本质差异
在香橙派 5 / 5 Pro 这类 RK3588 板子上,摄像头接入主要有两条路:MIPI CSI和USB。这两者不是"哪个更好"的问题,而是"哪个更适合你的场景"。
MIPI CSI 是板子原生的摄像头接口,走的是 MIPI 协议,直接连到 RK3588 的 ISP(图像信号处理器)。它的优势是带宽高、延迟低、CPU 占用小,因为图像数据从传感器出来经过 ISP 处理后,可以直接进内存,不需要 CPU 大量参与搬运。缺点是兼容性挑传感器,常见的 OV5647、OV2640、IMX 系列模组需要驱动支持,排线也娇气,插拔几次就容易接触不良。
USB 摄像头走的是 UVC(USB Video Class)标准协议,插上就能用,系统里会出现/dev/video*设备节点。它的优势是即插即用、型号随便换,缺点是带宽受 USB 限制、延迟相对高、CPU 占用大,因为图像数据要经过 USB 控制器搬运,格式转换也常落在 CPU 上。
我个人的经验是:做原型验证、快速迭代,优先 USB 摄像头;做产品、对延迟和功耗敏感,再上 MIPI。因为 USB 摄像头能让你把精力集中在模型和业务逻辑上,而不是和驱动较劲。
2.2 设备节点是怎么冒出来的
不管哪种接口,Linux 下摄像头最终都会以 V4L2(Video for Linux 2)设备节点的形式出现,通常是/dev/video0、/dev/video1这样。但这里有个特别容易踩的坑:一个物理摄像头可能对应多个 video 节点。
比如某些 USB 摄像头,/dev/video0是实际采集用的,/dev/video1是元数据节点(metadata),你打开/dev/video1是抓不到图像的。MIPI 摄像头在 RK3588 上更复杂,因为 ISP 的存在,可能一个 sensor 对应好几个节点,分别对应不同的处理通路。
所以第一步永远是先确认哪个节点是真正能出图的。用这条命令看设备列表:
v4l2-ctl --list-devices输出会按设备分组,比如:
USB Camera: USB Camera (usb-xhci-hcd.0-1): /dev/video0 /dev/video1再对具体节点查能力:
v4l2-ctl -d /dev/video0 --list-formats-ext这条命令会列出该节点支持的像素格式(如 YUYV、MJPG、NV12)和每种格式支持的分辨率、帧率。这一步非常关键,因为后面 OpenCV 打开摄像头时,如果请求的格式或分辨率不被支持,它会默默回退到默认值,导致你以为设了 1080p 实际拿到的是 640x480。
2.3 一个选型对照表
| 维度 | MIPI CSI 摄像头 | USB UVC 摄像头 |
|---|---|---|
| 接入难度 | 需要驱动和排线,挑型号 | 即插即用 |
| 延迟 | 低 | 中等偏高 |
| CPU 占用 | 低(ISP 硬件处理) | 高(软件转换多) |
| 带宽 | 高 | 受 USB 限制 |
| 适合场景 | 产品化、低延迟 | 原型、快速验证 |
| 常见型号 | OV5647、OV2640、IMX219 | 各类免驱 USB 摄像头 |
提示:香橙派官方系统里,MIPI 摄像头的启用往往需要在设备树或配置里打开对应 overlay,不是插上就有。动手前先查你所用镜像的文档,确认该型号是否被支持。
3. 用 OpenCV 打开摄像头:为什么 cv2.VideoCapture 经常"打不开"
3.1 最朴素的写法与它的陷阱
大多数人第一反应是这样写:
import cv2 cap = cv2.VideoCapture(0) ret, frame = cap.read() if ret: cv2.imwrite("frame.jpg", frame) cap.release()在 PC 上这行得通,但在 RK3588 上,cv2.VideoCapture(0)里的0到底指哪个设备,取决于 OpenCV 编译时用的后端。OpenCV 在 Linux 上默认走 V4L2 后端,0通常映射到/dev/video0。但正如上一节说的,/dev/video0不一定是能出图的那个。
更稳的写法是直接指定设备路径:
cap = cv2.VideoCapture("/dev/video0", cv2.CAP_V4L2)显式指定cv2.CAP_V4L2后端,避免 OpenCV 去尝试其他后端(比如 GStreamer)导致行为不一致。这一点在嵌入式板子上尤其重要,因为不同镜像里 OpenCV 编译选项不同,默认后端可能不一样。
3.2 打不开的几种典型原因
我踩过的"打不开"大致分这几类,按排查顺序列一下:
- 设备节点不对:
/dev/video0是元数据节点,真正采集的是/dev/video1。用v4l2-ctl --list-devices确认。 - 权限不足:普通用户没有访问
/dev/video*的权限。临时用sudo验证,长期方案是把用户加入video组:sudo usermod -aG video $USER,然后重新登录。 - 设备被占用:另一个进程(比如之前没退出的脚本)还占着摄像头。用
fuser /dev/video0或lsof /dev/video0查。 - 格式协商失败:请求的像素格式或分辨率不被支持,OpenCV 返回
False。这时候要回到--list-formats-ext看支持列表。 - MIPI 摄像头未启用:设备树 overlay 没开,节点根本不存在。
3.3 一个健壮的打开函数
把上面的经验固化成一个函数,打开后立刻校验分辨率和格式是否真的生效:
import cv2 def open_camera(dev="/dev/video0", width=640, height=480, fps=30): cap = cv2.VideoCapture(dev, cv2.CAP_V4L2) if not cap.isOpened(): raise RuntimeError(f"无法打开摄像头 {dev}") # 设置 MJPG 格式,很多 USB 摄像头只有 MJPG 才能上高分辨率高帧率 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) cap.set(cv2.CAP_PROP_FPS, fps) # 回读实际生效的参数 real_w = cap.get(cv2.CAP_PROP_FRAME_WIDTH) real_h = cap.get(cv2.CAP_PROP_FRAME_HEIGHT) real_fps = cap.get(cv2.CAP_PROP_FPS) print(f"实际分辨率: {real_w}x{real_h}, 帧率: {real_fps}") return cap这里有个关键点:cap.set是"请求"不是"命令"。摄像头可能不支持你请求的参数,它会回退到最接近的可用值。所以必须用cap.get回读确认。我见过太多人设了 1920x1080 结果实际是 640x480,然后纳闷为什么检测框位置全偏了。
注意:
cv2.CAP_PROP_FOURCC设置 MJPG 对 USB 摄像头很关键。YUYV 格式在 USB 2.0 带宽下,1080p 基本只能跑到个位数帧率,而 MJPG 是压缩格式,带宽占用小得多。代价是解码要花一点 CPU,但在 RK3588 上完全扛得住。
4. 抓一帧之后:从 BGR 到模型输入的完整对齐
4.1 OpenCV 抓到的帧长什么样
cap.read()返回的frame是一个 NumPy 数组,形状是(H, W, 3),通道顺序是BGR(不是 RGB)。这一点必须记牢,因为 YOLOv5 训练和推理时用的是 RGB。如果你直接把 BGR 帧喂进去,颜色通道反了,检测精度会掉,尤其是对颜色敏感的目标。
另外,OpenCV 抓到的帧是uint8类型,值域 0-255。而 YOLOv5 输入需要的是归一化到 0-1 的浮点张量,形状是(1, 3, 640, 640)(NCHW 格式)。所以中间要做几步转换。
4.2 预处理的标准流程
假设你的 YOLOv5 输入是 640x640,预处理大致是这样:
import cv2 import numpy as np def preprocess(frame, input_size=640): # 1. BGR -> RGB img = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # 2. letterbox 缩放,保持宽高比,短边补灰边 h, w = img.shape[:2] scale = min(input_size / w, input_size / h) new_w, new_h = int(w * scale), int(h * scale) resized = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LINEAR) canvas = np.full((input_size, input_size, 3), 114, dtype=np.uint8) top = (input_size - new_h) // 2 left = (input_size - new_w) // 2 canvas[top:top + new_h, left:left + new_w] = resized # 3. 归一化 + HWC -> CHW + 加 batch 维 blob = canvas.astype(np.float32) / 255.0 blob = np.transpose(blob, (2, 0, 1)) # HWC -> CHW blob = np.expand_dims(blob, axis=0) # 加 batch 维 return blob, scale, (left, top)这里有几个细节值得展开说。
为什么用 letterbox 而不是直接 resize?直接 resize 会改变宽高比,导致目标变形。YOLOv5 训练时用的是 letterbox,推理时也必须一致,否则模型看到的物体比例和训练时不同,精度会下降。letterbox 的做法是等比缩放后,在短边补灰边(值 114,这是 YOLOv5 的默认填充值),凑成正方形。
为什么填充值是 114?这是 YOLOv5 官方实现里的默认值,训练和推理保持一致即可。用 0 也能跑,但和训练分布不一致,理论上会有微小精度损失。既然官方用 114,我们就跟着用。
归一化为什么除以 255?因为 YOLOv5 训练时输入就是 0-1 的浮点。如果你用的是量化后的 RKNN 模型,归一化方式可能不同——有些量化模型要求输入是 uint8 的 0-255,归一化在模型内部做。这一点必须看你模型转换时的配置,不能想当然。
4.3 后处理:把检测框映射回原图
模型输出的是相对于 640x640 letterbox 图的坐标,要映射回原始帧,必须用上一步记录的scale和(left, top):
def scale_boxes(boxes, scale, pad, orig_shape): left, top = pad boxes[:, [0, 2]] = (boxes[:, [0, 2]] - left) / scale boxes[:, [1, 3]] = (boxes[:, [1, 3]] - top) / scale # 裁剪到原图范围内 h, w = orig_shape[:2] boxes[:, [0, 2]] = boxes[:, [0, 2]].clip(0, w) boxes[:, [1, 3]] = boxes[:, [1, 3]].clip(0, h) return boxes这一步是"检测框位置偏"问题的根源。如果你发现框整体偏移或者大小不对,八成是这里没对齐——要么忘了减 padding,要么 scale 用反了(应该除而不是乘)。
4.4 完整的一帧推理流程
把采集、预处理、推理、后处理串起来:
cap = open_camera("/dev/video0", 640, 480, 30) ret, frame = cap.read() if not ret: raise RuntimeError("抓帧失败") blob, scale, pad = preprocess(frame, 640) outputs = model_infer(blob) # 你前面跑通的推理函数 boxes = postprocess(outputs) # 解码出框、置信度、类别 boxes = scale_boxes(boxes, scale, pad, frame.shape) for box in boxes: x1, y1, x2, y2, conf, cls = box cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, f"{int(cls)}:{conf:.2f}", (int(x1), int(y1) - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imwrite("result.jpg", frame) cap.release()跑通这一步,你就完成了从"模型能跑"到"能处理真实画面"的跨越。接下来才是把它变成连续视频流、加多线程、优化帧率这些工程问题。
5. 实测中那些文档不会告诉你的坑
5.1 第一帧经常是坏的
cap.read()拿到的第一帧,很多时候是黑的、花的、或者曝光没稳定。这是摄像头启动后自动曝光、自动白平衡还在收敛。稳妥做法是打开后先丢弃前 5-10 帧:
for _ in range(10): cap.read() ret, frame = cap.read()这个习惯能省掉很多"为什么第一帧检测不到东西"的困惑。
5.2 MJPG 解码的 CPU 开销
前面说 USB 摄像头用 MJPG 能上高帧率,但 MJPG 是压缩格式,每帧都要解码。在 RK3588 上,OpenCV 默认用 CPU 解码,1080p 30fps 的 MJPG 解码会吃掉相当一部分 CPU。如果你的推理本身也吃 CPU,两者会抢资源。
解决办法有两个:一是降低分辨率,640x480 对大多数检测任务够用;二是如果 OpenCV 编译时带了硬件解码支持(比如通过 GStreamer 后端调用 RK3588 的 VPU),可以走硬件解码。RK3588 的 VPU 支持 H.264/H.265 硬解,但 MJPG 硬解支持要看具体实现。实测下来,640x480 的 MJPG 软解在 RK3588 上完全无压力,没必要上 1080p 给自己找麻烦。
5.3 设备节点编号会变
USB 摄像头插拔后,/dev/video*的编号可能变。今天/dev/video0是它,明天插了别的设备就变成/dev/video2了。产品化场景下,应该用/dev/v4l/by-id/下的稳定符号链接:
ls -l /dev/v4l/by-id/这里会列出基于设备序列号的稳定路径,比如usb-XXX-video-index0,用它替代/dev/video0就不会因为插拔顺序变化而错乱。
5.4 多摄像头场景的区分
如果你接了多个摄像头,OpenCV 的VideoCapture(0)、VideoCapture(1)这种索引方式很容易搞混。更可靠的是用/dev/v4l/by-id/的路径分别打开。另外要注意,多个 USB 摄像头同时跑,USB 带宽是共享的,总带宽不够时会出现掉帧、花屏。这时候要么降分辨率,要么把摄像头分散到不同的 USB 控制器上。
5.5 常见问题速查表
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
isOpened()返回 False | 节点错/权限/被占用 | v4l2-ctl --list-devices、lsof |
| 抓到全黑帧 | 曝光未收敛/镜头盖 | 丢弃前几帧、检查镜头 |
| 分辨率不是设定值 | 格式不支持被回退 | --list-formats-ext回读确认 |
| 检测框位置偏 | letterbox 参数没对齐 | 检查 scale 和 padding |
| 颜色异常 | BGR/RGB 搞反 | 确认 cvtColor |
| 帧率低 | 格式/带宽/CPU 瓶颈 | 换 MJPG、降分辨率 |
6. 从单帧到连续流:下一步的工程化思路
单帧跑通只是起点。真实系统里你要处理的是连续视频流,这里简单提几个方向,方便你后续扩展。
多线程采集与推理分离。采集和推理放同一个线程里,推理一慢,采集就卡,摄像头缓冲区会堆积旧帧,导致画面延迟越来越大。标准做法是一个线程专门read(),把帧放进队列;另一个线程从队列取帧做推理。队列要设上限,满了就丢最旧的帧,保证处理的是最新画面。
帧率与推理耗时的匹配。如果推理一帧要 100ms,那实际处理帧率就是 10fps,采集 30fps 也没用,中间 20 帧都浪费了。这时候要么降采集帧率到 10fps 省资源,要么跳帧处理。RK3588 上 YOLOv5s 用 NPU 推理,640x640 输入大概能到几十毫秒一帧,具体看你模型转换和量化的情况。
分辨率的选择。采集分辨率不一定要等于模型输入分辨率。比如采集 1280x720,预处理时 letterbox 到 640x640,这样视野更广,小目标也能保留更多细节。但采集分辨率越高,传输和缩放开销越大,需要权衡。
长时间运行的稳定性。摄像头连续跑几个小时,偶尔会出现 USB 掉线、驱动异常。健壮的代码要能检测到read()失败并尝试重新打开设备,而不是直接崩溃。这个重连逻辑在产品里是必须的。
我个人在 RK3588 上做这类项目,习惯先把单帧链路彻底跑通、把每个中间结果都存图确认,再上连续流。因为连续流一旦出问题,变量太多,很难定位是采集、预处理、推理还是后处理的问题。单帧跑通相当于给每个环节都建立了"基准真相",后面排查就有参照了。
另外提一句,如果你后面要换 YOLOv8 或者别的模型,摄像头采集和预处理这部分逻辑基本可以复用,变的只是模型输入尺寸和后处理解码。所以把这一层写成一个独立模块,接口固定为"输入设备路径,输出预处理好的张量和还原参数",后面换模型时改动最小。