☰
香橙派RK3588实战:YOLOv5摄像头抓帧与推理全链路
2026/9/29 7:26:38 网站建设 项目流程

1. 从零打通摄像头到推理的完整链路

香橙派RK3588上跑YOLOv5,模型转换和推理脚本都调通了,但一直用测试图片跑总觉得差点意思。真正要把这套东西用起来,第一步就是让板子自己“看见”画面——接上摄像头,抓一帧,送进模型,拿到检测结果。这个环节看着简单,实际上从设备识别、图像格式转换到推理输入适配,每一步都有坑。我前后折腾了三四块不同型号的摄像头,才把这条链路彻底跑顺。

这篇内容适合已经在香橙派RK3588上完成YOLOv5基础部署、模型能正常推理静态图片的开发者。如果你还没走到这一步,建议先把前面的模型转换和基础推理跑通再来看。整条链路的核心关键词就几个:香橙派、RK3588、YOLOv5、OpenCV、摄像头。我会从硬件选型开始,一步步讲到怎么用OpenCV抓帧、怎么把抓到的帧喂给YOLOv5、以及中间那些让人抓狂的报错怎么解决。

先说清楚最终要实现的效果:在香橙派RK3588上运行一个Python脚本,调用USB摄像头或者MIPI摄像头,实时抓取一帧画面,经过预处理后送入YOLOv5s模型进行推理,输出检测框和类别信息。整个过程不依赖网络,全部在板端完成。这个方案可以直接作为后续视频流实时检测的基础框架,也可以用来做定时抓拍分析。

2. 硬件选型与摄像头接入方案

2.1 USB摄像头 vs MIPI摄像头怎么选

香橙派RK3588支持两种摄像头接入方式:USB接口和MIPI CSI接口。这两种方案各有优劣,选哪个取决于你的具体场景。

USB摄像头最大的好处是即插即用,系统自带UVC驱动,基本上插上去就能在/dev/video*下面看到设备节点。我手头有一个罗技C270和一个杂牌1080P摄像头,两个都是免驱的,插上就能用。缺点是USB带宽有限,高分辨率下帧率会受限,而且USB摄像头的图像质量参差不齐,有些便宜货色彩偏差很大。

MIPI摄像头需要通过FPC排线接到板子的CSI接口上,香橙派官方有配套的摄像头模组,比如OV5647就是常见的选择。MIPI摄像头的好处是带宽大、延迟低、图像质量好,适合对实时性要求高的场景。但问题是驱动配置麻烦,设备树要改,不同模组的寄存器配置还不一样。我第一次接OV5647的时候,设备树没配对,/dev/video*下面根本看不到节点,折腾了大半天才搞定。

提示:如果你是第一次在香橙派RK3588上接摄像头,强烈建议先用USB摄像头把整个链路跑通,确认软件层面没问题之后,再换MIPI摄像头做优化。这样可以避免同时排查硬件和软件两个方向的问题。

2.2 设备节点识别与权限配置

摄像头接上之后,第一件事是确认系统有没有识别到设备。打开终端,执行:

ls /dev/video*

正常情况下会看到/dev/video0、/dev/video1这样的设备节点。如果有多个摄像头,编号会依次递增。我遇到过一种情况,板子自带的HDMI输入也会占用一个video节点,所以插上USB摄像头之后可能看到的是/dev/video1而不是/dev/video0。这时候需要用v4l2-ctl工具来确认哪个节点才是真正的摄像头:

v4l2-ctl --list-devices

这个命令会列出所有视频设备及其对应的节点,输出类似这样:

USB Camera: USB Camera (usb-fc800000.usb-1): /dev/video1 /dev/video2

看到这个信息,就知道/dev/video1是摄像头的采集节点。有些摄像头会同时注册两个节点,一个是采集用的,一个是元数据用的,通常采集节点是第一个。

权限问题也很常见。普通用户默认没有访问/dev/video*的权限,直接跑OpenCV会报“Permission denied”。解决办法有两个:一是把当前用户加入video组,执行sudo usermod -aG video $USER,然后重新登录;二是临时用sudo跑脚本。我推荐第一种,一劳永逸。

2.3 OpenCV的安装与摄像头支持验证

香橙派RK3588的Ubuntu 20.04系统上,安装OpenCV有好几种方式。最简单的是用apt:

sudo apt update sudo apt install python3-opencv

但apt装的OpenCV版本可能比较老,而且不一定带FFmpeg支持。如果你需要处理视频流或者用一些新特性,建议用pip装:

pip3 install opencv-python

不过要注意,pip装的opencv-python是预编译的wheel包,在ARM平台上可能没有针对RK3588的NPU做优化,但用来抓帧和预处理是足够的。我实测下来,pip装的OpenCV 4.5.x在香橙派上跑摄像头抓帧没问题,CPU占用也在可接受范围内。

装完之后验证一下:

import cv2 print(cv2.__version__) cap = cv2.VideoCapture(1) # 注意这里的编号 if cap.isOpened(): print("摄像头打开成功") ret, frame = cap.read() if ret: print("抓帧成功,图像尺寸:", frame.shape) cap.release() else: print("摄像头打开失败")

这段代码能跑通,说明OpenCV和摄像头的基本链路没问题。如果cap.isOpened()返回False,先检查设备节点编号对不对,再检查权限。

3. 抓帧与预处理的完整实现

3.1 用OpenCV抓取一帧图像

抓帧本身很简单,cap.read()就搞定了。但这里有几个细节值得注意。

首先是摄像头的初始化时间。有些USB摄像头插上之后需要一两秒才能稳定输出图像,如果VideoCapture之后立刻read(),可能会拿到空帧或者花屏。我的做法是在打开摄像头之后先空转几帧:

cap = cv2.VideoCapture(1) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) # 预热,丢弃前10帧 for _ in range(10): cap.read() ret, frame = cap.read()

设置CAP_PROP_FOURCC为MJPG很重要。很多USB摄像头默认输出YUYV格式,在640x480分辨率下YUYV的带宽占用比MJPG大得多,导致帧率上不去。改成MJPG之后,同样分辨率下帧率能翻倍。这个坑我踩过,一开始没设FOURCC,摄像头只能跑5帧,改成MJPG之后直接跑到30帧。

分辨率设置也有讲究。YOLOv5s的输入尺寸是640x640,如果你抓到的帧是1920x1080,后面需要缩放,会浪费CPU资源。直接在摄像头层面设置成640x480,后续处理更高效。但要注意,有些摄像头不支持任意分辨率,设置之后实际输出可能还是默认值,需要用cap.get()确认一下。

3.2 图像格式转换与YOLOv5输入适配

OpenCV抓到的帧是BGR格式的numpy数组,而YOLOv5模型通常期望RGB格式的输入。所以第一步是颜色空间转换:

img_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)

接下来是尺寸调整。YOLOv5s的标准输入是640x640,但直接resize会改变宽高比,导致检测框位置偏移。正确的做法是保持宽高比缩放,然后填充黑边:

def letterbox(img, new_shape=(640, 640), color=(114, 114, 114)): shape = img.shape[:2] 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, dh = new_shape[1] - new_unpad[0], new_shape[0] - new_unpad[1] dw /= 2 dh /= 2 img = cv2.resize(img, new_unpad, interpolation=cv2.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, value=color) return img

这个letterbox函数是YOLOv5官方仓库里的标准做法。它的逻辑是:先按比例缩放,让长边对齐到640,短边按比例缩放,然后在短边两侧填充灰色。这样既保持了宽高比,又满足了模型输入尺寸要求。

归一化也是必须的。YOLOv5期望输入像素值在0到1之间:

img = img.astype(np.float32) / 255.0

最后是维度变换。模型期望的输入形状是(1, 3, 640, 640),也就是batch、channel、height、width的顺序:

img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0) # 增加batch维度

3.3 推理输入的数据类型与内存布局

这一步是很多人容易忽略的。香橙派RK3588的NPU推理通常通过RKNN API或者ONNX Runtime来调用。如果你用的是RKNN模型,输入数据的类型和内存布局有特定要求。

RKNN模型通常要求输入是uint8类型,而不是float32。这意味着你不需要做归一化,而是直接把0到255的像素值传进去。但具体要看模型转换时的配置。我在转换YOLOv5s模型时,设置了mean_values=[[0, 0, 0]]和std_values=[[255, 255, 255]],这样模型内部会自动做归一化,外部输入就保持uint8即可。

如果你用的是ONNX模型加ONNX Runtime,那输入通常是float32,需要手动归一化。两种方式的预处理代码不一样,一定要根据实际使用的推理后端来调整。

内存布局方面,RKNN模型通常要求NHWC格式,而ONNX模型要求NCHW格式。这个也要根据后端来定。我建议在预处理阶段就把数据准备好,直接喂给推理接口,不要在推理接口内部再做转换,那样效率低。

4. 推理执行与结果解析

4.1 调用RKNN模型执行推理

假设你已经把YOLOv5s转换成了RKNN模型,文件名为yolov5s.rknn。推理的基本流程是:加载模型、初始化运行时、设置输入、执行推理、获取输出。

from rknnlite.api import RKNNLite rknn = RKNNLite() ret = rknn.load_rknn('yolov5s.rknn') ret = rknn.init_runtime() # 假设img是预处理好的NHWC格式uint8数据 outputs = rknn.inference(inputs=[img])

rknn.inference返回的是一个列表,里面包含模型的所有输出。YOLOv5s通常有三个输出层,分别对应不同尺度的特征图。每个输出的形状是(1, 25200, 85)或者类似的形式,其中25200是候选框数量,85是(x, y, w, h, obj_conf, class_conf_1, ..., class_conf_80)。

4.2 后处理:从输出张量到检测框

模型原始输出是一堆数字,需要经过后处理才能变成人类可读的检测框。后处理主要包括三步:解码边界框、置信度过滤、非极大值抑制。

解码边界框的公式是:

x_center = (sigmoid(tx) * 2 - 0.5 + grid_x) * stride y_center = (sigmoid(ty) * 2 - 0.5 + grid_y) * stride width = (sigmoid(tw) * 2) ** 2 * anchor_w height = (sigmoid(th) * 2) ** 2 * anchor_h

其中tx, ty, tw, th是模型输出的原始值,grid_x, grid_y是网格坐标,stride是特征图的下采样倍数,anchor_w, anchor_h是预设的锚框尺寸。

置信度过滤就是设定一个阈值,比如0.25,把obj_conf低于这个值的候选框全部丢掉。非极大值抑制则是把重叠度高的框合并,只保留置信度最高的那个。

def non_max_suppression(prediction, conf_thres=0.25, iou_thres=0.45): # 简化版NMS实现 output = [] for pred in prediction: # 过滤低置信度 mask = pred[:, 4] > conf_thres pred = pred[mask] if len(pred) == 0: continue # 按置信度排序 pred = pred[pred[:, 4].argsort(descending=True)] # NMS keep = [] while len(pred) > 0: keep.append(pred[0]) if len(pred) == 1: break iou = compute_iou(pred[0], pred[1:]) pred = pred[1:][iou < iou_thres] output.append(np.array(keep)) return output

实际项目中,我建议直接用YOLOv5官方仓库里的utils/general.py中的non_max_suppression函数,那个实现经过充分测试,支持批量处理和多种参数配置。

4.3 检测结果可视化与保存

拿到检测框之后,用OpenCV画出来:

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.imwrite('result.jpg', frame)

注意坐标要映射回原始图像尺寸。因为预处理时做了letterbox,检测框坐标是在640x640空间下的,需要反向变换回原始分辨率。反向变换的公式是:

x1 = (x1 - dw) / r y1 = (y1 - dh) / r x2 = (x2 - dw) / r y2 = (y2 - dh) / r

其中dw, dh是letterbox时填充的宽度和高度,r是缩放比例。这个映射关系一定要搞清楚,否则画出来的框位置会偏。

5. 常见问题与排查技巧实录

5.1 摄像头打不开的几种原因

摄像头打不开是最常见的问题,表现是cap.isOpened()返回False。排查思路如下:

现象可能原因解决方法
/dev/video*不存在驱动未加载或硬件未识别检查USB连接,`dmesg
权限拒绝用户不在video组sudo usermod -aG video $USER后重新登录
设备节点编号错误多个video节点用v4l2-ctl --list-devices确认
打开后读帧失败摄像头被其他进程占用fuser /dev/video1查看占用进程
图像花屏或绿屏格式不匹配设置FOURCC为MJPG

我遇到过一次特别诡异的情况:摄像头在/dev/video1上能打开,但读出来的帧全是绿色的。后来发现是FOURCC设置的问题,默认的YUYV格式在某个分辨率下驱动有bug,改成MJPG就正常了。

5.2 推理结果异常排查

推理结果异常通常表现为:检测框位置偏移、类别错误、置信度异常低。

检测框位置偏移最常见的原因是letterbox的反向映射没做对。检查一下dw, dh和r的计算是否正确,特别是当原始图像宽高比和640x640不一致时,填充量计算容易出错。

类别错误可能是类别标签映射错了。YOLOv5的COCO数据集有80个类别,索引从0到79。如果你自己训练的模型类别数不一样,要确保class_names列表和模型输出对应。

置信度异常低可能是预处理的问题。检查一下输入数据的归一化是否正确,uint8和float32有没有搞混。我有一次把float32的归一化数据喂给了期望uint8的RKNN模型,结果所有检测框的置信度都低于0.1,排查了半天才发现是数据类型的问题。

5.3 性能优化的一点经验

香橙派RK3588的NPU算力足够跑YOLOv5s实时推理,但前提是预处理和后处理不能成为瓶颈。

预处理的瓶颈通常在cv2.resize和cvtColor上。如果分辨率是1920x1080,这两个操作在CPU上跑会占用不少时间。我的做法是在摄像头层面直接设置成640x480,省掉resize的开销。如果摄像头不支持,那就用cv2.INTER_NEAREST插值,比默认的INTER_LINEAR快不少。

后处理的瓶颈在NMS上。如果检测框数量很多,NMS的循环会很耗时。可以用numpy向量化操作来加速,或者用Cython写一个扩展。不过对于YOLOv5s在640x640输入下的25200个候选框,numpy版本的NMS通常在几毫秒内完成,问题不大。

还有一个容易被忽略的点是内存拷贝。从摄像头读到帧之后,如果经过多次copy()操作,会累积不少开销。尽量用原地操作,比如img = img.transpose(2, 0, 1)之后直接传给推理接口,不要额外np.ascontiguousarray(),除非推理接口明确要求连续内存。

注意:RKNN模型在首次推理时会有一个预热过程,耗时明显比后续推理长。如果你在测帧率,记得把前几帧排除掉,否则数据不准。

5.4 摄像头热插拔与异常恢复

在实际部署中,摄像头可能会因为各种原因断开连接,比如USB接触不良、供电不足等。如果脚本没有异常处理,一旦摄像头断开,整个程序就会崩溃。

我的做法是在主循环里加一个重连机制:

def get_camera(cap, device_id): if cap is None or not cap.isOpened(): cap = cv2.VideoCapture(device_id) if cap.isOpened(): cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter_fourcc('M', 'J', 'P', 'G')) return cap cap = None while True: cap = get_camera(cap, 1) if cap is None or not cap.isOpened(): time.sleep(1) continue ret, frame = cap.read() if not ret: cap.release() cap = None time.sleep(1) continue # 推理处理...

这段代码的逻辑是:如果摄像头没打开或者读帧失败,就释放资源,等一秒后重试。这样即使摄像头临时断开,程序也能自动恢复,不需要人工干预。

6. 从单帧抓取到视频流推理的扩展思路

单帧抓取跑通之后,扩展到视频流推理其实很简单,就是把抓帧和推理放到一个循环里。但这里有几个实际问题需要考虑。

首先是帧率匹配。如果摄像头输出30帧,但推理只能跑15帧,那就会累积延迟。我的做法是丢帧处理,每两帧取一帧做推理,保证实时性。或者用多线程,一个线程专门抓帧,另一个线程专门推理,中间用一个队列做缓冲。

其次是显示问题。如果板子接了HDMI显示器,可以用cv2.imshow直接显示。但如果是无头模式,就需要把结果推流出去,或者保存成视频文件。香橙派RK3588支持硬件编码,可以用FFmpeg或者GStreamer把处理后的视频流推出去,CPU占用比软件编码低很多。

最后是模型切换。YOLOv5s跑通之后,你可能会想换更大的模型,比如YOLOv5m或者YOLOv5l。这时候要注意NPU的内存限制,RK3588的NPU内存是共享的,模型太大可能会初始化失败。我实测YOLOv5s在RK3588上跑得很流畅,YOLOv5m也能跑,但帧率会下降不少。

我个人在实际操作中的体会是,摄像头抓帧这个环节看似简单,但细节特别多。不同型号的摄像头行为不一样,同一个摄像头在不同分辨率下的表现也不一样。最稳妥的做法是先用v4l2-ctl把摄像头的所有支持格式列出来,然后从中选一个最合适的,而不是盲目设置参数。另外,预处理和后处理的代码一定要和模型转换时的配置严格对应,否则推理结果会莫名其妙地出错。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询