简介:这份资源是一套基于Python的在线课堂考勤系统完整项目源码,面向具备一定Python基础、希望将深度学习落地到教育场景的开发者与学习者。项目以卷积神经网络为核心,结合OpenCV与TensorFlow或PyTorch完成人脸特征提取与识别,并借助Flask或Django搭建可视化界面,覆盖数据收集、预处理、模型构建、训练优化到测试部署的完整流程,可用于课程设计、毕业设计或技术练手。压缩包共98个文件,约409.43MB,包含22个py源码、28张jpg样本图、15个xml配置、6张png截图、5个dat数据文件及3个pt模型权重等,目录按功能模块划分,便于对照学习。目前已有1461人学习下载,读者可从中获取可运行的人脸考勤实现、CNN训练脚本与Web端管理逻辑,快速理解从模型到系统的工程化落地思路。
1. 在线课堂考勤系统:为什么摄像头点名总翻车,Python 能不能救
线上课最让老师崩溃的不是讲课,是点名。五十个学生开着摄像头,一半人镜头对着天花板,另一半挂着静态头像,你喊三遍名字没人应,最后只能靠"扣平时分"威胁。基于 Python 的在线课堂考勤系统,要解决的就是这件事:把"谁在、谁不在、谁在假装在"从人工盯屏变成程序自动判定。它适合两类人——一类是正在做课程设计或毕业设计的学生,需要一套能跑起来、能演示、能写进论文的系统;另一类是培训机构或高校教务的技术负责人,想给现有直播平台加一层考勤能力。核心链路其实不复杂:摄像头或截图采集 → 人脸检测与识别 → 活体/在场判定 → 写入考勤记录 → 导出报表。Python 在这条链路上的优势是生态成熟,OpenCV、face_recognition、Flask、pandas 全都能直接拼起来,不需要从零造轮子。但我要先说清楚:这套东西的难点从来不在"识别人脸",而在"怎么定义出勤"和"怎么防止作弊",后面几章会把这两件事拆开讲透。
2. 技术选型:人脸识别方案怎么挑,别一上来就上深度学习
2.1 三种主流方案的能力边界对比
做考勤系统,第一步是决定用什么做人脸识别。市面上常见做法有三类,能力差距很大,选错了后面全是坑。
| 方案 | 依赖库 | 识别精度 | 硬件要求 | 适合场景 |
|---|---|---|---|---|
| Haar 级联 + LBPH | opencv-contrib-python | 中等,受光照影响大 | CPU 即可 | 课程设计、小班演示 |
| face_recognition(dlib) | face_recognition、dlib | 较高,128 维特征 | CPU 可跑,慢 | 中小规模真实考勤 |
| 深度学习模型(ArcFace 等) | insightface、onnxruntime | 高,抗角度光照 | 建议有 GPU | 生产级、大班额 |
我一般会建议:如果只是做课设或内部小工具,直接用 face_recognition,它在精度和上手难度之间平衡得最好。dlib 的 128 维特征向量做欧氏距离比对,阈值设在 0.45 到 0.5 之间比较稳。如果班级超过 100 人、或者要求毫秒级响应,再考虑 insightface 这类方案,但部署复杂度会明显上升。
提示:不要用 Haar 级联做人脸识别,它只能做检测(找到脸的位置),不能做识别(判断是谁)。很多新手把检测当识别,结果系统只能告诉你"画面里有张脸",说不出是谁。
2.2 环境搭建:从 python 安装到依赖库落地
环境这一步是新手最容易卡住的地方。python 安装教程、vscode python 环境配置、pycharm 配置 python 环境这些搜索词热度一直很高,说明大量人倒在了第一步。我推荐用 conda 建独立环境,避免和系统 Python 打架。
# 创建独立环境,指定 Python 3.9(dlib 对 3.10+ 兼容性偶有问题) conda create -n attendance python=3.9 -y conda activate attendance # 安装核心依赖 pip install opencv-python==4.8.1.78 pip install face_recognition==1.3.0 pip install flask==3.0.0 pip install pandas==2.1.3 pip install openpyxl==3.1.2这里有几个参数值得说明。opencv-python 选 4.8 版本是因为它对 VideoCapture 的摄像头兼容性比较稳定,太新的版本在某些 Windows 驱动上会报错。face_recognition 依赖 dlib,而 dlib 在 Windows 上需要 CMake 和 Visual Studio Build Tools,如果 pip 安装报错,常见做法是先装 cmake 再重试:
pip install cmake pip install dlib==19.24.2 pip install face_recognitionLinux 系统安装 python 环境相对省心,但要注意 dlib 编译需要libboost-python-dev和cmake,Ubuntu 下先apt install build-essential cmake libopenblas-dev libx11-dev再 pip 安装。装完之后跑一句验证:
import face_recognition import cv2 print("opencv:", cv2.__version__) print("face_recognition ok")如果这行能打印出来,环境就算通了。装不上的话,八成是 dlib 编译失败,去看报错里有没有 "CMake" 或 "boost" 字样,对症补依赖。
3. 核心实现:从人脸注册到考勤判定的完整链路
3.1 人脸库构建:注册阶段怎么存特征
考勤系统的第一步是"注册",也就是把每个学生的人脸特征存下来。我一般会为每个学生采集 3 到 5 张不同角度的照片,提取 128 维特征后取平均,这样比单张照片鲁棒得多。
import face_recognition import numpy as np import os import pickle def register_student(student_id, image_paths): """采集多张照片,提取平均特征向量""" encodings = [] for path in image_paths: image = face_recognition.load_image_file(path) # 只取画面中最大的一张脸,避免背景人脸干扰 locations = face_recognition.face_locations(image, model="hog") if not locations: print(f"跳过 {path}:未检测到人脸") continue # 按面积排序,取最大 locations.sort(key=lambda loc: (loc[2]-loc[0])*(loc[1]-loc[3]), reverse=True) encoding = face_recognition.face_encodings(image, [locations[0]])[0] encodings.append(encoding) if not encodings: raise ValueError(f"学生 {student_id} 没有可用的人脸样本") # 取平均特征,比单张更稳定 avg_encoding = np.mean(encodings, axis=0) return student_id, avg_encoding def build_database(students_dir, output_file="face_db.pkl"): """遍历目录,构建人脸特征库""" database = {} for student_id in os.listdir(students_dir): student_dir = os.path.join(students_dir, student_id) if not os.path.isdir(student_dir): continue image_paths = [os.path.join(student_dir, f) for f in os.listdir(student_dir) if f.lower().endswith(('.jpg', '.png', '.jpeg'))] try: sid, encoding = register_student(student_id, image_paths) database[sid] = encoding print(f"注册成功:{sid},样本数 {len(image_paths)}") except ValueError as e: print(f"注册失败:{e}") with open(output_file, "wb") as f: pickle.dump(database, f) print(f"人脸库已保存,共 {len(database)} 人") return database if __name__ == "__main__": build_database("./students")这段代码的逻辑是:每个学生一个文件夹,文件夹名就是学号,里面放多张照片。face_locations用 hog 模型,速度快,CPU 就能跑;如果照片质量差、侧脸多,可以换成model="cnn",但需要 GPU 才跑得动。取最大人脸是为了防止合照或背景里其他人脸被误当成目标。最后用 pickle 序列化存盘,下次直接加载,不用每次重新提取。
参数上,face_encodings返回的是 128 维 numpy 数组,np.mean做平均后维度不变。如果某个学生只成功提取到 1 张,平均就等于它本身,也能用,但鲁棒性差,建议至少 3 张。
3.2 实时考勤:一帧画面怎么判定谁到了
注册完就该做实时判定了。核心逻辑是:抓一帧画面 → 检测所有人脸 → 提取特征 → 和库里逐个比对 → 距离小于阈值就算匹配。
import face_recognition import cv2 import pickle import numpy as np from datetime import datetime # 加载人脸库 with open("face_db.pkl", "rb") as f: face_db = pickle.load(f) known_ids = list(face_db.keys()) known_encodings = list(face_db.values()) # 考勤记录:学号 -> 首次识别时间 attendance = {} # 匹配阈值,越小越严格 TOLERANCE = 0.48 def mark_attendance(frame): """处理单帧,返回本帧识别到的学号列表""" # 缩小到 1/4 加速处理,坐标记得乘回去 small = cv2.resize(frame, (0, 0), fx=0.25, fy=0.25) rgb_small = cv2.cvtColor(small, cv2.COLOR_BGR2RGB) locations = face_recognition.face_locations(rgb_small, model="hog") encodings = face_recognition.face_encodings(rgb_small, locations) recognized = [] for encoding in encodings: # 和库里所有人比对,返回距离数组 distances = face_recognition.face_distance(known_encodings, encoding) best_idx = np.argmin(distances) if distances[best_idx] < TOLERANCE: sid = known_ids[best_idx] recognized.append(sid) if sid not in attendance: attendance[sid] = datetime.now().strftime("%H:%M:%S") print(f"[考勤] {sid} 已签到 {attendance[sid]}") return recognized # 打开摄像头 cap = cv2.VideoCapture(0) if not cap.isOpened(): raise RuntimeError("摄像头打开失败,检查设备索引或驱动") frame_count = 0 while True: ret, frame = cap.read() if not ret: break # 每 5 帧处理一次,降低 CPU 占用 frame_count += 1 if frame_count % 5 == 0: mark_attendance(frame) cv2.imshow("Attendance", frame) if cv2.waitKey(1) & 0xFF == ord('q'): break cap.release() cv2.destroyAllWindows()关键参数说明:TOLERANCE=0.48是经验值,调低到 0.4 会漏识(本人被拒),调到 0.55 会误识(把别人认成你)。实际部署时建议先用一批测试照片跑一遍,看距离分布再定。fx=0.25的缩放是为了提速,代价是小脸可能检测不到,如果教室画面里人离得远,改成 0.5。每 5 帧处理一次是权衡,30fps 的摄像头相当于每秒判定 6 次,足够考勤用,CPU 也不会跑满。
face_distance返回的是欧氏距离,不是相似度,所以是"越小越像"。很多人第一次写会反过来判断,导致全员匹配失败,这是个高频翻车点。
3.3 考勤数据落库与导出:pandas 写入 Excel
识别出来只是第一步,老师要的是能交差的考勤表。用 pandas 把结果写成 Excel,顺便算个出勤率。
import pandas as pd from datetime import datetime def export_attendance(attendance, all_students, course_name="在线课堂"): """生成考勤报表,包含出勤状态和签到时间""" today = datetime.now().strftime("%Y-%m-%d") rows = [] for sid in all_students: if sid in attendance: rows.append({ "学号": sid, "姓名": "", # 可从学生信息表关联 "状态": "出勤", "签到时间": attendance[sid] }) else: rows.append({ "学号": sid, "姓名": "", "状态": "缺勤", "签到时间": "" }) df = pd.DataFrame(rows) total = len(df) present = len(df[df["状态"] == "出勤"]) summary = pd.DataFrame({ "课程": [course_name], "日期": [today], "应到": [total], "实到": [present], "出勤率": [f"{present/total*100:.1f}%"] }) filename = f"考勤_{course_name}_{today}.xlsx" with pd.ExcelWriter(filename, engine="openpyxl") as writer: df.to_excel(writer, sheet_name="明细", index=False) summary.to_excel(writer, sheet_name="汇总", index=False) print(f"报表已生成:{filename}") return filename # 使用示例 all_students = list(face_db.keys()) export_attendance(attendance, all_students, "Python程序设计")这里all_students必须来自完整的学生名单,而不是人脸库的 key,否则缺勤的人根本不会出现在表里——这是逻辑上的关键区别。ExcelWriter一次写两个 sheet,明细和汇总分开,老师一眼能看出出勤率。openpyxl 引擎支持 xlsx 格式,如果要用老版 xls 得换 xlwt,但没必要。
4. 避坑指南:考勤系统上线前必须处理的五个问题
4.1 照片冒名:静态图片也能骗过摄像头
现象:学生拿手机对着摄像头放一张同学的照片,系统照样判定出勤。
原因:face_recognition 只做特征比对,不判断画面里是真人还是照片。
解决:加一层活体检测。最轻量的做法是要求眨眼或转头,用 OpenCV 的 dlib 关键点检测眼睛开合;更简单的是随机要求"请向左转头",连续两帧检测头部姿态变化。如果不想做活体,至少加"随机抓拍三次、时间间隔大于 10 秒",静态照片很难在多个时间点保持同一姿态。
4.2 光照翻车:背光环境下全员识别失败
现象:教室窗户在摄像头对面,学生全是逆光剪影,识别率骤降到 20% 以下。
原因:hog 模型对光照敏感,背光时人脸梯度特征丢失。
解决:两个方向。一是物理层面,让摄像头顺光或加补光灯;二是算法层面,处理前做直方图均衡化:
import cv2 def preprocess(frame): gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) equalized = cv2.equalizeHist(gray) return cv2.cvtColor(equalized, cv2.COLOR_GRAY2BGR)但均衡化会改变颜色信息,对 face_recognition 的特征提取有轻微影响,建议只在光照确实差的时候开。
4.3 阈值玄学:同一个人今天能过明天过不了
现象:同一个学生,昨天识别成功,今天怎么都认不出。
原因:TOLERANCE 设得太贴近边界,光照、角度稍有变化距离就超过阈值。
解决:不要拍脑袋定阈值。写个小脚本,用同一个人的多张照片两两算距离,看分布:
import face_recognition import numpy as np def analyze_threshold(student_dir): """分析同一人不同照片的距离分布,辅助定阈值""" import os encodings = [] for f in os.listdir(student_dir): img = face_recognition.load_image_file(os.path.join(student_dir, f)) locs = face_recognition.face_locations(img) if locs: encodings.append(face_recognition.face_encodings(img, [locs[0]])[0]) distances = [] for i in range(len(encodings)): for j in range(i+1, len(encodings)): d = np.linalg.norm(encodings[i] - encodings[j]) distances.append(d) print(f"同人距离:均值 {np.mean(distances):.3f},最大 {np.max(distances):.3f}") return np.max(distances)如果同人最大距离是 0.42,那阈值至少设 0.5 才安全。同时也要测不同人之间的最小距离,两者之间要有间隔,否则说明你的照片质量太差,得重新采集。
4.4 多人同框:一张脸被匹配给两个人
现象:两个学生坐得近,系统把 A 的脸同时记到了 A 和 B 名下。
原因:face_distance对每个 encoding 只取最近的一个,但如果两个人特征接近,或者库里有重复注册,就会串。
解决:一是注册时去重,新注册的特征和已有库比对,距离小于 0.4 就提示"该人脸已注册";二是判定时加"第二名距离必须比第一名大 0.1 以上"的规则,否则标记为"不确定",人工复核。
4.5 摄像头索引错乱:代码跑起来黑屏
现象:cv2.VideoCapture(0)打开成功但读出来全是黑帧。
原因:笔记本有多个摄像头(内置+外接),索引 0 可能被占用或指向了错误设备。
解决:遍历索引试:
import cv2 for i in range(5): cap = cv2.VideoCapture(i) if cap.isOpened(): ret, frame = cap.read() if ret and frame is not None and frame.mean() > 10: print(f"可用摄像头索引:{i}") cap.release()frame.mean() > 10是排除全黑帧,正常画面均值不会这么低。找到正确索引再写进代码。
5. 进阶技巧:把考勤系统做成能交差的产品
前面四章把核心链路跑通了,但一个能真正拿去用的系统,还差几块拼图。这一章讲三个我踩过坑之后总结出来的技巧。
第一个是用 Flask 包一层 Web 接口,让老师不用装 Python 也能用。核心就一个上传接口加一个查询接口:
from flask import Flask, request, jsonify, send_file import face_recognition import pickle import numpy as np from datetime import datetime app = Flask(__name__) with open("face_db.pkl", "rb") as f: face_db = pickle.load(f) known_ids = list(face_db.keys()) known_encodings = list(face_db.values()) @app.route("/checkin", methods=["POST"]) def checkin(): """接收前端上传的截图,返回识别结果""" file = request.files.get("image") if not file: return jsonify({"error": "未收到图片"}), 400 # 从内存读取,避免落盘 import io from PIL import Image img = np.array(Image.open(io.BytesIO(file.read())).convert("RGB")) locations = face_recognition.face_locations(img, model="hog") encodings = face_recognition.face_encodings(img, locations) results = [] for enc in encodings: distances = face_recognition.face_distance(known_encodings, enc) best = np.argmin(distances) if distances[best] < 0.48: results.append({"student_id": known_ids[best], "confidence": round(1 - distances[best], 3)}) return jsonify({"recognized": results, "count": len(results)}) if __name__ == "__main__": app.run(host="0.0.0.0", port=5000)这个接口接收前端传来的截图,返回识别到的学号列表。前端可以用 HTML5 的 getUserMedia 抓帧,每 10 秒 POST 一次,老师端只看到一个网页,不用碰命令行。host="0.0.0.0"是为了局域网内其他设备能访问,生产环境记得加鉴权,别裸奔。
第二个是用 pandas 做周期性统计,把每天的考勤记录汇总成周报。思路是每天导出的 Excel 按日期命名,周末用一个脚本读进来合并:
import pandas as pd import glob def weekly_report(pattern="考勤_*.xlsx"): files = sorted(glob.glob(pattern)) all_records = [] for f in files: df = pd.read_excel(f, sheet_name="明细") df["日期"] = f.split("_")[-1].replace(".xlsx", "") all_records.append(df) merged = pd.concat(all_records, ignore_index=True) # 透视:每个学生每天的状态 pivot = merged.pivot_table(index="学号", columns="日期", values="状态", aggfunc="first").fillna("缺勤") # 算出勤率 pivot["出勤率"] = (pivot == "出勤").sum(axis=1) / (pivot.shape[1] - 1) pivot.to_excel("周考勤汇总.xlsx") print(f"合并 {len(files)} 天记录,已生成周报") return pivot weekly_report()pivot_table把长表转成"学号 × 日期"的矩阵,一眼能看出谁哪天没来。fillna("缺勤")处理的是那天根本没记录的情况,默认算缺勤,这个逻辑要跟教务确认,有些学校请假也算缺勤,有些不算。
第三个是打包成 exe,让完全不懂 Python 的助教也能双击运行。用 PyInstaller:
pip install pyinstaller pyinstaller --onefile --add-data "face_db.pkl;." --name 考勤系统 main.py--add-data把模型文件一起打进去,Windows 下分隔符是分号,Linux 是冒号。打包后 exe 体积会到 200MB 以上,因为 dlib 和 OpenCV 都很大,这是正常现象。第一次运行会解压到临时目录,启动慢几秒,别以为是卡死。
最后说个我自己的习惯:每次改完识别逻辑,先用一段录好的视频回放测试,而不是直接开摄像头。摄像头是实时的,出了问题你根本不知道是哪一帧、什么光照、什么角度翻的车。录一段包含各种情况的视频,跑一遍看日志,比对着摄像头反复试效率高十倍。这套系统我前后改了三版,前两版都栽在"以为阈值调好了"上,第三版老老实实做了距离分布分析才稳定下来。希望帮到你。
本文还有配套的精品资源,点击获取