
简介一份基于YOLOv5目标检测的网课专注度监测预警系统完整项目面向毕业设计、期末大作业及课程设计场景适合需要快速落地深度学习视觉方案的学生与开发者。项目不仅包含可直接运行的Python源码与训练好的模型权重还提供PyQt5图形界面配有代码注释部署门槛较低能够实时监测学习专注状态并对疲劳、分心等行为发出预警。资源包共121个文件压缩包大小约179.87MB涵盖Python脚本、YOLOv5配置yaml、预训练模型pt/onnx、人脸关键点检测dat模型以及提示音mp3、界面ui和项目计划书docx等各类型文件用途清晰便于二次开发与论文撰写。已有360人学习下载属于导师认可度高、项目完整度较好的实战资源。通过源码中的检测流程、界面交互逻辑及模型调用方式使用者可以掌握目标检测在专注度分析中的落地流程并基于现有框架扩展姿态识别、抬头率统计等功能是完成高分课设或毕设的高性价比选择。1. 网课专注度监测为什么得用目标检测网课场景下老师看不到学生的真实状态点名或连麦只能覆盖个别时刻靠助教盯监控画面更不现实。把「学生是否在认真听课」变成机器能判断的问题最直接的办法是让摄像头一直开着用目标检测把画面里的人、头部姿态、手部动作逐帧识别出来再按时间维度累计成专注度分数。这比心率、眼动仪方案便宜得多普通笔记本摄像头就能跑。这套方案的技术核心是 yolov5单阶段检测器推理速度快权重文件小能在没有独立显卡的机器上做实时检测。界面层交给 PyQt5做成一个带实时画面、预警日志、状态统计的桌面程序部署在学生端或教室端都合适。对于课程督导、在线考试防分心、自习室专注力统计这类需求它比云服务方案更可控数据也不出本地。下面按「行为定义 - 模型训练 - 界面集成 - 调优落地」这条链路展开照着做就能搭出一版可运行的原型。2. 用 yolov5 定义专注度先拆行为标签再谈检测2.1 专注度不是一个框能表达的先定类别体系目标检测的输出是「框 类别 置信度」所以第一步不是急着训模型而是想清楚什么样的画面状态应该被记作分心。我一般会把专注度拆成几个可观测的视觉信号头部是否正对屏幕、是否低头、是否趴在桌上、手里有没有手机、有没有频繁起身。这些信号不需要骨骼关键点模型yolov5 的目标框就能覆盖大部分。按照这个思路类别体系可以这样设计类别名标注含义典型行为默认是否算专注face_front面部正对摄像头学生看屏幕/书写是face_side面部明显侧转看窗外、与旁边人交谈是可配置head_down低头角度过大看手机、趴桌子否phone手部持手机刷手机否hand_up单手或双手举起举手提问是lying趴在桌面睡觉否这个表里的「是否算专注」不是写死在代码里的而应该做成配置文件。比如 face_side 在老师授课场景也算专注但在考试监考场景就应该记为异常。类别数量不建议超过 8 个类越多训练数据越难收集误检也越多。2.2 用 yolov5 推理单帧画面模型加载与坐标输出yolov5 的网络结构分三块CSPDarknet 做骨干特征提取PANet 做多尺度特征融合Detect 头输出三个尺度的预测框。理解到这一层就够了——训练和推理时真正要关心的是模型输出了什么格式的结果。推理代码用官方torch.hub写法最省事。如果训练好的权重在本地直接custom方式加载import cv2 import torch # 加载本地训练好的权重force_reload 避免使用缓存 model torch.hub.load(ultralytics/yolov5, custom, pathbest.pt, force_reloadTrue) # 这两个阈值直接决定误报率后面会反复调 model.conf 0.45 # 置信度阈值低于这个值的框直接丢弃 model.iou 0.45 # NMS 的 IoU 阈值重叠框合并的严格程度 cap cv2.VideoCapture(0) # 0 代表默认摄像头 while cap.isOpened(): ret, frame cap.read() if not ret: break results model(frame) df results.pandas().xyxy[0] # 转成 DataFrame包含 xmin, ymin, xmax, ymax for _, row in df.iterrows(): x1, y1, x2, y2 int(row[xmin]), int(row[ymin]), int(row[xmax]), int(row[ymax]) label f{row[name]} {row[confidence]:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 8), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(yolov5 focus detection, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()results.pandas().xyxy[0]是 yolov5 自带的数据框转换方法每一行对应一个检测目标包含归一化的置信度和类别名。model.conf和model.iou两个参数直接影响检测质量conf 调高会减少误报但也会漏掉置信度低的目标iou 调高会让重叠框更难合并适合多目标互相遮挡的场景。初次运行建议先保持 0.45等录完一段真实画面再回调。提示如果加载模型时报错AttributeError: NoneType object has no attribute shape多半是摄像头索引不对或者 OpenCV 与 torch 的 num_workers 冲突。先单独跑cap.read()确认摄像头能出帧。2.3 从单帧检测到连续专注度评分滑动窗口与时序平滑单帧检测出来的是瞬时标签直接用会出问题学生只是转头拿个笔就会被判成「分心一帧」画面抖动也容易让标签来回跳。常见做法是引入时间窗口统计最近 N 帧里各行为标签的出现比例。这里给出一个评分函数输入是滑窗内的标签序列输出 0 到 1 的专注度分数def compute_focus_score(labels_in_window, focus_classes(face_front, face_side, hand_up)): 统计最近 N 帧的行为标签计算专注占比。 labels_in_window: list时间顺序排列的标签 focus_classes: 哪些类别视为专注行为 if not labels_in_window: return 0.0 focus_hits sum(1 for lbl in labels_in_window if lbl in focus_classes) return focus_hits / len(labels_in_window)窗口长度 N 一般取 10 到 15 帧。按 25 FPS 算对应 0.4 到 0.6 秒。窗口太短头部晃动就会把分数拉低窗口太长学生玩手机玩了 3 秒才被捕捉到预警滞后明显。分数阈值定在 0.5 比较合理即窗口内专注标签少于一半就认为当前状态可疑。再结合持续时间判断连续 3 秒分数低于 0.5才触发预警。这样把「瞬时转头」和「持续分心」分开处理。这套「滑窗统计 持续计时」的逻辑放在后面集成阶段会非常有用——它不是模型的一部分但直接影响用户体验。3. 训练自己的专注度检测模型数据标注到 ONNX 导出3.1 数据来源与目录结构训练自己的数据集核心是保证类别均衡和场景多样。对专注度检测来说至少要覆盖不同光线白天/晚间、不同摄像头角度平视/俯视、不同人种和发型、是否戴眼镜。只用自己的脸训练换一个人效果就崩。数据可以录一段 2 到 3 小时网课视频按每 5 帧抽 1 帧的方式提图剔除重复度过高的相邻帧。每个类别至少收集 800 张图样本少的类别优先补。注意开源人脸数据集只能作预训练不能直接当专注度数据用因为缺少「低头看手机」「趴桌」这类行为的标注。目录结构按 yolov5 约定组织dataset/ ├── images/ │ ├── train/ # 约 80% │ └── val/ # 约 20% ├── labels/ │ ├── train/ │ └── val/ └── dataset.yaml3.2 标注格式与 dataset.yaml 配置yolov5 使用 YOLO 格式的 txt 标注文件每行代表一个目标class_id center_x center_y width height坐标值是相对于图片宽高的归一化小数。手工标注用 LabelImg 或 X-AnyLabeling 都可以导出前确认导出格式设为 YOLO。一张 640x480 的图片如果人脸框位于中心且占宽高的一半对应的标注行是0 0.500 0.500 0.500 0.7000对应类别 id后面四个数分别是框中心点 x、中心点 y、框宽、框高。这个顺序很容易和 COCO 的xmin ymin w h搞混改代码前先检查第一个数字是否是类别 id。dataset.yaml是整个训练流程的入口配置path: ../dataset # 数据集根目录 train: images/train val: images/val nc: 6 names: 0: face_front 1: face_side 2: head_down 3: phone 4: hand_up 5: lying3.3 训练命令与超参数调整模型规模选 yolov5s兼顾速度和精度。如果最终部署在无独显的旧笔记本上可以再换 yolov5n如果需要把精度拉满且不在乎速度才考虑 yolov5m。训练命令python train.py \ --data dataset/dataset.yaml \ --weights yolov5s.pt \ --img 640 \ --batch 16 \ --epochs 100 \ --cache参数含义--img 640表示输入分辨率训练时会被缩放到 640x640--batch 16看显存8GB 显存跑 16 以下较稳OOM 就降到 8--epochs 100对 6 类数据通常足够类别多或数据量超过 5000 张可以加到 150--cache把图片提前缓存进内存能明显提速但内存不足 16GB 时建议去掉。训练前重点看两个超参数lr0初始学习率和mosaic马赛克增强。数据量不足时把hyp.scratch-low.yaml里的mosaic: 1.0先调到 0.5防止太多拼接图破坏小目标检测效果。学习率用默认的 0.01 就行不要盲目调大容易在训练后期震荡。训练完成后把验证集上 mAP0.5 和 precision/recall 打出来python val.py --data dataset/dataset.yaml --weights runs/train/exp/weights/best.pt选择best.pt而不是last.pt前者是在验证集上表现最好的权重。此时模型还可以直接用但我一般会导出成 ONNX 再集成到 PyQt5 程序里因为 ONNX 不依赖 torch 的 Python 版本打包后体积更小加载也稳定python export.py --weights runs/train/exp/weights/best.pt --include onnx3.4 训练集和验证集划分的 3 个雷区数据划分不能简单随机。按视频抽帧得到的数据相邻帧高度相似随机分会导致训练集和验证集有大量「双胞胎」图片验证指标虚高。正确做法是按视频片段划分比如前 10 个片段进训练集后 3 个片段进验证集。其次类别不均衡时优先用--classes参数做加权采样而不是往验证集里塞负样本。第三label 文件里的坐标值大于 1 或小于 0通常是标注软件导出设置不对训练前写一段脚本扫一遍。4. 基于 PyQt5 的实时监测界面视频线程、检测线程与预警状态机4.1 PyQt5 界面结构左侧画面右侧统计PyQt5 界面不需要做得多花哨布局就按「左图右表」的经典结构来左边一个大面积 QLabel 显示实时检测画面右边上半部分放专注度趋势曲线用 QChart下半部分放行为计数表格和预警日志 QListWidget。顶部一个工具栏放「开始/停止」「保存日志」「截图」三个按钮。这个系统的界面逻辑可以做成主窗口持有模型实例点击「开始」后启动一个 QThread 线程负责读摄像头跑检测检测结果通过信号发给主线程刷新画面。主线程绝不能直接调cv2.VideoCapture.read()因为摄像头帧读取是阻塞的放到 GUI 线程会直接界面假死。4.2 用 QThread 隔离检测逻辑避免界面卡顿把检测流程封装成独立线程类核心代码结构如下from PyQt5.QtCore import QThread, pyqtSignal import cv2 class DetectWorker(QThread): frame_ready pyqtSignal(object) # 传 annotated 帧 alert_triggered pyqtSignal(str) # 传预警文本 def __init__(self, model, video_source0, parentNone): super().__init__(parent) self.model model self.video_source video_source self.running True self.focus_threshold 0.5 self.alert_seconds 3.0 def run(self): cap cv2.VideoCapture(self.video_source) labels_buffer [] # 滑窗存储 window_size 12 # 12 帧对应约 0.5 秒 low_focus_count 0 # 连续低专注帧计数 while self.running and cap.isOpened(): ret, frame cap.read() if not ret: break results self.model(frame) current_label self._majority_label(results) # 取当前帧主要行为 labels_buffer.append(current_label) if len(labels_buffer) window_size: labels_buffer.pop(0) score compute_focus_score(labels_buffer) if score self.focus_threshold: low_focus_count 1 else: low_focus_count 0 if low_focus_count int(self.alert_seconds * 25 / window_size) * window_size // window_size: self.alert_triggered.emit(f专注度不足: {score:.2f}) low_focus_count 0 annotated results.render()[0] # yolov5 自带画框渲染 self.frame_ready.emit(annotated) cap.release()这段代码把检测、滑窗评分、持续计时都放在工作线程里处理主线程只收信号。frame_ready信号的参数是 numpy 数组PyQt 的信号槽系统跨线程传递object类型是安全的不要直接传 QImage因为 QImage 的创建要在主线程做。alert_triggered信号触发后会弹提示条并在日志区写入一条警告。注意low_focus_count的触发只是暂时阻断重复预警更稳妥的做法是加一个冷却时间比如预警后 30 秒内不再重复触发防止学生一直低头导致日志刷屏。4.3 主线程刷新画面与预警落库主线程收到frame_ready信号后把 numpy 数组转成 QImage 再放到 QLabel 上。转换代码def update_frame(self, frame_bgr): rgb cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.video_label.setPixmap(QPixmap.fromImage(qimg))QImage的 bytes-per-line 参数必须写成ch * w这是最容易写错的地方。如果写成 w图片会斜切。rgb.data是内存地址引用信号传递期间不能对原数组做写操作所以要把frame_ready.emit(annotated.copy())中拷贝的部分做好。预警记录建议直接落到 SQLite方便后面按课程维度统计专注度趋势。建表和插入语句CREATE TABLE IF NOT EXISTS focus_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, event_time TEXT NOT NULL, event_type TEXT NOT NULL, score REAL, detail TEXT ); INSERT INTO focus_log (event_time, event_type, score, detail) VALUES (datetime(now, localtime), LOW_FOCUS, 0.23, head_down detected);SQLite 不需要额外启动服务PyQt5 程序内置数据库文件即可适合单机部署。查询时按event_time字段聚合比如统计每节课的低专注时段分布。4.4 打包成 exe 时的坑torch 与模型路径PyQt5 程序最后要发给别人用PyInstaller 打包是常规操作。但 torch 库打包体积大、依赖多最常遇到的错误是运行时提示找不到torch._C或模型路径不对。模型权重不要用相对路径建议在程序启动时动态拼接绝对路径import os model_path os.path.join(os.path.dirname(os.path.abspath(__file__)), weights, best.pt) model torch.hub.load(ultralytics/yolov5, custom, pathmodel_path, force_reloadFalse)PyInstaller 打包时在 spec 文件里把ultralytics的 yaml 文件作为 data 收进来否则换一台机器跑时会报找不到配置文件的错。--add-data参数可以指定需要附带的文件夹。程序体积通常会超过 1.5GB这是正常的可以用 UPX 压缩减少但 UPX 对部分 DLL 会误伤压缩后务必做冒烟测试。5. 部署前的验证方法与 3 个务实调优细节5.1 用录制视频代替摄像头做回归验证程序写完后不要直接拿真实课堂测试先录 3 到 5 段 10 分钟的真实场景视频注意隐私合规然后用video_source参数指向视频文件代替摄像头跑完整流程。这样每次改完阈值或模型都用同一批视频回归对比预警条数和时间点能快速发现「这个改动让误报变多还是变少」。验证时可以把滑窗大小和分数阈值做成配置文件用脚本批量跑python run_eval.py --video sample.mp4 --window 12 --threshold 0.5批量跑完后对比输出日志重点看「真实分心未预警」和「专注状态被误判」两类错误。这两类错误此消彼长把focus_threshold从 0.5 调到 0.4漏报减少但误报增加window_size从 12 调到 20误报减少但预警会晚 0.3 秒左右。调参就是在这两组指标之间找平衡。5.2 输入分辨率与 CPU 实时性的取舍低配机器跑 yolov5 实时检测最大的瓶颈是输入分辨率。默认 640 输入在纯 CPU 上推理一帧要几百毫秒学生画面会出现明显卡顿。常见做法是部署时降到 320 或 416。我通常在训练时用 640导出模型后推理时用较小输入因为 yolov5 的 PANet 结构对多尺度输入兼容性不错降分辨率不会导致模型崩溃只是 mAP 会有 2 到 4 个点的损失。若目标是「人不动、头转一下能抓到」320 分辨率完全够用。5.3 只保存预警前后的关键帧不录全程持续录视频会涉及隐私和设备存储压力。我一般用环形缓冲只保留预警时刻前后各 2 秒的帧检测到低专注度时把这 4 秒拼成小视频或导出几张截图。Python 侧用collections.deque(maxlen50)存最近 50 帧画面预警时一次性把缓冲里的帧写入cap.write()或拼接成 mp4。这样磁盘占用从每小时数 GB 降到每次预警几百 KB长期跑也不会把系统盘塞满。环形缓冲的代价是内存多占 50 帧的图大约 20MB对现代机器没有负担。最后再说一个细节预警文案里不要只写「专注度不足」把具体行为标签带上比如「head_down 持续 5 秒」。这样事后回溯日志时老师能直接知道学生当时在低头还是玩手机而不是面对一个抽象分数去猜。行为标签和预警文案之间的映射用配置文件维护别硬编码在界面代码里。本文还有配套的精品资源点击获取