☰
基于YOLOv8的课堂行为检测系统:从模型训练到部署落地
2026/9/28 15:47:04 网站建设 项目流程

课堂行为检测这个项目,在学校里最常见的就是毕业设计,其次是一些教育信息化公司的产品原型。很多同学上来就直接pip install ultralytics,然后跑默认的yolov8n.pt,看到框能出来就觉得自己做完了。但实际上,从“模型能检测”到“系统能落地”,中间还隔着数据标注、行为逻辑设计、性能优化三道坎。这篇文章就围绕“基于YOLOv8的课堂行为检测系统”这个题目,把我实际做过的流程、踩过的坑、调参心得全部写出来,给正在做课设、毕设或者准备部署到教室环境的朋友一个完整的参考。

这个系统能解决什么问题,我先说清楚:教室里的摄像头拿到视频流之后,系统自动识别学生的行为状态,比如举手、睡觉、玩手机、站立、低头写字、转头说话等,并把这些行为按时间统计成数据,支持实时报警和课后报表。适合谁来参考?如果你手里只有普通电脑,甚至只有CPU,想先跑通完整流程;或者你想了解YOLOv8从数据制作、模型训练、行为判定到界面展示的完整链路,这篇文章就是按这个顺序写的。

1. 整体设计:课堂行为检测系统到底在做什么

很多人拿到这个题目第一反应是“用YOLOv8检测学生行为”,但实际做的时候会发现,YOLOv8只负责输出“目标框+类别+置信度”,它不负责判断这个行为是否持续、是否违规,也不负责统计。所以系统真正的架构,是把YOLOv8当成一个“眼睛”,后面还要接一套逻辑层和数据层。

1.1 从业务需求到技术需求的映射

先去想用户是谁。这个系统的用户分三层:

  • 授课老师:需要实时知道哪些学生在玩手机、睡觉,最好能弹窗提醒。
  • 教务/管理人员:需要课后查看统计报表,比如某个班这节课的“低头率”“举手次数”。
  • 技术维护人员:需要能调视频源、改检测区域、标记误报。

不同用户对应不同的功能模块。老师要的是实时性,所以前端界面要简洁,检测结果延迟要低;教务要的是数据沉淀,所以要有SQLite或MySQL记录每次检测结果,并生成可视化图表;维护人员要的是可配置性,比如摄像头位置不同,检测区域就需要人工调整。

把这个需求拆成技术模块,大致是四块:

  1. 视频输入模块:读取本地视频、RTSP摄像头流或直接调用USB摄像头。
  2. 行为检测模块:YOLOv8模型负责目标检测,输出人的位置和行为类别。
  3. 行为判定与统计模块:根据类别、持续时间、空间位置做规则判断,比如“躺桌面上超过5秒”才判定为睡觉。
  4. 可视化与存储模块: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 ultralytics

CPU版不需要管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里就是矩形坐标,转换脚本才能正确处理。

数据来源主要有三个途径:

  1. 自己录教室视频,抽帧成图片(视频每2秒抽一帧)。
  2. 在公开数据集(如COCO、PASCAL VOC)中筛选包含学生的图片。
  3. 网上找课堂场景图片,注意版权问题。

我建议自己录视频抽帧,因为课堂场景的俯视角度、座位分布、光线条件都比较特定,网上图片风格杂,会让模型学得“乱”。

标注完所有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

这里有几个很重要的细节:

  1. conf_thres(置信度阈值)设置成多少合适?课堂场景背景相对单一,0.4比较合适;如果你发现误报率高,升到0.5;漏报高,降到0.3。这个参数直接影响用户体验,不要用默认的0.25直接上生产。
  2. iou_thres(NMS阈值)一般不用动,0.45到0.5之间都行,它决定同一目标多个重叠框怎么合并。
  3. 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而不直接命令行?因为答辩演示和实际使用都需要直观的“实时画面+数据看板”效果。

界面可以分成三个区域:

  1. 左侧:实时视频画面,上面画检测框、学生ID、行为标签。
  2. 右上:实时行为统计柱状图,显示当前各类别人数。
  3. 右下:异常记录列表,比如“学生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帧。这个帧率对上课场景来说勉强能用,但画面看起来会卡。瓶颈主要在三处:

  1. 模型推理本身:卷积计算量巨大,CPU算力不足。
  2. 图像预处理与后处理:letterbox缩放、NMS等过程中Python层的开销。
  3. 显示刷新: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工具链部署,一般流程是:

  1. 先用yolo export format=onnx导出ONNX。
  2. 在RK3588的开发板上或PC上安装rknn-toolkit2。
  3. 把ONNX转换成RKNN格式,转换过程中要做量化(int8),精度会掉一些,需要准备校准图片。
  4. 用RKNN Runtime的Python或C++ API加载模型推理。

量化这一步最容易出问题,尤其是课堂行为里的细小目标(比如“手机”),int8量化后可能直接消失。解决办法:校准图片尽量选和实际场景一致的教室截图,数量200到300张,覆盖不同光线和位置。如果“玩手机”这类小目标检测效果太差,考虑只对部分层量化或者改用YOLOv8n量化到int16。

6. 常见问题与排查实录

最后这部分把我遇到过的高频问题整理成速查表,方便大家直接对照。

6.1 环境配置阶段

问题现象可能原因解决办法
torch.cuda.is_available()返回FalsePyTorch装成CPU版重装对应CUDA版本的torch,检查nvidia-smi驱动版本
运行yolo命令提示无法加载模块conda环境混用或依赖冲突创建全新虚拟环境,先装torch再装ultralytics
训练时workers报错Windows下多进程数据加载bugworkers=0或workers=2
pip安装ultralytics卡住网络问题用清华镜像源pip install ultralytics -i https://pypi.tuna.tsinghua.edu.cn/simple
显存不足OOMbatch过大或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,如果你后面对数据集做了调整,会发现版本管理特别顺手。

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

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

立即咨询