课堂行为检测这个项目,在学校里最常见的就是毕业设计,其次是一些教育信息化公司的产品原型。很多同学上来就直接pip install ultralytics,然后跑默认的yolov8n.pt,看到框能出来就觉得自己做完了。但实际上,从“模型能检测”到“系统能落地”,中间还隔着数据标注、行为逻辑设计、性能优化三道坎。这篇文章就围绕“基于YOLOv8的课堂行为检测系统”这个题目,把我实际做过的流程、踩过的坑、调参心得全部写出来,给正在做课设、毕设或者准备部署到教室环境的朋友一个完整的参考。
这个系统能解决什么问题,我先说清楚:教室里的摄像头拿到视频流之后,系统自动识别学生的行为状态,比如举手、睡觉、玩手机、站立、低头写字、转头说话等,并把这些行为按时间统计成数据,支持实时报警和课后报表。适合谁来参考?如果你手里只有普通电脑,甚至只有CPU,想先跑通完整流程;或者你想了解YOLOv8从数据制作、模型训练、行为判定到界面展示的完整链路,这篇文章就是按这个顺序写的。
1. 整体设计:课堂行为检测系统到底在做什么
很多人拿到这个题目第一反应是“用YOLOv8检测学生行为”,但实际做的时候会发现,YOLOv8只负责输出“目标框+类别+置信度”,它不负责判断这个行为是否持续、是否违规,也不负责统计。所以系统真正的架构,是把YOLOv8当成一个“眼睛”,后面还要接一套逻辑层和数据层。
1.1 从业务需求到技术需求的映射
先去想用户是谁。这个系统的用户分三层:
- 授课老师:需要实时知道哪些学生在玩手机、睡觉,最好能弹窗提醒。
- 教务/管理人员:需要课后查看统计报表,比如某个班这节课的“低头率”“举手次数”。
- 技术维护人员:需要能调视频源、改检测区域、标记误报。
不同用户对应不同的功能模块。老师要的是实时性,所以前端界面要简洁,检测结果延迟要低;教务要的是数据沉淀,所以要有SQLite或MySQL记录每次检测结果,并生成可视化图表;维护人员要的是可配置性,比如摄像头位置不同,检测区域就需要人工调整。
把这个需求拆成技术模块,大致是四块:
- 视频输入模块:读取本地视频、RTSP摄像头流或直接调用USB摄像头。
- 行为检测模块:YOLOv8模型负责目标检测,输出人的位置和行为类别。
- 行为判定与统计模块:根据类别、持续时间、空间位置做规则判断,比如“躺桌面上超过5秒”才判定为睡觉。
- 可视化与存储模块:GUI界面实时显示画面、检测框、统计曲线,同时把记录写入数据库。
我见过不少同学只做了1和2,后面两个全用print输出代替,这样答辩时只能说明模型能用,不能证明系统完整。真正要加分的地方,恰恰是后面的判定、统计和可视化。
1.2 行为类别怎么定义
行为类别直接影响标注工作量、模型难度和最终准确率,所以第一步就得想清楚。课堂场景里常见的行为大概有这些:
| 行为类别 | 定义 | 检测难度 |
|---|---|---|
| 举手 | 手臂抬起高于肩部 | 中等 |
| 睡觉/趴桌 | 头部趴在桌面上 | 低 |
| 站立 | 人站起,目标框拉长 | 低 |
| 玩手机 | 手部持手机或低头看手机 | 高 |
| 抬头听课 | 正常坐姿,面向前方 | 低 |
| 低头写字/看书 | 头部低垂,手部有动作 | 中等 |
| 转头/说话 | 头部偏向侧面,嘴部动作 | 高 |
我建议第一次做不要贪多,选4到6个类别就够了。原因很简单:每个类别至少需要几百到上千张标注样本,类别越多,数据采集和标注的时间成本翻倍上升,而且相似类别(比如“低头写字”和“玩手机”)会导致模型混淆,准确率反而下降。
如果只是毕设,推荐这个组合:person_raise_hand、person_sleep、person_stand、person_phone、person_write、person_turn_head。既有区分度,又覆盖了课堂场景的主要管理需求。
1.3 为什么选YOLOv8而不是其他算法
YOLOv8是Ultralytics在2023年推出的版本,相比YOLOv5,它在C2f模块、Anchor-Free检测头、Decoupled Head这些结构上做了改动,在同等参数规模下精度和速度都有提升。更重要的是它的工具链非常完整:训练、验证、导出ONNX/TensorRT、可视化都在一个命令行里完成,这对做系统的人来说太重要了。
对比一下常见方案:
- YOLOv5:生态成熟,资料多,但官方维护进入后期,新特性少。
- YOLOv8:文档全、API干净、支持旋转框和姿态估计,适合做课堂行为这种多类别检测。
- YOLOv9/YOLOv10:精度更高,但对新手环境配置要求也更高,且部分部署工具链还没完全跟上。
- Faster R-CNN:精度尚可但速度太慢,实时教室场景基本不考虑。
- 基于Transformer的DETR:小数据集上收敛慢,部署成本高,不适合课设毕设体量。
另外,YOLOv8的n/s/m/l/x五个规格里,nano和small在CPU上也能勉强跑实时,这给没有NVIDIA显卡的同学留了后路。我的判断是:做课堂行为检测,YOLOv8是综合性价比最高的选择。
2. 环境搭建与数据准备:地基不牢,后面全白干
很多人的项目死在第一步:环境装不上、版本对不上、数据集格式不对。我建议把环境搭建和数据处理当作独立阶段来做,不要急着跑训练。
2.1 CPU还是GPU?环境怎么选
先明确一个事实:YOLOv8训练时,GPU比CPU快几十倍,但推理时CPU也不是不能用。如果你只有普通笔记本,训练一个6000张图片的模型可能要十几个小时,但做课设毕设完全能接受,晚上挂机跑就行。如果要用GPU,优先考虑显存6G以上的显卡,比如GTX 1660 Ti、RTX 2060及以上,显存低于4G建议直接用YOLOv8n。
搭建环境分两种场景,我分别写步骤。
GPU版本(推荐):
# 1. 安装NVIDIA驱动和CUDA # 在终端执行nvidia-smi,看右上角Driver Version和CUDA Version # 根据CUDA版本选择对应的PyTorch安装命令 # 2. 创建conda虚拟环境 conda create -n yolo python=3.9 -y conda activate yolo # 3. 安装PyTorch(以CUDA 11.8为例,其他版本去PyTorch官网生成命令) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 4. 安装ultralytics pip install ultralytics # 5. 验证 python -c "import torch; print(torch.cuda.is_available())"CPU版本:
conda create -n yolo_cpu python=3.9 -y conda activate yolo_cpu pip install torch torchvision pip install ultralyticsCPU版不需要管CUDA,PyTorch会自动安装CPU版本。但要注意三个坑:一是Python版本别选3.12及以上,很多依赖还没适配;二是Windows下如果pip下载慢,用清华源;三是ultralytics依赖的opencv-python版本要大于4.7,安装时如果冲突就手动指定pip install opencv-python==4.8.1.78。
2.2 标注工作:Labelme转YOLO格式的完整步骤
课堂行为检测属于“自建数据集”,不能用现成的COCO预训练模型直接输出这些类别,你必须自己采集和标注数据。我推荐用Labelme做标注,因为它可以直接把标注结果转换成YOLO格式,比LabelImg更省事。
标注流程:
# 安装labelme pip install labelme # 启动 labelme打开标注工具后,左侧选择Open Dir加载图片文件夹,然后用Create Polygons拉框。这里有个关键点:Labelme默认创建的是多边形,而YOLO需要的是矩形框。所以画框的时候用鼠标拖出一个矩形区域,四个点落在目标边缘上,保存后生成的JSON里就是矩形坐标,转换脚本才能正确处理。
数据来源主要有三个途径:
- 自己录教室视频,抽帧成图片(视频每2秒抽一帧)。
- 在公开数据集(如COCO、PASCAL VOC)中筛选包含学生的图片。
- 网上找课堂场景图片,注意版权问题。
我建议自己录视频抽帧,因为课堂场景的俯视角度、座位分布、光线条件都比较特定,网上图片风格杂,会让模型学得“乱”。
标注完所有JSON之后,用脚本转换成YOLO格式。核心转换逻辑如下:
import json import os from PIL import Image # 类别映射 class_names = ['person_sleep', 'person_stand', 'person_phone', 'person_raise_hand', 'person_write', 'person_turn_head'] def convert_labelme_json(json_path, image_dir, output_dir): with open(json_path, 'r', encoding='utf-8') as f: data = json.load(f) # 获取图像尺寸(注意源文件是相对路径,要结合image_dir) img_path = os.path.join(image_dir, data['imagePath'].split('/')[-1]) img = Image.open(img_path) img_width, img_height = img.size # 生成txt文件 base_name = os.path.basename(json_path).replace('.json', '') txt_path = os.path.join(output_dir, base_name + '.txt') with open(txt_path, 'w') as out_f: for shape in data['shapes']: label = shape['label'] if label not in class_names: continue class_id = class_names.index(label) # labelme的点坐标格式 [[x1,y1],[x2,y2],[x3,y3],[x4,y4]] points = shape['points'] x_coords = [p[0] for p in points] y_coords = [p[1] for p in points] x_min, x_max = min(x_coords), max(x_coords) y_min, y_max = min(y_coords), max(y_coords) # 归一化到0~1 x_center = (x_min + x_max) / 2 / img_width y_center = (y_min + y_max) / 2 / img_height width = (x_max - x_min) / img_width height = (y_max - y_min) / img_height out_f.write(f"{class_id} {x_center:.6f} {y_center:.6f} {width:.6f} {height:.6f}\n") print(f"转换完成: {txt_path}") # 批量转换目录下所有json import glob json_files = glob.glob('data/labelme/*.json') for json_path in json_files: convert_labelme_json(json_path, 'data/images', 'data/labels')转换完一定要抽查几个txt文件,用可视化工具把YOLO格式的坐标画回图片上检查。我最常见的错误是图片宽高顺序写反,导致框整体偏移,这种情况在训练时模型loss能降,但预测框位置会偏,特别坑。
2.3 数据集划分与目录结构
YOLOv8训练的数据集需要固定目录结构,既是给模型看的,也是给后续维护看的。我建议一开始就按下面这种方式组织:
dataset/ ├── images/ │ ├── train/ # 训练图片 │ ├── val/ # 验证图片 │ └── test/ # 测试图片 ├── labels/ │ ├── train/ # 对应训练图片的txt标注 │ ├── val/ │ └── test/ └── data.yaml # 数据集配置文件划分比例推荐8:1:1或者7:2:1,关键是保证验证集里每个类别都有样本,不要出现训练集有“睡卧”类别、验证集完全没有的情况。
data.yaml内容:
train: dataset/images/train val: dataset/images/val test: dataset/images/test nc: 6 names: ['person_sleep', 'person_stand', 'person_phone', 'person_raise_hand', 'person_write', 'person_turn_head']注意train和val路径用相对路径还是绝对路径取决于你执行训练命令的目录位置。为了避免路径坑,我统一建议使用绝对路径,除非你把yolo命令的cwd固定在同一层级。
2.4 数据增强:在有限样本下提高泛化能力
课堂场景数据往往只有几百张图片,模型很容易过拟合。YOLOv8内置了丰富的数据增强参数,在训练配置里直接设置即可:
hsv_h=0.015、hsv_s=0.7、hsv_v=0.4:颜色抖动,模拟不同光线。degrees=10:小角度旋转,因为摄像头角度各有不同。translate=0.1:目标平移,模拟人在画面中位置变化。scale=0.5:缩放,模拟摄像头远近变化。fliplr=0.5:水平翻转,注意这个只适合对称场景,如果行为类别是“举手”,翻转后左右手会换边,但语义不变,所以可以用。
但有个问题要提醒:如果你用默认配置,YOLOv8会在每个epoch动态做Mosaic增强,也就是把4张图拼在一起。Mosaic增强对小目标检测很有帮助,但也会引入大量小物体堆积,导致显存占用暴涨。如果你的显存只有4G,建议把mosaic=0.0或mosaic=0.5关掉,否则训练中期很容易OOM。
3. 模型训练:参数不是随便填的
训练是整套系统里最有“技术含量”的环节,但很多人的训练就是一句yolo detect train跑完。这个章节讲清每个关键参数的意义,以及怎么根据你自己的数据和硬件条件去设定,顺便教你看训练曲线。
3.1 训练命令与关键参数解读
我用一个最常见的训练命令展示:
yolo detect train \ data=/path/to/data.yaml \ model=yolov8n.pt \ epochs=100 \ imgsz=640 \ batch=16 \ workers=4 \ optimizer=AdamW \ lr0=0.001 \ lrf=0.01 \ warmup_epochs=3 \ patience=20 \ project=runs/detect \ name=train_classroom参数含义逐一说:
model=:可以填预训练权重路径(如yolov8n.pt),也可以填模型配置文件(如yolov8n.yaml)。我推荐用预训练权重,因为COCO预训练的知识能加速收敛,尤其对“人”这个基础目标帮助很大。epochs:总训练轮数。课堂行为类别数少、样本量几千时,100轮通常够;如果数据量大可以到150~200轮。imgsz:输入图片尺寸。640是平衡速度和精度的默认值,但如果教室摄像头画面大、目标小而密,可以升到1024,精度会提升,但显存消耗也增加不少。batch:每批图片数。显存8G用YOLOv8s,batch=16基本顶格;6G建议8;4G建议4,或者换YOLOv8n。workers:数据加载线程数,Windows系统建议设为0或2,设太大会报错。optimizer和lr0:AdamW收敛稳定,但学习率比SGD要小,我一般用0.001;SGD则用0.01。patience:早停机制,验证集指标连续patience轮不提升就停止训练,省时间。project和name:决定模型输出目录,比如runs/detect/train_classroom。后续导出、测试都去这个目录找best.pt。
训练完成之后,输出目录里最重要的几个文件是:
weights/best.pt:验证集上指标最好的权重。weights/last.pt:最后一轮的权重。results.png:总指标曲线图。confusion_matrix.png:混淆矩阵,看哪些类别互相混。val_batch*.jpg:验证集上的预测效果预览。
不要用last.pt,一定要用best.pt,除非你发现best对应的epoch太早、模型欠拟合,再去训练加轮次。
3.2 训练过程怎么判断好坏:看曲线而不是看loss数字
很多新手盯着终端里的loss数值,看到loss降到0.5就欢天喜地,其实不对。loss值本身没有绝对好坏意义,因为它由多个损失项加权组合而成,不同模型、不同数据集之间没有可比性。正确的做法是看results.png里的两条主线:
- mAP50和mAP50-95:平均精度,越高越好。
- precision(精确率)和recall(召回率):精确率关注“检出的框有多少是对的”,召回率关注“所有真实目标有多少被查出来了”。
课堂行为检测里,我会优先保住召回率。因为“漏报”一个睡觉的学生,比“误报”一个正常坐姿的学生严重得多。老师看到系统弹窗说某学生睡觉,下一秒发现是误报,整个系统的信任感就崩塌了。
如果训练曲线出现这种情况:训练loss一直降、验证mAP不涨甚至上升,说明过拟合了。解决方向:增加数据量、加强数据增强、把模型从yolov8n换成更小的yolov8n并减小参数量,或者提前早停。
如果是这个情况:训练和验证的loss都降不下去,mAP在0.2以下,先排查数据标注是不是有错,再检查类别定义是否有重叠(比如“低头写字”和“玩手机”确实容易混),最后才考虑调参数。
3.3 模型轻量改进与部署导出
训练好后,下一步是根据实际部署环境决定要不要对模型做改进和导出。如果你的目标设备是普通CPU工控机,直接用YOLOv8n就够了;如果还想轻量化,可以考虑把部分C2f模块换成更轻的结构,但新手不推荐魔改,容易把网络搞坏。先跑通,再优化。
导出成不同格式的命令:
# 导出ONNX yolo export model=best.pt format=onnx imgsz=640 # 导出OpenVINO(Intel CPU加速) yolo export model=best.pt format=openvino imgsz=640 # 导出TensorRT(需要NVIDIA GPU) yolo export model=best.pt format=engine imgsz=640我在实际项目里发现,同样一个best.pt,用ONNX Runtime推理比直接用PyTorch推理快1.5倍到2倍;用OpenVINO跑在Intel CPU上又能再快1倍。所以如果你的系统没有GPU,建议至少导出ONNX,用onnxruntime来跑,体验完全不同。
4. 系统实现:检测框怎么变成“行为记录”
模型训练结束后,真正体现“系统”二字的编码工作才开始。这一章是我认为整套项目价值最高的部分,也是很多教程里最缺的部分:怎么把YOLOv8的输出变成课堂行为数据。
4.1 实时视频流与检测循环
我用一个简单的Python类来组织检测模块,这样GUI和后台逻辑都能复用:
import cv2 import torch from ultralytics import YOLO class ClassroomDetector: def __init__(self, model_path, conf_thres=0.4, iou_thres=0.45, device='cpu'): self.model = YOLO(model_path) self.conf_thres = conf_thres self.iou_thres = iou_thres self.device = device self.class_names = self.model.names def detect_frame(self, frame): results = self.model.predict( frame, conf=self.conf_thres, iou=self.iou_thres, device=self.device, verbose=False )[0] boxes = results.boxes.xyxy.cpu().numpy() # [x1, y1, x2, y2] confs = results.boxes.conf.cpu().numpy() cls_ids = results.boxes.cls.cpu().numpy().astype(int) return boxes, confs, cls_ids这里有几个很重要的细节:
conf_thres(置信度阈值)设置成多少合适?课堂场景背景相对单一,0.4比较合适;如果你发现误报率高,升到0.5;漏报高,降到0.3。这个参数直接影响用户体验,不要用默认的0.25直接上生产。iou_thres(NMS阈值)一般不用动,0.45到0.5之间都行,它决定同一目标多个重叠框怎么合并。results对象里有plot()方法可以直接画检测结果图,但实际项目里建议自己画框,因为你还要加统计信息、编号和状态颜色。
4.2 行为判定规则与统计逻辑
单帧检测结果只是一堆“此时此刻的类别”,但课堂行为是有时间维度的。比如学生短暂低头拿了一下东西,不能被判成“玩手机”;学生趴在桌上5秒,才算“睡觉”。所以要引入一个状态机或者滑动时间窗口。
我的做法是:维护一个student_status字典,记录每个学生的ID、当前行为类别、行为开始时间、行为持续时间,然后每帧更新。
import time class BehaviorTracker: def __init__(self, min_duration=3.0): self.min_duration = min_duration # 最短持续时间,单位秒 self.status = {} # student_id -> (class_id, start_time) self.records = [] # 保存行为记录,用于统计 def update(self, boxes, cls_ids, frame_time): current_frame_ids = set() for i, cls_id in enumerate(cls_ids): # 这里需要一个目标跟踪算法来给每个框分配稳定ID # 简单场景可以用IoU匹配,复杂场景建议用ByteTrack student_id = assign_id(boxes[i]) current_frame_ids.add(student_id) # 类别变化时重置计时 if student_id in self.status: old_cls, start_time = self.status[student_id] if old_cls != cls_id: self.status[student_id] = (cls_id, frame_time) else: self.status[student_id] = (cls_id, frame_time) # 一段时间内未出现的ID,判断是否满足行为持续时间条件 for sid in list(self.status.keys()): if sid not in current_frame_ids: cls_id, start_time = self.status.pop(sid) duration = frame_time - start_time if duration >= self.min_duration: self.records.append((sid, cls_id, start_time, frame_time, duration))这里最容易被忽略的是目标跟踪问题。YOLOv8本身不做跨帧目标ID匹配,也就是说上一帧检测到的学生A和下一帧检测到的学生B,模型不知道是不是同一个人。如果你不跟踪,就没法准确统计每个学生的行为持续时间,更没法生成个人报告。
我推荐两个方案:
- 简单方案:每一帧用IoU匹配,把上一帧的框和当前帧的框重叠度最高的关联起来,适合目标少、画面稳定的课堂场景。
- 严谨方案:接入ByteTrack或者BoT-SORT跟踪算法。Ultralytics里有
model.track()方法,可以直接用ByteTrack。
model.track()用法:
results = self.model.track(frame, persist=True, tracker='bytetrack.yaml')persist=True意味着跟踪状态跨帧保留,输出结果里会有track_id属性。如果自己写追踪器,IOU匹配的伪代码大概是:
def iou(box1, box2): # 计算两个框的交并比 pass def assign_ids(prev_boxes, curr_boxes, prev_ids, iou_threshold=0.3): # 为每个当前框找一个与上一帧距离最近的框ID assigned = [None] * len(curr_boxes) used = set() for i in range(len(curr_boxes)): best_iou = 0 best_idx = -1 for j in range(len(prev_boxes)): if j in used: continue val = iou(prev_boxes[j], curr_boxes[i]) if val > best_iou: best_iou = val best_idx = j if best_iou > iou_threshold: assigned[i] = prev_ids[best_idx] used.add(best_idx) else: assigned[i] = new_id() return assigned如果你的目标就是毕设,这个简易IOU匹配完全够用。
4.3 界面与可视化怎么做
界面部分我推荐用PySide6(PyQt的升级版)。为什么要用GUI而不直接命令行?因为答辩演示和实际使用都需要直观的“实时画面+数据看板”效果。
界面可以分成三个区域:
- 左侧:实时视频画面,上面画检测框、学生ID、行为标签。
- 右上:实时行为统计柱状图,显示当前各类别人数。
- 右下:异常记录列表,比如“学生5 玩手机 持续23秒”,以及数据库写入状态。
GUI线程和检测线程必须分离。否则视频帧渲染会遇到卡顿、界面无响应。用QThread做后台推理线程,用信号把检测结果传给主线程更新UI。我给出一个基础框架:
from PySide6.QtCore import QThread, Signal import cv2 class DetectThread(QThread): frame_ready = Signal(object, object) # 原始帧 + 检测结果 def __init__(self, detector, video_source=0): super().__init__() self.detector = detector self.cap = cv2.VideoCapture(video_source) self.running = True def run(self): while self.running: ret, frame = self.cap.read() if not ret: break boxes, confs, cls_ids = self.detector.detect_frame(frame) self.frame_ready.emit(frame, {'boxes': boxes, 'confs': confs, 'cls_ids': cls_ids}) def stop(self): self.running = False self.wait()主窗口里连接信号:
self.detect_thread.frame_ready.connect(self.update_ui) def update_ui(self, frame, detections): # 在frame上画框和标签 for box, conf, cls_id in zip(...): cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, f"{class_names[cls_id]} {conf:.2f}", ...) # 转成QImage显示 self.video_label.setPixmap(QPixmap.fromImage(qimage))除了PySide6,还可以用Flask做Web界面,浏览器直接访问。如果做产品原型,Web方案更方便对接大屏和管理后台。但纯毕设演示,PySide6写起来更直接,桌面程序形态也更容易看到“系统”的感觉。
数据存储方面,SQLite足够用了。建一张行为记录表:
CREATE TABLE behavior_records ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id INTEGER, behavior_class TEXT, start_time TEXT, end_time TEXT, duration REAL, camera_id INTEGER DEFAULT 1 );每一条满足持续时间阈值的行为事件,都写入一条记录。课后统计直接查这张表,按student_id分组算各类别总时长,按behavior_class分组算班级行为分布。
5. 性能优化与边缘部署
课堂场景是固定摄像头,对实时性要求没有自动驾驶那么高,一般每秒5到10帧人眼就能接受。但理想的系统当然越流畅越好。这一章我从CPU实测优化、边缘设备部署两个角度展开。
5.1 推理速度瓶颈在哪里
用YOLOv8n在纯CPU上跑640分辨率,平均推理时间大概是300到800毫秒,也就是每秒1到3帧。这个帧率对上课场景来说勉强能用,但画面看起来会卡。瓶颈主要在三处:
- 模型推理本身:卷积计算量巨大,CPU算力不足。
- 图像预处理与后处理:
letterbox缩放、NMS等过程中Python层的开销。 - 显示刷新:OpenCV在界面上绘制检测框时如果每帧都重新绘制整幅图,也有额外开销。
先明确需求:真正需要高帧率的不是“每一帧都检测”,而是“告警要及时”。所以可以用定帧检测+显示不卡的策略:每隔200ms推理一帧,其他时间显示最近一次结果。这个策略能将可感知的流畅度提升不少。
5.2 实测生效的CPU优化方案
我在一台普通的i5-1240P便携设备上做过测试,不加任何优化时只有2帧左右,做了下面几步后稳定在8到10帧。
第一步:降低输入分辨率。把imgsz从640降到480。直观感觉模型精度会掉,但课堂行为检测的目标通常比较大(人占画面比例高),480影响很小。也可以用半精度推理,model.predict(..., half=True),CPU上开启half有时不支持,但ONNX Runtime可以试试。
第二步:线程与队列解耦。读视频流、推理、画框、显示分别放到不同线程,中间用队列缓存。不要同步阻塞地“读一帧→推理一帧→显示一帧”,因为相机帧率和推理帧率不同步会互相拖累。更好的模型:用一个线程专门读帧,把最新帧存到queue;推理线程消费队列里的帧;显示线程最近一帧结果画到界面上。
第三步:导出模型加ONNX Runtime。
import onnxruntime as ort import numpy as np import cv2 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] def letterbox(img, new_shape=(640, 640)): # 保持长宽比的缩放和填充,YOLO模型的输入要求 pass def infer_onnx(frame): img, scale, pad = letterbox(frame) img = img.astype(np.float32) / 255.0 img = np.transpose(img, (2, 0, 1))[None] # HWC转CHW并增加batch维度 outputs = session.run(None, {input_name: img})[0] # outputs shape: [1, 84, 8400],经过解析得到boxes和class return parse_outputs(outputs)ONNX解析yolov8输出时要注意:输出是[1, 84, 8400]的格式,其中84 = 4(框坐标)+ 80(COCO类别分数),如果你自定义了6个类别则对应[1, 10, 8400]。要先把8400个候选框里的坐标解码,再做置信度过滤和NMS。这部分复杂度不小,建议直接用ultralytics库的predict()配合ONNX Runtime,或者用yolo export format=onnx后通过onnxruntime自带的NMS插件处理。
如果不想写解析代码,还有一个取巧的方案:在ultralytics环境里直接用model.export(format='onnx')之后,用onnxruntime跑,但解析交给已内置的ultralytics.nn.autobackend。其实Ultralytics自己也支持指定model='best.onnx'来做predict,内部自动用ONNX Runtime。这个方法最省事:
model_onnx = YOLO('best.onnx') results = model_onnx.predict(frame, device='cpu')实测用这个方式比纯torch推理快,而且不用自己写预处理后处理。
5.3 RK3588等边缘设备部署经验
如果之后想把系统装到教室里的边缘盒子而不是PC,RK3588是国产边缘设备里很热门的选择。YOLOv8可以在RK3588上通过RKNN工具链部署,一般流程是:
- 先用
yolo export format=onnx导出ONNX。 - 在RK3588的开发板上或PC上安装
rknn-toolkit2。 - 把ONNX转换成RKNN格式,转换过程中要做量化(int8),精度会掉一些,需要准备校准图片。
- 用RKNN Runtime的Python或C++ API加载模型推理。
量化这一步最容易出问题,尤其是课堂行为里的细小目标(比如“手机”),int8量化后可能直接消失。解决办法:校准图片尽量选和实际场景一致的教室截图,数量200到300张,覆盖不同光线和位置。如果“玩手机”这类小目标检测效果太差,考虑只对部分层量化或者改用YOLOv8n量化到int16。
6. 常见问题与排查实录
最后这部分把我遇到过的高频问题整理成速查表,方便大家直接对照。
6.1 环境配置阶段
| 问题现象 | 可能原因 | 解决办法 |
|---|---|---|
torch.cuda.is_available()返回False | PyTorch装成CPU版 | 重装对应CUDA版本的torch,检查nvidia-smi驱动版本 |
运行yolo命令提示无法加载模块 | conda环境混用或依赖冲突 | 创建全新虚拟环境,先装torch再装ultralytics |
训练时workers报错 | Windows下多进程数据加载bug | workers=0或workers=2 |
| pip安装ultralytics卡住 | 网络问题 | 用清华镜像源pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple |
| 显存不足OOM | batch过大或mosaic增强 | 减小batch、关闭mosaic、降低imgsz |
6.2 训练指标异常
问题一:训练loss正常下降但mAP一直很低。
这种情况在我做过的一个项目里出现过,最后发现是标注文件里所有坐标都大于1,导致归一化失效。原因是我转换脚本里图片宽高取反了,PIL.Image.open和OpenCV读取的宽高顺序不同。检查方法很简单:随机读取一个txt文件,打印第一行坐标,再对照原图看框是否对得上。
问题二:混淆矩阵里“低头写字”和“玩手机”严重混在一起。
这个在课堂场景非常典型。解决思路不是调参,而是重新定义类别:要么把这两类合并成“非听课状态”,要么增加更多的标注样本,尤其是从侧面角度拍摄的数据。不要指望模型能自动区分两者,如果人眼也难分辨,模型当然也难。
问题三:训练到后期mAP波动很大。
可能是学习率太高,或者验证集太小。先把lr0降一个量级,再看验证集图片是不是只有几十张,如果只有几十张,每轮效果波动大是正常的。把验证集扩到100张以上,曲线就会平滑很多。
6.3 推理阶段的问题
问题一:检测框乱跳,同一学生ID反复变化。
这就是没有跟踪或者IOU匹配阈值不合理。先确认是不是用了model.track(),如果用了,检查tracker='bytetrack.yaml'的配置;如果自己写IOU匹配,阈值不要设太严,0.2到0.3就够宽,不然目标一转身就换ID。
问题二:CPU推理太慢。
按前面章节的方法,导出ONNX+ONNX Runtime,把imgsz降到480,用队列解耦。如果还慢,考虑换设备,或者接受5帧左右的帧率,课堂行为检测本身不需要30fps。
问题三:检测结果很准确,但统计报表里行为时长全是0。
排查BehaviorTracker的持续时间判断逻辑,常见错误有:记录时间用的是本地系统时间,但每帧时间没有更新;或者类别频繁变化导致每次迭代都重置计时。建议在update里打日志,看同一个学生ID的类别是否稳定。
写在最后的一点体会
这套系统做完之后,我最大的感触是:目标检测模型只占整个项目40%的工作量,剩下60%都在处理“检测之后的事”。模型给你一个框,你要让它变成“学生5从10:23开始玩了23秒手机”这条信息,再变成一张老师看得懂的报表,中间需要设计跟踪、状态机、存储和界面。很多同学把大量时间花在调参和换模型上,反而不愿意写这段逻辑,但恰恰是这段逻辑,才让项目称为“系统”而不是“模型演示”。如果你是按照这篇博文的思路来做,建议先跑通一个最简单的端到端流程:用公开数据集训练模型、写一个最小GUI显示结果、在SQLite里记录一条行为记录,然后再逐步扩充。这样每天都能看到可演示的成果,而不是闷头处理数据。最后分享一个小技巧:训练时把project和name设置得规范一点,比如runs/detect/classroom_v1,如果你后面对数据集做了调整,会发现版本管理特别顺手。