去年底我在树莓派5上折腾了一套“VScode + 模型训练 + 图片识别”的组合,系统用的Ubuntu 24.04,最终跑通了从数据集标注、模型训练到推理部署的完整流程。整个过程踩了不少坑,尤其是ARM架构下的依赖安装、VScode远程开发的连接配置、以及模型导出后的兼容性问题。这篇学习笔记就把这套链路完整记录下来,内容包括方案选型、环境搭建、数据准备、YOLO模型训练、模型转换部署,以及我在实际调试中遇到的各种问题和排查思路。
如果你是第一次接触边缘端AI,或者想在树莓派上落地一个视觉识别项目,这篇文章应该能帮你省下不少时间。
1. 整体思路:为什么这么拆
动手之前先把方案想清楚,能省掉后面大量返工。
1.1 训练和推理为什么要分开
我在做这套项目时,最大的体会是:不要试图在树莓派5上完成所有事情。树莓派5虽然比前代强了不少——四核A76、最大16GB内存、支持PCIe——但它终究是一块ARM架构的嵌入式开发板。模型训练这种高密度计算任务,在CPU上即使能跑,效率也低得让人难以接受。以YOLOv8n为例,在树莓派5的CPU上训练100个epoch、几百张图片的小数据集,可能要走几个小时甚至十几个小时。但同样的任务放到一台带GPU的PC上,十几分钟就能完成。
所以我的做法是:
- PC端负责训练,用GPU加速。
- 树莓派5负责推理和部署验证。
- VScode通过Remote SSH插件连接树莓派,作为统一的开发和调试入口,这样训练脚本、部署脚本可以在同一个界面里管理。
这个方案的优势在于:树莓派上跑的东西是最终要交付的东西,所有依赖、路径、运行环境都和实际部署一致,避免了“在我电脑上没问题”的尴尬。
1.2 整体流程串起来
整个项目可以画成一条清晰的流水线:
数据采集 → 图片标注 → 数据集划分 → YOLO模型训练 → 模型导出(ONNX) → 树莓派部署 → 图片/视频实时推理
每一步都有明确的输入输出,我最开始就是按这个顺序推进的。中间卡住的地方集中在两步:一是树莓派上PyTorch的安装,二是ONNX Runtime的版本选择。如果前置环境没弄好,后续所有操作都会跟着出问题。
1.3 模型选型:为什么是YOLOv8n
图片识别模型有很多种,分类网络(ResNet)、检测网络(YOLO系列、SSD)各有侧重。我做的是“识别指定图片中的目标”,属于目标检测任务,所以直接用YOLO系列。
在YOLO家族里,我选了YOLOv8n。原因很实际:
n代表nano版本,模型权重只有6MB左右,参数量最少,推理速度在树莓派上最友好。- Ultralytics这个开源库把训练、验证、导出、推理做了统一封装,几乎一行命令就能完成。
- YOLOv8的资料在中文社区非常充足,遇到问题搜一下就能解决。
如果你对YOLO11感兴趣,思路完全一样,只是把yolov8n.pt换成yolo11n.pt,命令格式基本兼容。新手建议先用YOLOv8n把流程跑通,再尝试其他版本。
2. 环境准备:树莓派5上的Ubuntu 24.04与VScode远程开发
工欲善其事必先利其器。环境这部分我折腾了两天,写下来给后来人避坑。
2.1 树莓派5安装Ubuntu 24.04
树莓派5目前可选的系统很多,官方Raspberry Pi OS、Ubuntu Server、Ubuntu Desktop等。我选Ubuntu 24.04是因为它是LTS版本,软件源里的包比较新,而且社区支持周期长。
安装过程不复杂,我用的官方Raspberry Pi Imager工具烧录Ubuntu镜像。有一点必须提醒:树莓派5是ARM64架构,不要下成amd64的镜像,否则根本启动不了。
系统烧录完成后,第一次开机需要接显示器键盘完成初始配置,包括创建用户、设置WiFi等。如果像我一样手头没有多余显示器,可以直接在烧录时预配置好SSH和无线网络。Raspberry Pi Imager支持在烧录前设置主机名、开启SSH、添加WiFi信息,非常方便。
建议装Desktop版本而不是Server版本,理由很简单:树莓派虽然可以纯命令行操作,但有时候看推理结果图片、调试显示问题时,图形界面能省很多时间。不需要的时候把桌面关掉就行,不影响性能。
2.2 开启SSH与网络配置
系统起来后,先做两件事:开启SSH、确认网络连接。
# 如果SSH没开启,执行: sudo systemctl enable --now ssh # 查看IP地址 ip addr show我把树莓派的IP固定下来(静态IP),避免路由器重新分配IP导致VScode远程连不上。静态IP配置在Ubuntu 24.04上可以用netplan:
sudo nano /etc/netplan/01-config.yamlWiFi连接的典型配置:
network: version: 2 ethernets: eth0: dhcp4: true wifis: wlan0: dhcp4: false addresses: - 192.168.1.100/24 routes: - to: default via: 192.168.1.1 nameservers: addresses: - 192.168.1.1 access-points: "你的WiFi名称": password: "你的WiFi密码"然后sudo netplan apply,重启后IP就固定了。
2.3 VScode Remote SSH连接树莓派
VScode本身只是编辑器,但装上Remote - SSH插件后,它就变成了远程开发终端。这是整个工作流里体验提升最明显的一步。
操作步骤:
- 本机VScode安装扩展:
Remote - SSH、Python、Pylance。 - 按
F1,输入Remote-SSH: Connect to Host,选择Add New SSH Host。 - 输入
ssh 用户名@192.168.1.100,选择保存到SSH配置文件。 - 连接后VScode会自动在远端安装VScode Server,首次会稍等一会儿。
- 打开远程文件夹后,在扩展面板给远端装上Python插件,选择远程解释器。
这里有几点实际操作经验:
- 树莓派上首次运行VScode Server,可能因为缺少
libstdc++等运行库导致启动失败,执行sudo apt update && sudo apt install -y build-essential libssl-dev libffi-dev python3-dev可以解决大部分依赖问题。 - 如果连接后界面卡顿,关掉VScode的文件自动保存、文件监视功能,远程编辑大文件会流畅得多。
- GitHub Copilot等AI插件在远程开发模式下也能用,直接在远端装就行,实测体验和本地几乎没区别。
2.4 Python环境与PyTorch安装
树莓派上我用的Python环境是系统自带的Python 3.12。为了避免污染系统环境,我为项目建了一个独立的虚拟环境:
python3 -m venv ~/venvs/ai_env source ~/venvs/ai_env/bin/activatePyTorch在树莓派上安装有个坑:默认pip install torch会尝试安装CUDA版本的Linux x86_64包,但在ARM64上会失败。正确做法是指定ARM64 CPU版本:
pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu实测这个方式能装上纯CPU版本的PyTorch。准确说,PyTorch官方现在已经发布了支持ARM64 Linux的轮子,不需要再手动源码编译。如果你下载速度不理想,可以考虑从其他镜像站抓取,但记住一定要带aarch64后缀。
顺带把常用库一起装上:
pip install numpy opencv-python onnxruntime ultralytics注意,在树莓派的ARM64架构下,有些pip包没有预编译轮子,会现场源码编译,需要系统提前装好build-essential和cmake,否则编译到一半报错。
3. 数据准备:做出“指定图片”识别模型的基础
模型识别什么,取决于你给它看什么数据。数据质量直接决定模型上限,这部分值得花时间认真做。
3.1 采集图片素材
我这次做的demo是识别三种水果:苹果、香蕉、橙子。每类采集了大约150张图片,来源包括手机拍摄、网络公开图片、以及用USB摄像头拍的照片。
采集时有几个原则:
- 背景尽量多样:不要只在单一纯色背景上拍,实际推理场景的背景往往更复杂,多样背景能提升泛化能力。
- 目标尺度有变化:有些图片目标占画面很大,有些很小,让模型学会在不同尺度下识别。
- 光照条件丰富:强光、弱光、室内、室外都来一些,避免模型只在特定亮度下表现好。
如果你要识别的是工业零件、文档、特定物体,思路一样,但建议每类至少200张,类别边界模糊的还得加量。
3.2 标注工具与标注格式
YOLO系列模型需要的是YOLO格式的标注文件。每一张图片对应一个同名.txt文件,里面每一行表示一个目标:
class_id x_center y_center width height注意,x_center、y_center、width、height都是归一化到0到1之间的小数,不是像素坐标。
标注工具我用的是X-AnyLabeling,它的优点是支持自动标注辅助(基于深度学习模型预标注),能大幅减少手动工作量。如果你用不上自动标注,labelImg也是经典选择,轻量稳定。
标注完的目录结构整理成这样:
datasets/ ├── images/ │ ├── train/ │ ├── val/ ├── labels/ │ ├── train/ │ ├── val/ ├── data.yamldata.yaml内容如下:
train: datasets/images/train val: datasets/images/val nc: 3 names: ['apple', 'banana', 'orange']3.3 数据集划分
划分比例我用的8:2,即80%训练、20%验证。划分时注意打乱顺序,避免同一个场景的图片全堆在训练集或验证集里。
这个划分我是用Python脚本完成的:
import os import random import shutil IMG_DIR = "datasets/images" LABEL_DIR = "datasets/labels" TRAIN_RATIO = 0.8 SEED = 42 random.seed(SEED) images = [f for f in os.listdir(IMG_DIR) if f.endswith(('.jpg', '.jpeg', '.png'))] random.shuffle(images) split_idx = int(len(images) * TRAIN_RATIO) train_imgs = images[:split_idx] val_imgs = images[split_idx:] for split, imgs in [("train", train_imgs), ("val", val_imgs)]: os.makedirs(f"datasets/images/{split}", exist_ok=True) os.makedirs(f"datasets/labels/{split}", exist_ok=True) for img_name in imgs: shutil.move(os.path.join(IMG_DIR, img_name), os.path.join(f"datasets/images/{split}", img_name)) label_name = img_name.rsplit(".", 1)[0] + ".txt" if os.path.exists(os.path.join(LABEL_DIR, label_name)): shutil.move(os.path.join(LABEL_DIR, label_name), os.path.join(f"datasets/labels/{split}", label_name))这个脚本会把数据集移动到images/train/、images/val/、labels/train/、labels/val/四个目录。
3.4 数据增强
数据量不够又不想去补拍的时候,数据增强是有效手段。YOLOv8自带了一批增强策略,如随机翻转、缩放、色域变化等,训练时默认开启了一部分,因此不用手动预处理太多。
如果你的场景有特殊需求,比如产品检测需要识别特定角度的零件,可以在训练前自己用OpenCV生成一批旋转、加噪声的图片,但注意增强后的图片标签文件也需要相应变换,否则标注就错位了。我一开始直接对图片做旋转然后沿用原标注,结果训练loss一直降不下去,排查半天才发现是标签和图像内容对不上。
4. 模型训练:YOLOv8全流程实操
这一段是整套流程的核心,从训练命令到参数解析,再到评估指标怎么看,一次讲清楚。
4.1 训练前的准备
在PC上安装ultralytics库:
pip install ultralytics确认安装版本,如果网络不好,建议指定版本号安装,比如pip install ultralytics==8.3.x,避免每次拉最新版带来的API变动。
训练之前先跑一个验证命令,确认环境没问题:
yolo predict model=yolov8n.pt source='https://ultralytics.com/images/bus.jpg'能输出检测结果就说明环境正常。
4.2 训练命令与参数解析
训练命令如下:
yolo detect train data=datasets/data.yaml model=yolov8n.pt epochs=100 imgsz=640 batch=16 device=0这里device=0是使用第一张GPU,如果没有GPU就换成device=cpu。参数含义:
model=yolov8n.pt:加载预训练权重,官方预训练模型是在COCO数据集上训练的,迁移到自己的小数据集能加速收敛。epochs=100:训练轮数。数据量小的情况下100轮足够,过大容易过拟合。imgsz=640:输入图片分辨率。YOLOv8默认640,想加快训练可以改成320,但精度会下降。batch=16:单批次图片数量。显存不够就调低,我的GPU是8GB显存,16没问题,4GB显存建议8。patience=20:早停机制,验证集指标连续20轮不提升就自动停止训练。
训练过程中,终端会实时显示每一轮的loss、精度、召回率、mAP等指标,同时runs/detect/train/目录下会生成训练曲线图、混淆矩阵等文件。
4.3 训练监控与结果评估
训练完成后,重点看两个文件:best.pt和last.pt。best.pt是验证集上表现最好的权重,部署时用这个。
评估指标方面,核心看这几个:
mAP50:IoU阈值为0.5时的平均精度均值,数值越接近1越好。mAP50-95:多个IoU阈值(从0.5到0.95,步长0.05)的平均精度,比mAP50更严格。precision:预测为正样本中真正正确比例,越高说明误检越少。recall:实际正样本中被找出来比例,越高说明漏检越少。
我这次训练的结果,mAP50大约在0.93左右,mAP50-95大约0.72,对于一个小数据集和三分类的识别任务来说,这个效果已经够用了。
如果想看更细致的可视化结果,VScode远程开发模式下,图表还可以在终端中用tensorboard或ultralytics自带的训练结果曲线查看。
4.4 训练过程中的参数调优
我记得训练过程中,前几十轮loss下降明显,后面开始平缓。如果loss不降,原因大概率出在数据集上:标注错误、类别不平衡、图片分辨率过低。我第二次训练时,因为标注工具导出时误用了Pascal VOC格式,YOLO训练直接乱套,loss反复横跳。这种低级错误检查起来很费时间,建议训练前手动抽查几张图片和对应txt文件的坐标是否在合理范围。
类别不平衡的明显特征是:某个类的recall远低于其他类。解决思路是增加该类样本、对少数类做过采样、或者使用class_weight参数。
5. 模型导出与树莓派部署:跑起来才算数
训练完模型不是终点,部署在树莓派上能实时跑起来才是目标。
5.1 导出ONNX格式
PyTorch训练出的模型是best.pt,里面包含的是PyTorch的权重结构,树莓派上也能直接加载推理,但速度不理想。更好的方案是先导出为ONNX格式,再用ONNX Runtime推理,速度会明显提升。
导出命令:
yolo export model=runs/detect/train/weights/best.pt format=onnx opset=12 imgsz=640导出后同目录会生成best.onnx。这里有几个注意点:
opset=12是兼容性较好的默认值,ONNX Runtime在不同设备上都能解析。- 导出时会要求安装
onnx和onnxslim,直接pip install即可。 - 导出成功的标志是终端里打印出模型摘要,并提示ONNX文件保存位置。
如果想进一步极限压缩,可以导出为NCNN格式。但树莓派上跑NCNN需要额外编译,流程比ONNX Runtime麻烦,除非对性能非常敏感,否则先用ONNX就够了。
5.2 树莓派端环境准备
把best.onnx、data.yaml、推理脚本通过scp拷贝到树莓派。在树莓派的虚拟环境里安装:
pip install onnxruntime opencv-python numpyonnxruntime在ARM64平台上有官方预编译包,直接pip安装就行。安装完成后,可以用一行代码验证ONNX模型能否正常加载:
python -c "import onnxruntime as ort; ort.InferenceSession('best.onnx')"如果不报错,说明模型格式兼容,可以进入下一步。
5.3 图片推理脚本实现
下面这个脚本是核心推理逻辑,我加了详细的注释:
import cv2 import numpy as np import onnxruntime as ort import time CLASSES = ['apple', 'banana', 'orange'] CONF_THRESHOLD = 0.5 IOU_THRESHOLD = 0.45 INPUT_SIZE = 640 session = ort.InferenceSession("best.onnx") def letterbox(img, new_size=(640, 640)): h, w = img.shape[:2] scale = min(new_size[0] / h, new_size[1] / w) nw, nh = int(w * scale), int(h * scale) resized = cv2.resize(img, (nw, nh), interpolation=cv2.INTER_LINEAR) canvas = np.full((new_size[1], new_size[0], 3), 114, dtype=np.uint8) x_off, y_off = (new_size[0] - nw) // 2, (new_size[1] - nh) // 2 canvas[y_off:y_off + nh, x_off:x_off + nw] = resized return canvas, scale, x_off, y_off def detect(img_path): img0 = cv2.imread(img_path) img, scale, x_off, y_off = letterbox(img0) blob = img[:, :, ::-1].transpose(2, 0, 1).astype(np.float32) / 255.0 blob = np.expand_dims(blob, axis=0) input_name = session.get_inputs()[0].name outputs = session.run(None, {input_name: blob})[0][0] boxes, scores, class_ids = [], [], [] for pred in outputs: pred = np.array(pred) cls_id = int(pred[5:].argmax()) score = float(pred[5 + cls_id]) if score < CONF_THRESHOLD: continue cx, cy, w, h = pred[:4] x1 = (cx - w / 2 - x_off) / scale y1 = (cy - h / 2 - y_off) / scale x2 = (cx + w / 2 - x_off) / scale y2 = (cy + h / 2 - y_off) / scale boxes.append([x1, y1, x2, y2]) scores.append(score) class_ids.append(cls_id) indices = cv2.dnn.NMSBoxesBatched(boxes, scores, class_ids, CONF_THRESHOLD, IOU_THRESHOLD) for i in indices: i = i[0] if isinstance(i, (list, tuple)) else i x1, y1, x2, y2 = map(int, boxes[i]) label = f"{CLASSES[class_ids[i]]} {scores[i]:.2f}" cv2.rectangle(img0, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(img0, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) return img0 if __name__ == "__main__": start = time.time() result = detect("test.jpg") print(f"Inference time: {(time.time() - start) * 1000:.2f} ms") cv2.imwrite("result.jpg", result)5.4 USB摄像头实时检测
在图片推理跑通后,我接上了USB摄像头做实时检测。注意树莓派5的USB摄像头默认识别为/dev/video0。OpenCV调用摄像头的代码如下:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) while True: ret, frame = cap.read() if not ret: break result = detect_frame(frame) fps = cap.get(cv2.CAP_PROP_FPS) cv2.putText(result, f"FPS: {fps:.1f}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2) cv2.imshow("Detection", result) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()实际操作时有几个注意点:
- 树莓派的桌面环境下显示OpenCV窗口,需要在带桌面的Ubuntu里运行。如果通过SSH纯命令行,无法直接弹出窗口,需要把图像保存下来再查看。
- 摄像头权限:如果
cv2.VideoCapture(0)打不开,检查当前用户是否在video组里,不在的话执行sudo usermod -aG video $USER后重启。 - 实时推理FPS很重要。树莓派5纯CPU跑YOLOv8n-ONNX,640分辨率下大概能达到4-6 FPS,如果卡顿得厉害,把检测输入分辨率降到320或者使用NCNN优化,会有明显改善。
5.5 在VScode里调试整个流程
树莓派上跑推理脚本时,我是在VScode的远程窗口里完成的。VScode的Python调试器非常有用,可以打上断点查看outputs原始shape、scores分布、以及NMS结果,比盲写脚本调参高效得多。
值得一提的细节:VScode远程开发时,直接在终端里跑python detect.py和在调试模式里跑,用的解释器必须一致。如果你在VScode里选择了系统Python,但终端里激活的是venv,两边解释器不同会有一堆“ModuleNotFoundError”。解决办法是在VScode底部的Python解释器选到venv路径,或者在settings.json里指定:
{ "python.defaultInterpreterPath": "/home/用户名/venvs/ai_env/bin/python" }6. 常见问题与排查技巧实录
这部分记录我在实操中遇到的高频问题,整理成表格,方便后续排查。
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
| VScode远程连接不上树莓派 | SSH服务未开启 / IP地址变化 | 开启ssh服务,固定静态IP,确认同一局域网 |
| 树莓派pip安装速度慢 | 默认源在境外 | 切换为国内常用pip镜像源 |
pip install torch报错/找不到匹配版本 | 下载了x86_64的包而非aarch64 | 指定CPU版--index-url https://download.pytorch.org/whl/cpu |
| onnxruntime安装失败 | ARM64下选错了包 | 确认用pip install onnxruntime,官方有aarch64 wheel |
cv2.VideoCapture(0)返回False | 用户没有video设备权限 | 将用户加入video组并重启 |
| 推理结果框位置偏移 | 没有做letterbox反算,坐标直接映射回原图 | 推理后坐标减去padding并除以缩放比例 |
| mAP一直很低 | 标注格式不对 / 标签类别索引错位 | 抽检txt标注,核对class_id与类别名对应关系 |
| 树莓派推理太慢 | 模型太大 / 输入分辨率高 | 换轻量模型如YOLOv5n、降低imgsz、转换为NCNN |
| 内存不足训练过程被杀 | 数据一次性加载太多 | 调小batch,扩大swap分区 |
6.1 定位坐标反算错误
我第一次在树莓派上跑通推理时,检测框位置完全错乱。排查后发现问题是这样的:输入到模型的图片经过了letterbox处理,也就是把原始图等比缩放后填充到640x640的画布里。但我把模型输出的归一化坐标直接乘上原图尺寸映射回去,没去掉padding、没除以缩放比例,坐标自然偏了。
解决方式就是上面代码里的反算公式:
x1 = (cx - w / 2 - x_off) / scale y1 = (cy - h / 2 - y_off) / scale这类问题光靠肉眼调试很难发现,建议在调试模式下把预测框输出打到图片上看效果,确认偏差方向再反推公式。
6.2 环境一致性是最大陷阱
反复出现的一个重要问题就是环境不一致。树莓派上的Python虚拟环境、系统Python、VScode选中的解释器,如果各是各的,就会出现“终端能跑但VScode报错”或相反的情况。建议从始至终固定一个虚拟环境,所有依赖都安装在虚拟环境内,不要在系统Python里乱装包。
另外,树莓派上如果同时装了系统版的OpenCV和虚拟环境的OpenCV,版本不同可能互相干扰。我遇到过cv2.imshow在虚拟环境里报错,但系统Python里能正常打开的情况,后来发现是OpenCV依赖的GTK库版本不一致导致。解决方案是不要装多个OpenCV,虚拟环境里装的版本和系统版本统一。
6.3 swap分区设置
训练或处理大批量数据时,树莓派8GB内存也可能不够用。当终端抛出Killed字样时,大概率是OOM。临时方案是扩大swap:
sudo nano /etc/dphys-swapfile # 把 CONF_SWAPSIZE 改大,例如 2048 sudo systemctl restart dphys-swapfile但注意,swap是SD卡或固态硬盘上的空间,频繁读写会降低寿命,部署推理阶段建议关闭swap或保持默认。
6.4 树莓派开机自启检测脚本
最后分享一个小技巧:让检测脚本开机自动运行。我把推理脚本做成了systemd服务:
sudo nano /etc/systemd/system/detector.service内容:
[Unit] Description=YOLO Detector After=network.target [Service] User=你的用户名 WorkingDirectory=/home/你的用户名/yolo_project ExecStart=/home/你的用户名/venvs/ai_env/bin/python /home/你的用户名/yolo_project/detect.py Restart=always [Install] WantedBy=multi-user.target然后:
sudo systemctl enable detector sudo systemctl start detector这样树莓派一开机,目标检测程序就会自动运行,配合VScode查看日志,整个部署体验会顺畅很多。最终我是把它接在一个项目里,把检测结果写入本地日志和图片目录,定时回传,满足了实际应用需求。
这套方案从VScode远程开发、模型训练到树莓派5上的Ubuntu 24.04推理运行,整条链路已经验证稳定。后续如果还想扩展,可以做多路摄像头并发检测、接入mqtt上报结果、或者用TensorRT Lite进一步加速。先把手上的流程跑起来,后续优化就有了基准。