1. 从模型推理到真实画面为什么这一步是分水岭前面几篇我们把 YOLOv5s 在香橙派 RK3588 上跑通了模型能加载、能推理、能输出检测框但用的都是本地图片或者构造的假数据。说实话这种跑通只能算“环境验证”离真正能用的东西还差一步——让模型看到真实世界的画面。这一步就是把摄像头接进来抓一帧送进推理管线拿到检测结果。为什么说这一步是分水岭因为一旦摄像头通了你后面能做的事情就完全不一样了可以做实时目标检测、可以做视频流分析、可以做边缘端的智能监控原型。而且摄像头这条链路涉及的东西比纯推理多得多——V4L2 设备节点、MIPI CSI 或 USB 的枚举方式、OpenCV 的采集后端、像素格式转换、分辨率与帧率的权衡每一个环节都可能让你卡住。这篇内容适合谁看如果你已经跟着前面的教程把 YOLOv5s 在 RK3588 上跑起来了现在想接摄像头做真实推理那这篇就是给你写的。如果你还没跑通模型推理建议先回去把前面的环境搭好不然接上摄像头你也验证不了结果。另外如果你用的是香橙派 5 或者其他 RK3588 开发板操作逻辑基本一致只是设备节点名称和摄像头模组可能有差异。我实测用的是香橙派 5RK3588系统是 Ubuntu 20.04摄像头用的是 USB 免驱摄像头和 MIPI CSI 的 OV5647 模组各试了一遍。两种方式各有坑后面会分别讲。OpenCV 用的是系统自带的 Python 版本YOLOv5 是官方仓库的 v7.0 分支推理后端走的是 RKNN。提示这篇的核心目标是“抓一帧并推理”不是做实时视频流。先把单帧链路打通再扩展到连续帧会稳很多。很多人一上来就搞实时流结果采集、推理、显示三个环节互相干扰出了问题根本不知道是哪一层的事。2. 摄像头接入方案选型USB 还是 MIPI CSI2.1 两种接口的本质差异RK3588 支持多种摄像头输入方式最常见的就是 USB 摄像头和 MIPI CSI 摄像头。这两者在硬件层面完全不同软件层面的枚举方式和设备节点也不一样。USB 摄像头走的是 USB 协议插上之后内核通过 UVCUSB Video Class驱动自动识别通常不需要额外配置。设备节点一般是/dev/video*具体是哪个号取决于你插了几个摄像头以及系统启动时的枚举顺序。优点是即插即用兼容性好随便拿一个几十块的 USB 摄像头就能用。缺点是带宽受 USB 限制高分辨率下帧率上不去而且延迟相对较大。MIPI CSI 摄像头走的是 MIPI 接口直接连到 RK3588 的 ISP 或 VICAP 控制器上。这种方式的带宽高、延迟低适合高分辨率高帧率场景。但问题是驱动配置复杂不同模组的设备树DTS配置不一样内核里要有对应的 sensor 驱动。OV5647 是比较常见的 MIPI CSI 模组树莓派上用得很多但在 RK3588 上需要确认内核是否已经使能了对应的驱动。我个人的建议是如果你只是想快速验证推理链路先用 USB 摄像头。等链路通了再换成 MIPI CSI 做性能优化。这样排错成本最低。2.2 设备节点确认与权限处理不管用哪种摄像头第一步都是确认设备节点。插上 USB 摄像头后执行ls /dev/video*你会看到类似/dev/video0、/dev/video1这样的节点。有些 USB 摄像头会枚举出两个节点一个是视频采集节点一个是元数据节点。通常video0是采集节点但也不绝对。用v4l2-ctl可以查看设备能力v4l2-ctl --device/dev/video0 --info输出里会显示Device Caps如果看到Video Capture就说明这个节点支持采集。还可以列出支持的格式v4l2-ctl --device/dev/video0 --list-formats-ext这一步很关键因为不同摄像头支持的像素格式不一样。常见的有YUYV、MJPG、NV12等。OpenCV 采集时如果格式不匹配可能会拿到花屏或者直接失败。权限方面普通用户默认可能没有访问/dev/video*的权限。你可以把自己加到video组sudo usermod -aG video $USER然后重新登录生效。或者临时用sudo跑但长期来看加组更规范。注意如果你用的是 MIPI CSI 摄像头设备节点可能不是/dev/video0而是类似/dev/video11这样的高位节点。而且需要确认内核加载了对应的 sensor 驱动可以用dmesg | grep -i ov5647或者dmesg | grep -i csi来看内核日志。3. OpenCV 采集环境搭建与验证3.1 OpenCV 安装方式选择在 RK3588 的 Ubuntu 20.04 上装 OpenCV有几种方式apt 直接装、pip 装、源码编译。我推荐先用 apt 装系统包因为最省事而且和系统库的兼容性最好sudo apt update sudo apt install python3-opencv装完之后验证import cv2 print(cv2.__version__)如果输出了版本号比如4.2.0就说明装好了。apt 版本的 OpenCV 可能不是最新的但对于我们抓帧推理来说完全够用。如果你需要更新的版本或者特定的功能比如 CUDA 支持虽然 RK3588 上用不上可以考虑 pip 装pip3 install opencv-python但 pip 版本在某些 ARM 平台上可能会有依赖问题比如缺少libGL之类的。遇到的话补一下sudo apt install libgl1-mesa-glx libglib2.0-0源码编译是最灵活的但也是最耗时的RK3588 上编译一次 OpenCV 可能要一两个小时。除非你有特殊需求否则没必要。3.2 用 OpenCV 抓一帧并保存装好 OpenCV 之后先别急着接 YOLOv5单独验证摄像头采集是否正常。写一个最简单的脚本import cv2 cap cv2.VideoCapture(0) if not cap.isOpened(): print(摄像头打开失败) exit() ret, frame cap.read() if ret: cv2.imwrite(test_frame.jpg, frame) print(抓帧成功尺寸:, frame.shape) else: print(抓帧失败) cap.release()这里cv2.VideoCapture(0)里的0对应/dev/video0。如果你的是video1就改成1。跑通之后你会得到一个test_frame.jpg打开看看画面是否正常。这一步看起来简单但实际踩坑的人不少。常见问题包括摄像头被其他进程占用、权限不足、像素格式不匹配导致花屏、曝光没调好导致全黑或全白。后面会专门讲排查。3.3 指定分辨率与像素格式默认情况下 OpenCV 会用自己的默认参数打开摄像头可能是 640x480 的 YUYV 格式。但有些摄像头默认输出 MJPGOpenCV 如果不指定可能会拿到异常帧。你可以显式设置cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G))设置完之后最好读一下实际生效的值print(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) print(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))因为摄像头不一定支持你设的分辨率可能会回退到最接近的值。这个习惯很重要不然你以为采的是 1080p实际是 640x480后面推理的输入尺寸对不上。提示YOLOv5s 的默认输入是 640x640所以采集分辨率不需要太高。1280x720 足够了再高只是浪费带宽和内存。如果你做的是小目标检测可以适当提高采集分辨率但推理时还是要 resize 到模型输入尺寸。4. 把采集帧送进 YOLOv5 推理管线4.1 从 OpenCV 帧到模型输入的转换OpenCV 抓到的帧是 BGR 格式的 numpy 数组形状是(H, W, 3)。YOLOv5 的推理管线通常期望 RGB 格式而且需要做归一化和维度变换。如果你用的是 RKNN 的推理接口还需要把数据转成 NHWC 或 NCHW 的格式具体取决于模型转换时的配置。一个典型的转换流程是这样的import cv2 import numpy as np def preprocess(frame, input_size640): # BGR 转 RGB img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # resize 到模型输入尺寸 img cv2.resize(img, (input_size, input_size)) # 归一化到 0-1 img img.astype(np.float32) / 255.0 # 增加 batch 维度 img np.expand_dims(img, axis0) return img如果你用的是 RKNN 的 Python 接口可能还需要把数据转成uint8并且保持 NHWC 格式因为 RKNN 的inference接口对输入格式有要求。具体要看你的模型转换时用的是哪个配置。这里有个容易忽略的点YOLOv5 官方仓库的推理代码默认用的是 letterbox 缩放不是直接 resize。letterbox 会保持宽高比用灰边填充到正方形。如果你直接 resize检测框的位置会有偏差。所以更严谨的做法是def letterbox(img, new_shape640, color(114, 114, 114)): shape img.shape[:2] r min(new_shape / shape[0], new_shape / shape[1]) new_unpad (int(round(shape[1] * r)), int(round(shape[0] * r))) dw new_shape - new_unpad[0] dh new_shape - new_unpad[1] dw / 2 dh / 2 img cv2.resize(img, new_unpad, interpolationcv2.INTER_LINEAR) top, bottom int(round(dh - 0.1)), int(round(dh 0.1)) left, right int(round(dw - 0.1)), int(round(dw 0.1)) img cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, valuecolor) return img这段代码看起来有点绕但核心思想很简单按比例缩放然后补边到正方形。这样检测框映射回原图时只需要反向计算缩放比例和偏移量不会变形。4.2 推理结果的后处理与坐标映射模型输出的是归一化的检测框坐标cx, cy, w, h、置信度和类别概率。后处理要做的事情包括置信度过滤、NMS非极大值抑制、坐标反归一化、映射回原图尺寸。RKNN 的输出格式和 ONNX 可能略有不同具体取决于你转换模型时的配置。一般来说YOLOv5 的输出是三个尺度的特征图每个位置有(5 num_classes)个值其中 5 是(cx, cy, w, h, obj_conf)。后处理的核心步骤def postprocess(outputs, conf_thres0.25, iou_thres0.45): # 解码检测框 # 过滤低置信度 # NMS # 返回检测框列表 pass这部分代码比较长建议直接参考 YOLOv5 官方仓库的utils/general.py里的non_max_suppression函数。如果你用的是 RKNN 的示例代码通常也会带一个后处理脚本可以直接拿来改。坐标映射的关键是记住 letterbox 时的缩放比例和填充量。假设原图是(H, W)letterbox 之后是(640, 640)缩放比例是r填充量是(dw, dh)。那么检测框映射回原图的公式是x_orig (x_letterbox - dw) / r y_orig (y_letterbox - dh) / r这个计算一定要做对不然画出来的框会偏。4.3 完整链路串起来把采集、预处理、推理、后处理、画框串起来大概是这样import cv2 import numpy as np cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) ret, frame cap.read() if not ret: print(抓帧失败) exit() # 预处理 img letterbox(frame, 640) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 img np.expand_dims(img, axis0) # 推理这里用你的 RKNN 推理接口 # outputs rknn.inference(inputs[img]) # 后处理 # detections postprocess(outputs) # 画框 # for det in detections: # x1, y1, x2, y2, conf, cls det # cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.imwrite(result.jpg, frame) cap.release()实际跑的时候推理那一步会替换成你的 RKNN 调用。如果你前面已经把 YOLOv5s 的 RKNN 推理跑通了这里只需要把输入从图片换成摄像头帧就行。注意RKNN 的推理接口通常要求输入是uint8或者int8具体取决于模型量化时的配置。如果你在预处理时做了/255.0归一化但模型期望的是uint8输入结果会完全错误。这个一定要对照模型转换时的配置来。5. 常见问题与排查技巧实录5.1 摄像头打开失败或抓帧返回空这是最常见的问题。排查顺序如下现象可能原因排查方法cap.isOpened()返回 False设备节点不对ls /dev/video*确认节点号打开成功但read()返回 False权限不足ls -l /dev/video0看权限打开成功但画面全黑曝光未生效或镜头盖没开用手电筒照一下镜头画面花屏或条纹像素格式不匹配v4l2-ctl --list-formats-ext查看支持格式画面卡顿或延迟高分辨率过高或 USB 带宽不足降低分辨率或换 USB 3.0 口我遇到过一次抓帧一直失败最后发现是摄像头被另一个进程占用了。用fuser /dev/video0可以查看哪个进程在占用。杀掉之后就好了。还有一个坑是有些 USB 摄像头在 OpenCV 里默认走的是 YUYV 格式但实际输出的是 MJPG导致花屏。解决办法是显式设置 FOURCCcap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G))5.2 MIPI CSI 摄像头不识别MIPI CSI 摄像头的问题通常出在驱动和设备树。先确认内核有没有加载 sensor 驱动dmesg | grep -i ov5647 dmesg | grep -i csi dmesg | grep -i rkisp如果没有任何输出说明驱动没加载。可能是内核配置里没使能对应的 sensor 驱动或者设备树里没有正确配置。这种情况需要重新编译内核或修改 DTS门槛比较高。另一个常见问题是设备节点不对。MIPI CSI 摄像头可能枚举成/dev/video11或更高的号而不是video0。可以用v4l2-ctl --list-devices列出所有设备找到对应的节点。5.3 推理结果坐标偏移如果你发现画出来的框位置不对大概率是 letterbox 的坐标映射算错了。检查两点一是缩放比例r是否用了正确的原图尺寸二是填充量dw和dh是否除了 2。这两个地方最容易出错。还有一个可能是模型输出的坐标格式和你以为的不一样。有些 RKNN 转换后的模型输出的是(x1, y1, x2, y2)有些是(cx, cy, w, h)。这个要看模型转换时的配置和导出脚本。5.4 推理速度慢单帧推理如果超过 500ms说明有问题。RK3588 的 NPU 跑 YOLOv5s 应该在几十毫秒级别。慢的原因可能是模型没跑在 NPU 上跑在 CPU 上了、输入尺寸太大、后处理用了 Python 循环导致效率低。检查模型是否跑在 NPU 上可以看 RKNN 初始化时的日志或者用rknn.query查一下。后处理如果太慢可以考虑用 numpy 向量化操作替代循环或者用 C 写后处理。提示如果你只是验证链路单帧推理慢一点无所谓。但如果要做实时检测后处理的优化和采集、推理的流水线设计就很重要了。可以考虑用多线程一个线程采集一个线程推理一个线程显示。6. 从单帧到连续帧的扩展思路单帧跑通之后扩展到连续帧其实不难核心就是把采集和推理放到循环里。但直接串行跑会有问题采集一帧、推理一帧、显示一帧帧率会被最慢的环节拖累。如果推理要 50ms采集要 30ms显示要 10ms那整体帧率就只有 11fps 左右。更好的做法是用生产者-消费者模型一个线程专门采集把帧放进队列另一个线程从队列取帧做推理主线程负责显示。这样采集和推理可以并行帧率能提升不少。import threading import queue frame_queue queue.Queue(maxsize2) def capture_thread(): cap cv2.VideoCapture(0) while True: ret, frame cap.read() if ret: if frame_queue.full(): frame_queue.get() frame_queue.put(frame) def inference_thread(): while True: frame frame_queue.get() # 推理并画框 # ... threading.Thread(targetcapture_thread, daemonTrue).start() threading.Thread(targetinference_thread, daemonTrue).start()队列大小设为 2 是为了避免积压太多旧帧。如果推理跟不上采集队列会满这时候丢弃旧帧比堆积更好因为实时检测关心的是当前画面不是几秒前的画面。另外显示环节如果不需要实时预览可以只保存结果或者通过网络发送出去。RK3588 上有硬件编码器可以用 FFmpeg 或者 GStreamer 做推流把检测结果实时传出去。这个后面可以单独展开讲。我在实际使用中的体会是先把单帧链路跑通再考虑性能优化。很多人一上来就搞多线程、搞流水线结果出了问题根本不知道是哪一层的事。单帧跑通了至少证明采集、预处理、推理、后处理、画框这条链路是通的后面优化才有基础。