简介:这是一份基于PyQt5与深度学习技术的智慧课堂项目源码包,面向计算机相关专业(计科、人工智能、大数据等)的在校学生、教师及企业开发者,用于线下课堂学生专注度的自动分析与评估,可直接运行也可二次开发。代码完整、功能验证通过,可作毕业设计、课程设计或初期项目立项演示。压缩包共收录218个文件,含88个Python源码及对应pyc缓存、9个Qt界面文件、图标演示图片、配置文档,并附CUDA加速源码(如GPU NMS)与模型下载,整体约17MB。目录结构清晰,设计文档、界面与模型分离,便于按模块查阅。已有151人学习下载,除完整软件系统外,还提供视频源配置、演示动图、摄像头/二维码图标素材;深度学习推理部分涉及GPU加速,适合深入理解检测与专注度分析细节,也可扩展其他课堂行为功能。
1. 课堂专注度分析系统:不是摄像头监控,而是把“走神”变成可量化的课堂观察工具
一个老师面对四十几个学生,单靠肉眼很难判断后排某个学生到底是在低头看书还是低头玩手机。智慧课堂项目里的这套基于 PyQt5 + 深度学习的线下课堂学生专注度分析系统,做的就是把摄像头画面转成一条条可统计的专注度分数:检测人脸、估计头部姿态、看眼睛开合程度,然后把“低头 0.5 秒、侧脸 20 度、闭眼 1.2 秒”这类原始信号聚合成课堂报告。它解决的不是“拍下来看回放”,而是“课上到一半就知道哪些时段全班专注度在掉,哪些学生状态异常”。适合课设、毕设、教研室项目二次开发,也适合想快速在实验室里跑通一套“视频流 → 推理 → 可视化报表”完整链路的人。
2. 从摄像头到专注度分数:先搭处理主循环再谈界面
很多拿到源码的人第一件事是打开界面文件,我建议反过来:先看推理主循环,确认每一帧图像经过哪些处理变成分数,再去折腾界面。专注度系统的核心链路是固定的:摄像头取帧 → 人脸检测 → 人脸关键点回归 → 头部姿态估计 → 眼睛开合度计算 → 专注度打分 → 落库 → 可视化。PyQt5 只是把这套链路包上一层壳。
2.1 系统模块划分与源码包里的常见文件布局
一个完整的课堂专注度分析系统,源码包里的模块划分普遍长这样:
| 文件/目录 | 职责 | 对应环节 |
|---|---|---|
| main.py / app.py | 程序入口,启动 PyQt5 主窗口 | 界面与生命周期 |
| ui/ 目录 | 主窗口、视频显示、学生列表、报表区域 | 交互展示 |
| core/detector.py | 人脸检测与关键点回归 | 感知层 |
| core/pose.py | 头部姿态估计,输出 pitch/yaw/roll | 姿态层 |
| core/focus.py | 眼睛 EAR 计算与专注度打分 | 决策层 |
| core/db.py | SQLite 读写,保存每节课的逐秒数据 | 存储层 |
| models/ 目录 | 深度学习模型权重文件 | 离线资产 |
| requirements.txt | Python 依赖清单 | 环境配置 |
这种分层在源码里最常见。设计文档一般会把每一层对应的算法、指标、输入输出格式写清楚,拿到手以后先对照这份文档把模块之间的接口找出来,比直接跑界面更省时间。注意接口命名不一定要完全照抄,但“detector 返回人脸框和关键点、pose 返回三个欧拉角、focus 返回分数”这条数据流是几乎所有方案都绕不开的。
2.2 最小推理主循环:一帧图像怎么变成一条分数记录
先不讨论界面,把这条链路用最朴素的方式跑通。下面是我最常用的组织方式,把检测、姿态、打分三个函数当成黑盒调用,先看数据怎么流转:
import cv2 import numpy as np import time # 三个核心函数:detector.detect 返回人脸框和关键点 # pose_estimation 返回头部姿态角度 pitch/yaw/roll # eye_aspect_ratio 返回眼睛开合度,focus_score 返回最终分数 cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) frame_stride = 2 # 每 2 帧做一次推理,约 15fps focus_window = [] # 用于 10 秒窗口聚合 while True: ok, frame = cap.read() if not ok: break current_frame += 1 if current_frame % frame_stride != 0: continue boxes, landmarks = detector.detect(frame) # 人脸框和 68/5 个关键点 per_face_scores = [] for box, lm in zip(boxes, landmarks): pitch, yaw, roll = pose_estimation(lm) # 3D 头部姿态 ear = eye_aspect_ratio(lm) # 双眼开合度 score = focus_score(pitch, yaw, ear) # 单帧专注度 0-100 per_face_scores.append(score) cv2.rectangle(frame, (box[0], box[1]), (box[2], box[3]), (0, 255, 0), 2) cv2.putText(frame, f"score:{score:.0f}", (box[0], box[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) # 把当前帧所有学生的平均分放入窗口 if per_face_scores: focus_window.append(np.mean(per_face_scores)) # 窗口长度约 150 帧时计算一次 10 秒聚合值 if len(focus_window) >= 150: avg_focus = np.mean(focus_window) save_to_db(course_id, student_id, round(avg_focus, 1)) focus_window.clear() cv2.imshow("classroom_focus", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break这段代码里有几个参数需要注意。frame_stride = 2是为了降低推理频率,人脸关键点模型在 CPU 上单帧推理 20-40ms,但加上检测和姿态求解之后,30fps 全量推理会把 CPU 打满,界面交互会卡;每两帧推理一次,人眼基本感知不到差异,专注度分数反而不抖动。focus_window的 150 帧是配合 frame_stride 算出来的,约等于 10 秒(15fps × 10 秒),这个窗口长度对应课堂里最小的可观察单位——一个持续 10 秒以上的低头或侧脸才值得标记为状态异常,单帧的低头判断没有统计意义。
这里特别提醒:同步循环只适合先跑通流程,实际做成软件后不能把这段代码塞进 UI 线程,第 4 章会展开讲线程方案。
2.3 专注度粒度:单帧、10 秒窗口、整节课聚合
专注度分数不能只存一个值,必须按时间粒度组织。不同角色看到的粒度不同:实时监控看单帧或 10 秒窗口,课后报告看整节课曲线。我一般分四层存:
| 粒度 | 计算方式 | 用途 |
|---|---|---|
| 单帧 | 每帧每个学生一条 score | 调试算法、观察瞬时状态 |
| 10 秒窗口 | 窗口内所有帧的平均分 | 实时折线图、警报触发 |
| 1 分钟 | 每 10 秒窗口加权平均 | 教师课中查看 |
| 整节课 | 全时段分数的均值、方差与低谷区间 | 课后报告、学生个体分析 |
数据库里通常建两张表:一张frame_scores存原始的逐秒聚合数据,包含时间戳、课程 ID、学生 ID、专注度分数;另一张course_report存整节课的统计结果,包含平均分、最低分时段、波动率。这样既能在界面上做实时折线,也能在课程结束后直接生成 Excel 报表。很多新手只存了最终报告,结果想回查某一段视频对应的分数变化时没有原始数据,只能重跑一遍,这是设计上的后悔药没留够。
3. 深度学习模型选型与专注度打分:关键点、头部姿态和眼睛开合怎么组合不翻车
拿到这个项目名称,最容易误解的地方是“深度学习模型直接输出专注度分数”。实际上端到端训练一个“上课走神分类器”需要大量课堂实拍标注数据,普通课题组很难凑齐。更可靠的落地做法是把任务拆开:用深度学习做目标检测和关键点回归,再用几何算法和规则做分数决策。这样每个模型都换得掉,分数逻辑也方便调。
3.1 模型选型:人脸检测、关键点、姿态估计怎么搭配
在桌面端专注度分析场景里,常见搭配有以下三种,我按可复现程度排序:
| 方案 | 人脸检测 | 关键点/姿态 | 特点 |
|---|---|---|---|
| A(轻量) | OpenCV DNN 人脸检测器 | 5 点关键点 + solvePnP 头部姿态 | 纯 CPU 可跑,部署简单 |
| B(常见) | RetinaFace 或 MTCNN | 68 点关键点 + 头部姿态 + EAR | 关键点丰富,眼睛检测准确 |
| C(研究向) | MediaPipe FaceMesh | 468 点网格 + 视线方向 | 特征多,但工程依赖较重 |
方案 B 在这个项目里最常出现,因为它把“深度学习”和“传统几何”结合的度最合适:RetinaFace 在课堂多人场景下对小尺寸人脸召回率明显好于 OpenCV 的 DNN 检测器,而 68 点关键点足够算眼睛开合度和头部姿态。MediaPipe 虽然点数更密集,但封装较深,换成 ONNX 模型部署时反而要多一层转换。
模型文件放在models/目录下就够用,不要放仓库根目录。另外所有检测器都有一个置信度参数,课堂场景建议人脸检测阈值设 0.5,低了会产生大量误检框,高了会漏掉后排小脸。NMS 阈值一般保持默认 0.4,不需要动。
3.2 专注度打分核心公式:EAR 与头部姿态惩罚
专注度打分不能只看一个特征。低头不一定是在睡觉,可能是记笔记;侧脸不一定是在聊天,可能是讨论。所以分数模型必须是加权惩罚制:初始 100 分,出现低头、侧脸、闭眼、玩手机等信号就扣分,最后截断到 0-100 区间。
眼睛开合度用 EAR(Eye Aspect Ratio),这是人脸关键点里最常用的一个指标,不需要训练额外模型:
import numpy as np def dist(p1, p2): return np.linalg.norm(np.array(p1) - np.array(p2)) def eye_aspect_ratio(lm): # lm 为 68 点关键点,索引对应左眼 36-41、右眼 42-47 left_ear = (dist(lm[37], lm[41]) + dist(lm[38], lm[40])) / (2 * dist(lm[36], lm[39])) right_ear = (dist(lm[43], lm[47]) + dist(lm[44], lm[46])) / (2 * dist(lm[42], lm[45])) return (left_ear + right_ear) / 2EAR 的正常值在 0.25-0.35 之间,闭眼时会掉到 0.15 以下。注意不同人脸有差异,不能把 0.2 这个阈值写死,最好在系统初始化时让每个学生正对镜头 1 秒采集基线 EAR,再用基线值乘 0.7 作为闭眼阈值。
头部姿态估计的常见做法是用 68 点中的稳定点(鼻尖、下巴、左右眼外角、左右嘴角)作为 3D 参考点,调用 OpenCV 的solvePnP求解旋转向量,再转成欧拉角:
import cv2 model_points = np.array([ [0.0, 0.0, 0.0], # 鼻尖 [0.0, -63.6, -12.5], # 下巴 [-43.3, 32.7, -26.0], # 左眼外角 [43.3, 32.7, -26.0], # 右眼外角 [-28.9, -28.9, -24.1], # 左嘴角 [28.9, -28.9, -24.1], # 右嘴角 ], dtype=np.float32) def pose_estimation(image_points, camera_matrix, dist_coeffs): success, rvec, tvec = cv2.solvePnP( model_points, image_points, camera_matrix, dist_coeffs, flags=cv2.SOLVEPNP_ITERATIVE ) # 旋转向量转欧拉角,注意不同旋转顺序结果不同 rmat, _ = cv2.Rodrigues(rvec) pitch = np.degrees(np.arctan2(rmat[2, 1], rmat[2, 2])) yaw = np.degrees(np.arctan2(rmat[0, 2], rmat[2, 2])) return pitch, yaw, 0.0这里的坑在旋转顺序。OpenCV 的solvePnP输出的是旋转向量,转成欧拉角时很多人直接抄网上的公式,结果 pitch 和 yaw 颠倒,低头被识别成摇头。我的习惯是先用手机拍自己低头和左右转脸各 5 秒,打印角度验证符号方向,再做打分。
打分函数把特征和权重集中在同一个地方,方便后期校准:
def focus_score(pitch, yaw, ear, baseline_ear=0.3, mouth_open_ratio=0.0): score = 100.0 # 低头惩罚:pitch 为负表示低头,阈值与课堂场景相关 if pitch < -15: score -= 25 elif pitch < -8: score -= 10 # 侧脸惩罚 if abs(yaw) > 20: score -= 15 elif abs(yaw) > 35: score -= 30 # 闭眼惩罚 if ear < baseline_ear * 0.6: score -= 30 elif ear < baseline_ear * 0.75: score -= 10 # 张嘴惩罚,可扩展为打哈欠检测 if mouth_open_ratio > 0.5: score -= 10 return max(0, min(100, score))这些惩罚权重不是官方标准,是我在课堂场景里调试出来的经验值:低头和闭眼是最强信号,权重最高;侧脸单独出现可能是讨论,权重低一些;张嘴作为哈欠辅助信号,只扣少量分。权重直接影响最终报告的准确度,后面第 6 章会给校准方法。
3.3 模型推理的预处理和性能参数
模型选好了,推理配置也要跟上,否则在教室电脑上跑不动。常见设置是:人脸检测输入尺寸 320 或 640,关键点模型输入 112×112;推理框架用 ONNX Runtime 比直接用 PyTorch 更省内存,CPU 下延迟和显存占用都更可控。
import onnxruntime as ort providers = ["CUDAExecutionProvider", "CPUExecutionProvider"] session = ort.InferenceSession("models/retinaface.onnx", providers=providers) session.set_intra_op_num_threads(4) # 限制 CPU 线程,避免和界面抢资源 # 关键参数:检测置信度阈值 conf_threshold = 0.5 nms_threshold = 0.4显存方面,课堂摄像头分辨率建议直接降到 1280×720,不要用 4K 原图推理;输入图像在送入检测器前还要按模型要求做 letterbox 缩放,保持宽高比而不是直接 reshape,否则人脸形状被拉伸,关键点坐标会偏移。边框坐标缩放回去时要做等比例映射,这个映射经常在多人检测时出错,代码里保留一份 letterbox 的 scale 和 offset,裁剪框和画框都用它转换。
4. 用 PyQt5 把推理流程包成桌面应用:线程分离比布局设计更早动手
系统一旦做成 PyQt5 桌面应用,最大的问题不是界面丑,而是摄像头推理把界面主线程卡死。PyQt5 的事件循环跑在 GUI 线程里,如果直接在这个线程里执行cap.read()和模型推理,鼠标点击、窗口拖动都会无响应。正确顺序是先搭线程骨架,再填充控件。
4.1 UI 布局与常用功能分区
一个能实际用于课堂的系统,主界面通常分成四个区域:左侧大画布显示摄像头画面和检测框;右侧上方显示当前班级学生列表和每个学生的实时分值;右侧下方是专注度折线图和状态日志;底部工具栏放开始、暂停、停止、导出报告按钮。
不需要上多复杂的样式,关键是把数据绑定关系理清:视频帧由推理线程产生,通过信号发给界面画布;平均分通过另一个信号发给折线图;日志通过第三个信号追加到文本框。同一份数据不要跨线程直接读,否则轻则显示不对,重则程序崩溃。
4.2 QThread 与信号槽:推理循环的标准写法
PyQt5 里最常用的做法是继承QThread,把推理循环放进run(),通过pyqtSignal把结果传回主线程。下面是一段可以直接套用的骨架:
from PyQt5.QtCore import QThread, pyqtSignal from PyQt5.QtGui import QImage import cv2 import numpy as np class InferenceThread(QThread): frame_signal = pyqtSignal(QImage) score_signal = pyqtSignal(float) log_signal = pyqtSignal(str) def __init__(self, camera_id=0, parent=None): super().__init__(parent) self.camera_id = camera_id self.running = True self.skip = 0 def run(self): cap = cv2.VideoCapture(self.camera_id) if not cap.isOpened(): self.log_signal.emit("摄像头打开失败,请检查设备索引") return while self.running: ok, frame = cap.read() if not ok: self.log_signal.emit("读取不到视频帧") break # 间隔推理,降低 CPU 占用 self.skip += 1 if self.skip % 2 == 0: self.skip = 0 boxes, scores = self.infer_once(frame) self.draw_boxes(frame, boxes, scores) avg_score = float(np.mean(scores)) if scores else 0.0 self.score_signal.emit(avg_score) # OpenCV 的 BGR 转 QImage 的 RGB rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy() self.frame_signal.emit(qimg) cap.release() def stop(self): self.running = False self.wait()这段代码里最容易被忽略的是.copy()。QImage构造时传入的是rgb.data的指针,如果原数组在帧循环里被回收,界面槽函数再去画这张图就会得到一块野数据,出现花屏或段错误。加上.copy()让 QImage 持有自己的内存,代价是每帧多一次拷贝,1280×720 分辨率下完全可接受。
槽函数连接写法:
self.infer_thread = InferenceThread(camera_id=0) self.infer_thread.frame_signal.connect(self.update_video) self.infer_thread.score_signal.connect(self.update_focus_curve) self.infer_thread.log_signal.connect(self.append_log) self.infer_thread.start()逻辑说明:score_signal发送的是当前帧所有学生平均分,主线程收到后追加到折线图数据里。注意不要在update_focus_curve里做耗时操作,比如写文件或大量字符串拼接,否则信号排队导致视频卡顿。写数据库的活放在推理线程里做,或者用队列丢给另一个线程。
4.3 实时折线与数据导出:PyQtGraph 比 matplotlib 更合适
专注度曲线是课堂系统的刚需,但用 matplotlib 嵌入 PyQt5 会有点重:首次导入慢、刷新效率一般、跟 PyQt5 的版本兼容偶尔会出问题。更常见的选择是 PyQtGraph,纯 Qt 绘图,刷新快,代码也更短:
import pyqtgraph as pg self.curve_widget = pg.PlotWidget() self.curve_widget.setLabel("left", "专注度", units="分") self.curve_widget.setLabel("bottom", "时间", units="秒") self.curve = self.curve_widget.plot(pen="y") def update_focus_curve(self, avg_score): self.time_axis += 1 self.history.append(avg_score) self.curve.setData(list(range(self.time_axis)), self.history)数据导出部分,常见做法是课程结束后把整节课的逐秒数据从 SQLite 里 query 出来,写成 CSV 或 Excel。导出的内容不要只给数字列,最好带三列:时间戳、专注度分数、是否处于低谷区间。这样教师在复盘时能直接看到第 12 分钟全班分数显著下滑,对应到课堂环节去分析原因。
5. 常见问题与避坑清单:从装 PyQt5 到跑通推理的五个典型翻车点
这条技术路线我跑过不止一次,下面五个问题基本是必遇到的,按出现的概率排序,每一条都按现象、原因、解决三部分记录。
5.1 PyQt5 安装失败或 labelme 无法安装 pyqt5
现象:pip install pyqt5报错,或者安装 labelme 时提示找不到 PyQt5;有时候安装成功但import PyQt5直接报No module named。
原因:多数情况下是 Python 版本和 PyQt5 预编译包不匹配。PyQt5 对 Python 3.12 的官方支持滞后,而 labelme 依赖的旧版 PyQt5 在部分镜像源里没有对应版本。另外,如果你用的是虚拟环境,pip 默认源也可能把它装到错误的位置。
解决:固定版本安装,不要装最新版。常见可用组合是 Python 3.10 + PyQt5 5.15.9:
pip install PyQt5==5.15.9 PyQt5-sip==12.13.0 pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5==5.15.9如果项目里还要用 labelme 做数据集标注,建议先装 labelme 再装 PyQt5,或者干脆用 conda 创建 Python 3.8 环境,conda 会把 Qt 依赖一起解决。
5.2 界面启动后卡死,摄像头预览黑屏不动
现象:点下开始按钮,窗口立刻无响应,标题栏出现“未响应”;或者摄像头画面一直是一片黑,只有关闭窗口才会退出。
原因:这是把推理逻辑直接写在了按钮 clicked 槽函数或__init__里。cap.read()和模型推理都是阻塞操作,主线程事件循环被占住,界面当然不会刷新;摄像头黑屏通常是打开了设备但没有持续读取帧,或者camera_id不对导致读取失败后没有处理。
解决:按第 4 章的QThread方案隔离推理线程。槽函数里只负责self.thread.start()和初始化界面状态。摄像头接枚举:笔记本内置摄像头通常是 0,外接 USB 摄像头可能是 1,但驱动不同顺序也会反,做一个下拉框让用户手动选择最省事。每次启动前先cap.isOpened()判断,失败时给日志而不是默默黑屏。
5.3 画面颜色发蓝,人脸呈青色
现象:摄像头画面上整个人脸发蓝,肤色变成青灰色,和 OpenCV 单独显示时颜色不一样。
原因:OpenCV 读进来是 BGR 顺序,PyQt5 的QImage默认按 RGB 解析。同一个字节流,B 和 R 通道对调后肤色会严重失真。少数情况下是 QImage 的bytesPerLine参数填错,图像出现斜切。
解决:先cv2.cvtColor(frame, cv2.COLOR_BGR2RGB),再构造QImage;bytesPerLine要填ch * w而不是w,因为三通道图像每行字节数是宽度的 3 倍。如果处理后的图像仍然偏色,检查 QImage 构造时传的是copy()之后的rgb.data,确保缓冲长度正确。
5.4 模型文件加载失败,Windows 路径带中文或空格
现象:源码能在自己电脑跑通,换到教室电脑上报FileNotFoundError或 onnxruntime 弹窗找不到路径;项目路径里有“智慧课堂”四个字时特别容易出问题。
原因:onnxruntime、OpenCV 在 Windows 上对中文和空格路径的支持不稳定,部分底层 C++ 库使用窄字符 API 解析路径,遇到多字节字符直接失败。
解决:模型目录用英文命名,比如models/retinaface.onnx,整个项目路径保持纯英文。如果老师坚持用中文路径,可以在启动时把模型复制到系统临时目录再加载,或者用sys._MEIPASS逻辑兼容 PyInstaller 打包后的临时解压路径。源码里所有模型路径用os.path.join拼,不要手写死“C:\智慧课堂...”。
5.5 显存不足或 cuda 相关 DLL 加载失败
现象:启动推理时提示CUDA out of memory,或者直接报ImportError: DLL load failed。低显存笔记本上,PyTorch 版模型只要 batch 大于 2 就崩。
原因:课堂摄像头是连续视频流,如果每帧都扔给 GPU,显存峰值会累加;很多预编译的 onnxruntime/opencv 需要对应的 CUDA 和 cuDNN 版本,版本不匹配就报 DLL 错。
解决:推理 batch 强制设 1,不要缓存多帧批量推理;换用 ONNX Runtime 的 CPU 执行提供方,或者在有 NVIDIA 卡时只启用CUDAExecutionProvider并设arena_extend_strategy限制显存预留。如果模型本身是 MobileNet 这类轻量网络,CPU 4 线程即可跑到 15fps,不需要强上 GPU。最后一招是看模型导出时是否带 fp16 版本,半精度能省一半显存,但要注意 CPU 上 fp16 不一定更快。
5.6 中文显示成方块乱码
现象:界面上所有中文标签都变成一个个小方块,或者putText在画面里画出的中文全是乱码。
原因:Qt 的默认字体族可能没有覆盖中文字符,OpenCV 的putText只支持内置英文字体,不支持中文。
解决:PyQt5 界面在程序入口设置全局字体:
from PyQt5.QtGui import QFont app.setFont(QFont("Microsoft YaHei", 10))Windows 用微软雅黑,Linux 用文泉驿或 Noto Sans CJK。OpenCV 画中文不要用putText,把画面转成QImage后用 Qt 的drawText画,或者先把中文换成拼音/英文缩写显示在检测框上。否则调试一天也只会得到一屏乱码。
6. 专注度分数校准:让课堂报告不只是一堆数字
源码自带的权重参数跑通容易,但换一个教室、换一个摄像头高度,分数就可能偏离实际情况。我习惯每到一个新环境先做一次快速校准:找一段 20 分钟的真实课堂录像,挑 5 个片段,每段 2 分钟,请助教或教研员给这 5 段的人工专注度打分,范围 0-100;让系统跑同一段视频,输出同样 5 段的分值,然后做相关性分析:
from scipy.stats import pearsonr system_scores = [78.5, 66.2, 45.8, 82.0, 51.3] manual_scores = [85, 60, 40, 80, 55] r, p = pearsonr(system_scores, manual_scores) print(f"Pearson r = {r:.3f}") if r < 0.5: print("权重不合适:优先调低头阈值和闭眼阈值") elif r < 0.7: print("趋势可行,注意摄像头角度是否有遮挡") else: print("可以用于日常统计")如果相关系数低于 0.5,说明现有打分逻辑没有抓住这个场景里的主要分心信号。优先检查两个参数:低头阈值pitch < -15是否太严,后排学生俯身写字时可能误判为走神;闭眼阈值是否基于每个学生的基线 EAR 校准过,有些学生天生眼睛小,固定阈值会一直误报。
我最常用的手段是把权重表做成可配置的 JSON 文件,放到设计文档旁边:
{ "pitch_drop": 8, "pitch_threshold": -12, "yaw_threshold": 20, "ear_close_threshold": 0.65, "ear_close_penalty": 30 }这样不用改代码,现场调参后重新跑一遍验证样本就能对比效果。最后再按班里中位数做一次归一化:因为活跃课堂和自习课的专注度基线天然不同,用原始分比较跨班级意义不大,转成相对基线的偏差更符合教学观察习惯。每节课后我会顺手导出“专注度低谷时间段”,和任课教师的随堂笔记对照,几次对照下来就能看出系统分数和真实课堂状态是否对得上。这个习惯帮我避开了很多“数字好看但不反映事实”的假项目,也希望帮到你。
本文还有配套的精品资源,点击获取