基于YOLOv5与PyQt5的网课专注度监测系统设计与实现
2026/9/16 21:23:00 网站建设 项目流程

简介:网课专注度监测预警系统是一套面向毕业设计场景的深度学习实战项目,基于YOLOv5目标检测框架,通过PyQt5构建桌面端交互界面,可实时识别网课学习中的人脸状态并输出专注度预警。系统适合计算机相关专业学生用于毕设、课程设计或期末大作业,尤其推荐给需要快速搭建可演示项目的学习者。资源包共含122个文件,压缩后约179.87MB,主要包含Python源码、模型权重(pt/onnx/pkl)、YAML训练配置、PyQt5界面文件、依赖文件及Docker搭建说明等,目录组织规范,便于理解与二次开发。目前已有128人学习/下载。项目曾获导师认可的高分评价,代码完整可运行,还附带人脸关键点检测模型、提示音频、项目计划书等配套材料,能够帮助使用者快速跑通‘目标检测—状态识别—专注度预警’的完整链路,同时可借鉴其中的工程部署与界面设计思路,是适合练手和答辩展示的实用资源。

1. 网课专注度监测为什么选 YOLOv5 + PyQt5 这套组合

线上教学场景里,老师看不到学生的实时状态,家长也没法一直坐在旁边盯;单纯靠课堂互动和随堂测验去反推注意力,往往已经滞后了半节课。而一套基于摄像头的专注度监测预警系统,可以在学生走神、趴桌、低头玩手机的几秒内就给出一键提示,把“发现注意力下降”这件事从人眼盯屏变成机器实时判断,这就是 YOLOv5 目标检测加 PyQt5 界面这套组合最常见的落地形态。

选 YOLOv5 而不是更大规模的检测框架,核心原因是网课场景的输入来源是摄像头画面,单帧计算量、延迟和部署难度都要控制在普通 PC 能扛住的范围内;YOLOv5 的推理路径短,模型体积小,训练自己的数据集有成熟脚本,社区资料也多,遇到环境报错几乎都能搜到现成解法。PyQt5 这边则负责把检测结果做成可交互的桌面工具,视频流、状态灯、累计专注时长、预警记录都集中在一个窗口里,不用去折腾 Web 服务的部署链。这套系统的目标用户很清楚:做在线教育工具或自习室管理系统的开发人员,以及需要给毕设项目搭一套可演示原型的在校学生。下面按理论、训练、界面、调优四个层面把整条链路拆开讲。

2. 专注度判断的核心逻辑:从 YOLOv5 目标检测到行为特征打分

2.1 只检测“有没有人”不够,专注度需要行为特征支撑

目标检测的输出是一个个带类别标签的矩形框,YOLOv5 做完检测,我们拿到的只是“画面哪块区域有一个人”,但这跟“专注度”之间还隔着一层推理。网课专注度监测里常见的目标类别不是通用的 person,而是更细粒度的状态,比如 focus 表示正常听课,drowsy 表示瞌睡或长时间低头,phone 表示正在使用手机。想让检测模型直接输出这三种状态,需要准备对应的标注数据,这比单纯用 COCO 预训练权重检测 person 再自己猜状态要可靠得多。

还有一种常见做法是保留 YOLOv5 检测出的人脸或头部区域,再用 MediaPipe 或 OpenCV 的面部关键点模型提取眼部、头部姿态信息。两个思路的区别在于:前者把专注度当成目标检测的分类问题,简单直接,训练成本高一点但推理最省事;后者是目标检测加关键点回归的混合方案,能算出更细的连续指标,比如视线偏角、眼睛闭合比例,但工程链路更长。工程上多数项目会选“YOLOv5 检测人脸 + 关键点判断状态”的折中路线,因为这样可以让 YOLOv5 只做一个二分类任务,数据准备量小很多。

2.1.1 从目标框到专注度分数的映射方式

假设我们只需要检测 face 这一类,检测结果里每个人脸框记为 bbox,坐标是 (x1, y1, x2, y2),置信度是 conf。专注度打分通常由三个可量化指标组成:

  • 头部姿态角:人低头、转头时俯仰角 pitch 和偏航角 yaw 会发生明显偏移
  • 眼睛开合程度:用眼睑纵横比来判断是否长时间闭眼
  • 目标框的纵向位移:学生站起来、离座或趴下时,人脸框在画面中的高度和位置会发生突变

每个指标可以单独设阈值,也可以加权成一个 0 到 100 的连续分数。下面这段伪代码展示了最常见打分逻辑:

def compute_focus_score(face_bbox, landmarks, head_pose): # face_bbox: 目标检测输出的人脸框 # landmarks: 68点或6点面部关键点 # head_pose: 由关键点求解的旋转向量转换得到的欧拉角 ear = eye_aspect_ratio(landmarks) # 0~1,值越小表示眼睛越闭合 pitch, yaw, roll = head_pose # 角度,单位度 score = 100.0 if ear < 0.15: score -= 40 # 眼睛闭合的惩罚 if abs(pitch) > 30 or abs(yaw) > 50: score -= 30 # 低头或转头过大的惩罚 if conf < 0.6: score -= 10 # 低置信度惩罚 return max(score, 0.0)

这里 eye_aspect_ratio 用的是经典的 EAR 算法,取人眼周围 6 个关键点的距离比值。当 EAR 持续低于 0.15 且超过 3 秒,基本可以判定为闭眼或瞌睡状态;低于 0.1 且持续 5 秒以上,就需要触发预警。这套映射逻辑的价值在于把 YOLOv5 输出的检测框变成了业务系统可以直接消费的分数,后续做预警阈值和图表统计都有了数学依据。

2.2 YOLOv5 网络结构与实时推理的优势在哪里

YOLOv5 属于单阶段目标检测器,它把目标检测做成一个端到端的回归问题:图像输入网络后,不需要像 Faster R-CNN 那样先跑区域提议再逐区域分类,而是直接在特征图上预测边界框和类别概率。在网课专注度监测这种对实时性要求高于极致精度的场景里,单阶段结构天然占优。YOLOv5 的骨干网络采用 CSPDarknet,通过跨阶段局部连接减少重复梯度计算,在保持特征提取能力的同时降低了参数量。颈部网络用 PANet 结构做多尺度特征融合,把浅层的位置信息和深层的语义信息拼在一起,这样对不同距离的人脸都能有响应。检测头在三个尺度上输出预测结果,分别负责小目标、中目标和大目标。

推理链路里有一个容易被忽略的参数叫锚框。YOLOv5 默认给每个检测尺度分配 3 个锚框,标注数据里人脸框的长宽分布会直接影响锚框的匹配效果。用 COCO 预训练权重去检测人脸没问题,但如果训练自己的专注度状态数据集,建议先用下面命令自动重算锚框:

python train.py --data focus.yaml --epochs 300 --batch-size 16 --img 640 --noautoanchor

不加 --noautoanchor 时,YOLOv5 会在训练开始阶段自动运行 k-means 聚类,重算适合当前数据集的锚框尺寸。加了之后则是完全跳过重算步骤,适合你已经验证过自己的锚框设置无误的情况。多数人脸目标的宽高比接近 1:1,用默认锚框也能跑,但类别是 phone 或者 drowsy 这类形态差异大的目标时,自动重算锚框往往能带来 2 到 3 个百分点的 mAP 提升。

2.2.1 模型体量选择:s、m、l 怎么取舍

YOLOv5 官方提供 n、s、m、l、x 五个体量,针对网课专注度检测这种场景,通常不用一上来就选最大的。桌面端 CPU 推理选 s 或 n,有独立显卡选 m 就足够。在相同数据集上用 s 模型训练 100 个 epoch,推理延迟在 GTX 1660 上大约是 15 到 25 毫秒,这比人眼的反应速度快一个数量级,预警系统要的就是这种“发现走神瞬间”的能力。换个角度看,如果模型过大导致帧率掉到 10 FPS 以下,学生低头再抬头的动作就可能被漏检,专注度分数会出现明显的锯齿状跳变,预警体验会很差。

2.3 头部姿态角怎么解算:从 2D 关键点到欧拉角

YOLOv5 只负责给出人脸框,头部姿态角的计算要另想办法。最常见的是用 OpenCV 的 solvePnP 函数,将人脸关键点的二维像素坐标与三维人脸模型点对应起来,求解旋转向量后再用 Rodrigues 变换得到欧拉角。下面是一段可以直接跑通的改写示例:

import cv2 import numpy as np # 标准正面人脸三维模型点,单位毫米,仅列少数关键点 model_points = np.array([ [0.0, 0.0, 0.0], # 鼻尖 [0.0, -63.6, -12.5], # 下巴 [-43.3, 32.7, -26.3], # 左眼外眼角 [43.3, 32.7, -26.3], # 右眼内眼角 [-28.9, -28.9, -24.1], # 左嘴角 [28.9, -28.9, -24.1] # 右嘴角 ], dtype=np.float64) # 与上面模型点对应的、由关键点检测得到的 2D 像素坐标 image_points = np.array([ [230, 280], [225, 370], [180, 260], [280, 260], [200, 340], [260, 340] ], dtype=np.float64) focal_length = 640 center = (320, 240) camera_matrix = np.array([ [focal_length, 0, center[0]], [0, focal_length, center[1]], [0, 0, 1] ], dtype=np.float64) dist_coeffs = np.zeros((4, 1)) success, rotation_vector, translation_vector = cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs) rotation_matrix, _ = cv2.Rodrigues(rotation_vector) pitch, yaw, roll = cv2.decomposeProjectionMatrix( np.hstack((rotation_matrix, translation_vector)))[-1] print(f"pitch={pitch[0][0]:.1f}, yaw={yaw[0][0]:.1f}, roll={roll[0][0]:.1f}")

这里的 focal_length 和 center 要与实际摄像头分辨率对应,如果你使用 1280×720 的分辨率,center 就要改为 (640, 360)。很多项目把焦距设成固定 640 也能出结果,但会带来几度的角度偏差;对专注度判断来说,绝对角度不如角度的相对变化重要,所以多数情况下固定值足够用。判断规则上,pitch 大于 25 度视为低头,yaw 大于 45 度视为侧身,持续超过 5 秒就应当触发预警。

3. 训练专注度检测模型:数据集标注与 YOLOv5 超参数设置

3.1 自采集样本的三个要点

整个系统里影响上线效果的不是模型结构,也不是 PyQt5 界面做得有多好看,而是训练数据的质量和分布。采集数据时要注意三个点。第一,要覆盖不同光线环境,上午逆光、晚上顶灯、屏幕补光这几种情况都要录一段;第二,被检测者最好有正对摄像头、侧对摄像头、低头写字、喝水、伸懒腰这几类动作,让模型见过不同角度的人脸;第三,类别标签要能覆盖预警的业务需求:建议至少包含 focus、drowsy、phone、away 四类,away 指的是画面中检测不到人脸,后续在逻辑层处理,不占用模型的分类负担。

采集完视频后抽帧保存,间隔取 5 到 10 帧抽一张即可,避免相邻帧太相似导致过拟合。然后用 LabelImg 或 LabelStudio 标注,导出为 YOLO 格式的 txt 文件,每个 txt 文件与图片同名,内容格式是:

class_id cx cy width height

其中 cx、cy 是归一化到 0 到 1 的 bbox 中心点坐标,width 和 height 是归一化的框宽高。一个容易踩的坑是,标注的人脸框必须紧贴目标,不要留过多背景;框大了会把背景纹理学进特征,框小了又会截断关键点区域,导致后续头部姿态解算时关键点落在画面外。

3.1.1 数据集目录结构与训练配置

训练前的目录结构建议按下面这样组织,这个结构和 YOLOv5 默认的读取逻辑兼容:

dataset/ ├── images/ │ ├── train/ │ └── val/ ├── labels/ │ ├── train/ │ └── val/ └── focus.yaml

focus.yaml 的内容如下:

train: ../dataset/images/train val: ../dataset/images/val nc: 3 names: ['focus', 'drowsy', 'phone']

这里 train 和 val 的路径是相对于你执行训练命令时的工作目录写的。YOLOv5 在读取数据集时会根据这个 yaml 中填写的路径拼接数据路径,建议全部写成绝对路径,或者用相对路径并始终在 yolov5 仓库根目录下执行训练命令,否则容易出现 File not found 的错误。验证集中每个类别的样本数不要少于 50 张,太少时 val 阶段的 mAP 波动会很大,导致你误判模型好坏。

3.2 训练命令与关键参数的含义

确认环境里已安装 PyTorch 和相应 CUDA 版本之后,从 GitHub 拉取 YOLOv5 官方仓库,然后用下面命令启动训练:

git clone https://github.com/ultralytics/yolov5 cd yolov5 pip install -r requirements.txt python train.py --data ../dataset/focus.yaml \ --weights yolov5s.pt \ --batch-size 16 \ --epochs 100 \ --img 640 \ --device 0

weights 参数是预训练权重的路径,如果本地没有,YOLOv5 启动时会自动从官方仓库下载。device 0 表示使用第一块 GPU,CPU 机器改成 --device cpu。batch-size 的选择取决于显存大小,8GB 显存跑 yolov5s 用 16 是稳妥的,显存不够可以减到 8 并同步调整学习率。epochs 设 100 是让模型充分收敛的起点,如果发现训练到 80 epoch 时 val loss 已经不再下降,可以提前终止,不必硬跑完。

3.2.1 训练过程中的日志怎么看

训练输出里最值得关注的是 training box loss、training cls loss 和 validation mAP50。前两个值在训练初期下降很快,说明网络在学习;第三个值在稳定后如果仍然低于 0.7,说明数据集的类别混淆比较严重,尤其是 focus 和 drowsy 在视觉上本身就接近,建议回到标注环节重新检查标签。YOLOv5 每训练完一个 epoch 都会在 runs/train/exp 目录下生成 weights 文件夹,里面保存的是 last.pt 和 best.pt,后者是验证集上表现最好的权重文件,部署和测试一律用 best.pt。

3.3 YOLOv5 超参数表与调参推荐

超参数配置文件在 data/hyps/hyp.scratch-low.yaml 里,下面表格列出最关键的几个参数以及网课场景下的推荐值:

参数默认值推荐调整调整理由
lr00.010.005~0.01数据集小时调低避免震荡
momentum0.937保持默认值稳定
weight_decay0.0005保持防止过拟合
warmup_epochs3.03~5数据噪声大时加大
box 损失权重0.05保持无需频繁改动
cls 损失权重0.50.5~1.0解决类别不平衡

数据集中 phone 样本明显少于 focus 时,把 cls 损失权重提高到 0.8 左右,可以让模型更重视分类错误,代价是定位能力可能轻微下降。修改方式是直接编辑 yaml 文件,或者在 train.py 里使用 --hyp 指定新的配置文件。训练完成后用下面命令在视频上验证效果:

python detect.py --weights runs/train/exp/weights/best.pt \ --source test.mp4 \ --conf-thres 0.5 \ --iou-thres 0.45 \ --save-txt \ --save-conf

conf-thres 是置信度阈值,低于此值的检测结果会被丢弃。0.5 是一个比较平衡的起点,如果你发现漏检多,降到 0.3 试一下;如果你发现误检多,升到 0.7。iou-thres 控制去重时两框的重合度阈值,多人同框的画面里调成 0.5 以下可以保留更多邻近的小目标框,但也会增加重复检测的可能。

4. 基于 PyQt5 的实时监测界面实现

4.1 界面布局与线程模型设计

PyQt5 界面的核心任务有三块:实时显示摄像头画面、渲染检测框和专注度分数、触发预警。这三项任务如果全部塞在主线程里,画面会卡住,因为视频解码和目标检测都是耗时操作。常见的做法是采用采集线程加检测线程加 UI 主线程的三线程模型。摄像头读帧放采集线程,YOLOv5 推理放检测线程,两个线程之间用队列传递数据。界面每秒刷新 15 到 30 帧即可,不需要与摄像头帧率保持一致。

界面布局上,左侧是视频显示区域,右侧放状态面板,面板包含当前专注度分数条、状态指示灯、今日累计专注时长和预警记录列表。底部是控制按钮区,包括开始监测、暂停、导出报告、预警开关。这里用 Qt 的 QGridLayout 配合 QSplitter 就能搭出不错的效果,不建议用绝对坐标写死,因为窗口拉伸时控件会错位。

4.2 在 PyQt5 里接入 YOLOv5 推理的代码骨架

下面给出一段可以运行的 PyQt5 集成示例,核心是把检测封装成一个独立线程,这里以 QThread 的方式实现:

import cv2 import torch from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage, QPixmap from PyQt5.QtWidgets import QApplication, QLabel, QMainWindow class DetectThread(QThread): frame_ready = pyqtSignal(QImage) score_updated = pyqtSignal(float) def __init__(self): super().__init__() self.model = torch.hub.load('./yolov5', 'custom', path='best.pt', source='local') self.model.conf = 0.5 self.model.iou = 0.45 self.running = True def run(self): cap = cv2.VideoCapture(0) while self.running: ret, frame = cap.read() if not ret: break results = self.model(frame) detections = results.pandas().xyxy[0] focus_score = self.calc_focus_score(detections) self.score_updated.emit(focus_score) # 将 OpenCV BGR 转成 QImage 用于界面显示 rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape bytes_per_line = ch * w qimg = QImage(rgb.data, w, h, bytes_per_line, QImage.Format_RGB888) self.frame_ready.emit(qimg) cap.release() def calc_focus_score(self, detections): if len(detections) == 0: return 0.0 # 简化版:以同时检测到 focus 和未检测到 phone 作为基础分 score = 80.0 if 'phone' in detections['name'].values: score -= 50 if 'drowsy' in detections['name'].values: score -= 40 conf = detections['confidence'].mean() score += (conf - 0.5) * 20 return max(min(score, 100.0), 0.0)

这段示例里有两个值得展开的点。第一,torch.hub.load 加载本地仓库的官方代码和权重,要求当前目录下有 yolov5 文件夹且里面存在 best.pt 文件。改成自己的模型时只需要替换路径即可。第二,calc_focus_score 里的逻辑是简化版本,生产环境中建议结合前一章提到的 EAR 和头部姿态角进行综合加权,因为单纯依赖类别名做判断会把很多本来就难分的边界情况搞错。

UI 主线程负责把信号绑定到控件上更新显示内容:

class MainWindow(QMainWindow): def __init__(self): super().__init__() self.video_label = QLabel(self) self.score_label = QLabel('专注度: 0', self) self.worker = DetectThread() self.worker.frame_ready.connect(self.update_frame) self.worker.score_updated.connect(self.update_score) def update_frame(self, qimg): self.video_label.setPixmap(QPixmap.fromImage(qimg)) def closeEvent(self, event): self.worker.running = False self.worker.wait() event.accept()

注意一个常见的低级错误:线程耗资源较大启动后未响应 closeEvent 直接退出,造成 Python 异常。上面的 wait() 调用保证了线程在窗口关闭时能及时回收资源。另一个细节是在开发时把 yolov5 推理相关代码全部放在 DetectThread 中,而不是在 UI 主线程里 import torch,否则窗口启动会明显卡住几秒钟,用户会以为程序崩溃了。

4.3 预警规则:滑动窗口与连续状态判定

专注度分数单帧波动很大,可能上一帧得 90 分下一帧得 45 分,如果每帧波动都触发弹窗就会造成严重干扰。正确做法是引入滑动窗口统计。我一般维护一个长度为 30 帧的得分队列,每次新得分进入队列后计算最近 30 帧的平均值,只有平均值低于预警阈值时才触发预警。这样做的依据是学生走神通常是一个持续过程,单帧低分可能是转头拿东西或者摄像头短暂遮挡,不值得立刻弹窗。

from collections import deque class FocusMonitor: def __init__(self, window_size=30, alert_threshold=50): self.window = deque(maxlen=window_size) self.alert_threshold = alert_threshold self.alerted = False def update(self, score): self.window.append(score) avg = sum(self.window) / len(self.window) if avg < self.alert_threshold and not self.alerted: self.alerted = True self.trigger_alert(avg) elif avg >= self.alert_threshold: self.alerted = False return avg def trigger_alert(self, avg): print(f"触发预警: 平均专注度 {avg:.1f}")

预警方式除了使用 QMessageBox 弹窗外,还可以在状态栏里置一个红色高亮 LED 小图标,配合蜂鸣提示。频繁弹窗会打断使用者,建议把弹窗改为跨天不去重复。每次预警产生一条记录,写入本地 SQLite 数据库,方便课后导出专注度曲线。

5. 三个能明显提升检测效果的调整技巧

5.1 把输入分辨率从 640 降到 480 或 320,换取更高帧率

网课摄像头采集的原始画面通常是 1280×720,但注意力状态的判断不需要这么高的分辨率。在训练时用 640 是为了让模型见过更多尺度的特征,推理时降采样到 480 或 320 几乎不会影响对低头和玩手机这些宏观动作的判别。帧率提升后,目标框的连续性会变好,专注度分数的曲线也更平滑。如果模型推理耗时仍然成为瓶颈,可以尝试将 YOLOv5 导成 TensorRT 或 ONNX 格式,但这需要额外搭环境,在普通 PyQt5 项目里不是必须的。

5.2 难例挖掘:把误报样本回填进训练集

训练完第一版模型后,拿 20 分钟真实课堂视频跑一遍检测,把误检和漏检对应的帧截图保存下来,重新标注并加入训练集。这是一个迭代过程,循环做两到三轮,模型对低头看键盘、手撑着脸颊这类动作的判断准确率会明显上升。实际表现中,people 误检为 focus、屏幕反光误检为 phone 是最常见的两类错误,针对这两类回填样本比单纯调参有效得多。标注时注意保持与原有类别定义一致,不能上一个阶段把某人低头记成 focus,下一个阶段又记成 drowsy,标签前后矛盾会让模型无所适从。

5.3 用滑动窗口融合判断替代逐帧绝对判断

单帧检测结果只是瞬时的目标框和置信度,如果把 30 帧内的检测类别分布做一个软投票,就能过滤掉大部分由突然转头带来的误判。软投票的实现和 4.3 节那个滑窗逻辑类似,区别在于这里统计的是类别占比而不是分数均值。一个简单有效的规则是:当连续 30 帧内有超过 60% 的帧数出现 phone 类别,且期间没有检测到 focus 类别,才判定为玩手机状态。这套规则用在教室场景里,能把后排同学短暂低头捡东西造成的误报几乎清零。落到底层逻辑上,目标检测的置信度是独立事件,用群体的统计规律来做状态判定,比单帧的绝对阈值可靠得多。

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

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

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

立即咨询