ARTICLE DETAIL

资讯详情

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

香橙派5实战:从图片到摄像头,YOLOv5s实时目标检测全流程指南

香橙派5实战:从图片到摄像头,YOLOv5s实时目标检测全流程指南 从“图片测试通过”到“摄像头能出画面”中间其实隔着一条不小的沟。因为跑图片推理的时候数据是现成的喂进去就行但摄像头是外部硬件要经过驱动识别、节点选择、格式协商、抓帧这一大串环节任何一个地方卡住模型再好都白搭。这篇是在前面教程基础上的第09篇硬件依旧是香橙派5RK3588模型依旧是YOLOv5s不过目标从“检测一张测试图片”变成了“打开摄像头抓一帧推理把框画出来”。听起来简单实际推进的时候踩了好几个坑我把完整的自检流程、核心代码和排查过程都整理在下面照着做基本能少走一半弯路。想直接看代码的可以直接跳到第4节想在动手前搞明白摄像头和推理链路怎么配合的建议从头看。1. 为什么前8篇都过来了这一篇才接摄像头先说个背景。前面的教程里我们已经完成了系统烧录、Python环境搭建、YOLOv5源码下载、权重文件准备、用detect.py跑了静态图片这几件事。按理说YOLOv5这套东西已经能跑了。但这个“能跑”是指你把一张图给它它给你吐一张带框的图。真实项目里没有谁会拿张照片来检测都是要处理摄像头实时画面。之所以把摄像头放到第9篇才讲是因为“增加一个摄像头外设”这个动作会把原来纯软件的事情一下子拉回到硬件层面。内核有没有识别到你的摄像头芯片这里涉及UVC协议和驱动。设备节点是/dev/video0还是/dev/video1多摄像头设备经常在这里翻车。OpenCV的VideoCapture能不能跟摄像头完成格式协商比如MJPG、YUYV协商失败就直接打不开。抓出来的帧格式是BGR还是RGBYOLOv5训练时用的是RGBOpenCV默认读出来的是BGR这个不一致会让颜色通道错乱。手机或PC上用QQ视频、会议软件时觉得摄像头“插上就能用”是因为系统已经把底层驱动和格式协商处理完了。但到了香橙派这种嵌入式板子上尤其是精简系统、或者自己编译的固件里很多环节都暴露在你面前每个都是一道坎。另外还要说一下这次我用的是USB免驱摄像头市面上常见的海康威视、罗技C270、还有那种几十块的USB工业相机都是UVC协议插上就能被Linux识别。为什么不选MIPI CSI接口的摄像头比如树莓派OV5647那种摄像头类型接口优点缺点适合场景USB UVC摄像头USB 2.0/3.0即插即用免驱或半免驱OpenCV直接支持延迟稍高CPU参与传输线缆容易松动快速起步、原型验证MIPI CSI摄像头MIPI CSI-2排线延迟低画质稳定带宽独立香橙派5需要特定转接板设备树配置复杂不同板子驱动不通用量产级视觉方案RTSP网络摄像头以太网/Wi-Fi布线方便距离远多路复用需要网络环境推流延迟受带宽影响分布式监控、远程巡检如果你手头只有MIPI摄像头也不是不能用但需要提前确认香橙派5的内核和设备树是否支持还要配置dts节点这个作为单独话题展开比较合适。基础教程阶段老老实实上USB摄像头是最稳的路线一次配好后面所有例子都能复用。还有有些朋友问我“RK3588不是有NPU吗为什么还不直接上NPU推理”。原因很简单原生YOLOv5是PyTorch框架NPU不认识PyTorch模型的算子需要先用RKNN-Toolkit2做模型转换和量化再走RKNN Runtime接口。那一步涉及较多工具链适配。这一篇先把PyTorch版本的推理链路跑通之后单独开一篇讲NPU转换。硬件平台不变模型不变先有流程再优化性能顺序不会错。2. 动手前先过一遍自检清单30秒定位摄像头问题很多新手接到“打开摄像头”这个任务第一反应就是直接开写cap cv2.VideoCapture(0)然后跑然后报错然后不知道该干嘛。我自己的习惯是先花半分钟做系统层面的检查确认板子真的“看见”了摄像头再去碰代码。2.1 用 lsusb 确认硬件枚举lsusb正常的情况下会看到一行类似Bus 002 Device 003: ID 0c45:636b Microdia USB Camera0c45这类ID是常见的摄像头芯片厂商标识。如果这行都没有说明物理链路就有问题先不要纠结代码试试换一个USB口优先USB3.0口有些USB2.0口供电不够摄像头初始化失败。确认线材是不是只供电不通数据。遇到过好几次充电线当数据线用灯亮但枚举不上。拔插之后再跑一次lsusb排除接触不良。热词里出现频率很高的“ubuntu连接不到摄像头”十有八九就是死在物理枚举这一关。2.2 确认设备节点接着看节点编号ls /dev/video*如果板子上没有别的摄像头一般是/dev/video0有时候会系统同时生成/dev/video1、/dev/video2这实际上是同摄像头的不同元数据节点通常用video0就可以了。多摄像头场景下如何区分video0、video1这类的设备编号是很多开发者的痛点这部分后面找机会单独展开。这一篇先专注单摄像头单节点。2.3 用 v4l2-ctl 看摄像头支持的能力Linux自带的v4l2-ctl工具非常好用如果系统没装先补一下sudo apt install v4l-utils -y v4l2-ctl --list-devices v4l2-ctl -d /dev/video0 --list-formats-ext这个命令会罗列摄像头支持的像素格式与分辨率。有些摄像头只支持YUYV 640x480你却在代码里非要设1280x720OpenCV不会报错但默默不生效读出来的帧还是640x480。提前看一眼能少怀疑一次人生。2.4 Python 裸测先不加载模型只看它能吐帧python3import cv2 cap cv2.VideoCapture(0) if not cap.isOpened(): print(摄像头打开失败) exit(1) ret, frame cap.read() print(ret, frame.shape if ret else 无帧) cap.release()如果上面这段能输出True (480, 640, 3)之类的形状信息说明摄像头链路完全没问题。这步先跑通再往下面走。从这步开始就可以准备把后面所有摄像头相关代码写进一个脚本了。这一篇的核心思路是“抓一帧推理一帧”跟那种持续跑视频流的逻辑不太一样先把这个差异说清楚再动手改代码。3. 抓一帧和跑视频流底层逻辑不是一回事YOLOv5官方仓库里的detect.py其实写得很完整也支持摄像头输入。你直接运行python detect.py --weights yolov5s.pt --source 0它会一直循环读摄像头帧持续推理并弹窗显示实时画面。你要退出就得按CtrlC。看起来好像这一篇的核心需求已经满足了其实不然。官方detect.py的设计目标是“持续检测”它会一帧接一帧地跑推理中间还包含FPS统计、窗口显示等逻辑。但我们是“抓一帧并推理”的场景典型的比如按一次按键触发一次拍照检测定时巡检测试来了某一帧才去做一次推理嵌入式终端通过串口或GPIO收到外部信号才抓拍一张。这些场景如果直接跑官方detect.py要么得靠外部进程发信号去kill它要么就得改它的循环逻辑非常别扭。更关键的问题是官方脚本把整个“推理管线”封装得太严密新手很难看清楚数据在每一层到底发生了什么变化。所以我在这里坚持自己写一个独立脚本把链路拆开边写边讲道理。YOLOv5的单帧推理链路本质上是五步抓帧从摄像头获得一张BGR格式的原始图像。前处理把输入缩放并填充到模型输入尺寸通常640x640同时保持原始宽高比防止目标变形。张量变换把HWC布局转成CHW把BGR转成RGB归一化到0~1并且补上batch维度。网络推理模型输出一个三维张量维度分别是候选框数量、每个框的坐标、置信度、类别概率。后处理非极大值抑制去掉重复框坐标映射回原始图像绘制边框和标签。其中第2步的letterbox可能是最容易被忽视的。很多人图省事直接resize成640x640结果就是用方形变歪的图像喂给网络detect精度下降明显。letterbox的做法是保持比例缩放图像剩余区域填充灰色像素这样目标虽然变小了一点但形状不变模型识别的可靠性高得多。这个细节官方的detect.py是帮你处理好的自己写代码就必须自己看着办。我再强调一点很多嵌入式板子上的教程告诉你“拿OpenCV读帧然后直接调模型”看起来像是一个整体实际上每一行代码都对应着模型训练时的预处理方式。训练时用letterbox推理时就必须匹配训练时在RGB空间推理时就必须转RGB。链路里任何一个环节和训练时不匹配推理结果都会大打折扣。自己写脚本的另一个好处就是能准确感知这种匹配关系。4. 操刀改造抓一帧并推理的完整代码下面这个脚本就是这一篇的核心产物。它做的事情很简单打开摄像头抓一帧跑一次YOLOv5s推理在原始帧上画出检测框保存结果图。代码基于YOLOv5官方v6.x/v7.x源码结构建议放在官方仓库根目录下运行这样models、utils这些包能直接导入。# single_shot_camera.py import cv2 import time import torch import numpy as np from pathlib import Path from models.experimental import attempt_load from utils.augmentations import letterbox from utils.general import non_max_suppression, scale_coords from utils.torch_utils import select_device # 基本参数 weights yolov5s.pt # 换成你自己的best.pt也行 source 0 # /dev/video0 imgsz 640 # 输入尺寸 conf_thres 0.25 # 置信度阈值 iou_thres 0.45 # NMS IoU阈值 save_path camera_result.jpg # 设备选择香橙派上没有NVIDIA GPU老老实实用CPU device select_device(cpu) torch.set_num_threads(4) # A76核心留2个给系统 # 加载模型 model attempt_load(weights, map_locationdevice) model.eval() # 摄像头初始化 cap cv2.VideoCapture(source) if not cap.isOpened(): print(摄像头打开失败检查节点) exit(1) # 先丢几帧让摄像头完成自动曝光避免第一帧全黑 for _ in range(5): cap.read() # 抓一帧 ret, frame cap.read() if not ret: print(抓帧失败) cap.release() exit(1) original_shape frame.shape[:2] # H, W # letterbox前处理等比缩放 灰边填充 img letterbox(frame, new_shapeimgsz, stride32)[0] # BGR - RGB, HWC - CHW, 归一化 img img[:, :, ::-1].transpose(2, 0, 1) img np.ascontiguousarray(img) img_tensor torch.from_numpy(img).to(device) img_tensor img_tensor.float() / 255.0 if img_tensor.ndimension() 3: img_tensor img_tensor.unsqueeze(0) # 推理 with torch.no_grad(): pred model(img_tensor)[0] pred non_max_suppression(pred, conf_thresconf_thres, iou_thresiou_thres) # 后处理坐标映射回原图 det pred[0] if det is not None and len(det): det[:, :4] scale_coords(img_tensor.shape[2:], det[:, :4], original_shape).round() # 绘制结果 for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 map(int, xyxy) label f{int(cls)} {conf:.2f} color (0, 255, 0) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, label, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 2) cap.release() cv2.imwrite(save_path, frame) print(f推理完成结果保存在 {save_path}共检测到 {0 if det is None else len(det)} 个目标)代码本身不长核心逻辑清晰。有几个地方值得多说一句4.1 为什么 letterbox 要指定 stride32YOLOv5的输出特征图有三个尺度总下采样倍数正好是32倍。letterbox如果不带stride参数填充后尺寸可能不是32的整数倍模型输入尺寸不对会报错。指定了stride32之后它会自动把尺寸向上取整到32的倍数保证网络各层特征图尺寸整除。4.2 为什么先丢5帧摄像头刚打开的瞬间自动曝光和自动白平衡还没稳定前几帧通常是黑的或一片惨白。直接抓第一帧去推理很可能得到一张全黑图片。所以我习惯先read几张扔出去再从第6帧开始正式抓取。这是很多教程不会写的小细节。4.3 BGR、RGB、HWC、CHW容易混的四个字母OpenCV读进来的图是BGR、HWC布局YOLOv5模型运行时需要的是RGB、CHW布局且归一化到0~1。代码里用img[:, :, ::-1]完成了BGR到RGB的翻转用transpose(2, 0, 1)完成了HWC到CHW再除以255归一化。如果这步偷懒模型的类别预测会错乱因为颜色通道输入错了。这也是很多“同样代码为什么我效果差”的悬案原因之一。4.4 为什么要 cap.release() 释放设备摄像头设备是独占资源如果不释放下一次运行脚本就可能报“can not open camera”。特别是在反复调试环节脚本没走到release就异常退出的话那个video0节点会被占用一段时间必须等内核超时释放或重启板子才能恢复。调试期建议尽量用try/finally保证释放。到这里本篇文章的核心代码就算给出来了。你可以先跑通看看输出图片里有没有框。第一次跑速度和效果可能都不理想所以下一步我把会遇到的问题集中复盘一遍。5. 排坑实录我从这个流程里走出来踩了不止5个坑作者按下面的内容全部来自实测。香橙派5RK3588的硬件条件决定了它的坑位跟x86 PC不太一样CPU架构、USB控制器、供电方案都有差别。一个在PC上正常运行的脚本拿到板子上可能就是打不开、不出图、巨卡原因各不相同。5.1 摄像头“打不开”/“can not open camera by index 0”按出现的概率排原因有三个。一是权限问题。默认情况下/dev/video0只有root和video组能访问而你的shell用户不一定在video组里。解决办法sudo usermod -a -G video $USER然后重新登录或者直接重启板子。不想重启的话临时给个权限sudo chmod 777 /dev/video0二是系统里其实有多个video节点但你没有选对。查看一下v4l2-ctl --list-devices视频设备名字下面会跟着一串子节点选一个看起来是“主视频流”的节点。三是USB控制器供电不足。香橙派这种主板USB口带大功率外设时初始化容易失败。测试方法很简单把摄像头插到另一个口最好插USB3.0直连的口避开扩展HUB。遇到过好几次摄像头在PC上正常、插板子就枚举不到十有八九是供电问题。5.2 打开成功了但画面花屏或撕裂花屏通常不是模型问题而是帧格式与尺寸不匹配。有些摄像头的出厂默认格式是MJPGOpenCV打开后如果系统里没有对应的解码器就会出现马赛克或者半绿半花的情况。这时候可以强制指定期望的格式cap cv2.VideoCapture(0) 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)设了之后读一帧打印frame.shape确认是否生效。不生效就换分辨率摄像头通常只有固定的几组分辨率比如640x480、1280x720不能随意指定。这在第2.3节查list-formats-ext的时候就能提前避雷。5.3 抓到的帧全黑不是bug大部分时候就是“第一帧曝光未完成”。前面代码里已经加了丢帧逻辑。如果你是自己写循环也建议抓帧之前time.sleep(0.2)或者丢弃前几帧。还有两种情况一是镜头盖没摘别笑真遇到几次二是晚上反光导致自动曝光算法起不来可以用手遮挡镜头一下再试。5.4 速度慢得离谱一帧推理要好几秒先明确一个预期。香橙派5RK3588的CPU是4个A76大核加4个A55小核在纯PyTorch CPU推理下YOLOv5s输入640x640单帧的推理时间大约在0.3~0.8秒之间具体跟板子的散热、频率、内存配置都有关系。跑出1秒上下都属于正常。如果你发现一帧要3秒以上先排查下面几个点确认板子是不是被调度到了小核。可以用cat /sys/devices/system/cpu/cpu*/cpufreq/policy/scaling_cur_freq看看当前频率如果一直在1.8GHz以下可能是系统负载高或过热降频。在脚本里加上torch.set_num_threads(4)给足并发线程后面再考虑“NPU部署”“RKNN模型转换”的效率提升路径。如果4个线程都满负荷再往上加线程其实性能收益很小反而可能因为线程切换变慢。确认用的是yolov5s.pt而不是m或l版本。之前试过拿yolov5l.pt模型里的l版大约200MB在板子上跑单帧推理4秒以上内存也吃紧。低算力平台选模型要克制。5.5 内存不足推理到一半进程被杀香橙派5有4GB/8GB/16GB内存版本如果你用的是4GB版本加载PyTorch约800MB YOLOv5s权重约14MB 输入张量 OpenCV缓冲加上系统本身内存确实会比较紧张。如果同时开了桌面环境非常容易触发OOM。建议终端命令行启动不要进桌面环境推理前用free -h检查剩余内存如果实在不够把imgsz从640降到480显存占用会减少很多速度也更快代价只是小目标检测率下降。5.6 detect.py 弹窗显示的问题香橙派如果通过SSH远程连接操作不带图形环境cv2.imshow会直接报错或者无输出。所以我在代码里没有用imshow显示而是把结果imwrite保存到本地然后用scp拉回来查看。如果是接了HDMI屏幕、在板子上本地操作改成cv2.imshow也没有问题。远程调试是这个阶段比较省事的做法不用来回切环境。这些坑看着零散但几乎每一个都有明确的排查方向和解决套路。把自检清单跑一遍再对照上面这几条大部分问题都能在5分钟内定位。6. 从单帧到连续帧只需要加一个while循环单帧抓取推理跑通之后很多人第一反应就是“我要看实时效果”。这时候把代码改造成连续帧循环其实非常简单。核心就三件事把cap.read()放进while True循环每次推理完把绘制结果imshow出来按q键退出并释放资源。while True: ret, frame cap.read() if not ret: continue # 前处理 img letterbox(frame, new_shapeimgsz, stride32)[0] img img[:, :, ::-1].transpose(2, 0, 1) img np.ascontiguousarray(img) img_tensor torch.from_numpy(img).float() / 255.0 img_tensor img_tensor.unsqueeze(0) with torch.no_grad(): pred model(img_tensor)[0] pred non_max_suppression(pred, conf_thres, iou_thres) det pred[0] if det is not None and len(det): det[:, :4] scale_coords(img_tensor.shape[2:], det[:, :4], frame.shape[:2]).round() for *xyxy, conf, cls in reversed(det): x1, y1, x2, y2 map(int, xyxy) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f{int(cls)} {conf:.2f}, (x1, y1 - 5), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) cv2.imshow(yolov5s, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()日志里如果输出帧率不稳定多半是受推理耗时本身影响而不是摄像头帧率低。要算FPS的话在循环首尾各取一次time.time()用1 / (end - start)就行。值得留意的是连续帧循环里每帧都跑一次实时推理CPU占用率会很高。有人会在这里加“跳帧”策略逻辑也很朴素摄像头抓帧是实时的但检测不必每帧都执行可以每抓3帧只推理1帧让占用率和响应速度达到一个可接受的平衡。这个策略实际工程中很常见下次做视频流项目时可以直接套用。再说一个后续方向。如果要在香橙派5上把YOLOv5s的推理性能真正拉满纯CPU这次只是“跑通”下一步就是走RK3588自带的NPU。实现方式不是重新写一套逻辑而是用RKNN-Toolkit2把.pt权重转换成.rknn格式再用Rockchip的Python推理库去加载。那段链路跟本篇的代码风格接近但多了模型预处理、量化、板端验证几个步骤到时候单独开一篇工程量不比这一篇小。我个人建议是这篇的单帧推理脚本别看它简单它是后面所有“触发式检测”功能的地基。你想做按键抓拍、定时巡检、串口/GPIO联动拍照全部是从“抓一帧推一帧”这个模式延伸出来的。把今天这条链路吃透摄像头这块的架子就算搭稳了。
返回列表