这次我们继续树莓派5玩转AI系列,第三集的任务很明确:在树莓派5上部署YOLOv8,实现人员检测。不过这一集不走Ultralytics完整推理链路,而是用OpenCV的DNN模块加载ONNX模型,也就是标题里强调的“dnn版”。
为什么这么选,后面会详细展开。简单说,树莓派5是ARM架构CPU,没有NVIDIA GPU,直接跑PyTorch推理一方面依赖太重,另一方面速度也不占优势。用OpenCV DNN加载YOLOv8导出的ONNX模型,依赖只有opencv-python和numpy两个库,模型文件只有十几MB,整个链路非常适合嵌入式设备。
本文会带你完成以下内容:准备树莓派5的Python环境、在PC上把YOLOv8导出为ONNX模型、编写OpenCV DNN推理代码、用图片验证人员检测效果、再用摄像头做实时检测,最后给出一套性能观察和常见问题排查清单。无论你是刚开始玩树莓派5,还是想把手里的YOLOv8模型部署到边缘设备,这篇文章都值得收藏。
1. 核心能力速览
先把这一集的技术方案和硬件门槛摆在前面,方便你快速判断要不要继续往下看。
| 能力项 | 说明 |
|---|---|
| 项目定位 | 树莓派5上的YOLOv8人员检测 |
| 推理框架 | OpenCV DNN(cv2.dnn) |
| 模型格式 | ONNX |
| 推荐硬件 | 树莓派5,4GB / 8GB 内存均可,建议使用64位系统 |
| 是否需要GPU | 不需要,纯CPU推理 |
| 显存需求 | 无 |
| 主要功能 | 图片、视频、摄像头中的人员检测 |
| 是否内置API | 无内置API,可自行封装HTTP服务 |
| 批量任务 | 没有现成队列,可对图片目录循环处理 |
| 依赖复杂度 | 低,只需 opencv-python + numpy |
| 主要优势 | 依赖少、模型体积小、部署简单、适合边缘设备 |
| 合适场景 | 嵌入式AI入门、人员计数、区域安防原型、机器人视觉 |
需要说明的是,“API”和“批量任务”不是这个DNN版内置的能力,但通过Python脚本可以很轻松地扩展。后面会给出示例代码。
2. 适用场景与使用边界
YOLOv8人员检测在树莓派5上最常见的落地场景包括:
- 实验室或办公区人员存在检测,用于设备空闲时自动休眠。
- 门店客流统计,统计画面中出现的人数。
- 课堂出勤辅助,通过摄像头判断教室是否有人。
- 安防监控原型,检测划定区域内是否出现人员。
- 机器人避障与跟随,把人员检测结果作为视觉输入之一。
这一集只解决“检测到人”这一步。它不等于人脸识别,YOLOv8输出的是一个包含人体的矩形框,不能直接判断这个人的身份。这一点在选型时要先想清楚:如果你的需求是“识别出具体是谁”,需要在这个基础上再接人脸识别或ReID模型,而不是单纯依靠YOLO。
使用边界方面要特别注意:
- 人员检测涉及人体画面,属于敏感隐私数据。测试时建议使用公开数据集图片、自拍素材或实验室内部实景,不要未经许可拍摄路人。
- 任何人形检测系统在部署到公共区域前,都要经过合规评估。不要用这类技术做偷拍、未经授权的跟踪或大规模监控。
- 树莓派5 CPU推理能力有限,不要对检测精度和实时性抱有太高期望。它是入门和原型验证的好工具,但不适合直接承担高危安防任务。
3. 为什么是DNN而不是直接跑Ultralytics
很多新手拿到树莓派5,第一反应是pip install ultralytics,然后像在PC上一样直接跑model.predict()。这条路在树莓派5上不能说完全不能走,但体验会很差。
原因主要有三点。
第一,Ultralytics依赖PyTorch。PyTorch的ARM版本虽然存在,但体积大、依赖多,在树莓派5上安装耗时很长,而且推理时会占用大量内存。树莓派5的4GB/8GB内存看起来不小,但PyTorch加载YOLOv8模型后,留给图像处理和系统的余量会明显减少。
第二,树莓派5的CPU是Cortex-A76,性能比树莓派4强不少,但没有CUDA、没有TensorRT,PyTorch在CPU上的算子优化不一定能发挥到最好。OpenCV DNN针对CPU推理做了很多优化,在嵌入式平台上反而可能比裸跑PyTorch更快。
第三,从工程化角度,DNN版的部署更干净。OpenCV DNN只需要一个ONNX文件,不依赖模型训练时的完整Python环境。模型导出一次,后面不管换到树莓派、Jetson还是Windows环境,只要OpenCV能跑,同一份代码就能复用。
所以这一集的选择很明确:在PC上用Ultralytics导出ONNX,在树莓派上用OpenCV DNN做推理。这也是很多边缘部署项目的通用套路。
4. 环境准备与前置条件
先说明一下我建议的环境配置。树莓派5建议使用64位的Raspberry Pi OS Bookworm,因为OpenCV Python包的arm64 wheel可以直接通过pip安装,省去源码编译的麻烦。如果你还在用32位系统,opencv-python新版本对armhf(32位ARM)的支持很不友好,可能会被迫使用旧版OpenCV,DNN模块的ONNX算子支持也会差一些。
开始之前,先更新系统并安装Python虚拟环境:
sudo apt update sudo apt upgrade -y sudo apt install -y python3-pip python3-venv python3-dev然后创建并激活虚拟环境。这里建议建一个独立环境,避免和系统Python打架。
mkdir -p ~/yolov8-dnn cd ~/yolov8-dnn python3 -m venv venv source venv/bin/activate激活后,安装OpenCV和NumPy:
pip install --upgrade pip pip install opencv-python numpy国内网络环境下如果下载慢,可以加清华镜像源:
pip install opencv-python numpy -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,验证OpenCV版本。这一步很重要,DNN模块对ONNX的支持在不同版本间差异较大。
python3 -c "import cv2; print(cv2.__version__)"如果能看到类似4.10.0的输出,说明OpenCV安装成功。建议OpenCV版本不低于4.8,这样才能获得较好的ONNX算子支持。
到这里,树莓派5这边的软件环境就准备好了。本集不需要额外安装PyTorch、Ultralytics或CUDA相关组件。
5. 模型准备:从YOLOv8导出ONNX
YOLOv8官方仓库和Ultralytics包都支持导出ONNX。这里推荐在PC上完成导出,因为PC通常配备NVIDIA显卡,导出速度快,环境也稳定。导出的ONNX文件是纯模型文件,不依赖PyTorch,直接拷贝到树莓派5就能用。
在PC上安装Ultralytics:
pip install ultralytics然后导出YOLOv8n模型。n是nano版本,模型最小、速度最快,最适合树莓派5这种CPU推理场景。命令行方式:
yolo export model=yolov8n.pt format=onnx opset=12 simplify=True也可以写Python脚本:
from ultralytics import YOLO model = YOLO("yolov8n.pt") model.export(format="onnx", opset=12, simplify=True)这里有两个关键参数需要注意。
opset=12是为了兼容OpenCV DNN。OpenCV对高版本ONNX算子支持有时候会滞后,设置opset=12能避开不少算子兼容问题。simplify=True会调用onnx-simplifier对计算图做简化,去掉一些OpenCV不支持的冗余算子。对于YOLOv8来说,这两项设置基本可以保证cv2.dnn.readNetFromONNX一次加载成功。
导出完成后,目录下会生成yolov8n.onnx。这个文件体积很小,通常在十几MB级别。把它拷贝到树莓派5上:
scp yolov8n.onnx pi@raspberrypi.local:/home/pi/yolov8-dnn/如果你的树莓派5已经联网,也可以直接在树莓派上下载别人导出的公共ONNX模型。但更稳妥的方式还是自己导出:一方面可以保证模型结构符合预期,另一方面后续如果要换yolov8s、yolov8m,训练自定义数据集,流程是统一的。
这里再补充一点关于模型大小的选择。YOLOv8官方提供n、s、m、l、x五个规格,复杂度递增。树莓派5的CPU更适合yolov8n和yolov8s,不要一上来就选yolov8x,否则会非常慢。
6. 编写DNN推理代码并运行
模型文件准备好后,接下来写核心推理代码。先跑图片检测,把整套后处理逻辑跑通,再上摄像头。
创建detect_person.py:
import cv2 import numpy as np import time # 配置参数 MODEL_PATH = "yolov8n.onnx" IMAGE_PATH = "test.jpg" CONF_THRESHOLD = 0.5 IOU_THRESHOLD = 0.4 INPUT_SIZE = 640 TARGET_CLASS_ID = 0 # COCO中person是第0类 # 加载ONNX模型 net = cv2.dnn.readNetFromONNX(MODEL_PATH) # 读取图片 img = cv2.imread(IMAGE_PATH) if img is None: raise FileNotFoundError(f"无法读取图片: {IMAGE_PATH}") H, W = img.shape[:2] # 构造blob blob = cv2.dnn.blobFromImage( img, 1.0 / 255.0, (INPUT_SIZE, INPUT_SIZE), swapRB=True, crop=False ) net.setInput(blob) # 推理并统计耗时 start = time.time() outputs = net.forward() infer_time_ms = (time.time() - start) * 1000 print(f"模型推理耗时: {infer_time_ms:.1f} ms") # 后处理 # YOLOv8导出ONNX后,输出形状通常是(1, 84, 8400)或(1, 8400, 84) # 其中84 = 4个坐标 + 80个COCO类别置信度 if outputs.shape[1] == 84: detections = outputs[0].T # (8400, 84) else: detections = outputs[0] # 已经是(8400, 84) boxes = [] scores = [] for det in detections: cx, cy, w, h = det[:4] class_scores = det[4:] class_id = int(np.argmax(class_scores)) confidence = float(class_scores[class_id]) # 只检测person类别 if class_id != TARGET_CLASS_ID: continue if confidence < CONF_THRESHOLD: continue # 将640x640坐标映射回原图坐标 x1 = int((cx - w / 2.0) * W / INPUT_SIZE) y1 = int((cy - h / 2.0) * H / INPUT_SIZE) x2 = int((cx + w / 2.0) * W / INPUT_SIZE) y2 = int((cy + h / 2.0) * H / INPUT_SIZE) # 边界裁剪 x1 = max(0, x1) y1 = max(0, y1) x2 = min(W - 1, x2) y2 = min(H - 1, y2) if x2 <= x1 or y2 <= y1: continue boxes.append([x1, y1, x2 - x1, y2 - y1]) scores.append(confidence) # NMS去掉重叠框 indices = cv2.dnn.NMSBoxes(boxes, scores, CONF_THRESHOLD, IOU_THRESHOLD) if len(indices) > 0: for i in indices.flatten(): x, y, w, h = boxes[i] cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2) label = f"person {scores[i]:.2f}" cv2.putText(img, label, (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) print(f"检测到 {len(indices)} 个人") else: print("未检测到人员") cv2.imwrite("result.jpg", img) print("结果已保存到 result.jpg")在树莓派5上运行时,把test.jpg换成你自己的测试图片:
python3 detect_person.py预期输出是一行推理耗时的统计,以及检测人数的提示。如果图片中存在人体,result.jpg中会出现红色框和person 0.9x的标签。
这里补充一下代码里的关键点。
blobFromImage的swapRB=True是因为OpenCV读入图像默认是BGR格式,而YOLO训练时使用的是RGB格式。1.0 / 255.0负责归一化。输入尺寸固定为640x640,这是YOLOv8的标准推理尺寸。
输出维度需要理解一下。8400来自三个检测尺度:80x80、40x40、20x20,加起来正好是8400个锚框。84中的前4个值是cx、cy、w、h,后80个值是COCO数据集80个类别的置信度。COCO类别中person是第0类,所以代码里用TARGET_CLASS_ID = 0做过滤。
如果你不想只检测人,可以去掉class_id != TARGET_CLASS_ID这段过滤,再写一个coco.names文件,把80个类别名称加载进来,就能输出所有检测类别。
7. 摄像头实时检测
图片检测跑通后,摄像头实时检测就没多少新东西了。核心是把图片检测的逻辑封装成一个函数,在摄像头循环里逐帧调用。
创建detect_camera.py:
import cv2 import numpy as np import time MODEL_PATH = "yolov8n.onnx" CONF_THRESHOLD = 0.5 IOU_THRESHOLD = 0.4 INPUT_SIZE = 320 # 实时检测建议用320,提升帧率 TARGET_CLASS_ID = 0 net = cv2.dnn.readNetFromONNX(MODEL_PATH) def detect_person(frame): H, W = frame.shape[:2] blob = cv2.dnn.blobFromImage( frame, 1.0 / 255.0, (INPUT_SIZE, INPUT_SIZE), swapRB=True, crop=False ) net.setInput(blob) outputs = net.forward() if outputs.shape[1] == 84: detections = outputs[0].T else: detections = outputs[0] boxes = [] scores = [] for det in detections: cx, cy, w, h = det[:4] class_scores = det[4:] class_id = int(np.argmax(class_scores)) confidence = float(class_scores[class_id]) if class_id != TARGET_CLASS_ID or confidence < CONF_THRESHOLD: continue x1 = int((cx - w / 2.0) * W / INPUT_SIZE) y1 = int((cy - h / 2.0) * H / INPUT_SIZE) x2 = int((cx + w / 2.0) * W / INPUT_SIZE) y2 = int((cy + h / 2.0) * H / INPUT_SIZE) x1 = max(0, x1) y1 = max(0, y1) x2 = min(W - 1, x2) y2 = min(H - 1, y2) if x2 <= x1 or y2 <= y1: continue boxes.append([x1, y1, x2 - x1, y2 - y1]) scores.append(confidence) indices = cv2.dnn.NMSBoxes(boxes, scores, CONF_THRESHOLD, IOU_THRESHOLD) result = frame.copy() count = 0 if len(indices) > 0: for i in indices.flatten(): x, y, w, h = boxes[i] cv2.rectangle(result, (x, y), (x + w, y + h), (0, 0, 255), 2) cv2.putText(result, f"person {scores[i]:.2f}", (x, y - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 0, 255), 2) count += 1 return result, count # 打开摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): print("无法打开摄像头,请检查USB摄像头或权限") exit(1) print("摄像头已打开,按 q 退出") while True: ret, frame = cap.read() if not ret: break start = time.time() result, count = detect_person(frame) fps = 1.0 / (time.time() - start) cv2.putText(result, f"FPS: {fps:.1f} person_count: {count}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("Raspberry Pi 5 YOLOv8 DNN", result) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()运行:
python3 detect_camera.py这里有一个重要提醒:摄像头推荐优先用USB摄像头,也就是/dev/video0设备。树莓派官方CSI接口的摄像头在Bookworm系统上默认走libcamera框架,OpenCV直接VideoCapture(0)不一定能打开,需要额外配置v4l2兼容层。新手第一次调试,直接上一个免驱USB摄像头最省事。
另外,实时检测时我把INPUT_SIZE降到320。原因很简单:输入尺寸越大,树莓派5 CPU推理越慢。640是标准精度,但实时性明显吃力;320能换取更高的帧率,人员检测这种相对宽松的场景,320已经完全够用。
8. 性能观察与调优建议
树莓派5跑YOLOv8,最关心的就是性能。这里不写死具体帧率,因为性能受很多因素影响,比如模型规格、输入尺寸、图片分辨率、散热状态、后台任务负载。但可以明确一点:树莓派5没有NVIDIA GPU,CPU推理的性能上限摆在那里,不要指望和PC显卡相比。
你可以在代码中加上耗时统计,通过实际数据来判断优化方向。上一节代码里已经包含了time.time()计时,它会输出每帧的检测耗时和FPS。
影响性能的主要因素:
- 模型规格:
yolov8n比yolov8s快,帧率差距明显。树莓派5建议固定用n。 - 输入尺寸:640输入比320输入慢约3到4倍。人员检测可以用320,如果检测目标较小再考虑提升到480。
- 后处理逻辑:代码里为了可读性用了Python循环遍历8400个检测框,这一部分也会占用一些时间。如果要做高帧率优化,可以用NumPy向量化操作或Cython。
- 后台任务:树莓派5同时在跑桌面环境、摄像头的内存拷贝等任务,都会占用CPU。最小化桌面服务、用命令行模式运行,能省出一些资源。
给几个可落地的调优建议:
- 先固定使用
yolov8n,不要轻易换大模型。 - 实时检测用
INPUT_SIZE=320起步,先跑通流程,再根据效果调整。 - 如果只是做人员存在检测,不需要每帧都跑推理。可以改成每隔3帧或5帧检测一次,中间帧直接沿用上一帧的检测结果。
- 摄像头分辨率不要拉满。树莓派5处理720p绰绰有余,1080p反而增加缩放负担。
- 想进一步提速,可以研究
setNumThreads,设置线程数。树莓派5有四核,OpenCV默认有时反而会开过多线程,手动设置为4有时能提升稳定性。
后续想要大幅提升帧率,可以考虑外接树莓派AI Kit(Hailo-8L加速器),把模型部署到NPU上。这一集先不展开,等下一篇可以专门聊。
9. 常见问题与排查
实际部署中,最容易踩坑的主要是模型加载、检测结果不理想和摄像头打不开。整理成下表,方便查阅。
| 问题现象 | 可能原因 | 排查方式 | 解决方案 |
|---|---|---|---|
readNetFromONNX报错 | OpenCV版本过低,或ONNX算子不兼容 | 查看报错中提到的算子名称 | 升级OpenCV;导出时设置opset=12 simplify=True |
| 模型加载成功但检测不到人 | 置信度阈值过高;模型识别能力不足;测试图太模糊 | 临时把CONF_THRESHOLD降到0.25 | 清晰图片;换一张包含完整人体的测试图 |
| 检测出很多重叠框 | NMS阈值设置不合适 | 观察IOU_THRESHOLD取值 | 把IOU_THRESHOLD降到0.3或0.35 |
| 检测框和人物不匹配 | 坐标映射出错 | 检查原图宽高和INPUT_SIZE的映射代码 | 确认x1、y1是按原图尺寸计算的 |
| 摄像头打不开 | 设备不是USB摄像头;v4l2驱动未加载 | ls /dev/video*确认设备存在 | 换USB摄像头;加载bcm2835-v4l2模块 |
| 画面显示正常但推理很慢 | 输入尺寸太大;模型规格太大 | 打印每帧推理耗时 | 降到320;换yolov8n |
import cv2报错或版本不对 | 虚拟环境和系统包冲突 | which python3、pip list检查 | 严格在venv里安装依赖 |
| 代码启动后内存持续升高 | 摄像头帧被缓存;后处理数组频繁分配 | 使用htop观察内存 | 避免每帧创建大数组;检测帧间隔拉长 |
这里单独说一下OpenCV算子兼容问题。cv2.dnn.readNetFromONNX加载失败时,报错一般会指向某个具体算子。如果遇到这种情况,优先考虑两个手段:升级OpenCV,或者重新导出模型时强制opset=12加simplify=True。只要模型能加载成功,后面就基本畅通了。
10. 最佳实践与使用建议
把这一套东西从“能跑”变成“好用”,有几个工程化建议值得提前布局。
第一,文件目录要清晰。树莓派5上的工作目录建议按模型、脚本、素材、结果分开。
~/yolov8-dnn/ ├── models/ # ONNX模型文件 ├── scripts/ # Python推理脚本 ├── data/ # 测试图片、视频 ├── output/ # 检测结果 └── venv/ # Python虚拟环境第二,批量检测很容易实现,只要把图片检测的代码包成一个函数,然后用os.listdir遍历目录即可。比如对data/目录下所有jpg文件循环执行检测,结果统一保存到output/。如果图片数量多,建议在脚本里加日志,每处理一张输出一条记录,方便定位中途失败的文件。
第三,如果想把模型封装成HTTP接口,可以在Linux下用Flask或FastAPI包一层,把detect_person的逻辑暴露成POST接口。但要注意,树莓派5性能有限,接口并发能力不强,适合内网调试,不适合直接做高并发生产服务。
第四,合规红线不要碰。人员检测涉及人体画面,测试素材要自己拍或者用公开数据集。不要把摄像头对准街对面或办公室隔壁区域。部署到任何公共场合前,先确认当地隐私法规和使用授权。如果检测结果要长期存储,必须对视频数据进行脱敏和加密管理。
第五,系统散热会影响性能。树莓派5在高负载推理时发热明显,最好配一个主动散热风扇。CPU过热降频后,推理速度会明显下降,排查性能问题时先看温度。
11. 总结与下一步
这一集最值得动手验证的点,就是在树莓派5上用最短的依赖链把YOLOv8人员检测跑通。整个过程不装PyTorch、不装Ultralytics,只靠OpenCV和一个十几MB的ONNX文件,依赖干净,模型可复用,是非常典型的边缘AI部署思路。
建议你第一次运行时,先找一张包含多个人的清晰全身照作为测试图片,把整套流程跑通。接着再加摄像头实时检测,观察不同输入尺寸对帧率的影响。最容易踩的坑是OpenCV版本与ONNX算子兼容问题,以及非USB摄像头无法被OpenCV直接捕获,这两点提前准备好,就不会被卡太久。
后续可以继续扩展的方向很多:在PC上训练自定义人员数据集再导出ONNX、用Hailo-8L NPU加速、把检测结果接入MJPEG推流服务、或者结合MQTT实现区域人员告警。只要掌握了“Ultralytics导出ONNX + OpenCV DNN部署”这条链路,换模型、换任务都只是参数层面的调整。建议收藏备用,后面几集会在这套基础上继续叠加更多树莓派5的AI玩法。