☰
树莓派上跑YOLO模型:从ONNX转换到实时检测的完整部署指南
2026/10/2 8:44:56 网站建设 项目流程

1. 为什么非要用树莓派跑YOLO:场景与选型思考

如果你和我一样,在电脑上用GPU训好了YOLO模型,然后想让它在树莓派上跑起来,这件事听起来简单,真正动手会发现全是细节。

先说清楚一个很多人不愿意面对的事实:树莓派不是做边缘推理性能最强的板子,但它可能是综合折腾成本最低、资料最全、社区生态最成熟的板子。我见过有人用树莓派4B做车间设备状态识别,有人拿它做宿舍门口的猫脸门禁,还有人直接拿来当毕设的硬件载体。这些场景都有一个共同点:不需要每秒处理30帧,不需要4K实时流,但需要低功耗、离线运行、稳定不宕机。

树莓派4B的CPU是BCM2711,四核Cortex-A72,主频默认1.5GHz,可以超到1.8GHz。没有独立NPU,没有GPU加速(那个VideoCore VI活在很底层的地方,别指望它跑YOLO)。所以你的整个推理负载全部压在CPU上。这意味着什么?意味着模型选型和优化比训练时更重要,YOLOv8x这种大模型想都别想,老老实实回到轻量级模型这条路上。

很多人纠结一个问题:既然树莓派算力这么有限,为什么不用Jetson Nano,或者干脆买个RK3588的板子?我的看法是,算力高当然好,但价格、功耗、开发资料、上手难度是四座大山。Jetson Nano的Maxwell GPU跑TensorRT确实猛,但那个镜像、SDK Manager、CUDA版本之间的相互折磨,不是每个人都能用周末时间搞定的。树莓派上你只需要一个apt update和一个pip install就能开始干活,这种“开箱即用”的体验,决定了它更适合做验证原型和教学平台,也适合预算有限的个人项目。

另一个让人意外的好处是生态兼容性。你训练YOLO时用的PyTorch、Ultralytics这些库,在树莓派上都有ARM版本的wheel包。ONNX Runtime官方提供aarch64的安装包,TFLite也有对应的轮子。这种“训练环境到推理环境无缝迁移”的顺畅感,恰恰是其他边缘硬件最稀缺的东西。

所以我的结论很明确:如果你的项目要求的是“在一个可控的环境里、以足够用的速度、让模型稳定跑起来”,树莓派是一个非常合适的选择。接下来我按自己的实际部署流程,把从模型转换到摄像头运行的完整链路拆开讲。

2. 从训练权重到边端格式:模型转换这条路

2.1 为什么不能直接在树莓派上跑PyTorch权重

你的训练产物是一个.pt文件,比如best.pt。这在树莓派上能用吗?能,但很不推荐。

先算一笔账:PyTorch的CPU推理,在x86机器上已经比GPU慢一个数量级了,在树莓派这种Cortex-A72上只会更惨。而且PyTorch本体的体积和依赖树非常重,装完torch和torchvision,SD卡空间去掉好几个G,内存占用也会被顶到很高。树莓派4B的4GB内存版本,光加载PyTorch环境就已经吃掉一个G了,留给图像数据的空间所剩无几。

更重要的是,PyTorch的推理路径在ARM上并没有做深度优化。你会发现同一个模型,用ONNX Runtime加载跑,比用PyTorch原生跑快20%到40%。原因很简单,ONNX Runtime针对ARM架构做了线程调度和算子融合优化,而PyTorch的CPU后端在移动端和嵌入式端的优化一直不是重点。

所以,部署的第一步永远是格式转换。这一步在电脑上完成,树莓派只吃转换后的产物。

2.2 部署格式怎么选:ONNX、TFLite还是NCNN

如果你在CSDN和GitHub上翻过树莓派部署YOLO的帖子,会发现三种格式最常见:ONNX、TFLite、NCNN。它们成都差不多,但各有偏向。

格式适合场景树莓派上的感受量化支持
ONNX通用性强、PyTorch到部署的桥头堡ONNX Runtime在ARM上有官方优化,速度适中支持INT8/FP16量化,但ARM CPU上的FP16优势不明显
TFLite嵌入式设备、移动端支持Delegate加速,但树莓派上没有GPU Delegate,优势主要在量化和核数优化INT8量化成熟,转换工具链顺滑
NCNN国产方案、手机/IoT需要交叉编译或在板上编译,门槛稍高也支持INT8,但配置过程费时

如果你只是想把训练好的YOLO在树莓派上稳定跑起来,我最推荐的是ONNX。原因很简单:它离你训练的PyTorch生态最近,中间环节少,转换过程最不容易出幺蛾子。等你后续想玩量化或者跑更极致的优化,再切到TFLite或NCNN也不迟。

如果你的模型是在Ultralytics的YOLO框架(比如YOLOv8、YOLOv11)下训练出来的,转换只需要一行命令:

yolo export model=best.pt format=onnx opset=12 simplify=True

注意opset参数。ONNX的算子版本太高,ONNX Runtime的ARM版本可能不认。实测在树莓派上opset 12到14之间最稳,别一上来用opset 17,那不是给自己找麻烦吗。

转换完之后,用净输入检查一下是不是真的转换成功了。别急着关电脑,先在本机跑一遍ONNX Runtime的推理,确认输出和PyTorch原版一致,再拷贝到树莓派上。

2.3 更细一步:尝试INT8量化

在树莓派的CPU上,模型量化带来的收益极其明显。同一个YOLOv8n模型,FP32的ONNX在树莓派4B上跑一帧大概要500到600毫秒,INT8量化之后能压缩到300毫秒左右。这就是质的区别了。

TFLite生态里做量化最顺,用Ultralytics直接导出TFLite模型也行:

yolo export model=best.pt format=tflite int8=True

不过这里有个坑:TFLite的INT8量化需要标定数据集。Ultralytics集成的转换流程默认用一部分训练数据做标定,但这会显著拖慢转换时间,而且如果你的训练数据和实际应用场景差异很大,量化后精度掉得很厉害。我自己的实测结果,在交通标志识别任务上,FP32的mAP是0.89,INT8量化之后掉到0.85左右。识别一些对比度不明显的目标时,漏检率明显上升。所以量化前一定要拿一批真实场景图片做验证集,最好通不过再用回FP32。

另一个选项是用ONNX Runtime的动态量化,把FP32权重量化到INT8,代码很简单:

from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic("best.onnx", "best_int8.onnx", weight_type=QuantType.QUInt8)

动态量化不需要标定数据集,实现成本低,速度提升大概25%到35%。但我的体感是,动态量化的数值稳定性不如TFLite的静态量化,某些层的输出浮动会稍微大一点。对于毕设、课程设计、以及在比较稳定的光照条件下运行的视觉任务来说完全够用。

2.4 验证转换结果:这一步不能省

转换完模型后,我强烈建议你在电脑上写一个最小验证脚本。用一张训练集之外、最好是从实际场景里拍的照片,分别喂给PyTorch模型和转换后的模型,对比检测框的坐标和置信度。

import onnxruntime as ort import torch import numpy as np from ultralytics import YOLO # PyTorch原版推理 model = YOLO("best.pt") results = model("test.jpg", verbose=False) boxes_pt = results[0].boxes.xyxy.cpu().numpy() scores_pt = results[0].boxes.conf.cpu().numpy() # ONNX Runtime推理 session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = session.get_inputs()[0].name img = preprocess("test.jpg") # 1x3xHxW的float32数组 outputs = session.run(None, {input_name: img}) # 后处理拿到boxes和scores # ...

对比两组检测框,允许有微小误差(坐标差几个像素、置信度差零点零几),但如果出现漏检或者大量错检,说明转换过程出了问题。常见的原因包括:输入尺寸与训练时不匹配、归一化方式不一致(应该除以255还是减均值除方差)、模型本身包含了非标准算子导致转换后行为漂移。

这个验证脚本建议直接使用venv的Python环境,配好onnxruntime之后测试,确认无误后再把模型拷贝到树莓派上。

3. 树莓派环境搭建:别急着pip install

3.1 系统选型与基础配置

树莓派部署YOLO,系统层面我最推荐Raspberry Pi OS Bookworm的64位版本。原因有三个:

第一,64位系统能充分利用4GB甚至8GB内存,跑YOLO推理时不会因为地址空间不足而莫名崩溃。第二,Bookworm系统本身自带的Python版本是3.11,对ONNX Runtime、OpenCV这些库的兼容性很好。第三,libcamera和Picamera2这些摄像头库在Bookworm上是默认集成的,直接可以用,不用像老系统那样折腾raspistill那一套。

拿到系统并烧录到SD卡之后,不要急着装环境,先把以下几件事做完:

  • 执行sudo raspi-config,在Performance Options里把GPU Memory设成16MB或者32MB。YOLO推理用不到GPU内存,留太多反而挤压系统可用内存。
  • 开启SSH,方便你从电脑上远程操作。我个人习惯是全程SSH连接到树莓派,推理结果显示在电脑上,这样不用额外接显示器和键鼠。
  • 更换软件源到国内镜像,这一步在部署大包时能省下一大把时间。

还有一个小坑必须提一下:SD卡的空间。一个干净的Bookworm系统大概占用4GB到5GB,安装OpenCV、ONNX Runtime以及后续的模型文件,很容易突破15GB。建议直接用32GB以上的SD卡,否则装到一半空间满,所有工作白费。

3.2 swap分区与内存之道

树莓派4B的4GB内存版本在跑YOLO推理时,情况是这样的:模型加载到内存大概占用500MB到800MB,输入图像缓存几百MB,OpenCV和系统本身再占几百MB,整体压力不小。如果你用的是2GB版本,内存吃紧的情况更明显。

我的做法是:先把swap从默认的100MB调高到1024MB。方法很简单:

sudo nano /etc/dphys-swapfile # 找到 CONF_SWAPSIZE=100,改成 CONF_SWAPSIZE=1024 sudo systemctl restart dphys-swapfile

但注意,swap只是兜底方案,它能防止程序因内存不足被杀掉,但swap的读写速度比内存慢得多,如果你发现推理进程经常卡住不动,大概率是内存不够导致跑到swap上去了。这时候更好的办法是换更小的输入尺寸或者更轻量的模型,而不是加大swap死扛。

3.3 安装OpenCV与ONNX Runtime

树莓派上装OpenCV,我强烈建议直接pip安装,不要自己编译。OpenCV从源码编译需要数小时,而且内存小的板子容易在编译过程中OOM。pip上已经有编译好的ARM版本:

sudo apt update sudo apt install -y python3-pip python3-venv python3 -m venv yoloenv source yoloenv/bin/activate pip install opencv-python-headless

这里选择opencv-python-headless而不是opencv-python,因为我们在树莓派上跑的是纯Python推理脚本,不需要GUI显示窗口。Headless版本体积更小、依赖更少,也更不容易跟系统的摄像头库发生冲突。

接着安装ONNX Runtime:

pip install onnxruntime

在树莓派的aarch64架构上,官方PyPI源会直接分发对应的wheel包,不需要自己编译。装好后可以验证一下能加载的provider:

import onnxruntime as ort print(ort.get_available_providers())

正常情况下会输出['CPUExecutionProvider']。不要指望这里出现CUDA或TensorRT,树莓派上没有这些东西。

还有一个容易被忽略的包:numpy。ONNX Runtime的wheel有一定版本的numpy依赖,如果pip自动给你装了最新版导致冲突,手动把numpy降到指定版本即可。我的经验是pip install 'numpy<2'比较稳,ONNX Runtime 1.16以上的版本对新numpy的兼容性有坑。

3.4 把模型和代码同步到树莓派

模型文件(比如best.onnx)大小一般在10MB到50MB之间,最省事的方式是在电脑上直接用scp传过去:

scp best.onnx pi@树莓派IP:~/yolo/

如果你的电脑和树莓派不在同一局域网,也可以用U盘拷贝,反正模型文件不大,传输成本忽略不计。代码文件可以直接写一个Python脚本传过去,或者直接在树莓派上用nano编辑。我习惯在电脑上写好测试OK再传,因为在树莓派上编辑代码实在不是一种享受。

4. 推理代码的完整拆解:从读图到画框的过程

4.1 预处理:letterbox为什么不能省

YOLO训练时会对输入图像做letterbox操作,也就是把原始图像等比缩放到目标尺寸(比如640×640),剩余部分用灰色填充。这样做的目的是保持图像的长宽比,避免目标被拉伸变形。推理时的预处理必须和训练时保持一致,否则检测精度会明显劣化。

大部分教程给出的预处理是把图像直接resize到640×640,这种写法是错的。直接resize会让原本圆形的物体变成椭圆,YOLO虽然有一定鲁棒性,但小目标的检测框会明显偏移。我用一个真实的对比数据:在安全帽检测任务中,直接resize的mAP比letterbox低了4个点,这个差距在部署中是不能接受的。

正确的letterbox实现:

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]) / 2 # 宽方向的padding dh = (new_shape[0] - new_unpad[1]) / 2 # 高方向的padding dw, dh = int(dw), int(dh) img = cv2.resize(img, new_unpad, interpolation=cv2.INTER_LINEAR) top, bottom = dh, new_shape[0] - new_unpad[1] - dh left, right = dw, new_shape[1] - new_unpad[0] - dw img = cv2.copyMakeBorder(img, top, bottom, left, right, cv2.BORDER_CONSTANT, value=color) return img, r, dw, dh

注意函数返回的r、dw、dh,这三个值在后处理的坐标还原时会用到,很多人在这里丢掉了还原参数,导致检测框位置完全错掉。这是新手最容易踩的坑。

4.2 前向推理:ONNX Runtime的会话管理

加载ONNX模型并执行推理,代码非常简洁:

session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = session.get_inputs()[0].name input_shape = session.get_inputs()[0].shape # 假设输入是 [1, 3, 640, 640]

这里有一个性能的关键点:会话(Session)应该只初始化一次,然后在循环里复用。很多人的做法是在每一帧里重新创建InferenceSession,这样模型每次都要重新加载和解析,速度会慢五到十倍。正确的方式是在程序启动时加载模型,之后主循环只做前向计算。

还有一个小细节:输入张量的数据布局。ONNX Runtime默认输入是NCHW格式,也就是[batch, channel, height, width]。从OpenCV读出来的图像是HWC格式,所以需要先做通道变换和归一化:

img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img = img.astype(np.float32) / 255.0 img = img.transpose(2, 0, 1) # HWC -> CHW img = np.expand_dims(img, axis=0) # 加batch维 outputs = session.run(None, {input_name: img})

如果你跳过/255.0这一步,模型的输出置信度会全部错乱。YOLO训练时的输入分布通常是[0,1]区间,推理时的预处理必须保持一致。

4.3 后处理:从原始输出到可见的检测框

ONNX Runtime输出的原始张量长什么样,取决于模型的导出方式。以YOLOv8为例,典型的输出形状是[1, 84, 8400],其中84代表4个框坐标加上80个类别置信度,8400代表所有尺度的候选框数量。

很多人拿到这个输出直接被吓住了,其实后处理逻辑并不复杂,核心分三步:

第一,把输出的形状从[1, 84, 8400]转成[8400, 84],方便逐行处理。第二,筛选出置信度超过阈值的行。第三,对这些候选框做NMS(非极大值抑制)合并重叠框。

这里我直接给出一个精简但可用的实现:

def postprocess(output, conf_thres=0.5, iou_thres=0.45): # 压缩batch维度 [1,84,8400] -> [84,8400] -> [8400,84] preds = output[0].squeeze(0).transpose(1, 0) # 需要根据实际输出维度调整 boxes = preds[:, :4] # cx,cy,w,h class_scores = preds[:, 4:] class_ids = np.argmax(class_scores, axis=1) confs = class_scores[np.arange(len(class_scores)), class_ids] mask = confs > conf_thres boxes = boxes[mask] class_ids = class_ids[mask] confs = confs[mask] # 把cx,cy,w,h转成x1,y1,x2,y2 xyxy = np.zeros_like(boxes) xyxy[:, 0] = boxes[:, 0] - boxes[:, 2] / 2 xyxy[:, 1] = boxes[:, 1] - boxes[:, 3] / 2 xyxy[:, 2] = boxes[:, 0] + boxes[:, 2] / 2 xyxy[:, 3] = boxes[:, 1] + boxes[:, 3] / 2 # NMS indices = cv2.dnn.NMSBoxes(xyxy.tolist(), confs.tolist(), conf_thres, iou_thres) if len(indices) > 0: indices = np.array(indices).flatten() return xyxy[indices], confs[indices], class_ids[indices] return [], [], []

这套后处理逻辑不依赖任何YOLO特有的库,纯numpy和OpenCV实现,在树莓派上跑完全没问题。把它封装成函数之后,主程序的逻辑就变得非常清晰了。

4.4 坐标还原:letterbox参数的最后一次使用

获得检测框之后,需要把框坐标映射回原始图像尺寸。这一步必须用到预处理时保存的r(缩放比)、dw(宽pad)和dh(高pad)。公式很简单:

restored_x1 = (xyxy[:, 0] - dw) / r restored_y1 = (xyxy[:, 1] - dh) / r restored_x2 = (xyxy[:, 2] - dw) / r restored_y2 = (xyxy[:, 3] - dh) / r

注意:这里的框坐标是相对于预处理后640×640的图像而言的,减去pad并除以缩放比之后,就回到了原始分辨率坐标系。画框时候直接用cv2.rectangle在原图上画即可。

到这里,一个完整的静态图片检测脚本就全部跑通了。接下来是这个项目真正体现价值的地方——让模型对摄像头画面做实时检测。

5. 摄像头实时检测:迈向可用的边缘设备

5.1 摄像头选型:CSI还是USB

树莓派的摄像头方案有两种主流选择:一个是CSI接口的摄像头模块(比如树莓派官方的OV5647或者更高像素的IMX219),另一个是普通USB摄像头。

从延迟角度讲,CSI摄像头有明显优势。CSI通过专用接口直连SoC,数据不走USB带宽,延迟低,CPU占用也小。USB摄像头插上就能用,通用性强,但图像通过USB总线传输时会有额外的复制和调度开销,在低帧率场景下体感不明显,但在追求稳定性的实时检测中,我会选CSI摄像头。

从软件栈来说,Bookworm系统上必须使用libcamera和Picamera2库。老教程里那些raspistill、picamera库已经过时了,直接用会报错。用Picamera2读取CSI摄像头的流程如下:

from picamera2 import Picamera2 import cv2 import numpy as np picam2 = Picamera2() config = picam2.create_preview_configuration( main={"size": (640, 480), "format": "RGB888"} ) picam2.configure(config) picam2.start() frame = picam2.capture_array() # 返回numpy数组,格式为RGB

注意Picamera2返回的是RGB格式,和OpenCV的BGR格式不一致。如果你习惯用OpenCV的函数做后续处理,记得加一行cv2.cvtColor(frame, cv2.COLOR_RGB2BGR),或者干脆保持RGB处理,只在用cv2.imwrite或cv2.imshow时转换。

5.2 实时检测主循环

把前面所有代码整合起来,一个完整可用的实时检测脚本是这个样子的:

import cv2 import numpy as np import onnxruntime as ort from picamera2 import Picamera2 # 初始化模型会话(一次) session = ort.InferenceSession("best.onnx", providers=["CPUExecutionProvider"]) input_name = session.get_inputs()[0].name # 初始化摄像头 picam2 = Picamera2() config = picam2.create_preview_configuration(main={"size": (640, 480), "format": "RGB888"}) picam2.configure(config) picam2.start() def detect_frame(frame): # 预处理 letterboxed, r, dw, dh = letterbox(frame, new_shape=(640, 640)) blob = letterboxed.astype(np.float32) / 255.0 blob = blob.transpose(2, 0, 1)[np.newaxis, ...] # 推理 outputs = session.run(None, {input_name: blob}) # 后处理 boxes, confs, cls_ids = postprocess(outputs[0]) # 还原坐标 if len(boxes) > 0: boxes[:, [0, 2]] = (boxes[:, [0, 2]] - dw) / r boxes[:, [1, 3]] = (boxes[:, [1, 3]] - dh) / r return boxes, confs, cls_ids while True: frame = picam2.capture_array() boxes, confs, cls_ids = detect_frame(frame) for box, conf, cls_id in zip(boxes, confs, cls_ids): x1, y1, x2, y2 = box.astype(int) cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"{names[cls_id]} {conf:.2f}", (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 2) # 只在电脑端往前传一帧用于显示,或者保存到本地 # cv2.imshow("result", frame) # if cv2.waitKey(1) & 0xFF == ord("q"): # break

在实际的树莓派部署中,我通常不会用cv2.imshow在树莓派本机显示画面,因为树莓派可能没有接显示器。替代方案有两种:一是把检测结果用MQTT或HTTP发给电脑端,二是直接把叠加了检测框的帧保存到SD卡或网络磁盘。这两种方式都需要额外的网络代码,这里就不展开了。我用过的最简单方案是在树莓派上开启一个HTTP服务,用WebSocket往网页端推流,延迟在200毫秒以内,效果还可以。

5.3 实时运行时的性能瓶颈分析

跑通实时检测之后,你应该做一个瓶颈分析,而不是直接认为“工作完成了”。我自己实测的典型数据,在树莓派4B上:

环节耗时
摄像头取一帧(640×480)30~50ms
letterbox + 预处理15~20ms
ONNX推理(FP32,640×640输入)500~600ms
后处理(NMS等)10~20ms

推理占了绝对大头。这就是为什么我强调模型格式转换和量化,因为不优化推理时间,其他环节再怎么折腾都没意义。

如果推理时间降不下来,退而求其次的做法是降低输入分辨率。YOLOv8n在416×416输入下,比640×640大约快25%到30%,精度下降对小目标不太友好,但如果你检测的目标本来就比较大,降低分辨率完全可行。

另一个方案是开启ONNX Runtime的多线程:

session = ort.InferenceSession( "best.onnx", providers=["CPUExecutionProvider"], sess_options=ort.SessionOptions() ) session.set_intra_op_num_threads(4) session.set_inter_op_num_threads(1)

树莓派4B是四核CPU,把intra-op线程数设成4能让推理过程中的矩阵运算利用多核。实测下来,线程数从1调成4,推理速度大约提升1.5倍。但注意,线程数不是越大越好,超过4反而会因为线程切换开销导致性能下降。

5.4 长时间运行时的内存与温度问题

实时检测意味着程序可能长时间运行。这里有两个隐藏问题,一个是内存泄漏,一个是过热降频。

先处理内存泄漏的排查方法。在你的主循环里加一段打印内存占用的代码,比如用psutil:

import psutil mem = psutil.virtual_memory() print(f"used: {mem.used / 1024 / 1024:.1f}MB, percent: {mem.percent}")

如果发现内存占用随时间线性增长,先检查是不是每一帧都在创建新的numpy数组而没有释放旧引用。特别注意np.expand_dims和transpose会产生新的数组,这些数组如果没有及时赋值为局部变量并在循环结束后释放,内存会越堆越高。最稳妥的办法是:把预处理后的blob变量在每一轮循环末尾主动置为None,让Python的引用计数立刻归零。

温度问题同样重要。树莓派4B的CPU在达到85℃时会主动降频,从1.8GHz一路掉到600MHz,推理速度会断崖式下跌。所以如果你的设备要在机箱里密闭运行,必须加强散热。我的实测数据:裸板带一个小散热片,持续跑YOLO推理时CPU温度稳定在65℃左右;加了风冷散热器能压到50℃以下;如果什么都不加,运行半小时温度突破85℃,然后推理时间从500ms涨到800ms,体验非常拉胯。

6. 速度优化与散热玄学:实测数据说话

6.1 让CPU火力全开

树莓派默认的CPU调频策略是ondemand或者schedutil,这意味着CPU频率会根据负载动态调整。对于实时推理这种突发负载,CPU频率切换会有延迟,导致单帧耗时波动。把CPU governor固定为performance模式可以消除这种波动:

sudo sh -c 'echo performance > /sys/devices/system/cpu/cpu0/cpufreq/scaling_governor'

这条命令在重启后会失效,如果不想每次都手动执行,可以把它写进/etc/rc.local。

另一个容易被忽略的设置是raspi-config里的超频选项。树莓派4B可以安全地把CPU超到1.8GHz,在官方散热器加持下稳定性没有问题。超频对YOLO推理的收益很明显,实测下来推理时间能再降10%左右。

6.2 不同模型和格式在树莓派4B上的实测对比

为了让你对目标帧率有个直观预期,我把自己在树莓派4B 4GB版本上的测试数据整理成表格。模型统一用COCO预训练或自己微调的变体,输入640×640,CPU四线程:

模型格式单帧推理耗时约等帧率
YOLOv5nONNX FP32420ms2.4 FPS
YOLOv8nONNX FP32560ms1.8 FPS
YOLOv8nONNX INT8动态量化360ms2.8 FPS
YOLOv8nTFLite INT8310ms3.2 FPS
YOLOv8nTFLite INT8 + 输入416230ms4.3 FPS
YOLOv11nONNX FP32520ms1.9 FPS

这里有一个重要信息:YOLOv8n在纯CPU推理下比YOLOv5n慢,原因在于v8的anchor-free结构在解码阶段计算量更大。如果你的部署任务对帧率要求高,优先考虑YOLOv5n而不是盲目用最新版本。老模型在边缘推理里更吃香,这是很多人不会告诉你的事。

6.3 减少输出层冗余:模型裁剪

很多YOLO模型在导出ONNX时,输出张量包含了训练时用到的多余分支。比如YOLOv8的export默认会带上一个1×84×8400的输出,但如果你只做检测,不需要输出类别概率矩阵中的细粒度信息,可以尝试用模型重参数化或者导出时去掉不必要的头。不过这个操作对代码能力要求较高,如果你只是想快速部署,可以不折腾。

6.4 如果还是不够快,怎么办

在树莓派上把YOLO跑到4 FPS到5 FPS,基本到了CPU方案的极限。如果这个速度还不能满足需求,接下来的道路就分叉了:

  • 换更轻量的模型。YOLOv8n已经是最small的版本,但你可以换更大的输入尺寸?不,是换更小的输入尺寸,或者换成专门为边缘设备设计的轻量模型,比如NanoDet、PP-PicoDet。这些模型在CPU上的推理速度比YOLO快不少,代价是精度略低。
  • 加一块神经计算棒。英特尔的Movidius NCS2或者Google Coral TPU都是插在USB上的推理加速设备,Coral TPU跑INT8模型的延迟能压到20ms左右。但注意,Coral TPU支持的是TFLite模型,而且对算子的支持有限,你的YOLO模型需要先用工具链转换和校准,这一步可能会让你怀疑人生。
  • 换一块更强的板子。这是个很实在的建议。如果你的项目已经跑通了逻辑,只是性能不够,直接考虑瑞芯微RK3588或者英伟达Jetson Orin Nano。这两类板子都有NPU或GPU加速,跑YOLO的体验和树莓派完全不是一个量级。树莓派的价值在于快速验证和算法可行性研究,到了要交付性能指标的阶段,该换平台就换平台,不要恋战。

我自己的体会是:树莓派部署YOLO这个事,真正的价值不在于最终能跑到多少帧率,而在于它让你把整个部署链路完整走了一遍,从模型转换、环境搭建、推理优化到硬件散热,每个环节都逼着你思考为什么这样做而不是那样做。这套能力和经验,换到任何一个平台都能复用。

最后分享一个小技巧。在树莓派上写推理脚本时,日志输出一定要克制。每帧打印检测结果会导致串口和SSH的IO成为瓶颈,甚至会反过来拖慢推理速度。我习惯用logging模块接管所有输出,设置level为WARNING,只在检测到目标数量变化时才打印摘要。程序稳定运行比花哨的输出重要得多。如果你照着上面的代码一步步走,遇到问题欢迎对照自己的报错信息排查,多数坑都是预处理环节的细节没对齐。祝你的模型在树莓派上跑得顺。

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

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

立即咨询