ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

香橙派RK3588实战:YOLOv5接入USB摄像头完成单帧推理

香橙派RK3588实战:YOLOv5接入USB摄像头完成单帧推理 1. 从推理到“看见”为什么这一步是YOLOv5落地的分水岭很多人跟着教程走到第08篇已经在香橙派RK3588上把YOLOv5s的推理跑通了——给一张静态图片模型能框出目标、打出置信度终端里刷刷输出结果。但说实话这离“能用”还差得远。静态图片推理本质上是个离线批处理你喂它什么它看什么而真实场景里我们要的是设备自己“看见”世界摄像头实时抓帧、送进模型、拿到结果。这一步跨过去你的香橙派才算从一台“会算题的开发板”变成“有眼睛的边缘设备”。这篇要干的事很具体给已经跑通的YOLOv5示例接上一个摄像头抓取一帧画面然后完成一次完整的推理。听起来简单但里面藏着不少坑——摄像头在RK3588上怎么被识别、OpenCV的VideoCapture为什么有时候打不开、MIPI摄像头和USB摄像头在代码层面有什么区别、抓到的帧格式和模型输入格式怎么对齐。这些问题不解决你会卡在“代码没报错但就是没结果”的状态里。关键词里提到的香橙派、RK3588、YOLOv5、OpenCV、摄像头正好构成了这条链路的核心五要素。适合谁看如果你已经完成了前面的环境搭建和模型部署手里有一块香橙派5RK3588或者香橙派5 Plus系统是Ubuntu 20.04并且想把自己的YOLOv5示例从“读图推理”升级到“摄像头推理”那这篇就是为你写的。如果你还没跑通静态推理建议先回去补课因为摄像头推理的调试复杂度会高一个量级。我自己的设备是香橙派5RK35888GB内存系统烧的是官方Ubuntu 20.04镜像摄像头用的是常见的USB免驱摄像头UVC协议也试过MIPI接口的OV5647模块。两种方案我都会讲但重点放在USB摄像头上因为它对新手最友好插上就能用不需要改设备树。2. 摄像头选型与RK3588上的识别逻辑2.1 USB摄像头和MIPI摄像头的本质区别在RK3588上接摄像头你面前有两条路USB摄像头和MIPI摄像头。这两条路在硬件层面完全不同软件层面的访问方式也不一样。USB摄像头走的是UVCUSB Video Class标准协议操作系统自带驱动插上之后系统会把它识别成一个/dev/videoX设备节点。你不需要关心它用的是OV2710还是IMX415传感器内核已经帮你处理好了。香橙派5有多个USB口其中一个是USB 3.0带宽足够跑1080P甚至4K的帧率。我实测下来一个普通的1080P USB摄像头在香橙派5上跑30帧毫无压力。MIPI摄像头走的是CSI-2接口直接连到RK3588的ISP图像信号处理器上。这条路性能更好、延迟更低但配置复杂得多。你需要确认摄像头模组的型号是否被内核支持可能需要改设备树Device Tree来启用对应的CSI通道和I2C地址。OV5647是树莓派生态里很常见的MIPI模组在香橙派上也能用但需要加载对应的驱动模块并且用media-ctl工具配置管线。对于第一次接摄像头的朋友我强烈建议先用USB方案跑通全流程再折腾MIPI。对比项USB摄像头UVCMIPI摄像头CSI-2驱动支持内核自带免驱需要对应驱动和设备树配置设备节点/dev/videoX/dev/videoX但需media-ctl配置带宽USB 3.0足够1080P30更高支持4K调试难度低高适合场景快速验证、原型开发产品化、低延迟场景2.2 确认摄像头被系统正确识别插上USB摄像头之后第一件事不是写代码而是确认系统认到了这个设备。打开终端执行ls /dev/video*正常情况你会看到/dev/video0如果摄像头自带麦克风可能还会有/dev/video1那是音频设备不是视频。如果什么都没看到先检查dmesg的输出dmesg | tail -30看看有没有类似uvcvideo: Found UVC 1.00 device的日志。如果没有换一个USB口试试特别是换到USB 3.0口上。香橙派5的USB口供电能力有限有些功耗大的摄像头可能需要带供电的Hub。确认设备节点存在后可以用v4l2-ctl工具查看摄像头支持的格式sudo apt install v4l-utils v4l2-ctl --device/dev/video0 --list-formats-ext这个命令会列出摄像头支持的所有像素格式和分辨率。你会看到YUYV、MJPG、H264等格式。这里有个关键点OpenCV的VideoCapture在RK3588上默认可能走MJPG解码路径但有些摄像头的MJPG流在OpenCV里解码会出问题。如果后面抓帧失败可以尝试强制指定格式。2.3 为什么OpenCV是这里的最佳粘合剂在RK3588上做摄像头抓帧你有几种选择直接用V4L2的API、用GStreamer管线、或者用OpenCV的VideoCapture。前两种性能更好、控制更精细但代码复杂度高。OpenCV的VideoCapture封装了底层V4L2的调用几行代码就能打开摄像头、抓帧、转格式对于快速验证和原型开发来说效率最高。而且OpenCV的Mat格式和YOLOv5的输入预处理天然契合。YOLOv5的推理需要把图像resize到640x640、归一化、转成NCHW的tensor这些操作OpenCV都能做。你抓到的帧直接就是一个numpy数组送进模型之前只需要做简单的颜色空间转换和尺寸变换。当然OpenCV在RK3588上也有它的脾气。如果你装的是系统自带的OpenCV版本Ubuntu 20.04默认是4.2可能缺少某些后端支持。我建议用pip安装opencv-python版本选4.5以上兼容性更好。安装命令pip3 install opencv-python4.5.5.64如果你需要用到RK3588的硬件编码器来做视频流处理那可能需要编译带GStreamer支持的OpenCV但那是后话。现阶段抓单帧推理pip装的版本足够了。3. 用OpenCV打开摄像头并抓取一帧的完整实操3.1 最小可用代码从打开设备到保存图片先写一个最简单的脚本验证摄像头能不能被OpenCV打开并且抓一帧保存成图片。新建文件capture_test.pyimport cv2 import time # 打开摄像头参数0表示/dev/video0 cap cv2.VideoCapture(0) if not cap.isOpened(): print(错误无法打开摄像头) exit() # 设置分辨率有些摄像头需要显式设置 cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) # 预热丢弃前几帧让摄像头自动曝光稳定 for i in range(10): ret, frame cap.read() time.sleep(0.05) # 正式抓取一帧 ret, frame cap.read() if ret: cv2.imwrite(capture.jpg, frame) print(f抓帧成功图像尺寸{frame.shape}) else: print(抓帧失败) cap.release()这段代码里有几个细节值得说。第一cap.set设置分辨率不一定生效取决于摄像头是否支持你设定的分辨率。你可以通过cap.get(cv2.CAP_PROP_FRAME_WIDTH)读回来确认实际生效的值。第二预热帧非常重要。USB摄像头刚打开时自动曝光和自动白平衡还在调整前几帧往往是过曝或过暗的。丢弃前10帧再抓图像质量会稳定很多。第三cap.read()返回两个值ret表示是否成功frame是图像数据。如果ret为False后面的frame可能是空的直接使用会报错。运行这个脚本python3 capture_test.py如果一切正常当前目录下会出现capture.jpg终端会打印出图像尺寸。用ls -lh capture.jpg看一下文件大小1080P的JPEG大概在200KB到500KB之间。如果文件只有几KB那说明抓到的可能是黑屏或者花屏。3.2 抓帧失败的常见原因与排查链路如果cap.isOpened()返回False或者cap.read()一直失败别急按下面的顺序排查。第一确认设备节点权限。普通用户可能没有/dev/video0的读写权限。执行ls -l /dev/video0如果权限是crw-rw----且所属组是video那把你的用户加到video组sudo usermod -aG video $USER然后注销重新登录或者执行newgrp video让组权限生效。第二确认没有其他进程占用摄像头。如果你之前跑过程序没正常释放或者系统里有其他服务在轮询摄像头OpenCV会打不开。用fuser /dev/video0查看占用进程或者直接sudo lsof /dev/video0。第三尝试不同的后端。OpenCV在Linux上默认用V4L2后端但有时候GStreamer后端更稳定。可以显式指定cap cv2.VideoCapture(0, cv2.CAP_V4L2)或者cap cv2.VideoCapture(0, cv2.CAP_GSTREAMER)第四检查MJPG格式问题。有些USB摄像头默认输出MJPG格式OpenCV解码时可能出错。可以强制用YUYV格式cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc(Y, U, Y, V))但注意YUYV格式在USB 2.0下带宽有限1080P可能只能跑5帧。如果要用高分辨率还是得用MJPG。第五确认摄像头本身没问题。把摄像头插到电脑上用系统自带的相机应用或者cheese工具测试一下。如果电脑上也打不开那就是硬件问题。我踩过的一个坑是香橙派5的Ubuntu镜像里默认的uvcvideo驱动模块可能没有加载。执行lsmod | grep uvc确认一下如果没有手动加载sudo modprobe uvcvideo然后重新插拔摄像头。3.3 把抓到的帧对齐到YOLOv5的输入格式抓到的帧是BGR格式的numpy数组形状是(H, W, 3)。YOLOv5的输入要求是RGB格式、尺寸640x640、归一化到0-1、形状(1, 3, 640, 640)。转换代码如下import cv2 import numpy as np def preprocess(frame, target_size640): # BGR转RGB img cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) # resize到640x640 img cv2.resize(img, (target_size, target_size)) # 归一化 img img.astype(np.float32) / 255.0 # HWC转CHW img np.transpose(img, (2, 0, 1)) # 增加batch维度 img np.expand_dims(img, axis0) return img这里有个容易忽略的点resize会改变图像的宽高比。如果你直接把1920x1080的帧硬拉到640x640物体会被压扁检测精度会下降。正确的做法是保持宽高比缩放然后填充黑边letterbox。YOLOv5官方代码里就是这么做的。简化版的letterbox实现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用letterbox处理之后图像不会变形但推理结果里的坐标需要反向映射回原图尺寸这个后面讲后处理的时候再说。4. 从抓帧到推理把摄像头接入YOLOv5示例4.1 推理脚本的整体结构设计现在把抓帧和推理串起来。整个脚本的流程是打开摄像头 → 抓一帧 → 预处理 → 送进RKNN模型推理 → 后处理 → 画框显示/保存。这里假设你已经有了一个能跑的RKNN推理脚本前面的教程里应该已经准备好了我们只需要把输入源从图片文件换成摄像头帧。先看一下推理脚本的核心结构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) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) # 预热 for _ in range(10): cap.read() # 抓一帧 ret, frame cap.read() cap.release() 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.transpose(img, (2, 0, 1)) img np.expand_dims(img, axis0) # 推理 outputs rknn.inference(inputs[img]) # 后处理 boxes, scores, classes post_process(outputs, frame.shape) # 画框 for box, score, cls in zip(boxes, scores, classes): x1, y1, x2, y2 box cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{cls}:{score:.2f}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imwrite(result.jpg, frame)这个结构看起来清晰但实际跑起来会遇到几个问题。第一rknn.inference的输入要求是numpy数组形状和数据类型必须和模型转换时一致。如果你在转换RKNN模型时用的是NHWC格式那这里就要传NHWC。第二后处理的坐标映射要考虑letterbox的缩放和填充偏移。第三RKNN的推理输出格式取决于模型转换时的配置可能是三个输出头也可能是一个合并的输出。4.2 RKNN推理输入的形状陷阱RK3588的NPU对输入tensor的形状有要求。YOLOv5s的RKNN模型通常接受(1, 3, 640, 640)的NCHW输入数据类型是float32或者int8。如果你在转换模型时做了量化quantized那输入需要是int8并且要传入量化参数。这个在之前的教程里应该已经处理过了这里假设你用的是浮点模型或者已经正确配置了量化。一个常见的错误是OpenCV读到的帧是uint8类型范围0-255。如果你直接送进去而模型期望的是0-1的浮点数推理结果会完全错误。所以归一化这一步不能省。另外np.transpose之后的数组在内存里可能不是连续的RKNN的推理接口有时候要求连续内存。加一个np.ascontiguousarray更保险img np.ascontiguousarray(img)还有一个坑RKNNLite的inference方法接受一个列表列表里每个元素是一个输入tensor。如果你只有一个输入就传[img]。返回的outputs也是一个列表每个元素对应一个输出层。YOLOv5s通常有三个输出层分别对应80x80、40x40、20x20的特征图。后处理需要把这三个输出解码成边界框。4.3 后处理从NPU输出到屏幕上的框后处理是摄像头推理里最绕的部分。RKNN输出的三个特征图每个位置都有(5 num_classes) * 3个值其中5是(x, y, w, h, obj_conf)num_classes是类别数COCO是803是每个grid的anchor数量。解码的过程就是把这些值还原成原图上的像素坐标。简化版的后处理逻辑def post_process(outputs, img_shape, conf_thres0.25, iou_thres0.45): anchors [[10,13, 16,30, 33,23], [30,61, 62,45, 59,119], [116,90, 156,198, 373,326]] strides [8, 16, 32] boxes [] scores [] classes [] for i, output in enumerate(outputs): stride strides[i] anchor anchors[i] h, w output.shape[2], output.shape[3] output output.reshape(3, 5 80, h, w) output output.transpose(0, 2, 3, 1) # (3, h, w, 85) for a in range(3): for y in range(h): for x in range(w): data output[a, y, x] obj_conf data[4] if obj_conf conf_thres: continue cls_scores data[5:] cls_id np.argmax(cls_scores) cls_conf cls_scores[cls_id] score obj_conf * cls_conf if score conf_thres: continue # 解码坐标 cx (data[0] * 2 - 0.5 x) * stride cy (data[1] * 2 - 0.5 y) * stride bw (data[2] * 2) ** 2 * anchor[a*2] bh (data[3] * 2) ** 2 * anchor[a*21] x1 cx - bw / 2 y1 cy - bh / 2 x2 cx bw / 2 y2 cy bh / 2 boxes.append([x1, y1, x2, y2]) scores.append(score) classes.append(cls_id) # NMS indices cv2.dnn.NMSBoxes(boxes, scores, conf_thres, iou_thres) # 坐标映射回原图 # ...letterbox反向变换 return boxes, scores, classes这段代码是简化版实际生产里会用向量化操作加速不然在RK3588上跑三个特征图的逐像素循环会非常慢。但作为理解后处理原理的起点它足够清晰。关键点在于NPU输出的坐标是相对于特征图的需要乘以stride才能映射回640x640的输入空间然后再根据letterbox的缩放比例映射回原图。我实测下来如果后处理用纯Python循环一帧推理加上后处理大概要200-300毫秒。用numpy向量化之后能降到50毫秒以内。对于单帧抓取来说这个速度可以接受但如果要做实时视频流后处理必须优化。5. 实测中的意外情况与经验总结5.1 摄像头打开慢和首帧黑屏USB摄像头从cap cv2.VideoCapture(0)到第一帧可用中间可能有1-2秒的延迟。如果你在打开后立刻cap.read()拿到的可能是黑屏或者绿屏。我的做法是打开后先time.sleep(1)然后连续读10帧丢弃再抓正式帧。这个预热过程在代码里看起来多余但实际能避免很多“为什么抓到的图是黑的”这类问题。另外有些摄像头在cap.release()之后不会立刻释放设备节点如果你紧接着再次打开可能会失败。加一个time.sleep(0.5)再重新打开成功率会高很多。5.2 分辨率设置不生效的真相cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)这行代码不一定能生效。摄像头的分辨率是离散的它只支持特定的几档。如果你设了一个不支持的值OpenCV会静默失败实际用的还是默认分辨率。正确的做法是设置之后读回来确认cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080) actual_w cap.get(cv2.CAP_PROP_FRAME_WIDTH) actual_h cap.get(cv2.CAP_PROP_FRAME_HEIGHT) print(f实际分辨率{actual_w}x{actual_h})如果实际值和设定值不一致说明摄像头不支持你设的分辨率。这时候要么换一个支持的分辨率要么接受默认值。我手头这个摄像头默认是640x480设1080P不生效设1280x720就成功了。5.3 推理结果坐标偏移的修正用了letterbox之后推理输出的坐标是在640x640的填充图上的。要映射回原图需要减去填充偏移再除以缩放比例。具体公式# letterbox参数 r min(640 / h0, 640 / w0) dw (640 - w0 * r) / 2 dh (640 - h0 * r) / 2 # 反向映射 x1 (x1 - dw) / r y1 (y1 - dh) / r x2 (x2 - dw) / r y2 (y2 - dh) / r如果忘了这一步画出来的框会整体偏移而且尺寸也不对。我一开始就是漏了反向映射框全跑到图像外面去了排查了半天才发现是letterbox的锅。5.4 内存和NPU核心的分配RK3588有多个NPU核心init_runtime的时候可以指定core_mask。如果你只跑一个模型用NPU_CORE_0就够了。但如果同时跑多个模型或者要做视频流的连续推理可以考虑用NPU_CORE_0_1_2让三个核心协同工作。不过对于单帧抓取推理来说单核心足够了多核心反而增加初始化开销。内存方面RKNN模型加载后会占用一部分内存摄像头抓帧的buffer也会占内存。香橙派5的8GB版本完全够用4GB版本在跑1080P推理时可能会有点紧张。如果遇到内存不足可以降低摄像头分辨率或者用del及时释放不再使用的numpy数组。5.5 从单帧到视频流下一步的扩展方向单帧抓取推理跑通之后下一步自然是做连续的视频流推理。但这里有个性能问题如果每帧都做完整的预处理、推理、后处理RK3588上大概只能跑到5-10帧。要提升帧率有几个方向用多线程把抓帧和推理分开、用RK3588的硬件编码器做视频解码、把后处理放到NPU上做需要模型支持。这些是后续教程的内容但你现在就可以开始思考自己的应用场景需要多高的帧率以及是否值得投入时间做这些优化。我个人在实际操作中的体会是单帧抓取推理是验证摄像头链路是否通畅的最佳方式。它把问题域缩小到了“摄像头能不能打开、帧能不能抓到、模型能不能吃进去”这三个点上。这三个点通了后面的视频流只是加了一个循环和线程调度的问题。所以别急着上视频流先把这一帧跑稳。
返回列表