把YOLOv5模型从训练环境搬到树莓派5上跑目标检测,最直接的办法当然是在树莓派上装PyTorch,然后老老实实跑detect.py。但真走到部署这步,你会发现这条路在ARM开发板上走得太不顺:依赖装起来费劲,推理慢,内存动不动就吃紧。我最后选的方案是先把YOLOv5导出成ONNX格式,再用ONNX Runtime在树莓派5上做推理。整套流程跑通之后,单次前向推理的速度、内存占用、稳定性都明显比硬跑PyTorch舒服。这篇就把我的完整流程整理一下,从PC端导出模型、验证,到树莓派5上装环境、写推理代码、调优和排坑,全程都是可以直接照着做的操作,适合正在做边缘部署、低成本视觉方案,或者刚拿到树莓派5不知道从哪下手的开发者。
1. 为什么非转ONNX不可:先从部署思路说起
1.1 直接跑PyTorch的问题在哪
如果你的项目只是想在PC上验证模型效果,PyTorch当然没问题。但要部署到树莓派5这种ARM环境,PyTorch就成了一个很重的包袱。首先是安装问题,树莓派官方系统自带的Python版本和PyTorch的whell 不一定对齐,宝石系统不到位就编译安装,一套编译下来不仅耗时间还容易把系统环境莫名其妙搞乱。哪怕你运气好装上了,推理时每一个算子都要经过Python运行时调度,启动时还要加载计算图,模型越大开销越明显。
树莓派5的CPU比前代强了不少,四核Cortex-A76,但它的优势不在单核爆发力,而在整体功耗控制和并行处理能力。PyTorch在x86上有编译优化,在ARM上很多算子并没有做到同等优化,跑起来往往发挥不出硬件全部能力。再加上训练好的模型里可能包含大量仅训练阶段需要的逻辑,直接部署等于带着一堆用不上的包袱上场。
所以我个人的习惯是:训练归训练,部署归部署。训练用PyTorch没问题,但部署时我会把模型转成更适合目标环境的中间格式,再交给轻量推理引擎。这个思路在工业界已经很成熟,边缘设备上加一步转换,换来的是安装简单、启动快、推理稳定的综合收益。
1.2 ONNX为什么适合树莓派5
ONNX本质上是一个开放的模型交换格式,不管模型是用PyTorch、TensorFlow还是其他框架训练出来的,都能导成ONNX。它对部署最大的意义在于,模型的计算图被标准化了,不依赖某个特定框架的Python环境也能跑推理。
在树莓派5上,我更看重ONNX Runtime三点优势:
- 轻量。ONNX Runtime的ARM版安装包干净,依赖少,装完不拖泥带水。
- 有专门的ARM优化。它在CPU上针对不同指令集做了算子优化,树莓派5支持Armv8架构,ONNX Runtime能利用这条指令集的特性。
- 后续还能继续做量化。边缘设备存储和算力都有限,模型瘦身几乎是必须的,ONNX的量化流程相对成熟。
转换一次模型之后,PC、树莓派、其他ARM设备都能用同一个ONNX文件跑,部署路径就统一了。
1.3 树莓派5的硬件适配套路
树莓派5目前常见的是4GB和8GB内存版本,CPU是Broadcom BCM2712,四核Cortex-A76,最高频率2.4GHz左右,跟树莓派4B的Cortex-A72相比代差明显。CPU支持NEON SIMD指令,这对跑卷积其实挺关键,ONNX Runtime在ARM上能利用NEON加速部分算子。
但要注意,我们在树莓派上默认是CPU推理,不是GPU推理。树莓派虽然有VideoCore图形核心,但拿它做通用计算支持有限,所以目标检测这种任务还是得靠CPU扛。这就决定了你能跑多大模型,完全取决于CPU吞吐量。
我实测下来的经验是,YOLOv5s配合640输入尺寸,树莓派5上能跑到可用的程度,但如果想做得更实时,就需要用小模型、降分辨率或者量化。后面我会专门讲优化,这里先记住一个原则:树莓派5适合做边缘侧轻量推理,不适合跟桌面级GPU比,模型选型要一开始就控制住量级。
2. 环境准备:PC端和树莓派端分别要装什么
2.1 PC端导出环境
导出模型这一步不需要GPU,CPU就够用,但Python环境和依赖版本要注意。我用的是Python 3.10,PyTorch用了2.x的CPU版本,TensorFlow不用碰,导出YOLOv5只需要PyTorch生态。
建议在PC上独立建一个虚拟环境,不要直接在系统Python里装一堆包,后面换项目容易冲突:
python -m venv yolov5_export source yolov5_export/bin/activate pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu然后拉取YOLOv5官方仓库,装依赖:
git clone <你的YOLOv5仓库地址> cd yolov5 pip install -r requirements.txt这里有个细节,requirements.txt里包含了训练、验证、导出需要用到的所有依赖,包括pandas、matplotlib、seaborn这些,如果只想导出,会觉得装多了。但官方导出脚本会检查依赖,所以我一般直接全装,省得后面缺什么补什么。
装完后在PC上先跑一次简单的推理,确认模型权重能正常加载。这一步别跳,很多问题如果从一开始就乱了,后面排查起来会非常费劲。
2.2 树莓派5系统与Python环境
树莓派5的操作系统,我建议直接使用官方64位Raspberry Pi OS,或者Ubuntu Server 24.04 ARM版。两个我都试过,官方系统对树莓派5的外设支持最好,Ubuntu Server则更接近服务器习惯,适合无桌面部署。
系统装好后,Python版本一般是3.11。需要提醒一下,不要用系统Python全局装包,一定用虚拟环境隔离。树莓派的系统包管理器NPK有时候会升级Python,全局环境很容易被搞乱:
python3 -m venv ~/rpi_deploy source ~/rpi_deploy/bin/activatepip在国内网络环境下经常卡,直接配一个国内镜像源能省非常多时间:
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple2.3 安装ONNX Runtime
ONNX Runtime在aarch64架构上的安装比PyTorch简单太多,直接pip安装就行:
pip install onnxruntime装完快速验证一下是否可用:
python -c "import onnxruntime; print(onnxruntime.__version__); print(onnxruntime.get_available_providers())"只要能正常打印版本号和AvailableProviders列表,说明基础环境没问题。这里留意一下,树莓派5上可用Provider一般就是CPUExecutionProvider,别指望有GPU相关的Provider,这在后面推理配置时要写对。
3. YOLOv5导出ONNX的全过程
3.1 准备权重文件
导出的前提是你已经有一个训练好的权重文件,一般叫best.pt。如果你还没有,先用官方仓库提供的预训练权重也可以,流程一样。把权重直接放到YOLOv5仓库目录下,方便命令行引用。
3.2 导出命令与参数解读
YOLOv5官方仓库里的export.py就是干这个的。最常用的导出命令:
python export.py --weights best.pt --imgsz 640 --batch 1 --opset 12 --dynamic --simplify --include onnx一个个解释一下这些参数,因为后面你肯定会根据自己的模型调整:
- --weights:训练的权重路径。
- --imgsz:模型输入尺寸,YOLOv5默认训练是640,导出时保持一致。如果你打算后面用416推理,导出时也可以直接用416,但精度会有点差异。
- --batch 1:导出时batch设为1,部署时一般就是单张图推理。
- --opset 12:ONNX算子集版本。太低会缺算子支持,太高又可能遇到树莓派ONNX Runtime版本兼容问题。12是比较稳的折中选择。
- --dynamic:让ONNX模型的输入尺寸和batch维度变成动态的。如果固定输入尺寸,可以不要这个参数,固定尺寸在推理时效率更高。
- --simplify:用onnx-simplifier对计算图做简化和常量折叠,能减小模型体积,去掉一些冗余节点。
- --include onnx:只导出ONNX格式。
导出成功后会生成best.onnx文件,这就是我们后面要部署的模型。
3.3 导出后先在PC上验证
导出完成不等于模型就能直接用,这一步很多人直接跳过了,结果部署到树莓派上才发现问题。建议先在PC上用ONNX Runtime推理同一张测试图,确认输出跟PyTorch基本一致。
验证有一个简单粗暴的方式:把模型用ONNX Runtime跑一遍输出张量,再和PyTorch inference的输出比一下shape和大概的数值范围。YOLOv5的输出是三维张量,具体来说是1×25200×85(假设是COCO 80类且输入640,三个检测尺度的anchor总数是25200),前5个值对应坐标和置信度,后面是类别得分。只要张量形状对、数值范围正常,通常就没大问题。
我踩过的一个坑是:简化操作之后模型计算图里的节点名变了,Python脚本里如果依赖节点名去定位输入输出,会报找不到,所以后面读取输入输出的时候不要硬编码节点名,用session.get_inputs()动态获取。
3.4 常见导出报错与处理
导出过程最常见的一类报错是“export failure”后面跟着一堆Traceback。多数情况下逃不开这几个原因:
- Python版本太高或太低,YOLOv5官方仓库对Python 3.12的支持有段时间不完善,建议3.9到3.11之间。
- torch版本和仓库代码兼容性有问题,torch 2.0以上的某些版本需要相应升级仓库代码,拉最新的release分支一般能解决。
- 滤波器参数算错、yml配置读取失败这些相对少见,但会在导出时暴露出来,问题出在训练后权重与模型结构不匹配。
我的经验是:导出报错时,先看Traceback最后几行,不要被大段堆栈信息吓到。绝大多数情况下,把torch升级到仓库要求的兼容版本,问题就解决了一半。
4. 树莓派5上的推理代码:从预处理到后处理
4.1 推理流程总览
ONNX模型只是个静态计算图,真正的工程难点在输入输出的处理和业务逻辑的衔接。完整流程可以拆成四步:
- 读取图片,做letterbox处理。
- 把图像数据从HWC转CHW,BGR转RGB,归一化到0到1。
- 送进ONNX Runtime推理,得到原始输出张量。
- 后处理:置信度过滤、NMS、坐标还原到原图。
git clone这里不展开了,后面每步都有代码。
4.2 Letterbox预处理
为什么用letterbox而不是直接resize?因为模型训练时输入图像就是letterbox之后的画面。简单resize会把目标拉变形,导致检测框的坐标分布和训练时不一样,精度明显下降。letterbox的做法是把图像按比例缩放到目标尺寸内,剩余区域填充灰边(通常用114这个RGB值填充,因为训练时用这个值做padding)。
import cv2 import numpy as np def letterbox(img, new_shape=(640, 640), color=(114, 114, 114), stride=32): shape = img.shape[:2] if isinstance(new_shape, int): new_shape = (new_shape, new_shape) r = min(new_shape[0] / shape[0], new_shape[1] / shape[1]) ratio = r, r new_unpad = int(round(shape[1] * r)), int(round(shape[0] * r)) dw = (new_shape[1] - new_unpad[0]) / 2 dh = (new_shape[0] - new_unpad[1]) / 2 if shape[::-1] != new_unpad: 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, ratio, (dw, dh)关键是同时要把ratio和pad存下来,后处理还原坐标时要用。
4.3 ONNX Runtime推理主代码
树莓派上的推理代码很简短,难的是细节。ONNX Runtime以Session为单位管理模型,输入输出都按Numpy数组格式走:
import onnxruntime as ort sess = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name output_name = sess.get_outputs()[0].name def preprocess(img): img, ratio, pad = letterbox(img, (640, 640)) img = img[:, :, ::-1].transpose(2, 0, 1) img = np.ascontiguousarray(img) img = img.astype(np.float32) / 255.0 img = np.expand_dims(img, 0) return img, ratio, pad img0 = cv2.imread("test.jpg") tensor, ratio, pad = preprocess(img0) out = sess.run([output_name], {input_name: tensor})[0]注意几点:
- 树莓派上图片数据量不大,但一定要确保数组连续内存布局,ascontiguousarray能避免很多隐形性能问题。
- 输入数据必须是float32,YOLOv5的ONNX模型默认输入就是NCHW的float张量。用float64会被报错,而且推理速度会异常慢。
- providers参数不要省略,虽然默认就是CPU,但显式写清楚不会有歧义。
4.4 后处理:从原始张量到检测框
YOLOv5的原始输出张量,以640输入、COCO 80类为例,shape是1×25200×85。25200是三种尺度下所有anchor的总数,85的意思是每个anchor有4个坐标值(cx, cy, w, h)、1个目标置信度、80个类别的分类得分。
先解析坐标和置信度,过滤掉置信度过低的框,再做NMS(非极大值抑制),最后把框的坐标还原到原始图像上:
def postprocess(out, ratio, pad, conf_thres=0.25, iou_thres=0.45): pred = out[0] # 去掉batch维度 xc = pred[:, 4] > conf_thres # 目标置信度过滤 pred = pred[xc] boxes = [] scores = [] class_ids = [] for det in pred: cx, cy, w, h = det[:4] score = det[4] cls_scores = det[5:] cls_id = int(np.argmax(cls_scores)) cls_score = cls_scores[cls_id] final_score = score * cls_score if final_score < conf_thres: continue x1 = cx - w / 2 y1 = cy - h / 2 x2 = cx + w / 2 y2 = cy + h / 2 boxes.append([x1, y1, x2, y2]) scores.append(final_score) class_ids.append(cls_id) if len(boxes) == 0: return [], [], [] boxes = np.array(boxes) scores = np.array(scores) # 坐标还原到原图 x1 = (boxes[:, 0] - pad[0]) / ratio[0] y1 = (boxes[:, 1] - pad[1]) / ratio[1] x2 = (boxes[:, 2] - pad[0]) / ratio[0] y2 = (boxes[:, 3] - pad[1]) / ratio[1] boxes = np.stack([x1, y1, x2, y2], axis=-1) # NMS,这里用简单的基于IoU的实现,也可以用nms库 indices = nms(boxes, scores, iou_thres) return boxes[indices], scores[indices], np.array(class_ids)[indices]NMS也可以直接用ONNX Runtime自带的算子实现,但我更推荐在Python里用numpy实现。原因很简单,树莓派版本上调试方便,逻辑透明。等确实有性能瓶颈了再考虑用算子替换。
4.5 第一版跑通的实测参考
先给一个参考区间:在树莓派5上,YOLOv5s、640输入、ONNX Runtime默认配置,单张图片从读到出结果的推理耗时大概在1到2秒之间。这个数字会因为系统负载、散热条件、线程数设置和模型微调而变化,所以别拿着我的数字去跟别人比较,关键看你自己项目里能不能接受这个速度。
如果只是做单张图片检测,这个速度完全能接受。但做视频流或实时摄像头检测就不太够,得走优化,这就是下一章的内容。
5. 性能优化:让树莓派5跑得更快
5.1 从模型和输入尺寸下手
性能优化的优先级,我排在第一位的一定是模型选型。YOLOv5系列从大到小有好几个规格,树莓派5上最合适的其实是YOLOv5s和YOLOv5n。YOLOv5n比v5s轻快不少,精度降低一点,但在树莓派上跑起来流畅度明显更好。
第二个优先项是输入分辨率。640降到416或者320,推理耗时会大幅下降。用416输入时,anchor数量和计算量比640少很多,如果你的目标检测物体不是特别小,416是个很实用的平衡点。我测过的几个项目里,416输入在树莓派5的推理延迟基本能控制在1秒以内。
表格里放一下我比较常用的调优组合:
| 模型 | 输入尺寸 | 推理耗时参考 | 适用场景 |
|---|---|---|---|
| YOLOv5n | 416 | 约0.4-0.8秒 | 实时性要求较高 |
| YOLOv5s | 416 | 约0.6-1.2秒 | 综合平衡 |
| YOLOv5s | 640 | 约1-2秒 | 精度优先 |
| YOLOv5m | 640 | 约2秒以上 | 很少用,不推荐 |
这里的耗时是CPU全核推理的参考,实际会浮动。选择策略很简单:先用你现有模型跑一遍,如果延迟可接受就不动,不可接受就先换小模型,再降分辨率,最后才考虑量化。
5.2 ONNX Runtime线程和配置调优
ONNX Runtime有一个容易被忽视的参数:intra_op_num_threads。它控制算子内部并行用的线程数,在树莓派5四核CPU上,把线程数设为4通常能获得最好的吞吐:
sess_options = ort.SessionOptions() sess_options.intra_op_num_threads = 4 sess_options.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL sess = ort.InferenceSession("best.onnx", sess_options, providers=["CPUExecutionProvider"])graph_optimization_level默认是ENABLE_ALL,但如果你的onnxruntime版本比较旧,最好显式指定一下。优化等级越高,计算图在加载时被简化的程度越大,推理速度会有些微提升。
另外还有一个思路是threads = 0表示让ORT自己决定,但我在树莓派上试过,有时候计数器调度不如手动指定为4稳。如果你跑的是多路并发,可能还要留出主线程的资源,这时线程数设3更合适,具体情况以压测为准。
5.3 模型量化方向
树莓派5上的模型量化主要分两种:动态量化和静态量化。动态量化对权重做int8/uint8处理,推理时动态反量化,实现简单但加速有限。静态量化需要一批校准数据估算激活值范围,加速更明显,但对算子的支持要求更高。
YOLOv5转出来的ONNX模型,主体是卷积和上采样,量化适配性整体不差。树莓派5的CPU支持int8计算加速,ONNX Runtime在ARM上对int8算子有NEON优化,IPC吞吐会明显提升。我实际试过YOLOv5s量化后的版本,体积从14MB压到4MB左右,推理速度也能提升不少,但检测精度确实会下降。具体下降多少,和你校准数据集的质量强相关,所以量化前一定要留一批有代表性的图片做测试。
这里提醒一句:量化不是万能的。如果某个算子量化后跑出来的结果全是NaN,检查一下这个算子是否支持int8,不支持的算子可以用mixed precision方式保留float计算。别一上来就追求全图量化,走偏了容易把自己坑进去。
5.4 硬件层面:别让CPU过热降频
树莓派5性能比4强,但发热也明显变大。CPU在高负载推理时,如果温度超过85度左右,系统会自动降频保护,推理速度反而比正常情况慢很多。我一开始没装散热,连续跑推理的时候温度和延迟明显异常,后来加了风扇散热片组合,速度稳定了很多。
所以如果你的项目是长时间连续推理,散热绝对是必选件,不是可选项。另外电源也要用官方推荐的5V/5A电源,供电不足会导致USB外设异常和CPU性能波动,这个坑很隐蔽。
6. 排查实录:树莓派5部署的常见问题速查表
最后整理一下我在实际部署中遇到的高频问题和解决思路,做成一张速查表方便收藏:
| 现象 | 可能原因 | 解决办法 |
|---|---|---|
| pip安装onnxruntime失败 | Python版本过旧或wheel不匹配 | 确认系统是64位,Python 3.11,再重新pip install |
| 导入onnxruntime报缺少libpython3.so | 系统Python环境混乱,或使用了源码编译的Python | 重建虚拟环境,使用系统自带Python |
| 推理输出全是NaN或维度不对 | 预处理写错,输入没有归一化或没有转float32 | 检查ndarray的dtype和shape,用ascontiguousarray确保内存连续 |
| 检测框位置偏移明显 | letterbox还原坐标时ratio、pad算错 | 重新对比letterbox和还原公式,注意dw、dh是向下取整还是向上取整 |
| 推理速度突然变慢 | CPU过热降频,或线程数设置不合理 | 加散热,检查intra_op_num_threads设置 |
| 内存使用过高,甚至OOM | 输入尺寸太大、模型太大、测量时不释放旧数组 | 降低imgsz,换更小模型,释放不再用的numpy数组 |
| 导出时报“export failure” | torch版本与仓库代码兼容问题 | 查看Traceback尾部,升级或回退到仓库要求的torch版本 |
| 量化后精度严重下降 | 校准数据不具代表性,或量化敏感算子被一刀切 | 更换校准数据集,敏感算子保留float计算 |
单独说两个我踩得比较深的坑。
第一个是关于内存释放。树莓派内存虽然到8GB了,但推理是连续的视频流,图像帧和中间结果如果不及时释放,内存会被吃光。尤其是numpy数组,Python里看起来没有了,但引用还有残留,垃圾回收不及时就会越跑越卡。建议在循环里定期del掉已经没有用的帧,再显式gc.collect()。
第二个是关于模型文件的可复现性。导出的ONNX模型跟导出时的参数强相关,opset版本、输入尺寸、simplify处理,任何一个变了,模型文件对不上。我后来给自己的要求是:每个导出的模型旁边一定要放一个txt,写明权重版本、导出命令、opset、输入尺寸、onnxruntime版本。否则过了三个月,你可能完全不记得这个onnx是怎么来的,想迭代却根本无从下手。
部署到这一步,你已经能在树莓派5上稳定跑YOLOv5目标检测了。无论是做图像采集点的识别、流水线视觉质检,还是拿来做低成本实验平台,这套流程的扩展性都不错。如果后面想要更高帧率,可以继续走量化或者调用树莓派5的硬件加速能力,但基础打通之后,那都是水到渠成的事。