尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

YOLOv5+PyQt5课堂专注度检测系统实战指南

发布时间:2026/9/26 4:47:10

资讯中心
01
ARTICLE

YOLOv5+PyQt5课堂专注度检测系统实战指南

YOLOv5+PyQt5课堂专注度检测系统实战指南
简介本资源是一套面向计算机视觉初学者与教育技术开发者的课堂专注度实时检测预警系统实现方案基于YOLOv5目标检测模型与PyQt5构建跨平台桌面应用解决在线教学场景中学习者注意力状态自动识别与异常预警的实际需求。压缩包共146个文件含39个Python源码含主程序、数据预处理与UI逻辑、18个YAML配置文件模型结构与训练参数、1个.pt预训练权重、23个.zbak备份文件、3个JPG/PNG图像资源及UI图标、README.md项目说明与Dockerfile容器化支持整体大小为179.88MB。已有50人下载学习适合希望掌握端到端AI应用落地流程的开发者——不仅提供可直接运行的GUI界面与完整训练推理代码还包含人脸关键点检测dat模型、背景音效mp3、项目计划书docx等工程化配套素材目录组织清晰便于理解模块划分与系统集成逻辑。1. 为什么课堂里“低头刷手机”比“走神发呆”更难被老师发现——YOLOv5 PyQt5 实时专注度检测不是炫技而是解决真实教学盲区你见过这样的场景吗后排学生全程低头屏幕反光映着短视频封面前排学生坐得笔直眼神却飘向窗外云朵——两种状态在传统考勤或点名中完全等价。而真正影响学习效果的是视线方向、头部姿态、眼部开合、手持物类型这些细粒度行为信号。YOLOv5 不是拿来跑个 demo 的玩具它能以 42 FPS 在普通笔记本上稳定输出人脸框关键点眼睛状态PyQt5 也不是做 PPT 式界面的替代品它是唯一能在 Windows/macOS/Linux 上统一调度 OpenCV 视频流、模型推理、UI 响应和本地预警弹窗的成熟 GUI 框架。这个系统不依赖云端 API、不上传视频帧、不强制联网所有计算在本地完成——教师打开 exe 就能用学生无需安装任何插件校方不必担心数据合规风险。它面向的是教务处需要的「可导出时段专注度热力图」、班主任关心的「单节课异常行为频次统计」、以及教育技术供应商真正能打包交付的「离线可部署模块」。如果你正卡在「模型训好了但不会嵌进界面」「界面画好了但接不上摄像头」「预警弹窗一弹就卡死视频流」这三个现实断点上这篇笔记就是为你写的血泪复盘。2. 从 YOLOv5s 到专注度标签模型侧必须砍掉的冗余与必须保留的结构YOLOv5 官方仓库ultralytics/yolov5默认输出的是通用目标检测结果直接喂给课堂场景会翻车它把书本、水杯、耳机都当成独立目标框却无法区分“学生看黑板”和“学生看手机”。我们必须对模型结构和训练流程做三处硬性改造否则后续所有 UI 和预警逻辑都是空中楼阁。2.1 改造 backbone冻结前两层 Conv Focus强制保留空间感知能力YOLOv5s 的 backbone 以 Focus 层起始将 3 通道输入压缩为 64 通道。但在课堂小目标如手指、手机屏幕密集场景下过早降维会丢失关键纹理信息。我们保留原始 Focus 层但冻结其前两个 Conv2d 权重仅微调后续 C3 模块# models/yolo.py 中修改 detect() 方法前的加载逻辑 model attempt_load(yolov5s.pt, map_locationdevice) # 冻结 Focus 层前两层索引 0 和 1 for i, (name, param) in enumerate(model.model.named_parameters()): if model.0 in name and i 2: # model.0 是 Focus 层 param.requires_grad False提示冻结后训练 loss 下降变慢但 mAP0.5 在课堂数据集上提升 3.2%尤其对「握持手机」类小目标召回率从 61% → 79%。别信“全量微调更好”的玄学这里冻结是保底策略。2.2 替换 head用 5 分类输出替代 80 类通用检测头原始 YOLOv5 输出 80 类 COCO 标签但我们只关心 5 类行为状态0: face_front正脸、1: face_side侧脸、2: eyes_closed闭眼、3: phone_in_hand手持手机、4: book_open翻开书本。因此必须重写 detection head 的分类分支# models/yolo.py 中修改 Detect 类的 __init__ class Detect(nn.Module): def __init__(self, nc5, anchors(), ch()): # nc 从 80 改为 5 super().__init__() self.nc nc # number of classes self.no nc 5 # number of outputs per anchor # ... 其余不变同时在train.py中指定类别数python train.py --data data/attention.yaml --cfg models/yolov5s.yaml --weights yolov5s.pt --nc 5 --epochs 100参数说明--nc 5是硬性开关漏设会导致 loss 计算错位data/attention.yaml必须包含train,val,nc: 5,names: [face_front, face_side, eyes_closed, phone_in_hand, book_open]四要素缺一不可。2.3 构建课堂专用数据集LabelImg 标注 关键点增强双轨制YOLOv5 默认只支持 bbox 标注但专注度判断需结合头部偏转角yaw/pitch和瞳孔中心坐标。我们采用双轨标注法主轨用 LabelImg 生成.txt标签YOLO 格式标注上述 5 类辅轨用dlib在每张图上提取 68 个 facial landmarks保存为.npy文件与图片同名。# utils/augmentations.py 中新增关键点归一化函数 def landmarks_to_normalized(lmks, img_shape): h, w img_shape[:2] lmks_norm np.zeros((68, 2)) lmks_norm[:, 0] lmks[:, 0] / w # x 归一化到 [0,1] lmks_norm[:, 1] lmks[:, 1] / h # y 归一化到 [0,1] return lmks_norm # 在 dataset.py 的 __getitem__ 中加载并拼接 landmarks np.load(img_path.replace(.jpg, .npy)) labels np.concatenate([labels, landmarks.reshape(-1)], axis1) # shape: (n_obj, 5136)注意.npy文件必须与图像严格同名且同目录否则 DataLoader 会静默跳过该样本68 点坐标必须用dlib.get_frontal_face_detector()dlib.shape_predictor(shape_predictor_68_face_landmarks.dat)提取OpenCV 的 Haar 不够精度。3. PyQt5 不是“画按钮”而是构建视频流-模型-UI 三线程安全管道PyQt5 的核心陷阱在于所有 GUI 操作必须在主线程执行但 OpenCV 读帧和 YOLOv5 推理必须异步否则界面秒变白屏。常见错误是把cv2.VideoCapture.read()和model(img)直接塞进QTimer.timeout.connect()—— 这等于在 GUI 线程里堵死所有事件循环。正确解法是用QThreadmoveToThread构建三线程隔离。3.1 创建 VideoWorker 线程专责读帧与预处理# workers/video_worker.py class VideoWorker(QObject): frame_ready pyqtSignal(np.ndarray) error_occurred pyqtSignal(str) def __init__(self, src0): super().__init__() self.src src self.is_running False def run(self): cap cv2.VideoCapture(self.src) if not cap.isOpened(): self.error_occurred.emit(f无法打开摄像头 {self.src}) return self.is_running True while self.is_running: ret, frame cap.read() if not ret: self.error_occurred.emit(摄像头读取失败) break # BGR → RGB 转换PyQt5 QImage 只认 RGB frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) self.frame_ready.emit(frame_rgb) # 发射到主线程 cap.release()逻辑说明VideoWorker继承QObject而非QThread避免线程生命周期管理混乱frame_ready信号携带np.ndarray主线程接收后直接转QImage显示不经过任何拷贝。3.2 创建 InferenceWorker 线程专责模型推理与结果解析# workers/inference_worker.py class InferenceWorker(QObject): result_ready pyqtSignal(dict) # {boxes: [...], labels: [...], confidences: [...]} error_occurred pyqtSignal(str) def __init__(self, model_pathweights/best.pt): super().__init__() self.model torch.hub.load(ultralytics/yolov5, custom, pathmodel_path, force_reloadFalse) self.model.conf 0.4 # 置信度阈值 self.model.iou 0.5 # NMS 阈值 def run_inference(self, frame_rgb): # frame_rgb 是 VideoWorker 发来的 RGB 图像需转回 BGR 供 YOLOv5 处理 frame_bgr cv2.cvtColor(frame_rgb, cv2.COLOR_RGB2BGR) results self.model(frame_bgr) # 解析 results.pandas().xyxy[0] 获取 DataFrame df results.pandas().xyxy[0] output { boxes: df[[xmin, ymin, xmax, ymax]].values.astype(int), labels: df[name].values, confidences: df[confidence].values } self.result_ready.emit(output)参数说明model.conf0.4是平衡实时性与误报的关键——课堂场景中0.3 会触发大量“书本误检为手机”0.5 会漏掉侧脸学生model.iou0.5防止同一人脸被重复框出pandas().xyxy[0]比原生.xyxy更易索引避免 tensor 索引越界。3.3 主窗口线程绑定用 moveToThread 实现零阻塞调度# main_window.py class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setup_ui() self.init_threads() def init_threads(self): # 创建线程对象 self.video_thread QThread() self.infer_thread QThread() # 创建 worker 对象 self.video_worker VideoWorker(src0) self.infer_worker InferenceWorker() # 移动 worker 到对应线程 self.video_worker.moveToThread(self.video_thread) self.infer_worker.moveToThread(self.infer_thread) # 连接信号 self.video_worker.frame_ready.connect(self.on_frame_received) self.video_worker.error_occurred.connect(self.show_error) self.infer_worker.result_ready.connect(self.on_inference_result) self.infer_worker.error_occurred.connect(self.show_error) # 启动线程 self.video_thread.start() self.infer_thread.start() # 触发 video_worker.run() self.video_worker.run() # 注意不是 start()关键细节moveToThread()后必须用worker.run()启动逻辑而非worker.start()那是 QThread 的方法frame_ready和result_ready信号必须在 worker 内部 emit不能在主线程调用self.video_thread.start()是启动线程事件循环不是执行业务逻辑。4. 预警逻辑不是“阈值超了就弹窗”而是基于行为序列的状态机建模单纯用「单帧检测到 phone_in_hand 就报警」会引发灾难性误报学生掏纸巾、翻课本、调整眼镜都会触发。真正的专注度评估必须是时间维度上的状态迁移——连续 3 秒检测到phone_in_hand且face_side占比 70%才判定为「分心使用手机」连续 5 秒eyes_closed且face_front占比 20%才判定为「瞌睡」。这需要构建一个轻量级状态机。4.1 定义状态与迁移规则用字典而非 if-else 链# core/state_machine.py STATE_TRANSITIONS { normal: { phone_in_hand: {threshold: 3, next: distracted_phone, reset_on: [face_front, book_open]}, eyes_closed: {threshold: 5, next: drowsy, reset_on: [face_front]} }, distracted_phone: { phone_in_hand: {threshold: 3, next: distracted_phone, reset_on: []}, face_front: {threshold: 1, next: normal, reset_on: []} }, drowsy: { eyes_closed: {threshold: 5, next: drowsy, reset_on: []}, face_front: {threshold: 1, next: normal, reset_on: []} } } class AttentionState: def __init__(self): self.state normal self.counters {phone_in_hand: 0, eyes_closed: 0} self.last_labels [] def update(self, labels: List[str]) - str: self.last_labels labels[-10:] # 缓存最近 10 帧标签 current_label max(set(labels), keylabels.count) if labels else none # 更新计数器 for k in self.counters: if k in labels: self.counters[k] 1 else: self.counters[k] 0 # 检查迁移条件 if self.state in STATE_TRANSITIONS: for trigger, rule in STATE_TRANSITIONS[self.state].items(): if current_label trigger and self.counters[trigger] rule[threshold]: self.state rule[next] # 重置相关计数器 for r in rule[reset_on]: self.counters[r] 0 return self.state return self.state逻辑说明self.last_labels缓存最近 10 帧用于投票避免单帧抖动counters按标签累加而非帧数因为一帧可能含多个人状态迁移后立即重置reset_on标签计数器防止状态粘连。4.2 预警触发与 UI 反馈用 QTimer 控制弹窗频率与持续时间# main_window.py def on_inference_result(self, result): labels result[labels].tolist() state self.state_machine.update(labels) if state distracted_phone and not self.phone_alert_active: self.show_alert(⚠️ 学生使用手机, 请关注第3排右侧学生) self.phone_alert_active True # 30 秒内不再重复弹窗 QTimer.singleShot(30000, lambda: setattr(self, phone_alert_active, False)) elif state drowsy and not self.drowsy_alert_active: self.show_alert( 学生疑似瞌睡, 请提醒前排中间学生) self.drowsy_alert_active True QTimer.singleShot(30000, lambda: setattr(self, drowsy_alert_active, False)) def show_alert(self, title, message): # 使用非模态 QMessageBox不阻塞视频流 alert QMessageBox() alert.setWindowTitle(title) alert.setText(message) alert.setIcon(QMessageBox.Warning) alert.setStandardButtons(QMessageBox.Ok) alert.buttonClicked.connect(lambda: alert.close()) alert.show() # 关键设置 parent 为 self否则窗口可能脱离主程序 alert.setParent(self, Qt.Dialog | Qt.WindowStaysOnTopHint)参数说明QTimer.singleShot(30000, ...)是防刷屏核心30 秒冷静期由教学场景决定——教师需要时间走到学生身边而非被每秒弹窗淹没setParent(..., Qt.WindowStaysOnTopHint)确保弹窗始终在最前且随主窗口最小化而隐藏。5. 避坑指南那些让项目卡在交付前夜的 4 个致命细节这些坑不是文档里写的也不是 GitHub Issues 里搜得到的而是我在 3 所学校现场部署时盯着日志、抓包、逐行打 log 才定位到的真问题。跳过它们你的系统永远停留在「本地能跑」阶段。5.1 现象PyQt5 界面在 Windows 10 上显示黑屏但 Linux/macOS 正常原因Windows 默认启用 High DPI 缩放PyQt5 未适配会导致QImage数据指针错位cv2.cvtColor()后的 RGB 数据被当 BGR 解析最终QPixmap.fromImage()加载空图像。解决在main.py最顶部插入必须在import PyQt5之前import os os.environ[QT_SCALE_FACTOR] 1 # 或更彻底的方案推荐 if hasattr(Qt, AA_EnableHighDpiScaling): QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) if hasattr(Qt, AA_UseHighDpiPixmaps): QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True)5.2 现象YOLOv5 推理速度从 42 FPS 骤降至 8 FPSGPU 利用率仅 15%原因PyTorch 默认使用torch.backends.cudnn.benchmark False导致每次推理都重新优化卷积算法而课堂视频帧尺寸固定如 1280×720关闭 benchmark 反而更慢。解决在inference_worker.py初始化模型后添加torch.backends.cudnn.benchmark True # 启用 cuDNN 自动调优 torch.backends.cudnn.deterministic False # 允许非确定性算法加速验证方法运行nvidia-smi观察 GPU-Util 是否稳定在 85%~95%而非间歇性脉冲。5.3 现象LabelImg 标注的.txt文件在训练时报IndexError: index 0 is out of bounds原因LabelImg 保存时若图像无目标会生成空.txt文件0 字节YOLOv5 的dataset.py在load_image()中尝试读取lines[0]导致崩溃。解决在utils/datasets.py的LoadImagesAndLabels.__getitem__中增加空文件保护with open(label_path, r) as f: lines f.readlines() if not lines: # 空文件则跳过此样本 return self.__getitem__((index 1) % self.n)5.4 现象预警弹窗弹出后视频画面卡顿 2~3 秒原因QMessageBox默认模态modal会阻塞主线程事件循环导致QTimer无法触发下一帧读取。解决必须用非模态non-modal方式创建并显式设置Qt.Dialog | Qt.WindowStaysOnTopHint标志alert QMessageBox(self) # 显式传入 parent alert.setWindowFlags(Qt.Dialog | Qt.WindowStaysOnTopHint) alert.setAttribute(Qt.WA_DeleteOnClose) # 防止内存泄漏 alert.show()血泪经验曾因漏写self参数导致弹窗父窗口为空Windows 下弹窗悬浮于所有窗口之上教师关掉弹窗后发现主界面已假死——必须alert.setParent(self)。6. 进阶技巧用 OpenCV ROI 截图 OCR 实现「手机内容识别」的轻量级落地预警系统价值上限取决于你能告诉教师「学生在看什么」。纯 YOLOv5 检测只能回答「有手机」但教育场景真正需要的是「是否在刷短视频/打游戏/看小说」。我们不用接入大模型或云端 OCR而是用 OpenCV 的 ROI 截图 PaddleOCR 的轻量版ch_ppocr_mobile_v2.0实现本地化识别。6.1 动态 ROI 提取从 YOLOv5 框中抠出手机屏幕区域YOLOv5 输出的phone_in_hand框往往包含手部需进一步裁剪出屏幕区域。我们用长宽比约束 颜色聚类定位屏幕# core/roi_extractor.py def extract_phone_screen(frame, box): x1, y1, x2, y2 box roi frame[y1:y2, x1:x2] h, w roi.shape[:2] if w 50 or h 50: return None # 屏幕通常是高宽比 ≈ 16:9 的矩形且颜色较均匀非手部纹理 # 先按 16:9 比例居中裁剪 target_ratio 16/9 if w/h target_ratio: new_w int(h * target_ratio) x_offset (w - new_w) // 2 roi_cropped roi[:, x_offset:x_offsetnew_w] else: new_h int(w / target_ratio) y_offset (h - new_h) // 2 roi_cropped roi[y_offset:y_offsetnew_h, :] # 转灰度 Otsu 二值化提取屏幕亮区 gray cv2.cvtColor(roi_cropped, cv2.COLOR_RGB2GRAY) _, binary cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY cv2.THRESH_OTSU) contours, _ cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if not contours: return None # 取最大轮廓假设为屏幕 largest_contour max(contours, keycv2.contourArea) x, y, w_cont, h_cont cv2.boundingRect(largest_contour) return roi_cropped[y:yh_cont, x:xw_cont] # 在 inference_worker.py 中调用 if phone_in_hand in labels: for i, label in enumerate(labels): if label phone_in_hand: box result[boxes][i] screen_roi extract_phone_screen(frame_rgb, box) if screen_roi is not None: # 送入 OCR ocr_result self.ocr.ocr(screen_roi, clsTrue) if ocr_result and ocr_result[0]: text ocr_result[0][0][1][0] # 取最高置信度文本 self.ocr_text_signal.emit(text) # 发射到 UI 显示参数说明ch_ppocr_mobile_v2.0模型仅 8.3MBCPU 推理单图 120ms满足实时性clsTrue启用文本方向分类自动旋转竖排文字ocr_result[0][0][1][0]是 PaddleOCR 返回的(bbox, (text, score))结构取text即可。6.2 OCR 结果可信度分级用置信度 关键词库过滤噪声PaddleOCR 在低光照、反光屏幕下会输出乱码如???直接显示会损害系统可信度。我们建立三级过滤置信度区间关键词匹配输出策略≥0.85任意直接显示【高置信】{text}0.6~0.85匹配「抖音」「快手」「王者荣耀」等 23 个教育敏感词显示【中置信】疑似{关键词}0.6任意不显示记录日志low_conf_ocr: {raw_text}# core/ocr_filter.py EDU_SENSITIVE_WORDS [抖音, 快手, 王者荣耀, 和平精英, 原神, 崩坏, B站, 小红书, 微博, 知乎] def filter_ocr_result(text: str, score: float) - Optional[str]: if score 0.85: return f【高置信】{text} elif 0.6 score 0.85: for word in EDU_SENSITIVE_WORDS: if word in text or text in word or Levenshtein.ratio(text, word) 0.7: return f【中置信】疑似{word} return None # 丢弃低置信结果教训第一次部署时没加过滤OCR 把学生手表表盘识别成「微信支付」教师当场质疑系统可靠性。现在所有 OCR 结果必过此关宁可不报也不误报——这是教育场景的底线。我坚持在每所学校部署前用真实课堂录像非实验室环境跑满 2 小时压力测试检查内存泄漏PyQt5 的QPixmap不释放会吃光 16GB RAM、验证多摄像头切换稳定性USB 3.0 摄像头热插拔后自动重连、确认预警日志能导出为 Excel教务处要的不是截图是带时间戳的 CSV。这套流程跑下来交付成功率从 40% 提升到 92%。希望帮到你。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

◈

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

◐

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

▲

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。