
简介面向计算机相关专业毕业生与深度学习初学者的网课专注度检测系统基于YOLOv5目标检测算法并结合PyQt5开发图形界面可用于毕业设计、课程设计或期末大作业。压缩包共122个文件体积约179.87MB文件类型涵盖43个pyc、39个py、18个yaml以及模型权重pt/onnx/pkl、人脸关键点dat、界面设计ui/ico/png等另附docx项目计划书、Dockerfile、README说明文档和mp3音频素材整体目录按源码、模型、界面和文档分区方便快速定位、二次开发或模块替换。当前已有112人学习与下载对于需要快速搭建完整项目的毕业生尤其合适。代码经导师指导并以99分高分通过确保可运行读者可直接部署体验检测效果也可以结合内置的人脸关键点数据与音频文件进一步分析坐姿、视线与声音等专注度指标便于后续扩展与论文写作。1. 网课专注度检测不是玄学YOLOv5 管人在不在dlib 管眼睛和嘴网课专注度检测听起来像玄学拆开看就是一套 Python 程序盯着摄像头算状态YOLOv5 目标检测负责判断镜头前的人在不在dlib 68 点人脸关键点负责看眼睛有没有闭、嘴有没有张PyQt5 界面把结果实时摆出来异常时还能播放提醒音频。这份资源是个人大四毕设源码、模型、界面、Dockerfile、项目计划书都齐了评审做到 99 分完整度很高。对正在做毕设的学生来说它的价值不是算法多新而是代码完整、一条链路能跑通把 YOLOv5 权重换成自己训练的数据集调一下阈值改一句界面文案就是一份能现场演示、能写进论文的高分项目。想练实战的初学者也能从中学会目标检测、人脸关键点和 Qt 界面怎么拼在一起。2. 剖析资源包检测链路里的三个关键节点与文件分工2.1 先看文件清单再反推系统架构拿到一个毕设资源别急着跑先把文件清单当成图纸看一遍。这个包里有一个 68 点人脸关键点模型、一个 Dockerfile、一份项目计划书还有界面图标、背景图、提示音频和 README。把这些文件按职责归类系统的骨架就出来了模型负责算指标界面文件负责呈现文档负责答辩。文件在系统里的角色判断依据shape_predictor_68_face_landmarks.datdlib 官方预训练人脸关键点模型68 点模型专用于算眼睛、嘴部状态yolov5 权重文件weights 目录下YOLOv5 目标检测模型标题标注「源码模型」跑推理必需PyQt5 界面相关文件label.ico、logo.jpg、background.jpg界面图标、Logo 和启动背景界面初始化时被直接引用fenxin.mp3分心/离座报警提示音文件名是「分心」的拼音变体Dockerfile环境固化与一键部署容器里装好 torch、dlib 后可跨机器复现README.md、项目计划书.docx运行说明和毕设文档配套材料答辩和启动都看它光有 YOLOv5 是判断不了专注度的这是这套系统最容易被误解的地方。YOLOv5 能告诉你画面里有几个人、有没有手机但它不知道这个人是在看屏幕还是趴着睡觉。所以要补一层 dlib 68 点把眼睛的开合程度、嘴的张合程度变成数值再用一套状态融合逻辑把「人不在」「人在但没看屏幕」「人在打哈欠」「人在玩手机」归成明确的标签。PyQt5 在这条链路里是出口把每一帧的判定结果变成界面上的实时画面、状态文本和报警音。2.2 为什么第一棒是 YOLOv5 而不是传统人脸检测目标检测在毕设里选 YOLOv5不是因为版本新而是因为它对「人」这种目标的检出最稳。用摄像头拍网课场景人通常是半身或全身出现在画面里存在低头、侧身、戴帽子、光线偏暗的情况。传统 Haar 级联人脸检测在这种条件下漏检率很高而 YOLOv5 用 COCO 预训练权重时自带 person、cell phone 这些类别模型一加载就可以直接判断有没有人、人在画面什么位置、这个人是不是在玩手机。「在不在镜头前」是专注度判断的地基人都不在谈不上专注。从实现角度看YOLOv5 的网络结构分三块CSPDarknet 做主干特征提取PANet 做多尺度特征融合最后的检测头输出候选框、置信度和类别。你去看那张经典的 yolov5 网络结构图这三段一眼就能认出来。要是不做改动你只需要关注两个入口一是置信度阈值调它控制误检二是类别过滤只用 person 和 cell phone 这两类其他 78 类全部忽略。如果想换掉默认权重走一遍 yolov5 自带的 train.py用自己打标的数据集训练再把输出权重替换成 custom 加载其他逻辑可以不动。对毕设来说这就是最舒服的改法不需要动网络结构。2.3 dlib 68 点模型专注度的核心数据来源人脸关键点在这套系统里承担真正的专注度计算。shape_predictor_68_face_landmarks.dat 是 dlib 官方预训练模型输入一张人脸区域输出 68 个关键点坐标。眼睛周围是 36-41、42-47 两组点嘴部周围是 48-67 这组点。基于这些坐标可以算出两个关键指标EAR眼睛纵横比眼睛纵向距离与横向距离的比值。睁眼时 EAR 通常在 0.25 到 0.35 之间闭眼时跌到 0.1 附近。设定一个 0.2 左右的阈值就能把「闭眼」变成可计数的事件。MAR嘴部纵横比嘴部上下唇距离与左右嘴角距离的比值。打哈欠时嘴巴张大MAR 明显升高配合持续时间判断就能区分说话和打哈欠。这两个指标组合起来就是 PERCLOS 疲劳判定统计一个时间窗口内闭眼帧占总帧数的比例超过设定值就判定为疲劳。这是驾驶疲劳检测领域用得很成熟的方法拿来做网课专注度同样成立。还有一层容易被忽略的逻辑dlib 检测不到人脸的时刻本身就是一个状态信号——人在镜头前但低着脑袋看手机正面检测不出关键点这段时间就应该计入分心。2.4 音频、Dockerfile 和界面的连接关系PyQt5 界面是这条链路的收口摄像头画面、专注状态、报警音、日志记录都在这一层汇合。整个系统的数据流是摄像头取帧 → YOLOv5 检测人物和手机 → dlib 检测人脸关键点和计算 EAR/MAR → 状态融合模块输出标签 → 界面刷新画面和状态 → 触发 fenxin.mp3 报警并写入日志。这一层写不好前面模型再强也没法答辩演示。Dockerfile 的存在值得多说一句。torch、dlib、PyQt5 这几个库的版本组合在换机器时很容易翻车Dockerfile 把环境固化成镜像换一台电脑能直接复现出同一套运行环境。毕设里常见的使用方式是本机跑 PyQt5 界面做演示Docker 用于把检测服务标准化或者作为答辩前的环境后悔药——现场环境坏了用镜像一键起回来。README.md 和项目计划书则解决了怎么跑和为什么这样做两个问题答辩讲稿的结构基本能直接从计划书里拉出来。3. 把检测逻辑落到代码YOLOv5 推理、PERCLOS 计算与阈值参数3.1 加载 YOLOv5 本地权重做人员检测不管界面怎么做第一步永远是先跑通检测模型。项目里自带训练好的权重加载方式按 YOLOv5 官方的 hub 接口来import cv2 import torch # 加载项目自带的 YOLOv5 权重path 改成你解压后的实际路径 model torch.hub.load(ultralytics/yolov5, custom, pathweights/best.pt, force_reloadFalse) def detect_person(frame): # size 是输入分辨率conf_thres 是置信度阈值iou_thres 是 NMS 阈值 results model(frame, size640, conf_thres0.45, iou_thres0.45) df results.pandas().xyxy[0] # 转成表格方便过滤 persons df[df[name] person] # 只保留人的检测框 phones df[df[name] cell phone] # 只保留手机框 return persons, phonestorch.hub.load 的 custom 参数表示加载本地权重第一次运行会检查仓库和依赖建议把整个 yolov5 仓库放在项目目录里避免现场联网。conf_thres 控制置信度门槛调低能检出更多人但海报、影子、远处路过的人也容易变成误检调高则可能漏检半身人。size640 是推理分辨率画面较小或实时性要求高时可以降到 480但小目标检出会变差。返回的 persons 里每一行是 x1、y1、x2、y2 框坐标和置信度离座判断就靠这个框是否存在来实现。3.2 用 dlib 68 点计算 EAR 和打哈欠指标人脸关键点的加载和指标计算是这套系统的核心import dlib import numpy as np detector dlib.get_frontal_face_detector() predictor dlib.shape_predictor(shape_predictor_68_face_landmarks.dat) def eye_aspect_ratio(pts): # pts 是眼睛周围 6 个关键点坐标 # 纵向距离取两点均值横向距离取两眼角距离 a np.linalg.norm(pts[1] - pts[5]) b np.linalg.norm(pts[2] - pts[4]) c np.linalg.norm(pts[0] - pts[3]) return (a b) / (2.0 * c) def get_face_state(frame): gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces detector(gray, 0) if len(faces) 0: return {face_detected: False} # 没检到脸按未注视屏幕处理 shape predictor(gray, faces[0]) pts np.array([[p.x, p.y] for p in shape.parts()]) left_ear eye_aspect_ratio(pts[36:42]) # 一只眼的 6 个点 right_ear eye_aspect_ratio(pts[42:48]) # 另一只眼的 6 个点 ear (left_ear right_ear) / 2.0 mar mouth_aspect_ratio(pts[48:67]) # 嘴部 20 个点函数见源码 return {face_detected: True, ear: ear, mar: mar}dlib 的 shape_predictor 输入是灰度图和检测到的人脸框输出 68 个点。EAR 的计算逻辑是纵向距离除以横向距离闭眼时纵向距离趋近于零EAR 会跌到 0.1 上下。项目源码里的 EAR 阈值一般取 0.2 左右低于这个值算闭眼嘴部 MAR 取 0.6 左右高于这个值并持续若干帧算打哈欠。注意这里没有用瞳孔追踪或角膜反射那种方案精度更高但需要红外摄像头毕设场景下 CPU 实时跑 68 点模型已经足够。PERCLOS 的落地是滑动窗口统计维护一个长度 30 秒的帧状态队列窗口内闭眼帧占比超过 40% 判疲劳。这个窗口长度和阈值直接决定报警灵敏度我一般是先录三段不同状态的视频再回头调参而不是凭感觉改数字。3.3 专注度状态机把指标合并成标签单独看 EAR 和 MAR 没有意义要在一个时间窗口里把它们和 YOLOv5 的检测结果合并成状态from collections import deque def build_focus_state(frame, persons, phones, face_state, history): # history 存最近 N 帧的状态快照deque 满了自动丢旧帧 if len(persons) 0: history.append(away) # 人不在镜头前 return 离座 if not face_state.get(face_detected): history.append(distracted) # 人在但没露脸按低头处理 return 分心 if face_state.get(ear, 1.0) 0.2: history.append(eyes_closed) else: history.append(normal) if len(phones) 0: history.append(phone) return 玩手机 # 近 10 帧里闭眼占比超过一半判定为闭眼分心 closed_ratio history.count(eyes_closed) / max(len(history), 1) if closed_ratio 0.5: return 闭眼 return 专心状态机的意义在于防抖。单帧的检测结果波动很大一帧没检到人脸可能是低头捡笔连续 10 帧没人脸才是真的走神。所以离座判定不能只看当前帧要看历史队列里没有人脸和 person 框的占比。deque 的长度就是缓冲窗口合理做法是先判离座缓冲 10 帧、闭眼占比窗口 10 帧再根据实际帧率换算成秒数。摄像头是 30fps 时10 帧约 0.3 秒这个量级既能滤掉偶然抖动又不会让状态切换显得迟钝。3.4 阈值速查表与调参顺序参数推荐初始值影响size640越大小目标越清晰推理越慢conf_thres0.45调高减少误检调低减少漏检EAR 闭眼阈值0.2随摄像头距离和角度微调闭眼计数窗口10 帧避免单帧误判MAR 打哈欠阈值0.6随嘴型大小微调PERCLOS 窗口30 秒 / 占比 0.4决定疲劳报警灵敏度离座缓冲10 帧防止画面闪烁造成误报提示调参顺序一定按链路从前往后走先保证 YOLOv5 输出稳定再动 EAR 和 PERCLOS否则你根本分不清误报来自哪一层。调参的顺序要按链路从前往后走先保证 YOLOv5 能稳定检出人和手机再调 dlib 的 EAR 阈值最后才动 PERCLOS 窗口。一次性把灵敏度调得太高系统会陷入频繁报警老师一看就觉得是玩具调得太低答辩演示时又看不出效果。这时候录一段包含专心、分心、离座三种状态的视频用于比对调参是最省力也最有效的方法。这套项目的源码里这些阈值都集中在一个配置区改起来不用翻文件。4. PyQt5 界面集成把检测结果变成实时评分和报警4.1 界面骨架从资源文件到窗口布局PyQt5 的界面部分在文件清单里能对应到具体资源label.ico 是窗口图标logo.jpg 和 background.jpg 用于启动界面和背景。整体布局一般是左侧大块区域显示摄像头实时画面右侧放状态文本、专注度评分、检测日志底部是开始和停止按钮。界面逻辑文件里会引用这些资源跑起来后先把背景和 Logo 摆好再等摄像头数据流进来。这里有个常被忽略的点界面资源路径不能写死绝对路径最好用 os.path.join(os.path.dirname(file), logo.jpg) 这种写法否则项目换目录界面一启动就报找不到文件。import os from PyQt5.QtWidgets import QMainWindow, QLabel from PyQt5.QtGui import QIcon, QPixmap class MainWindow(QMainWindow): def __init__(self): super().__init__() base os.path.dirname(__file__) self.setWindowIcon(QIcon(os.path.join(base, label.ico))) bg_label QLabel(self) bg_label.setPixmap(QPixmap(os.path.join(base, background.jpg))) # 后续再往布局里加画面 QLabel、状态 QLabel、按钮等控件这段代码展示了资源加载的标准姿势。用file动态拼路径之后无论项目放在哪个目录双击启动都不会因为找不到资源而崩溃。icon 和背景图在项目里都有现成文件只需要确认文件名和代码里写的一致。4.2 QThread 跑检测线程信号槽回传画面界面最忌讳的做法是把检测循环直接写在按钮的点击事件里。目标检测加关键点检测一帧要几十毫秒到几百毫秒写在主线程里界面会直接假死拖动窗口都卡。标准做法是开一个 QThread 子线程跑摄像头读取和检测通过信号把画面和状态回传到主线程刷新界面from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage class DetectThread(QThread): frame_ready pyqtSignal(QImage) # 每帧画面供界面显示 state_ready pyqtSignal(dict) # 状态、评分、闭眼次数 def __init__(self, source0): super().__init__() self.source source # source0 是内置摄像头 self.running True def run(self): cap cv2.VideoCapture(self.source) while self.running and cap.isOpened(): ret, frame cap.read() if not ret: continue persons, phones detect_person(frame) # 见第 3 章 face_state get_face_state(frame) label build_focus_state(frame, persons, phones, face_state, self.history) # BGR 转 RGB再包成 QImage 交给界面 rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch rgb.shape qimg QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888) self.frame_ready.emit(qimg.copy()) # 必须 copy否则图像数据会被复用 self.state_ready.emit({label: label, score: score, frame: frame}) cap.release()这里最容易被忽略的是 emit 的对象生命周期。qimg.copy() 不加的话QImage 底层引用的是同一块内存下一帧数据写进来上一帧画面就会花屏。state_ready 信号把状态字典发给主线程主线程槽函数负责更新标签和评分这样界面刷新和检测计算分离互不阻塞。线程对象要保存为 self.detect_thread不然局部变量被回收线程会在启动瞬间默默消失。4.3 报警提醒与防打扰限频fenxin.mp3 在系统里是分心报警音触发条件一般是状态切到分心或离座。但不能每帧都播放否则连续报警时声音会变成噪音。我一般会在触发逻辑里加一个冷静时间同一状态持续超过 5 秒才播放一次或者状态发生切换时才播放。import time from PyQt5.QtMultimedia import QSound last_alert_time 0.0 def maybe_alert(state): global last_alert_time if state[label] in (分心, 离座, 玩手机): if time.time() - last_alert_time 5.0: QSound.play(fenxin.mp3) last_alert_time time.time()QSound.play 是异步的调用后立即返回不会卡界面。mp3 在部分系统上不支持解码遇到无声的情况把音频转成 wav 再试试是最快的排查方式这也是项目里保留音频文件的同时还需要做一个播放失败兜底的原因。冷静时间 5 秒这个值不是死的答辩演示时可以调到 2 秒让评委更快看到报警效果实际使用时调回 10 秒减少打扰。4.4 状态落盘给答辩准备一份日志数据专注度检测这类毕设现场演示的随机性很强评委可能想看你系统的真实输出。一个很实用的做法是把每次状态变化写成 CSV 日志答辩时把文件调出来配合时间轴给评委看哪一段闭眼、哪一段离座、哪一段在玩手机。日志的字段一般包括时间戳、状态标签、专注度评分、闭眼计数。写入时机放在状态变化时而不是每帧这样文件不会无限膨胀。import csv, time def log_state(csv_writer, state): # 只在状态变化时写一行避免日志爆炸 csv_writer.writerow([time.strftime(%Y-%m-%d %H:%M:%S), state[label], state[score], state.get(close_count, 0)])这个 CSV 会在跑完演示后留下一份完整记录拿来做论文的实验数据图表也够用。关键点是写入时机状态没变就不写状态切换才写否则一分钟的视频会产出几百毫秒级的大量冗余行反而看不出规律。答辩前把 CSV 打开挑几个典型片段截图比现场直播数据更有说服力。5. 避坑手册模型、环境、界面三个方向最常见的翻车现场5.1 模型与依赖环境的坑坑 1shape_predictor_68_face_landmarks.dat 加载即报错现象程序运行到 dlib.shape_predictor() 一行直接崩溃提示找不到文件或者文件存在但报 JSON 解码失败。原因最常见是相对路径问题。代码里写 shape_predictor_68_face_landmarks.dat但实际运行时工作目录在别的文件夹Python 找不到文件。另一种情况是从网盘下载的模型文件损坏体积不对或中途断点。解决路径用 os.path.join(os.path.dirname(file), shape_predictor_68_face_landmarks.dat)保证无论从哪里启动都能找到文件确认约 99MB小于这个数大概率下载不完整。我第一次跑这个项目就吃了路径的亏根目录对了子目录启动就翻车。坑 2torch 和 CUDA 版本不匹配导致推理崩溃或奇慢现象YOLOv5 推理时直接抛出 CUDA error: no kernel image is available或者显卡明明不错但推理速度还不如 CPU。原因PyTorch 的 CUDA 版本和显卡驱动不匹配。装了 cu121 的 torch驱动只支持到 CUDA 11.x底层 kernel 完全无法加载或者装了 GPU 版但在无 GPU 机器上没回退到 CPU。解决先跑 nvidia-smi 看驱动支持的 CUDA 大版本再装对应版本的 torch常见组合是 cu118 或 cu121。无 GPU 的机器直接装 CPU 版 torch推理慢一点但整个系统能跑通。这个坑在答辩前一天最容易暴雷因为平时用的机器没问题换了教室电脑就崩。坑 3torch.hub 下载权重卡住或失败现象第一次运行 torch.hub.load(ultralytics/yolov5, ...) 卡在 Downloading 或直接抛连接异常。原因YOLOv5 的 hub 接口默认会检查并拉取仓库文件网络环境不稳定时这个步骤会长时间没反应。答辩现场没有预下载的话这个坑会直接让演示中断。解决把 ultralytics/yolov5 仓库提前放到项目目录里权重文件也下载好放到 weights 目录代码里用本地路径加载全程离线可跑。这是我在答辩前必做的预检项之一——永远不要赌现场的网。5.2 界面与数据源的坑坑 4摄像头打不开或界面黑屏现象界面能启动但画面是黑的控制台没有任何报错或者 cv2.VideoCapture(0) 返回 False。原因摄像头被其他软件占用比如会议软件没退干净笔记本摄像头权限被系统关了或者外接摄像头的索引不是 0。解决先杀掉所有占用摄像头的进程再检查系统隐私设置里的摄像头权限最后用枚举方式逐个尝试索引 0、1、2找到能打开的索引再启动线程。我习惯在界面里放一个摄像头自检按钮启动时先探测避免演示当场黑屏。坑 5PyQt5 界面启动即崩溃或报线程错误现象点击开始按钮后窗口假死或者报 AttributeError提示 DetectThread 对象没有 start 方法画面显示错乱。原因检测循环写在了主线程UI 被推理阻塞线程对象是局部变量方法执行完就被回收QImage 没 copy底层内存被下一帧复用。还有个同族问题报警音不响多半是 mp3 解码不支持或路径不对转成 wav 就能解决。解决推理放在 QThread 的 run 方法里线程对象保存为 self.detect_threadQImage 在 emit 前调用 copy()。这三个问题几乎是 PyQt5 做界面必踩的连环坑解决后界面流畅度会有质的提升。6. 进阶技巧用视频文件模拟摄像头把答辩演示变成确定性的表演答辩演示最大的风险是现场环境不可控摄像头被占用、网络不通、环境变量不对。这套系统本身支持把检测源从摄像头换成视频文件这是我最推荐的验证方式。用录好的视频代替实时画面每次运行的结果完全一致调参和答辩都变得可预测。# DetectThread 的 source 参数换成视频路径即可 detect_thread DetectThread(sourcedemo_clip.mp4) # 其他逻辑完全不用改摄像头和视频文件共用同一套 read 接口cv2.VideoCapture 对摄像头索引和视频文件路径是统一的所以 QThread 的 run 方法不用动。我一般会在答辩前录一段三分钟视频第一分钟正常上课第二分钟趴桌子或玩手机第三分钟频繁打哈欠。跑完导出 CSV对照时间轴确认正常段没有误报警趴桌子段在 60 秒左右准确切到离座打哈欠段触发分心提示。这样演示时不用现场对着摄像头做动作结果稳定评委看着也直观。顺带可以做一个自动化调参的小技巧把第 3 章的阈值参数都提到一个配置文件里用脚本循环跑分段视频加 CSV 输出每调一次参数就对比一次状态切换时间点几轮下来就能找到最合适的数值组合。这比对着实时画面一遍遍试要高效得多。如果项目里已经带了 Dockerfile也可以把整套依赖构建成镜像在另一台机器上直接跑容器里的检测服务界面留在本机环境问题基本就被隔离了。我以前吃过一次亏答辩前夜换了台电脑现场网络拉胯torch.hub 一直卡在下载摄像头还被会议软件占用整个演示差点没跑起来。从那以后我每次演示前都强制走一遍本地权重、视频文件、音频绝对路径、摄像头权限四个预检再也不赌环境。这套资源包把 YOLOv5 检测、dlib 68 点指标、PyQt5 界面和 Dockerfile 都整合好了你拿到后按本文把阈值和视频源一换就能跑起来。希望帮到你。本文还有配套的精品资源点击获取