☰
YOLOv8+PyQt5课堂行为检测实战:从数据集到界面集成
2026/10/9 20:02:19 网站建设 项目流程

简介:一套基于YOLOv8与PyQt5的课堂行为实时检测系统完整工程包,面向教育技术开发者、计算机视觉初学者及需要快速搭建课堂管理原型的研究人员。系统以YOLOv8为检测核心,结合PyQt5构建友好界面,可识别学生出勤、专注度、互动等行为并实时反馈,操作简便,无需编程基础即可完成配置与监控,适合智慧教室、在线课堂等场景。包内共2000个文件、约720MB,以txt说明文档为主(1975个),另有Python脚本、XML配置、YAML模型配置和CSS样式文件,覆盖数据标注转换、数据集划分、界面实现与启动配置等环节,便于按模块学习与二次开发。已有109人学习下载。资源包含可直接运行的工程代码、实用工具脚本与界面样式,可帮助读者系统掌握YOLOv8目标检测与PyQt5桌面应用结合的完整思路,同时节省环境搭建与数据处理的时间,适合作为课程设计或科研项目的参考基础。

1. 课堂行为实时检测:YOLOv8 加 PyQt5,这套组合解决的项目痛点

教室里举手、趴桌、玩手机、交头接耳这些动作,想在监控画面里被自动框出来、实时计数并留下时间戳,过去要么靠人工盯屏,要么用老式检测模型在低帧率下硬撑。这套基于 YOLOv8 + PyQt5 的课堂检测系统,核心是把 YOLOv8 的推理能力封装进 PyQt5 图形界面,做成一个能接摄像头、能看实时画面、能统计行为数据的桌面应用。操作上不需要写代码就能启动检测,适合老师做课堂行为观察,也适合做教学硬件集成的开发者和刚接触目标检测的新手。顺着数据准备、界面集成、训练调参这条路走完,你会发现“数据集、YOLO、学生行为”这三件事串起来并没有想象中那么玄乎。

2. 学生行为数据集准备:YOLO 标注格式、类别设计与训练划分

2.1 课堂场景对检测模型的三个硬性要求

课堂场景和通用目标检测不太一样。第一,视角固定,画面基本是教室前排到后排的广角,或者讲台旁的斜俯视角度,学生的头、肩、手在画面里只会在有限的尺度范围内变化,不会像自动驾驶那样出现突然逼近的极端尺度。第二,目标之间高度重叠,“趴桌”和“低头写字”在画面上可能只有细微差别,头部边缘被前排遮挡是常态,数据标注时如果只框头部,模型很难区分动作状态。第三,对实时性敏感,检测环节拖到每秒十帧以下,界面上的学生动作就会明显发虚,后端的统计也会跟着失真。

YOLOv8 在同类模型里属于速度和精度权衡很实用的选择,n、s、m、l 几档覆盖了从 Jetson 板卡到普通台式机的部署区间。做课堂行为检测,我通常会先用 YOLOv8s 做起点,它在 640 分辨率下能跑到 50 FPS 左右,同时保留足够的特征表达能力去区分坐姿和趴桌的差异。如果你确认部署设备是低功耗板卡,再降一档用 n;如果画面里学生人数经常超过二十人,且教室纵深长,建议直接上 m,小模型对密集小目标的召回率明显不够。

很多公开的课堂行为数据集往往只覆盖“举手、阅读、写字”这几类高频动作,真正部署时还会碰到“打哈欠、交头接耳、玩手机”这些占比不小但样本稀少的行为。所以做这个项目时,我建议按“动作状态”而不是“人物身份”来定义类别,类别之间靠姿态和遮挡关系区隔,能减少模型在相似类上的混淆。数据集里至少包含八类:举手、低头、趴桌、玩手机、写字、看书、站立、交头接耳。多一类就多一个行为维度,后面做统计时价值会高很多。

2.2 YOLO 格式标签与数据集目录组织

搞课堂检测,第一步不是写模型代码,而是把手上的视频素材转成 YOLO 能吃的格式。课堂项目里最常采用的是 images 和 labels 分目录的布局,训练和验证互不干扰:

dataset/ ├── images/ │ ├── train/ │ │ ├── frame_0001.jpg │ │ └── frame_0002.jpg │ └── val/ │ ├── frame_0100.jpg │ └── frame_0101.jpg ├── labels/ │ ├── train/ │ │ ├── frame_0001.txt │ │ └── frame_0002.txt │ └── val/ │ └── frame_0100.txt └── classroom.yaml

对应的标注文本 .txt 里每行是一条检测框,格式是五个浮点数:类别 ID、中心点 x、中心点 y、框宽、框高。注意全部是归一化坐标,除以图片宽高后的结果,取值范围 0 到 1。举例说明:

3 0.642 0.183 0.120 0.290 0 0.224 0.324 0.095 0.220

这里第一行表示类别 3 的一只手机,中心点落在图片横向 64.2%、纵向 18.3% 的位置;第二行是类别 0 的举手,框的宽度占图宽 9.5%,高度占图高 22%。写标注的时候最容易犯的错,是把“趴桌”的人框成整个上半身,而“低头写字”只框头部区域,这两类框的纵横比不一致,训练时模型会拿头部的特征去套整个上半身,最后检测框跳来跳去。

2.3 数据增强与自动划分:让训练集更接近真实课堂

标注完成后,我习惯用一段脚本先检查类别分布,这是最容易暴露问题的环节:

from pathlib import Path labels_dir = Path("dataset/labels/train") txt_files = list(labels_dir.glob("*.txt")) class_count = {} for txt in txt_files: lines = txt.read_text(encoding="utf-8").strip().splitlines() for line in lines: cls_id = line.split()[0] class_count[cls_id] = class_count.get(cls_id, 0) + 1 print("类别分布:", sorted(class_count.items(), key=lambda x: x[0]))

这段脚本把训练集里所有标注文件的类别 ID 统计一遍,输出形如[('0', 3205), ('3', 412)]的结果。参数方面不需要改什么,关键是跑完脚本后肉眼看一下各类别占比。如果某一类占比不足 5%,训练出来的模型会倾向于把这类样本直接归并到相似类。比如“玩手机”和“低头”在画面里确实像,数据少就更容易混。

划分数据集时,我一般做 80/10/10 的 train/val/test 比例。有个细节不能省:不能把同一个训练视频里的相邻帧同时分进 train 和 val,否则验证集里全是高度相似的画面,评估结果会虚高,一上真实摄像头就原形毕露。常见做法是从视频里按 2 到 3 帧每秒抽帧,保证同一批素材进入数据池时已经天然降低了时间相关性。

增强策略直接用 YOLOv8 内置 mosaic 和随机透视就够,不需要额外堆花活。有一个禁忌:不要在增强里开左右翻转。教室里有黑板文字、门窗朝向,左右翻转会造成“向左看”和“向右看”的语义混乱,类别特征变得不稳定。如果你用的是 YOLOv8 默认增强,注意关闭 hsv 之外的几何随机裁切强度,课堂里密集人群被随机裁切后,很容易把单个学生切成碎片喂给模型。

数据准备完,回到 classroom.yaml 里把 path、train、val、names 写好,names 的顺序必须和标注文件的 class_id 一一对应,不能改。这一步错位了,后面所有训练和评估都会莫名其妙地失效,我把这个坑留到第 5 章详细说。

3. PyQt5 界面集成:双线程架构与实时视频流处理

3.1 UI 线程和推理线程的职责边界

很多人第一次写 PyQt5 检测界面,习惯把模型推理直接塞进界面的事件循环里,结果一运行就卡死。原因是模型推理在 CPU 上跑一次 640 分辨率的推理需要 30 到 80 毫秒,而 PyQt5 的界面刷新和按钮事件都在同一个主线程里排队。推理一次阻塞一百毫秒,鼠标点击和窗口拖动全部跟着延迟,看起来就像界面“假死”了。

正确的思路是用 QThread 把视频读取和推理放到工作线程,主线程只负责画界面和接收信号。代码骨架大致是这样:

from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectThread(QThread): frame_ready = pyqtSignal(object) result_ready = pyqtSignal(object) def __init__(self, model, source=0): super().__init__() self.model = model self.source = source self.running = True def run(self): cap = cv2.VideoCapture(self.source) while self.running: ok, frame = cap.read() if not ok: break results = self.model(frame, imgsz=640, conf=0.25, verbose=False) if results is not None: self.frame_ready.emit(frame) self.result_ready.emit(results) cap.release()

run 方法是 QThread 的入口,while 循环里做“读一帧、推理一次、发两个信号”。frame_ready 用来把原始画面送给界面显示,result_ready 用来把检测结果送给统计面板。注意工作线程里不要直接调用任何 QLabel 或 QTableWidget 的操作,界面控件的更新必须通过信号槽回到主线程,否则线程不安全会导致画面闪烁甚至崩溃。

3.2 视频流接入:帧缓冲与跳帧策略

摄像头输入通常走两种:USB 摄像头直接给设备索引,比如 0 表示第一路;网络摄像头走 RTSP 地址。PyQt5 里一般不直接处理 RTSP,而是交给 OpenCV 的 VideoCapture 去解,再把解出来的帧交给推理线程。这个分工让界面代码保持简洁,同时也方便以后把视频流换成离线视频做回放测试。

实际部署课堂场景时,摄像头一般是 1080p 三十帧。YOLOv8s 在普通台式机上推理一帧大约 20 到 30 毫秒,勉强能跟上,但一旦教室人数多、检测框多,后处理的时间会陡增。这时候我一般会做跳帧:

frame_skip = 2 frame_count = 0 while self.running: ok, frame = cap.read() if not ok: break frame_count += 1 if frame_count % (frame_skip + 1) != 0: continue results = self.model(frame, imgsz=640, conf=0.25, verbose=False)

frame_skip 等于 2 表示每三帧只做一次推理,另外两帧直接丢弃,不进入模型。这种方式能明显降低 CPU 占用,代价是输出帧率变成原来的三分之一。课堂行为检测不需要像目标跟踪那样逐帧连续,三帧一跳完全够用。要注意,跳帧跳的是“推理”,不是“显示”,界面显示仍可以通过缓存队列保持流畅,这一点后面还会提到。

读取 RTSP 时容易遇到连接不稳定,常见做法是给 VideoCapture 设置超时参数:

cap = cv2.VideoCapture("rtsp://your_camera_ip/stream") cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)

CAP_PROP_BUFFERSIZE 设成 1 能减少因解码积压导致的延迟。如果画面花屏或黑屏,先检查网络带宽,再检查摄像头编码格式,不要一上来就怀疑模型。

3.3 检测结果叠加与统计面板刷新

拿到 results 之后,要把框画到画面上再交给 QLabel 显示。这里的关键是坐标体系,推理产生的框是基于模型输入的 640x640 分辨率,而显示区域是原始帧的宽高,直接画会错位。YOLOv8 的 results 会返回原图坐标系下的 xyxy 坐标,前提是传入的 frame 本身没有经过额外缩放,这一步要特别留意。

def draw_results(frame, results, names, threshold=0.25): for box in results.boxes: cls_id = int(box.cls[0]) conf = float(box.conf[0]) if conf < threshold: continue x1, y1, x2, y2 = [int(v) for v in box.xyxy[0]] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) label = f"{names[cls_id]} {conf:.2f}" cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2) return frame

这段代码把每个框的类别、置信度和坐标取出来,画矩形和文字。threshold 建议和推理时的 conf 保持一致,否则画面上明明有框,统计里却对不上数。画完框之后,要把 OpenCV 的 BGR 图像转成 Qt 能识别的 QImage,再贴到 QLabel 上:

rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.label_video.setPixmap(QPixmap.fromImage(qimg))

format 用 RGB888 才能避免颜色通道错乱。如果你的 QLabel 是固定尺寸,不要用 setScaledContents(True),那会让图像拉伸变形。正确做法是手动缩放成与显示区域等比例的画面,再贴图,确保检测框和标注文字不错位。

统计面板的逻辑是每帧对检测结果做一次分类计数,但不要每帧都刷新表格,否则 QTableWidget 会闪得厉害。我会设一个 QTimer,每 500 或 1000 毫秒触发一次刷新,用缓存下来的累计计数更新界面。

4. YOLOv8 训练与推理:超参设置、曲线解读与模型导出

4.1 训练入口与关键参数

数据准备好之后,训练入口直接走 ultralytics 的命令行就能跑起来:

yolo detect train data=classroom.yaml model=yolov8s.pt epochs=120 imgsz=640 batch=16 lr0=0.01

这几个参数里,data 指向 classroom.yaml,model 用官方预训练权重做迁移学习,epochs 设 120 轮,imgsz 640。CPU 想快一点可以把 imgsz 降到 480,但课堂里小目标多,降分辨率会掉召回。batch 大小取决于显卡,16GB 显存跑 s 模型没问题,显存不足就减到 8,不要硬开大 batch,否则第一个 epoch 就报 CUDA out of memory。

classroom.yaml 的内容大致这样:

path: dataset train: images/train val: images/val names: 0: raise_hand 1: looking_down 2: sleeping 3: using_phone 4: writing 5: reading 6: standing 7: turning_around

names 顺序必须与标注文本里的 class_id 完全一致。增删类别时,要同步改标注脚本和这个 yaml,否则模型训练时的类别映射就乱了。训练过程中会自动保存 last.pt 和 best.pt,best.pt 是按验证集指标挑选出来的权重,后边部署一律用 best.pt。

4.2 训练过程:loss 曲线和 mAP 到底信哪个

训练时大多数人盯着终端里跳动的 loss 数字,其实更该看验证集的 mAP。loss 下降只能说明模型在拟合训练数据,课堂数据集样本量有限,模型很容易把训练集背下来,loss 继续降但验证集精度反而下滑。

我记录过一次比较典型的训练过程,三个阶段的验证集指标变化如下:

训练轮次mAP50mAP50-95
400.620.38
800.780.51
1200.810.54

可以看到 80 轮以后 mAP50 涨幅放缓,从 0.78 到 0.81 只涨了三个点。如果再跑 160 轮,训练 loss 可能还会降,但验证集的 mAP50-95 大概率横盘甚至回落,那是过拟合信号。课堂这个场景,120 轮左右基本够用,不必盲目贪多。

判断模型是否定版,我的习惯是看 mAP50-95 而不是只看 mAP50。mAP50 对边界框位置的要求宽松,只要求 IoU 大于 0.5 就算命中,而 mAP50-95 是多个 IoU 阈值下求平均,更能反映框位是否准确。课堂里“举手”和“低头”边界距离很近,框偏了哪怕一点,统计结果就从举手变成低头,这种误差在应用层很难察觉。

4.3 模型从训练权重到 PyQt5 可加载的格式

训练出来的 best.pt 可以直接加载,但桌面应用里直接跑 PyTorch 权重会比较慢。我一般会导出一份 ONNX,再用 onnxruntime 做推理,CPU 上能快不少:

from ultralytics import YOLO model = YOLO("runs/detect/train/best.pt") model.export(format="onnx", imgsz=640, opset=12)

导出后生成 best.onnx,推理会话的加载方式变成:

import onnxruntime as ort import numpy as np session = ort.InferenceSession("best.onnx") input_name = session.get_inputs()[0].name output = session.run(None, {input_name: preprocessed_img})

preprocessed_img 需要是形状为(1, 3, 640, 640)的浮点数组,像素值归一化到 0 到 1。不同导出参数下输出格式不完全一样,onnx 模型通常把边界框、置信度、类别概率合并成一个大数组,解析逻辑要按实际形状处理,不要照搬上一份代码。如果你只想快速跑通系统,不追求那 20% 到 40% 的速度提升,直接用 pt 文件最省事,PyQt5 里加载方式不变,后续换模型也简单。

5. 避坑指南:课堂检测实测中的典型问题与排查记录

5.1 戴眼镜学生边框漂移

现象:戴眼镜的学生在斜照光下,检测框忽大忽小,有时只框了半个头,人脸和镜框区域被反复切割。
原因:训练样本里白天自然光照片比例过高,傍晚教室灯光偏黄时,模型对低照度下的人脸头部特征响应不稳定。
解决:在训练增强里提高亮度扰动和色相扰动幅度,我常用参数是hsv_h=0.015, hsv_s=0.7, hsv_v=0.5。另外从教室里补拍一段傍晚灯光的视频,抽帧合入训练集,比任何参数调整都直接有效。

5.2 PyQt5 界面假死不响应

现象:摄像头画面停在最后一帧,窗口拖动时出现白色残影,点击关闭按钮反应延迟两秒以上。
原因:推理代码放在了界面主线程里,模型推理一次 60 毫秒,加上图像编码和绘制,界面事件被阻塞。
解决:把推理逻辑拆到 QThread,主线程只接收 signal 更新画面。另外一个细节是,如果在按钮槽函数里直接调用模型推理,同样会阻塞,需要把耗时操作丢给线程池或再开一个工作线程,别贪方便写在 clicked 处理里。

5.3 检测框和画面错位

现象:人在讲台左侧,框却偏到讲台右侧,置信度数值正常,就是画错位置。
原因:推理时把输入图缩放到了 640x640,而显示时又把画面拉伸到 QLabel 的尺寸,两者的宽高比不一致,坐标还原自然出错。
解决:显示区不要用 setScaledContents 直接拉满,先按原图宽高比计算出居中缩放区域,再贴 QPixmap。推理结果回传时统一用归一化坐标,也就是把 xyxy 除以原图的宽高,到界面端再按实际显示区域反算像素位置,两头对得上才不会错位。

5.4 验证集 mAP 挺高,现场漏检玩手机

现象:最后一轮验证集 mAP50 到 0.8 以上,但拉去真实课堂做一节课测试,“玩手机”行为的漏检率明显高于其他类别。
原因:课堂数据里玩手机样本占比低,且验证集和训练集来自同一批视频的相邻帧,模型表面的验证指标被数据冗余撑高了。
解决:划数据时按视频场景分组,同一段拍摄素材的帧不能同时进入 train 和 val。对少数类样本可以做适度复制,或者把两张含手机的小图拼接到同一张训练图里,让模型多看到这类目标,重新训练后再看现场表现。

5.5 摄像头自动曝光导致置信度抖动

现象:教室靠窗户光线变化快时,画面亮度一抖,置信度从 0.8 直接掉到 0.3,行为日志里出现大量“消失又出现”的空档。
原因:摄像头自动曝光和自动白平衡在持续调节,帧间亮度变化剧烈时,模型的置信度被拉低。
解决:固定机位下尽量手动关闭自动曝光,OpenCV 里对应cv2.CAP_PROP_AUTO_EXPOSURE设为 0.25 左右,同时锁定白平衡。如果设备不支持手动控制,可以在送入模型前做帧间亮度平滑,把连续三帧的均值作为当前帧输入,注意这样会增加一定延迟,但在固定场景里效果明显。

6. 进阶验证:视频回放构建行为时间线

模型部署进系统只是第一步,真正要说服使用者,需要让检测结果变成可查证的时间线。最常见的做法是录制一段十分钟的教室视频,用系统离线回放,把所有检测框和置信度导出成 JSON,再做标注对比。

import json from collections import defaultdict def export_timeline(results, fps=15): timeline = defaultdict(list) for frame_id, dets in enumerate(results): t = round(frame_id / fps, 1) for cls_id, conf, box in dets: if conf >= 0.25: timeline[cls_id].append({ "time": t, "conf": conf, "box": box }) return json.dumps(timeline, ensure_ascii=False)

参数方面,fps 是视频帧率,课堂回放通常取 15;conf 阈值要和界面里保持一致,否则统计结果对不上。导出的 JSON 可以直接喂给数据统计脚本,做累计时长、出现频次、时间分布的情况分析,学校用来做课堂观察报告也够用。

验证时我的习惯是抽 30 帧做人工复核,计算每一类的 precision 和 recall,而不是只看界面效果。人眼主观感受具有迷惑性,真正把 ground truth 拉出来对比,才发现漏检主要集中在后排小目标。

那次调试我印象很深,第一次用视频回放做验证,发现“看书”和“低头写字”互相跳变,换了更严格的 conf 阈值后才稳定下来。从那以后,我每次给课堂检测系统做演示之前,都会强制走一遍离线回放加上 JSON 导出的流程,确认时间线和统计口径都对齐了,再连摄像头实拍。这也算是我给自己定的“后悔药”步骤。课堂行为检测的门槛不在模型,而在数据颗粒度和验证流程够不够细。希望帮到你。

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

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

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

立即咨询