资讯动态

香橙派RK3588实战:OpenCV摄像头接入与YOLOv5推理全链路

发布时间:2026/10/3 7:09:11 来源:尧图企业网站定制
1. 从模型跑通到摄像头接入这一步到底卡在哪很多人跟着前面的教程把 YOLOv5 在香橙派 RK3588 上跑起来之后会有一个很自然的想法模型能推理了那我接个摄像头抓一帧丢进去不就行了听起来就是几行代码的事但真正动手的时候你会发现坑比想象中多得多。摄像头不识别、OpenCV 读不到设备、抓到的帧是绿的、推理结果画框错位、帧率低到怀疑人生这些问题几乎每个人都会遇到至少一个。这篇内容就是专门解决这个环节的。核心目标很明确在香橙派 RK3588 上通过 OpenCV 打开摄像头抓取一帧图像送入已经部署好的 YOLOv5 模型完成一次推理并且把检测结果可视化出来。听起来简单但这里面涉及摄像头设备节点确认、V4L2 后端适配、OpenCV 编译选项、图像格式转换、模型输入预处理、推理输出后处理、结果绘制这几个关键环节任何一个环节出问题都会导致最终跑不通。适合谁看如果你已经完成了 RK3588 上 YOLOv5 的基础部署模型能对静态图片推理出结果但还没把摄像头这条链路打通那这篇就是给你准备的。如果你连模型都还没跑通建议先回去把前面的环境搭建和模型转换部分搞定否则直接上摄像头只会让你在更多变量里迷失。我自己的设备组合是香橙派 5RK35888GB 版本系统用的是官方 Ubuntu 22.04 镜像摄像头用的是常见的 USB 免驱摄像头UVC 协议也测试过 MIPI 接口的 OV5647 模块。两种摄像头的打开方式略有不同后面会分别说明。OpenCV 用的是系统自带的 4.5.x 版本Python 环境是 3.10。如果你用的是其他版本大部分操作逻辑是一样的但个别参数可能需要微调。注意RK3588 的 NPU 推理和摄像头采集是两个独立的子系统摄像头走的是 V4L2 或 MIPI CSI 通道NPU 走的是 RKNN 运行时。两者本身不冲突但如果你在同一个进程里同时跑要注意线程调度和内存带宽的分配否则容易出现采集掉帧或者推理超时。2. 摄像头选型与设备节点确认别一上来就写代码2.1 USB 摄像头和 MIPI 摄像头的本质区别在 RK3588 上接摄像头你面前有两条路USB 摄像头和 MIPI CSI 摄像头。这两条路从硬件到软件都不一样选错了后面全是麻烦。USB 摄像头走的是 UVCUSB Video Class标准协议本质上是一个即插即用的设备。你插上去之后系统会自动识别并加载 uvcvideo 驱动然后在/dev/video*下面生成设备节点。优点是通用性强、热插拔方便、不需要额外配置设备树。缺点是带宽受 USB 总线限制高分辨率下帧率上不去而且延迟相对较高。MIPI CSI 摄像头走的是 MIPI 接口直接连接到 RK3588 的 ISP 或 VICAP 控制器。优点是带宽高、延迟低、适合高帧率场景。缺点是需要配置设备树、加载对应的 sensor 驱动、可能还需要通过 media-ctl 配置 pipeline。对于 OV5647 这类常见模块香橙派的官方镜像通常已经带了驱动但设备节点可能不是标准的/dev/video0需要你自己确认。我个人的建议是如果你只是做功能验证和原型开发先用 USB 摄像头把链路跑通因为变量最少。等逻辑没问题了再换成 MIPI 摄像头做性能优化。这样排查问题的时候不会同时面对硬件和软件两层不确定性。2.2 确认设备节点的正确姿势插上摄像头之后第一件事不是写 Python 代码而是确认系统到底认到了哪个设备节点。很多人直接cv2.VideoCapture(0)然后发现打不开就是因为节点号不对。先用ls /dev/video*看一下有哪些设备节点。你会发现可能有多个比如/dev/video0到/dev/video10甚至更多。这是因为 RK3588 的 VICAP 控制器会为每个可能的输入通道创建节点但并不是每个节点都能用来抓帧。更靠谱的方法是使用v4l2-ctl --list-devices命令它会列出每个设备对应的节点和名称。输出大概长这样# 安装 v4l-utils如果还没装 sudo apt install v4l-utils # 列出所有视频设备 v4l2-ctl --list-devices输出示例USB Camera: USB Camera (usb-xhci-hcd.6.auto-1): /dev/video0 /dev/video1 rkisp_mainpath (platform:rkisp-vir0): /dev/video10 /dev/video11从输出里你能清楚看到哪个节点对应 USB 摄像头哪个对应 MIPI ISP 输出。对于 USB 摄像头通常/dev/video0是采集节点/dev/video1是元数据节点你只需要用第一个。再用v4l2-ctl --device/dev/video0 --all查看这个节点支持的格式和分辨率。重点关注Video Capture部分下面的Width/Height和Pixel Format。USB 摄像头通常支持 YUYV 和 MJPEG 两种格式MIPI 摄像头可能输出 NV12 或 RGB888。实操心得如果你的 USB 摄像头同时支持 YUYV 和 MJPEG优先用 MJPEG 格式打开。因为 YUYV 是未压缩格式1080p 下每帧数据量很大USB 2.0 带宽根本扛不住帧率会掉到个位数。MJPEG 是压缩格式带宽占用小很多OpenCV 打开后会自动解码实际使用中帧率能提升三到五倍。2.3 权限问题别忽略确认完节点之后还要确保当前用户有权限访问/dev/video*。默认情况下这些节点属于video组如果你的用户不在这个组里打开会报权限错误。# 把当前用户加入 video 组 sudo usermod -aG video $USER # 重新登录生效或者临时用 sudo 测试 sudo chmod 666 /dev/video0我一般会在调试阶段直接用sudo跑 Python 脚本确认链路没问题之后再改权限配置。这样能快速排除权限因素避免在代码里绕圈子。3. OpenCV 打开摄像头的正确方式与常见坑3.1 VideoCapture 的后端选择OpenCV 的cv2.VideoCapture在 Linux 下默认使用 V4L2 后端但如果你安装的 OpenCV 是 pip 版本而不是系统编译版本可能会缺少 V4L2 支持导致摄像头打不开或者只能读到空帧。判断方法很简单跑一段代码看后端名称import cv2 cap cv2.VideoCapture(0) if cap.isOpened(): print(后端名称:, cap.getBackendName()) print(分辨率:, cap.get(cv2.CAP_PROP_FRAME_WIDTH), x, cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) print(帧率:, cap.get(cv2.CAP_PROP_FPS)) else: print(摄像头打开失败) cap.release()如果输出是V4L2说明后端正确。如果是GSTREAMER或者其他可能需要手动指定后端# 强制使用 V4L2 后端 cap cv2.VideoCapture(0, cv2.CAP_V4L2)如果 pip 安装的 OpenCV 不支持 V4L2最彻底的解决办法是卸载 pip 版本用 apt 安装系统版本pip uninstall opencv-python opencv-python-headless sudo apt install python3-opencv系统版本的 OpenCV 通常编译时已经开启了 V4L2、FFMPEG 等常用后端兼容性更好。缺点是版本可能偏旧但对于 YOLOv5 推理来说完全够用。3.2 设置分辨率和格式的正确顺序很多人会先打开摄像头然后再设置分辨率结果发现设置不生效。原因是 V4L2 在打开设备时就已经协商好了格式后续修改需要重新协商。正确的做法是在打开之后立即设置并且用set的返回值确认是否成功import cv2 cap cv2.VideoCapture(0, cv2.CAP_V4L2) # 设置 MJPEG 格式如果支持 cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) # 设置分辨率 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 设置帧率 cap.set(cv2.CAP_PROP_FPS, 30) # 确认实际生效的参数 actual_width cap.get(cv2.CAP_PROP_FRAME_WIDTH) actual_height cap.get(cv2.CAP_PROP_FRAME_HEIGHT) actual_fps cap.get(cv2.CAP_PROP_FPS) print(f实际分辨率: {actual_width}x{actual_height}, 帧率: {actual_fps})注意set返回True不代表参数一定生效有些摄像头会静默忽略不支持的配置。所以一定要用get读回来确认。如果读回来的值和设定值不一致说明摄像头不支持那个配置需要换一个。常见问题设置 640x480 之后读回来是 0x0或者读回来是 1920x1080。前者说明设置失败后者说明摄像头不支持你设定的分辨率自动回退到了默认值。遇到这种情况先用v4l2-ctl --list-formats-ext查看摄像头支持的所有分辨率和格式组合然后从中选一个。3.3 抓帧时的缓冲区问题V4L2 默认会维护一个帧缓冲区队列。如果你打开摄像头之后没有及时读取缓冲区里会堆积旧帧。等你真正开始读的时候拿到的是几秒前的画面这在实时推理场景里是致命的。解决办法是打开摄像头后连续抓几帧丢弃把缓冲区清空# 预热摄像头丢弃前几帧 for i in range(5): ret, frame cap.read() if not ret: print(f第 {i} 帧读取失败)另外一个方法是设置缓冲区大小为 1cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)不过这个参数在 V4L2 后端下不一定生效取决于 OpenCV 的编译选项。所以最稳妥的还是手动丢弃前几帧。4. 从抓帧到推理的完整链路实现4.1 图像预处理从 BGR 到模型输入YOLOv5 模型的输入是固定尺寸的 RGB 图像通常是 640x640。摄像头抓到的帧是 BGR 格式OpenCV 默认分辨率也不一定是 640x640。所以中间需要做几步转换。第一步是颜色空间转换从 BGR 转到 RGBrgb_frame cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)第二步是缩放加填充letterbox保持宽高比的同时把图像缩放到 640x640。直接 resize 会拉伸图像导致变形影响检测精度。letterbox 的做法是等比缩放后用灰色填充剩余区域。import numpy as np def letterbox(img, new_shape(640, 640), color(114, 114, 114)): shape img.shape[:2] # 当前 h, w r min(new_shape[0] / shape[0], new_shape[1] / shape[1]) # 计算缩放后的尺寸 new_unpad int(round(shape[1] * r)), int(round(shape[0] * r)) dw new_shape[1] - new_unpad[0] dh new_shape[0] - new_unpad[1] dw / 2 dh / 2 # 缩放 if shape[::-1] ! new_unpad: 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, r, (left, top)第三步是归一化和维度变换。YOLOv5 要求输入是 float32数值范围 0 到 1形状是 (1, 3, 640, 640)img img.astype(np.float32) / 255.0 img img.transpose(2, 0, 1) # HWC - CHW img np.expand_dims(img, axis0) # 添加 batch 维度如果你用的是 RKNN 模型输入格式可能要求 NHWC 而不是 NCHW具体要看转换模型时的配置。这个在 RKNN Toolkit 的文档里有说明转换时设置的mean_values和std_values也要和这里对应上。4.2 RKNN 推理接口调用假设你已经用 RKNN Toolkit 把 YOLOv5 模型转换成了.rknn文件并且在前面的教程里已经验证过静态图片推理没问题。这里直接复用那套推理代码只是把输入从图片文件换成摄像头帧。from rknnlite.api import RKNNLite # 初始化 RKNN rknn RKNNLite() ret rknn.load_rknn(yolov5s.rknn) ret rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 推理 outputs rknn.inference(inputs[img])outputs是一个列表里面包含三个尺度的输出特征图。后处理部分需要做解码、置信度过滤、NMS这些在前面的教程里应该已经实现过了直接调用即可。注意RKNN 的推理接口对输入数据的形状和类型很敏感。如果你传入的数组形状不对或者数据类型不是 float32会直接报错或者输出垃圾结果。建议在推理前打印一下img.shape和img.dtype确认。4.3 后处理与结果绘制后处理的核心逻辑是把模型输出的特征图解码成边界框过滤掉低置信度的框做 NMS 去除重叠框最后把框画到原图上。这部分代码比较长但逻辑是固定的。关键点在于坐标映射模型输出的是相对于 640x640 输入图像的坐标需要映射回原始摄像头帧的坐标。映射时要考虑 letterbox 的缩放比例和填充偏移。def scale_coords(img1_shape, coords, img0_shape, ratio_padNone): if ratio_pad is None: gain min(img1_shape[0] / img0_shape[0], img1_shape[1] / img0_shape[1]) pad (img1_shape[1] - img0_shape[1] * gain) / 2, (img1_shape[0] - img0_shape[0] * gain) / 2 else: gain ratio_pad[0][0] pad ratio_pad[1] coords[:, [0, 2]] - pad[0] coords[:, [1, 3]] - pad[1] coords[:, :4] / gain coords[:, 0].clamp_(0, img0_shape[1]) coords[:, 1].clamp_(0, img0_shape[0]) coords[:, 2].clamp_(0, img0_shape[1]) coords[:, 3].clamp_(0, img0_shape[0]) return coords绘制的时候用cv2.rectangle画框cv2.putText写标签和置信度。颜色可以根据类别固定方便区分。for det in detections: x1, y1, x2, y2, conf, cls_id det label f{class_names[int(cls_id)]} {conf:.2f} color colors[int(cls_id) % len(colors)] cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), color, 2) cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2)4.4 完整代码框架把上面的步骤串起来整个流程大概是这样的import cv2 import numpy as np from rknnlite.api import RKNNLite # 初始化 RKNN rknn RKNNLite() rknn.load_rknn(yolov5s.rknn) rknn.init_runtime(core_maskRKNNLite.NPU_CORE_0) # 打开摄像头 cap cv2.VideoCapture(0, cv2.CAP_V4L2) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(M, J, P, G)) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) # 预热 for _ in range(5): cap.read() while True: ret, frame cap.read() if not ret: break # 预处理 img, ratio, pad letterbox(frame, (640, 640)) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.astype(np.float32) / 255.0 img img.transpose(2, 0, 1) img np.expand_dims(img, axis0) # 推理 outputs rknn.inference(inputs[img]) # 后处理 detections post_process(outputs, conf_thres0.25, iou_thres0.45) detections scale_coords((640, 640), detections, frame.shape[:2], (ratio, pad)) # 绘制 for det in detections: x1, y1, x2, y2, conf, cls_id det label f{class_names[int(cls_id)]} {conf:.2f} cv2.rectangle(frame, (int(x1), int(y1)), (int(x2), int(y2)), (0, 255, 0), 2) cv2.putText(frame, label, (int(x1), int(y1) - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(YOLOv5 RK3588, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows() rknn.release()这段代码是单帧推理的循环版本每次抓一帧、推理一帧、显示一帧。实际跑起来帧率大概在 15 到 25 FPS 之间取决于摄像头帧率和 NPU 负载。5. 性能优化与常见问题排查5.1 帧率上不去的几个原因如果你发现帧率明显低于预期可以从以下几个方面排查。第一个是摄像头格式。前面说过YUYV 格式在 1080p 下带宽不够帧率会掉得很厉害。确认一下你设置的是 MJPEG 还是 YUYV。用v4l2-ctl --device/dev/video0 --get-fmt-video可以查看当前格式。第二个是推理耗时。RK3588 的 NPU 跑 YOLOv5s 在 640x640 输入下单帧推理大概在 30 到 50 毫秒。如果你用了多核 NPURK3588 有三个 NPU 核心可以设置core_mask来并行推理但单帧推理用单核就够了。第三个是图像预处理和后处理的 CPU 开销。letterbox 和颜色空间转换都是 CPU 操作640x480 的帧大概需要几毫秒。后处理的 NMS 如果框很多也会消耗时间。可以用time.time()打点测量每个环节的耗时找到瓶颈。import time t0 time.time() ret, frame cap.read() t1 time.time() # 预处理 t2 time.time() # 推理 t3 time.time() # 后处理 t4 time.time() print(f抓帧: {(t1-t0)*1000:.1f}ms, 预处理: {(t2-t1)*1000:.1f}ms, f推理: {(t3-t2)*1000:.1f}ms, 后处理: {(t4-t3)*1000:.1f}ms)5.2 常见问题速查表问题现象可能原因解决方法cap.isOpened()返回 False设备节点错误或权限不足用v4l2-ctl --list-devices确认节点检查用户是否在 video 组抓到的帧全绿或全黑格式不匹配或缓冲区未清空设置 MJPEG 格式丢弃前 5 帧推理结果框位置偏移letterbox 参数未正确传递确认 scale_coords 的 ratio 和 pad 与预处理一致帧率低于 10 FPSYUYV 格式或分辨率过高切换到 MJPEG降低分辨率到 640x480RKNN 推理报错输入形状或类型不对打印 img.shape 和 img.dtype确认是 (1,3,640,640) float32检测不到任何目标置信度阈值过高或预处理有误降低 conf_thres 到 0.1 测试检查颜色空间转换画面延迟严重缓冲区堆积设置 CAP_PROP_BUFFERSIZE 为 1或每帧都读取不跳过5.3 几个我踩过的坑第一个坑是 OpenCV 版本问题。我一开始用 pip 装的opencv-python结果VideoCapture死活打不开摄像头报的错误信息还很模糊。后来换成apt install python3-opencv就正常了。原因是 pip 版本的 OpenCV 编译时没有链接 V4L2 库或者链接了但运行时找不到。如果你也遇到类似问题优先换系统版本。第二个坑是 MIPI 摄像头的设备节点。OV5647 模块插上之后/dev/video0并不是采集节点真正的采集节点是/dev/video10或更高。而且 MIPI 摄像头需要通过 media-ctl 配置 pipeline否则抓到的帧是空的。具体配置命令取决于你的设备树和驱动版本建议参考香橙派官方 Wiki 里的摄像头配置章节。第三个坑是 letterbox 的 pad 计算。我一开始直接用 resize 不用 letterbox结果检测框位置总是偏。后来改成 letterbox 之后又因为 pad 计算时用了整数除法导致偏移几个像素。最后改成浮点计算再取整问题才解决。这个细节在代码里不起眼但对检测精度影响很大。第四个坑是 NPU 内存。RK3588 的 NPU 和 CPU 共享内存带宽如果你同时跑摄像头采集和 NPU 推理再加上显示输出内存带宽可能成为瓶颈。表现是帧率不稳定偶尔卡顿。解决办法是降低摄像头分辨率或者把显示输出改成保存到文件而不是实时预览。实操心得调试阶段建议先把推理结果保存成图片确认检测框位置正确之后再开实时预览。因为实时预览会占用额外的 CPU 和内存资源可能掩盖真正的问题。等逻辑全部验证通过再开预览做最终测试。6. 从单帧到视频流下一步可以怎么扩展单帧抓取和推理跑通之后下一步自然是做成连续的视频流处理。但这里有一个架构选择是在 Python 层面用循环逐帧处理还是用多线程把采集和推理分开。单线程循环的优点是逻辑简单缺点是采集和推理串行执行帧率受限于两者之和。如果采集耗时 10ms推理耗时 40ms那整体帧率就是 20 FPS。多线程的优点是采集和推理可以并行帧率能提升到接近推理耗时的倒数但需要处理线程安全和缓冲区管理。我自己的做法是用一个线程专门抓帧放入队列主线程从队列取帧做推理和显示。队列长度设为 1 或 2保证拿到的是最新帧而不是旧帧。这样即使推理慢一点画面也不会延迟太多。import threading import queue frame_queue queue.Queue(maxsize2) def capture_thread(cap, q): while True: ret, frame cap.read() if not ret: break if q.full(): try: q.get_nowait() except queue.Empty: pass q.put(frame) # 启动采集线程 t threading.Thread(targetcapture_thread, args(cap, frame_queue), daemonTrue) t.start() # 主线程推理 while True: frame frame_queue.get() # 推理和显示...这个架构在实际使用中比较稳定帧率能提升 30% 到 50%。如果你要做更复杂的应用比如多路摄像头同时推理那就需要考虑用 RK3588 的多核 NPU 做并行或者用 RGA 硬件加速做图像预处理进一步降低 CPU 负载。另外如果你用的是 RTSP 网络摄像头而不是 USB 摄像头OpenCV 也可以通过 FFMPEG 后端直接打开 RTSP 流。不过 RTSP 的延迟通常比 USB 摄像头高而且受网络状况影响。在 RK3588 上跑 RTSP 拉流加推理建议把分辨率控制在 720p 以内否则解码开销会很大。最后再分享一个小技巧如果你发现 OpenCV 的imshow在香橙派上显示效果不好或者卡顿可以改用cv2.imwrite保存帧到文件然后用其他工具查看。或者用 framebuffer 直接输出到屏幕绕过 X11 的开销。这些在嵌入式场景下都很实用。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑