
简介这是一套面向教育技术开发者与Python学习者的智慧教室综合实践源码围绕课堂专注度分析、考试作弊检测与动态点名三大场景展开适合具备一定Python基础、希望将计算机视觉与自然语言处理落地到教育场景的开发者参考。压缩包共218个文件约17.04MB以88个py源码与66个pyc编译文件为主体辅以14张jpg与3张png图像素材、9个ui界面文件、8个ico图标及若干xml、md说明文档另含cu与hpp等GPU加速相关文件结构完整便于按模块研读。项目整合OpenCV图像处理、TensorFlow或PyTorch模型训练、nltk文本预处理与face_recognition人脸识别等技术覆盖从数据采集、模型推理到界面交互的完整链路。目前已有390人学习读者可借此理解复杂教育系统的设计思路积累视觉与文本算法在真实场景中的工程化经验。1. 智慧教室这套源码真正难的不是算法而是工程缝合智慧教室这个词听起来像是一个算法问题实际上手做过的人都知道它更像是一个多模块的工程缝合问题。标题里提到的群体课堂专注度分析、考试作弊检测、动态点名这三件事单拎出来都不算前沿研究但要把它们塞进同一套 Python 源码里跑通还要保证摄像头、模型推理、数据库、前端界面之间不打架这才是真正卡人的地方。我见过太多人拿到一份智慧教室源码跑起来发现专注度模型是单独 demo作弊检测是另一个脚本点名又是第三份代码三者之间连数据格式都不统一。这套源码要解决的问题很具体一个教室场景下用普通摄像头或 RTSP 流实时判断学生是否专注、有没有作弊动作、谁在座位上。适合的人群是做过 Python 基础、想往教育信息化或视觉落地方向走的开发者也适合课程设计需要完整案例的学生。但如果你指望开箱即用、零配置跑通那大概率会翻车因为这类项目对环境和参数极其敏感。2. 群体专注度分析从人脸检测到注意力评分的完整链路2.1 为什么群体专注度不能直接套用单人模型单人专注度模型通常输入一张正脸图输出一个分数逻辑简单。但群体场景下摄像头拍到的是几十张大小不一、角度各异的人脸还有遮挡、低头、侧脸。直接对每个人脸跑单人模型会遇到两个问题一是检测框抖动导致分数跳变二是不同位置的人脸尺度差异大模型置信度不可比。常见做法是先做人脸检测再对每个人脸做头部姿态估计用偏航角、俯仰角、滚动角三个角度加权出一个注意力分数。这套源码里我一般会保留一个可调的角度阈值表因为不同教室摄像头安装高度不同固定阈值必然失效。import cv2 import numpy as np # 头部姿态角度到注意力分数的映射角度越小越专注 def pose_to_score(yaw, pitch, roll): # 偏航角超过30度基本可以认为在看别处 yaw_score max(0, 1 - abs(yaw) / 30.0) pitch_score max(0, 1 - abs(pitch) / 25.0) roll_score max(0, 1 - abs(roll) / 20.0) # 加权偏航角权重最高因为左右转头最影响听课 return 0.5 * yaw_score 0.3 * pitch_score 0.2 * roll_score # 对检测到的每张人脸计算分数 def batch_attention(faces, landmarks): scores [] for face, lm in zip(faces, landmarks): yaw, pitch, roll estimate_head_pose(lm) scores.append(pose_to_score(yaw, pitch, roll)) return np.mean(scores) if scores else 0.0这段代码的关键在pose_to_score的权重分配。偏航角给 0.5 是因为学生左右转头通常意味着注意力转移俯仰角给 0.3 是因为低头可能是看书也可能是玩手机需要结合其他信号滚动角给 0.2 是因为歪头更多是疲劳而非走神。batch_attention返回的是全班的平均分但实际使用时我建议同时保留每个学生的分数列表方便后续做个体追踪。参数方面yaw的阈值 30 度不是绝对的如果摄像头装在教室正前方可以放宽到 35 度如果装在侧面可能要收紧到 25 度。这个值需要根据实际画面调没有万能参数。2.2 用 OpenCV 和 ONNX 把专注度推理压到实时群体专注度最大的工程挑战是速度。一个 50 人的教室如果每帧对每个人脸跑一次完整模型普通 CPU 根本扛不住。这套源码里我一般会做三件事第一人脸检测用轻量级模型比如 YuNet 或 SCRFD 的小版本第二头部姿态估计用 ONNX Runtime 做推理开启 CPU 多线程第三不是每帧都跑全部人脸而是隔帧采样用跟踪算法补中间帧。import onnxruntime as ort import cv2 # 加载 ONNX 模型开启多线程 session ort.InferenceSession( head_pose.onnx, providers[CPUExecutionProvider], sess_optionsort.SessionOptions() ) session.set_providers([CPUExecutionProvider]) # 隔帧策略每3帧做一次完整检测中间帧用光流跟踪 frame_count 0 tracked_faces [] def process_frame(frame): global frame_count, tracked_faces frame_count 1 if frame_count % 3 0: # 完整检测 faces detector.detect(frame) tracked_faces faces else: # 用光流更新位置不重新检测 tracked_faces update_by_optical_flow(frame, tracked_faces) # 对跟踪到的人脸做姿态估计 for face in tracked_faces: crop crop_face(frame, face) blob preprocess(crop) outputs session.run(None, {input: blob}) yaw, pitch, roll parse_outputs(outputs) # 后续评分逻辑frame_count % 3这个数字是速度和精度的折中。如果教室人少、CPU 强可以改成% 2甚至每帧都检测如果人多、机器弱改成% 5也能接受但跟踪误差会累积。update_by_optical_flow我一般用 Lucas-Kanade 稀疏光流对每个人脸框内的特征点做跟踪比重新检测快一个数量级。注意ONNX Runtime 的providers如果写成[CUDAExecutionProvider, CPUExecutionProvider]在有 GPU 的机器上会自动优先用 GPU但需要装对应的 CUDA 版本。很多人在这一步翻车是因为 CUDA 版本和 onnxruntime-gpu 版本不匹配报错信息又很隐晦。我一般建议先用 CPU 跑通再换 GPU。3. 考试作弊检测动作识别和规则引擎怎么配合3.1 作弊检测不是分类问题是时序规则问题很多人一上来就想训一个二分类模型输入一帧画面输出“作弊/不作弊”。这个思路在考试场景下基本不可行因为作弊是一个过程不是单帧状态。低头看一眼小抄、转头看邻座、手伸到桌下这些动作单独看都可能是正常行为只有结合时间窗口和频率才能判断。这套源码里我一般用“动作检测 规则引擎”两层结构。第一层用轻量级姿态估计或手部检测输出每帧的关键点第二层用滑动窗口统计异常动作的频率和持续时间超过阈值才触发告警。from collections import deque # 滑动窗口保存最近5秒的动作记录 window deque(maxlen150) # 假设30fps5秒是150帧 def check_cheat(pose_keypoints, hand_bbox): # 判断低头鼻子y坐标低于肩膀y坐标一定比例 nose_y pose_keypoints[nose][1] shoulder_y (pose_keypoints[left_shoulder][1] pose_keypoints[right_shoulder][1]) / 2 head_down nose_y shoulder_y * 1.15 # 判断手在桌下手部框中心低于桌面线 hand_center_y (hand_bbox[1] hand_bbox[3]) / 2 hand_below_desk hand_center_y desk_line_y # 判断转头左右耳x坐标差值异常 ear_dist abs(pose_keypoints[left_ear][0] - pose_keypoints[right_ear][0]) head_turn ear_dist 20 # 正常正脸耳距较大侧脸会变小 window.append({ head_down: head_down, hand_below: hand_below_desk, head_turn: head_turn }) # 规则最近5秒内低头超过3秒或手在桌下超过2秒或转头超过2秒 if len(window) 30: return False head_down_count sum(1 for w in window if w[head_down]) hand_below_count sum(1 for w in window if w[hand_below]) head_turn_count sum(1 for w in window if w[head_turn]) if head_down_count 90 or hand_below_count 60 or head_turn_count 60: return True return False这段代码的核心是window的长度和三个计数阈值。maxlen150对应 5 秒是因为考试作弊动作通常不会只持续一两秒。head_down_count 90意味着 5 秒里有 3 秒在低头这个阈值可以根据考试严格程度调整。hand_below_count 60是 2 秒因为手伸到桌下拿东西通常很快但如果是反复动作累计时间会上去。desk_line_y这个桌面线需要根据摄像头画面手动标定没有自动方法。我一般会在系统初始化时让用户画一条线存到配置里。这个参数如果设错要么误报频繁要么漏报严重。3.2 误报控制为什么你的作弊检测总在报警作弊检测最怕的不是漏报而是误报。一旦误报率高老师和学生都会失去信任系统就废了。我踩过的坑里误报主要来自三个地方一是学生正常低头写字被判定为看小抄二是手放在腿上被判定为桌下动作三是转头看黑板被判定为看邻座。解决思路是加“上下文过滤”。比如低头写字时手通常在桌面上且有规律移动而看小抄时手可能不动或快速翻动。转头看黑板时头部姿态是朝向黑板的而看邻座是朝向侧方。这些上下文信号需要额外检测但能大幅降低误报。def context_filter(pose_keypoints, hand_bbox, prev_hand_bbox): # 如果手在桌面上且移动有规律认为是写字 hand_on_desk hand_bbox[3] desk_line_y if hand_on_desk: # 计算手部移动速度 dx hand_bbox[0] - prev_hand_bbox[0] dy hand_bbox[1] - prev_hand_bbox[1] speed (dx**2 dy**2) ** 0.5 # 写字时手速通常在2-10像素/帧之间 if 2 speed 10: return writing # 如果头部朝向黑板区域认为是看黑板 nose_x pose_keypoints[nose][0] if nose_x blackboard_center_x 100 and nose_x blackboard_center_x - 100: return looking_board return unknowncontext_filter返回writing或looking_board时即使触发了作弊规则也不报警。blackboard_center_x需要根据画面标定100是容差范围。这个过滤逻辑不是万能的但能把误报率降一半以上。提示作弊检测的阈值不要一次调死建议先跑一周只记录不报警看统计数据再定阈值。4. 动态点名人脸识别和随机策略的工程实现4.1 动态点名的核心不是识别率是响应速度动态点名听起来简单不就是随机选人然后人脸识别确认吗但实际做起来识别率反而不是最大问题响应速度才是。如果老师点一个名字系统要 3 秒才能确认课堂节奏就断了。这套源码里我一般把点名拆成两步第一步从学生库随机抽人第二步用轻量级人脸模型做 1:1 比对而不是 1:N 搜索。1:N 搜索是拿一张脸和库里所有人比找最像的。1:1 比对是拿抽到的学生照片和当前画面里的人脸比确认是不是本人。后者快得多因为只需要比一次。import random import numpy as np # 学生库学号到人脸特征的映射 student_db { 2024001: np.array([...]), # 128维特征向量 2024002: np.array([...]), } def dynamic_roll_call(frame, detector, recognizer): # 随机抽一个学生 student_id random.choice(list(student_db.keys())) target_feature student_db[student_id] # 检测当前画面所有人脸 faces detector.detect(frame) if not faces: return student_id, False, 画面中无人脸 # 对每张人脸提取特征和抽到的学生比对 for face in faces: crop crop_face(frame, face) feature recognizer.extract(crop) # 余弦相似度 similarity np.dot(feature, target_feature) / ( np.linalg.norm(feature) * np.linalg.norm(target_feature) ) if similarity 0.6: # 阈值可调 return student_id, True, f确认相似度{similarity:.2f} return student_id, False, 未找到匹配人脸similarity 0.6这个阈值是余弦相似度的常用值但不同模型不一样。ArcFace 类模型通常 0.5 以上就算匹配FaceNet 可能要 0.7。这个值需要根据实际模型调调太低会认错人调太高会认不出。random.choice是均匀随机但实际课堂可能希望多抽不常回答问题的学生或者避免连续抽同一个人。我一般会加一个权重表记录每个学生被抽中的次数次数少的权重高。4.2 点名结果怎么和专注度、作弊数据打通动态点名的结果不应该孤立存在。一个学生被点名时是否在座位上、专注度分数多少、有没有作弊告警这些数据合在一起才有价值。这套源码里我一般用 SQLite 存三张表学生表、点名记录表、行为记录表通过学号关联。CREATE TABLE students ( student_id TEXT PRIMARY KEY, name TEXT, face_feature BLOB ); CREATE TABLE roll_call ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT, timestamp DATETIME, present BOOLEAN, similarity REAL ); CREATE TABLE behavior ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT, timestamp DATETIME, attention_score REAL, cheat_flag BOOLEAN );查询时用 JOIN 就能拿到一个学生的完整画像。比如查某个学生最近一次点名时的专注度SELECT r.student_id, r.present, b.attention_score, b.cheat_flag FROM roll_call r LEFT JOIN behavior b ON r.student_id b.student_id WHERE r.student_id 2024001 ORDER BY r.timestamp DESC LIMIT 1;LEFT JOIN是因为行为记录可能缺失但点名记录一定有。ORDER BY ... LIMIT 1取最近一次。这个查询在 SQLite 上毫秒级返回不影响实时性。注意人脸特征存 BLOB 时要注意字节序不同库读出来可能不一致建议统一用 float32 小端。5. 避坑与排查这套源码最容易翻车的五个地方5.1 摄像头分辨率设太高导致帧率崩盘现象系统跑起来画面卡顿专注度分数几秒才更新一次。原因摄像头默认 1080p 甚至 4K每帧推理时间过长。解决把采集分辨率降到 640x480 或 720p专注度检测不需要高清。如果必须高清用双流高清流只录像低清流做推理。5.2 ONNX 模型输入尺寸和预处理不匹配现象推理结果全是乱码或者置信度极低。原因模型训练时输入是 112x112预处理却用了 224x224或者归一化参数不对。解决用 Netron 打开 ONNX 模型看输入层 shape预处理严格对齐。归一化通常是(pixel - 127.5) / 128但不同模型有差异。5.3 人脸库特征没做归一化导致比对失败现象明明是同一个人相似度却低于 0.3。原因提取特征后没有做 L2 归一化向量长度不一致。解决在存库和比对前都做feature feature / np.linalg.norm(feature)保证余弦相似度计算正确。5.4 多线程下 OpenCV 和 ONNX Runtime 抢资源现象程序跑几分钟后卡死或崩溃。原因OpenCV 内部也有线程和 ONNX Runtime 的线程池冲突。解决在导入 cv2 后设置cv2.setNumThreads(1)把线程控制权交给 ONNX Runtime。或者用ort.SessionOptions()限制 intra_op_num_threads。5.5 数据库写入频繁导致 IO 瓶颈现象行为记录每帧都写SQLite 文件迅速膨胀查询变慢。原因没有做批量写入和定期清理。解决行为记录每 5 秒写一次用事务批量提交历史数据超过 30 天自动归档或删除。PRAGMA journal_modeWAL也能提升并发写入性能。6. 把三个模块串成一条流水线我的调试习惯这套源码真正跑顺之后我发现最有效的调试方式不是盯着代码看而是把三个模块的输出打到同一个日志里按时间戳对齐。专注度每帧输出一个分数作弊检测每 5 秒输出一次状态点名是事件触发。三者时间尺度不同但放在一起看就能发现很多单独看发现不了的问题。比如有一次我发现某个学生专注度突然掉到 0.2同时作弊检测触发了低头告警但点名显示他在座位上。查日志发现是摄像头自动曝光调整导致画面变暗人脸检测框偏移姿态估计出错。这种问题只看一个模块永远找不到原因。我一般会加一个调试开关打开后把每帧的检测框、姿态角度、专注度分数、作弊状态画在画面上输出成视频。跑一天下来哪些时段误报多、哪些位置检测不到一目了然。这个习惯帮我省了无数后悔药。def debug_draw(frame, faces, scores, cheat_flags): for face, score, cheat in zip(faces, scores, cheat_flags): x1, y1, x2, y2 face color (0, 255, 0) if not cheat else (0, 0, 255) cv2.rectangle(frame, (x1, y1), (x2, y2), color, 2) cv2.putText(frame, f{score:.2f}, (x1, y1-10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, color, 1) return framecolor根据作弊标志切换绿色正常红色告警。score保留两位小数画在框上方。这个调试画面可以直接给老师看比任何报表都直观。参数调优上我习惯先固定其他模块只调一个。比如先关掉作弊检测只调专注度阈值跑一周看分布再调作弊。三个一起调永远调不明白。另外所有阈值都放配置文件不要硬编码换一个教室就要重新标定一次。这套方案值不值得做取决于你的场景是否真的需要实时性。如果只是课后分析录像用离线批处理更简单不用折腾多线程和流媒体。但如果是课堂实时反馈那这套流水线的工程复杂度是绕不开的。希望帮到你。本文还有配套的精品资源点击获取