☰
YOLOv8+PyQt5课堂行为检测系统:从数据集训练到界面部署全攻略
2026/10/11 18:18:11 网站建设 项目流程

简介:基于YOLOv8与PyQt5的课堂检测系统完整实现,面向教育技术研究者、计算机视觉初学者及有课堂管理需求的学校/机构,解决传统人工课堂监控效率低、无法实时量化学生行为的问题。系统利用YOLOv8目标检测算法进行高精度实时推理,通过PyQt5搭建可视化操作界面,用户无需编程基础即可完成参数配置、实时画面查看与行为结果分析,适用于出勤统计、专注度识别及异常行为预警等场景。资源包共2000个文件,压缩后约720.34MB,以txt文本文件为主(1975个),另有11个Python脚本、7个XML配置、6个YAML模型配置及1个CSS样式文件,分别覆盖数据集标注转换、模型训练配置、界面样式与主程序逻辑等环节。随包附带的工具脚本覆盖数据集格式转换、划分与预处理流程,有助于理解从数据准备到模型应用的整体逻辑。目前已有109人学习/下载,适合需要参考完整项目结构、快速上手YOLOv8+PyQt5开发,或希望将AI课堂行为分析落地到实际教学场景的开发者。

1. 课堂行为检测系统:从 YOLOv8 模型到 PyQt5 界面的全貌

期末答辩前一周,有学弟发消息问:一套能自动数学生举手次数、能识别玩手机和睡觉的课堂检测系统,到底怎么落地?我拆完这套资源后的回答是:三个东西配套才好用。YOLOv8 负责从视频帧里实时框出学生并判断行为类别,PyQt5 负责把识别结果装进图形界面,让你不用盯着终端输出看数字,资源里还带了一份已经按 YOLO 格式整理好的学生行为数据集,想自己重新训练也有数据可喂。

适合三类人:做课设、毕设要拿完整 demo 的在校生,给竞赛或教改项目搭原型的老师,以及想熟悉 YOLOv8 加 PyQt5 整套联动写法的开发者。需要提前泼冷水的也有一处:所谓"无需编程基础"指的是跑起来之后用鼠标点按钮,初次部署仍然要装 Python 环境、改配置文件、执行训练脚本,命令行这关绕不开。接下来按"数据→训练→界面→排错→部署"这条线说透。

2. 学生行为数据集:YOLO 格式标注规范和验收脚本

绝大多数课堂检测项目翻车的第一站不是模型,是数据。标注乱套、类别编号对不上、坐标范围错位,模型训练得再久也是白搭。这套系统里带的学生行为数据集已经按 YOLO 格式整理好,但你要把它用明白,得先知道里面是什么结构、能怎么改、怎么快速验收。

2.1 资源包里的三个模块:权重、数据集、界面代码

资源包根目录下分三层。模型层放训练好的 best.pt 权重以及 yolov8n.pt 预训练权重,前者让你开箱即用直接跑推理,后者用来做迁移学习重新训练;数据层是整理好的学生行为数据集,train 和 val 已按常见惯例做了 8:2 划分,图片是 jpg、标注是同名 txt;界面层是整套 PyQt5 工程源码,入口是 main.py,项目结构大致如下:

classroom_detection/ ├── models/ │ ├── best.pt │ └── yolov8n.pt ├── datasets/ │ └── classroom/ │ ├── train/ │ │ ├── images/ │ │ └── labels/ │ └── val/ │ ├── images/ │ └── labels/ ├── ui/ │ ├── main_window.py │ └── detect_thread.py ├── train.py └── main.py

理解这个结构有个关键点:best.pt 是训练产物,yolov8n.pt 是起点。你直接跑 main.py 能出界面,用的是 best.pt;但如果想把检测类别改成自己学校特有的场景,比如实验室安全行为,就得靠数据集重新训练。两层权重都放在包里,就是为了让"先跑起来"和"再自己训"两条路都走得通。

单独拿 best.pt 跑课堂监控,检测的是举手、阅读这类通用课堂行为,准确率在这个数据集覆盖的场景下表现不错。但是换一个教室、换摄像头角度,框就开始飘。这就是为什么要强调数据集的价值:它是给你留的"后悔药",别嫌占磁盘空间。

2.2 行为类别怎么定:八类还是六类,直接影响标注工作量

这套资源里常见做法是识别八类课堂行为:举手、阅读、写字、趴桌、困惑、专注、玩手机、睡觉。类别定得细,能覆盖教学评估里的大部分需求,代价也很直接——标注成本高,且 confusion 和 focus 这两类在视觉上边界模糊,手动标注时最容易产生噪声标签。如果你只是做答辩 demo,我会建议砍掉困惑和专注,用六类就够。

class_id类别名对应场景
0raise举手回答/提问
1read低头阅读教材
2write书写笔记
3bored趴桌、发呆
4confused困惑表情(边界模糊,易误标)
5focus专注看黑板(易和 read 混淆)
6play_phone低头玩手机
7sleep睡觉

改类别不是改个名字的事。你在 labelimg 或 Roboflow 里把类别列表改了,重新标注导出后,还得同步改 classroom.yaml 里的 nc 和 names 字段。nc 是类别总数,names 的索引顺序必须和标注文件里的 class_id 严格一致,顺序错了,训出来的模型会把猫叫成狗。我见过不止一个人在这个地方翻车,训练时 loss 降得很漂亮,一测全是错位的。

另外要提醒一句:sleep 和 bored 这类类别天然样本少,因为课堂监控视频里真正睡觉的学生占比不高。类别不平衡会导致小类别 mAP 特别低,后续训练时可以考虑给这类样本做水平翻转、亮度扰动,或者干脆把两类合并成"非学习状态"。

2.3 标注格式与验收:YOLO txt 的五个字段和一段检查脚本

YOLO 格式的标注是每张图片配一个同名 txt 文件,每行五个字段:类别 id、归一化中心 x、归一化中心 y、归一化宽、归一化高。最容易出问题的是"归一化"三个字——坐标是相对于图片宽高的比例,取值范围 0 到 1,不是像素坐标,也不是左上角加宽高的那种 VOC 格式。从 VOC 转过来的人常犯的错误是把像素值直接填进去,模型训练时坐标全飞到图片外面。

拿到数据集第一件事不是训练,是验收。我一般会先跑一段脚本检查标注文件完整性和坐标合法性:

import os from pathlib import Path root = Path("datasets/classroom") for split in ["train", "val"]: img_dir = root / split / "images" lbl_dir = root / split / "labels" print(f"===== 检查 {split} 集 =====") for img_path in sorted(img_dir.glob("*.jpg")): label_path = lbl_dir / (img_path.stem + ".txt") if not label_path.exists(): print(f"[缺失标注] {label_path}") continue with open(label_path, "r", encoding="utf-8") as f: lines = [line.strip() for line in f if line.strip()] for line in lines: parts = line.split() if len(parts) != 5: print(f"[字段数错误] {label_path}: {line}") break try: cx, cy, w, h = map(float, parts[1:]) except ValueError: print(f"[非数值坐标] {label_path}: {line}") break if not (0 <= cx <= 1 and 0 <= cy <= 1 and 0 <= w <= 1 and 0 <= h <= 1): print(f"[坐标越界] {label_path}: {line}") break print(f"{split} 集检查完成")

这段脚本的逻辑很简单:遍历 train 和 val 下的所有 jpg,检查同名 txt 是否存在,逐行解析五个字段,验证坐标是否都在 0 到 1 区间内。跑完没有输出基本可以说明标注文件在格式层面没问题。

但格式合法不代表标注贴框。我习惯再抽十张图,用 OpenCV 把标注框画出来人工看一眼,重点检查框是否包住人的上半身而不是只套住一个头。课堂行为检测的标注基准一般是"头肩区域加上手部动作范围",纯头部框会让 play_phone 和 read 这类依赖手部位置的行为完全分不开。这一步不要省,数据质量直接决定后面所有环节的上限。

3. 训练与推理:YOLOv8 参数设置与损失曲线解读

数据集验收通过,进入正题。这章我会把训练入口的每个参数讲清楚,再给两种推理写法,最后教你怎么从 results.csv 里判断模型是不是真的收敛了。很多初学者拿到代码就改 epochs 然后硬跑,跑完也不看曲线,这属于把唯一的调试手段扔了。

3.1 训练入口与核心参数:yolo train 一行命令背后的设置

资源里 train.py 本质上是调用 ultralytics 的训练接口,核心是 model.train 这一段。直接放能跑的版本:

import torch from ultralytics import YOLO def main(): # 有 NVIDIA GPU 就用 0 号卡,没有就退回 CPU device = "0" if torch.cuda.is_available() else "cpu" # 用 COCO 预训练权重做迁移学习,收敛速度远快于随机初始化 model = YOLO("yolov8n.pt") model.train( data="classroom.yaml", # 数据集配置文件 epochs=100, # 最大训练轮数 imgsz=640, # 训练图片分辨率 batch=16, # 每批图片数,显存小就调小 device=device, # 计算设备 workers=4, # 数据加载线程数 lr0=0.01, # 初始学习率 patience=20, # 连续 20 轮 val loss 不降则早停 ) if __name__ == "__main__": main()

参数里最需要理解的是四个。imgsz=640 是速度与精度的折中点,学生行为检测依赖手部和头部细节,640 够用,上 1280 精度提升有限但帧率掉一半以上,不建议上来就拉高。batch 受显存约束,GTX 1660Ti 这种 6GB 显存的卡,跑 YOLOv8n batch 可以到 16,换 YOLOv8s 就得降到 8,后面避坑章节会细说。lr0 在迁移学习场景下用 0.01 合适,如果是从零训练,这个值偏高,容易震荡。patience 是替你兜底的,loss 不降就停,省电省时间。

配套的 classroom.yaml 里面内容不多,但一行都不能错:

path: datasets/classroom train: train/images val: val/images nc: 8 names: ["raise", "read", "write", "bored", "confused", "focus", "play_phone", "sleep"]

path 是数据集根目录相对路径,train 和 val 指向图片目录,ultralytics 会自动去同级 labels 目录找标注。nc 和 names 必须和 2.2 节说的类别定义一致,这里写 8,你的标注里 class_id 最大就只能是 7,出现 8 训练会直接报错。

提示:训练前先确认数据集路径没有被中文或空格污染,yaml 解析对路径里的特殊字符容忍度很低,报错信息又不直观,这个坑我踩过一次,卡了半个多小时。

3.2 推理的两种写法:一次性识别和视频流处理

训练完得到 runs/detect/train/weights/best.pt,推理写法分两种。第一种是跑离线视频或图片集,适合批量验证:

from ultralytics import YOLO model = YOLO("runs/detect/train/weights/best.pt") results = model.predict( source="demo.mp4", # 支持图片、视频、文件夹 conf=0.5, # 置信度阈值,调低召回高但误检也多 save=True, # 保存带框输出 imgsz=640, )

predict 方式的好处是省事,save=True 会自动生成 annotated 的图片和视频,适合批量处理一整个文件夹的素材。conf 这个参数要注意:0.5 是通用值,但课堂场景里学生互相遮挡严重,目标框的置信度普遍偏低,我实际用下来 0.4 更合适,低于 0.3 就会把路过的人误判成学生。

第二种是实时视频流处理,咱们的资源核心场景就是摄像头实时检测,PyQt5 界面里跑的也是这套逻辑:

import cv2 from ultralytics import YOLO model = YOLO("best.pt") cap = cv2.VideoCapture(0) # 0 表示默认摄像头 while True: ret, frame = cap.read() if not ret: break # frame 直接传给模型,内部会自动处理预处理和后处理 results = model(frame, imgsz=640, conf=0.4, verbose=False) annotated = results[0].plot() # 画好框的帧 cv2.imshow("classroom_detect", annotated) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()

这里 model(frame) 返回的 results 是一个列表,每个元素对应一帧的检测结果。results[0].plot() 把框、类别名、置信度一次性画到帧上。verbose=False 很关键,不开的话终端会刷屏,每帧打印检测日志,长时间运行终端会爆。这段代码里我没做任何多线程处理,纯命令行演示够用,但到 PyQt5 界面里必须改造,原因下一章细讲。

3.3 从 results.csv 画损失曲线:判断模型是否真的收敛

训练结束后,runs/detect/train 下会生成 results.csv,里面逐行记录了每个 epoch 的 box_loss、cls_loss、dfl_loss、mAP50、mAP50-95 等指标。直接从 CSV 看数字不直观,我习惯画成曲线图,一眼就知道模型有没有收敛、有没有过拟合。

import pandas as pd import matplotlib.pyplot as plt df = pd.read_csv("runs/detect/train/results.csv") df.columns = [col.strip() for col in df.columns] # 列名首尾有空格,去掉 cols = ["train/box_loss", "val/box_loss", "metrics/mAP50(B)", "metrics/mAP50-95(B)"] fig, ax = plt.subplots(2, 2, figsize=(12, 8)) for idx, col in enumerate(cols): row, col_idx = divmod(idx, 2) ax[row][col_idx].plot(df[col], linewidth=1.5) ax[row][col_idx].set_title(col) ax[row][col_idx].grid(True) plt.tight_layout() plt.savefig("loss_curves.png", dpi=150)

这段代码做了三件事:读取 results.csv,把列名里的空格剥掉,然后在一个 2×2 的画布里分别画 train 和 val 的 box_loss、mAP50、mAP50-95 曲线。divmod(idx, 2) 负责把四个子图排成两行两列。

怎么判断曲线是健康的?看两个点。第一,train/box_loss 和 val/box_loss 应该同步下降并在某个 epoch 后趋于平缓,如果 train 还在降而 val 开始反弹,说明过拟合,应该调小 epochs 或加大数据增强。第二,mAP50 曲线如果在前 30 轮还在明显上升,说明训练不够,让 patience 继续跑;如果 mAP50 已经平了但 mAP50-95 还在爬,说明模型定位精度还在提升,可以多训几轮。只看 mAP50 不看 mAP50-95 是常见的误判方式,前者衡量框选得准不准,后者衡量框和真实标注的重合度,课堂行为检测对框的贴紧程度要求高,两个指标都要关注。

4. PyQt5 界面集成:从视频流到行为统计面板的完整链路

数据集能训、权重能推理,剩下就是把这一切装进图形界面。很多人在这一步把系统做成了"命令行里能跑但没人敢点"的半成品。PyQt5 这层要解决的核心问题不是画界面,而是怎么让摄像头帧、模型推理、UI 刷新三者不互相卡死。

4.1 界面模块拆解:视频区、统计表和控制按钮各归其位

课堂检测系统的图形界面按使用流拆分,四个区域:左侧大面积放摄像头预览,右侧放实时行为统计表,底部放开始、停止、导出报告三个按钮,顶部状态栏显示模型加载状态和当前帧率。布局上用 QVBoxLayout 套 QHBoxLayout,左侧视频区占窗口 70% 宽度,右侧统计表占 30%。

from PyQt5.QtWidgets import QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QPushButton, QTableWidget, QTableWidgetItem class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("课堂行为检测系统") self.resize(1280, 720) central = QWidget() self.setCentralWidget(central) layout = QHBoxLayout(central) left = QVBoxLayout() self.video_label = QLabel("等待摄像头开启") self.video_label.setMinimumSize(640, 480) left.addWidget(self.video_label) layout.addLayout(left, stretch=7) right = QVBoxLayout() self.table = QTableWidget(8, 2) self.table.setHorizontalHeaderLabels(["行为类型", "人数"]) right.addWidget(self.table) self.btn_start = QPushButton("开始检测") self.btn_stop = QPushButton("停止检测") right.addWidget(self.btn_start) right.addWidget(self.btn_stop) layout.addLayout(right, stretch=3)

这段代码搭建了最基础的骨架。video_label 用来贴视频帧,QTableWidget 用来刷新统计数字。布局里 stretch=7 和 stretch=3 是关键参数,控制左右区域宽度比,视频区太小看不清,统计区太大浪费。按钮先创建出来,信号槽连接放后面。资源里实际界面会比这个花哨,但剥掉样式,跑的核心结构就是这一套。

4.2 用 QThread 隔离推理,别让检测卡死界面

如果把 3.2 节的 while 循环直接塞进按钮的槽函数里,点击"开始检测"后窗口立刻白屏,整个界面变成"未响应"。原因很简单:QThread 之外的代码都跑在主线程,而 Qt 的事件循环被 while 循环阻塞,界面刷新、按钮点击、窗口拖动全部停摆。这是 PyQt5 做实时视频最经典的翻车点,必须把耗时操作挪到子线程。

import cv2 from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap from ultralytics import YOLO class DetectionThread(QThread): frame_ready = pyqtSignal(dict) # 把帧和统计结果一起发出去 def __init__(self, model_path, source=0, parent=None): super().__init__(parent) self.model = YOLO(model_path) self.source = source self.running = True self.statistics = {} def run(self): cap = cv2.VideoCapture(self.source) if not cap.isOpened(): self.frame_ready.emit({"error": "摄像头打开失败"}) return while self.running: ret, frame = cap.read() if not ret: break results = self.model(frame, imgsz=640, conf=0.4, verbose=False) boxes = results[0].boxes annotated = results[0].plot() # 统计每个类别的目标数量 self.statistics = {} if len(boxes) > 0: cls_ids = boxes.cls.tolist() names = results[0].names for cls_id in cls_ids: name = names[int(cls_id)] self.statistics[name] = self.statistics.get(name, 0) + 1 # 转成 QImage 交给主线程显示 rgb_image = cv2.cvtColor(annotated, cv2.COLOR_BGR2RGB) h, w, ch = rgb_image.shape qimg = QImage(rgb_image.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit({ "image": QPixmap.fromImage(qimg.copy()), "statistics": self.statistics, }) cap.release() def stop(self): self.running = False self.wait()

这个类有三个设计点。第一,QThread 的 run 方法里做全部耗时操作,读帧、推理、画框都在子线程,主线程只负责接收信号。第二,frame_ready 信号用 dict 打包数据,一张 QPixmap 和一份统计字典同时传出去,避免写多个信号。第三,stop 方法把 running 置为 False 后调用 wait(),确保线程安全退出,直接在 closeEvent 里粗暴 terminate 会造成段错误。

有个细节必须注意:QImage 构造时传的是 rgb_image.data,但如果这张图在信号发射后被主线程引用,底层数据可能被 Python 垃圾回收。所以我在 emit 之前调用 qimg.copy(),把像素数据复制一份,防止界面显示花屏或崩溃。这个坑排查起来非常隐蔽,问题不在推理,在 PyQt 对象生命周期。

4.3 信号槽把检测结果刷进统计面板

子线程把数据发出来,主线程槽函数负责刷新界面。这是整个系统的"最后一公里":

class MainWindow(QMainWindow): def __init__(self): super().__init__() # ... 省略 4.1 的界面初始化代码 ... self.detect_thread = DetectionThread("best.pt", 0, self) self.detect_thread.frame_ready.connect(self.update_ui) self.btn_start.clicked.connect(self.detect_thread.start) self.btn_stop.clicked.connect(self.detect_thread.stop) def update_ui(self, data): if "error" in data: self.statusBar().showMessage(data["error"]) return pixmap = data["image"].scaled( self.video_label.size(), aspectRatioMode=1, # 保持宽高比,不变形 ) self.video_label.setPixmap(pixmap) stats = data["statistics"] names = ["raise", "read", "write", "bored", "confused", "focus", "play_phone", "sleep"] for row, name in enumerate(names): self.table.setItem(row, 0, QTableWidgetItem(name)) self.table.setItem(row, 1, QTableWidgetItem(str(stats.get(name, 0)))) def closeEvent(self, event): self.detect_thread.stop() event.accept()

update_ui 里做了两件事:把子线程传来的 QPixmap 缩放后贴到 video_label,然后把统计字典刷新进表格。scaled 的第二个参数 aspectRatioMode=1 保持图片比例,如果不加,摄像头 16:9 的画面会被强行拉伸成 QLabel 的尺寸,人脸全变形,检测框也跟着视觉错位。

按钮连接用的是 clicked.connect(self.detect_thread.start),注意 QThread 继承自 QObject,start 是它的内置方法,clicked 信号会触发线程启动。停止按钮同理。closeEvent 里调用 stop 并 accept,确保关窗口时线程先退出,否则 Python 解释器退出时子线程还在读摄像头,经常报 cv2.error 或者直接崩溃。到这里,整套系统已经从摄像头像素流转换成了界面上不断滚动的统计数字,中间隔着一个信号槽的桥梁,链路完整闭合。

5. 避坑与常见问题:五条踩坑记录与修复方法

这套系统我拆的时候踩了不少坑,挑五个最常见的记录在下面。每条都按"现象→原因→解决"写,前两条是最多人问的,后三条属于那种一晚上才能定位的玄学问题。

5.1 一开摄像头界面就白屏卡死

现象:点击"开始检测"按钮后,窗口立刻白屏,标题栏出现"未响应",过几十秒后系统提示强制关闭。

原因:推理循环直接写在了按钮的槽函数里。我在 4.2 节强调过,Qt 的界面刷新依赖事件循环,而 while 循环读摄像头会霸占主线程不放,事件循环永远得不到执行机会。从外面看就是界面死了,实际是 CPU 在疯狂跑推理。

解决:把摄像头读取、模型推理、画框全部挪到 QThread 子线程,主线程只保留 setPixmap 和 setItem 这类极轻量的 UI 操作。如果你实在不想引入线程,另一个折中方案是用 QTimer 每 30 毫秒触发一次抓帧,但推理耗时超过间隔时 QTimer 会堆积回调,卡顿依旧。正解就是线程加信号槽,没有第二条路。

5.2 训练完 mAP 很低但 loss 正常

现象:训练日志里 box_loss 从 2 降到 0.4,曲线非常漂亮,但验证集 mAP50 只有 0.3 出头,界面上框乱飞。

原因:loss 下降只能说明模型在拟合训练集,不代表标注本身是对的。最常见的三个问题,第一是类别 id 从 1 开始标,而 yaml 里从 0 开始,所有类别整体错位;第二是图片和 txt 文件名对不上,模型学到的是图 A 的标注对应图 B 的内容;第三是数据集里混入大量没有目标的空图片,模型被迫把背景学成目标。

解决:先跑 2.3 节的验收脚本,确认每个 txt 都存在且类别 id 合法。然后看 runs/detect/train 下的 confusion_matrix.png,这张混淆矩阵图会告诉你哪些类别互相污染。如果 sleep 类大量被识别成 bored,说明这两类在视觉上确实接近,检查标注框是不是没区分"趴桌睡觉"和"趴桌发呆",必要时合并类别。最后确认数据集里空图片比例,超过 5% 就删掉,空图会让模型产生幽灵框。

5.3 import ultralytics 直接报错

现象:pip install ultralytics 显示安装成功,但代码里 import 时报缺 numpy 依赖或 torch 版本冲突,重装之后又报新的错。

原因:系统里装了多个 Python,pip 装进 A 环境,代码跑在 B 环境。更隐蔽的是 ultralytics 对 numpy 和 opencv 的版本范围有要求,你手动升级过的 numpy 2.x 和它不兼容,import 时在 C 扩展层直接炸掉。

解决:用 venv 建独立环境,Python 版本用 3.10,不要用 3.12(部分依赖尚未完全适配)。建完环境后按顺序装:pip install torch torchvision,再 pip install ultralytics,最后 pip install pyqt5 opencv-python。装完先跑一句 import torch; import ultralytics; print(ultralytics.version) 确认环境干净,再跑完整推理。不要偷懒在系统 Python 里直接装,血泪教训,环境混装的问题排查起来比写代码还耗时。

5.4 检测画面颜色发蓝发绿

现象:摄像头画面偏色,人脸的肤色变成青绿色,或者检测框位置和实际物体偏移几个像素。

原因:OpenCV 读到的帧默认是 BGR 三通道,而 YOLOv8 内部预处理按 RGB 处理。如果你在推理前手动做过某些转换,又在显示前少转了一次,颜色就会错位。PyQt 的 QImage 默认 Format_RGB888,要求输入 RGB,直接喂 BGR 数据就会看到蓝绿互换的诡异画面。

解决:统一颜色处理链。读取帧保持 BGR,推理时直接传给 YOLO(它会内部转),画框后的 annotated 也是 BGR,此时做一次 cv2.COLOR_BGR2RGB 转成 RGB 再交给 QImage。别在推理前后做多次转换,每转一次就多一次出错机会。颜色对了,框的位置自然跟着对——颜色通道颠倒不影响框坐标,但影响你肉眼看画面,后续手动标框和调试时会非常别扭。

5.5 训练时显存溢出

现象:训练到第 20 轮左右,终端报 CUDA out of memory,进程直接被杀,前面训的全部白费。

原因:batch 和 imgsz 同时开太大。GTX 1660Ti 这种 6GB 显存的卡,跑 YOLOv8n 时 batch=16 是极限,换 YOLOv8s 显存翻倍,不降 batch 必爆。如果开了多线程数据加载,每个 worker 也占额外显存。

解决:三个参数联动调。batch 从 16 降到 8,imgsz 从 640 降到 480,worker 从 4 降到 2。ultralytics 默认开启 AMP 混合精度,显存占用能省三分之一,确认训练日志里没有显示 amp=False。还炸就换模型,yolov8n 已经是 nano 版,再小只能剪枝。不要幻想用 CPU 训练替代,100 轮的训练量 CPU 跑三天三夜,GPU 两小时,该升级硬件就升级,实在没有 GPU 就租云实例,几百张图的数据集也不用斥巨资。

6. 进阶技巧:导出 ONNX 并部署到 RK3588

系统在 PC 上跑通只是第一步。很多读者拿到这套资源后问能不能部署到 RK3588 这类带 NPU 的边缘开发板上,让检测不依赖电脑。这条路是通的,但中间要过模型转换这一关。

先看单张测试图:

from ultralytics import YOLO model = YOLO("best.pt") results = model.predict(source="test_classroom.jpg", conf=0.4, save=True) print(results[0].speed) # 查看预处理/推理/后处理耗时

在 PC 上确认模型没问题后,导出 ONNX:

yolo export model=best.pt format=onnx opset=12 imgsz=640

导出的关键参数是 opset,RKNN 工具链对 opset 有兼容范围要求,12 是稳妥值。导出后不要在 PC 上验证 ONNX 精度就完事,RK3588 的 NPU 走的是 int8 量化,从 FP16 到 int8 精度会掉 1 到 3 个点。量化用的校准图片要从训练集里抽,不要用随机图片,否则分布偏差会让量化后的模型在真实场景里误报暴增。我之前贪省事用二十张网图做校准,部署后 conf=0.5 阈值下框全飘,后来才发现是把校准集给省没了。

部署后验证时,我会用一个固定视频做回归测试,记录三组数据:单帧推理延迟、mAP50、实际画面里的误报数。PC 上 GPU 跑 30 FPS 不代表 NPU 上不掉帧,RK3588 的 int8 推理对遮挡场景更敏感,conf 阈值从 0.4 回调到 0.45 到 0.5 之间观察一下。如果板子上只是为了跑推理服务,PyQt5 界面可以留在上位机,通过局域网把检测结果推过去,这样界面开发不用改,板子压力也小。

我头一回做 RKNN 量化时没核对工具链版本,板子端加载模型直接报版本错,折腾一晚上才发现是 rknn-toolkit 和板端驱动不匹配。从那以后我每次转模型前都先把版本号写进 README,转换完立刻在板子上跑一轮回归测试再收工。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询