简介:基于Python的多人人脸识别课堂考勤系统源码,是一份适合Python开发者、教育信息化人员及计算机专业学生学习的完整项目,可直接用于课程设计或实际课堂考勤场景。资源覆盖摄像头图像采集、人脸检测与特征提取、身份比对以及考勤记录自动生成等流程,同时包含管理员后台与可视化仪表盘,便于理解从算法调用到业务管理的完整链路。压缩包共43个文件,大小仅31KB,包含21个HTML前端页面、10个Python源码文件,以及SQL数据库脚本、依赖清单、配置文件和README说明文档,目录清晰,按模块组织,适合进行二次开发和学习。目前已有104人学习。项目中核心逻辑与前端页面分离,涵盖人脸识别核心模块、数据模型、考勤统计、后台管理等;借助OpenCV/Dlib等库的人脸检测与比对实现,以及配套的配置示例和说明文档,读者能较快搭建运行环境,掌握课堂考勤系统的数据存储与查询、用户认证和界面交互设计,是入门人脸识别应用开发的实用参考资料。
1. 引言:3秒识别50人,难点不在识别模型而在工程
用Python搭一套多人人脸识别课堂考勤系统,表面上是把摄像头对准教室,等下课自动生成名单。实际动手做过的人都知道,把人脸识别门禁那套单人逻辑搬过来,第一节课就会翻车:教室后排一张脸占比不到80像素宽,五十个人同框,有人低头有人侧脸,识别结果糊成一片。这个标题里最值钱的不是“人脸识别”这四个字,而是“多人”和“课堂”两个限定词——它决定了模型选型、追踪策略和数据采集方式。这篇文章把整套方案拆开讲:选什么模型、特征库怎么建、多人追踪怎么写、考勤怎么防重签到,最后落到源码工程里最容易踩坑的五个位置。适合打算用Python从零搭课堂考勤,或者拿到同类源码想二次开发的人。
2. 选型:单人门禁方案为什么搬不进教室,多人考勤该选什么模型
2.1 单人识别和多人识别是两套工程
人脸识别门禁机前站一个人,脸是正的,距离近,光照可控,最多再比个对。课堂考勤完全不是这个环境:摄像头架在教室前上方,第一排人脸巨大,最后一排人脸可能只有三四十像素,五十个人同框时彼此遮挡,还有人全程低头。这个时候识别精度反而不是第一位的,第一位是“能不能稳定找到每个人”。
另一个容易被忽略的差异是业务逻辑。门禁是“一次识别放行”,课堂考勤是“一节课内确认一次”,这要求系统能区分“这一帧里的张三”和“上一帧里的张三”是不是同一个人。单人识别根本不需要追踪这个概念,多人考勤必须要有。所以选型的第一条原则:不要选只能做单张人脸识别的方案,要选“检测+追踪+识别”链路完整的方案。
2.2 三套常见方案对比
| 方案 | 检测与特征 | 多人同框能力 | CPU占用 | 适合场景 |
|---|---|---|---|---|
| OpenCV Haar+LBP | 传统手工特征 | 弱,遮挡即丢 | 低 | 单人门禁/演示 |
| dlib / face_recognition | 深度特征但管线老旧 | 中,人数一多检测器先掉 | 高 | 小规模人脸库 |
| InsightFace + onnxruntime | 检测、对齐、特征一体 | 强,同框50人可检出 | 中,GPU可加速 | 课堂、闸机 |
我一般直接锁定第三套。dlib那套在实验室人脸库上效果不错,但课堂这种“小脸+密集+光线难看”的组合,dlib的检测器经常先把后排漏掉。InsightFace的det_10g检测器对小脸和遮挡更友好,特征模型w600k_r50的辨别力也够课堂用。
这里还要提醒一句:别被“人脸识别门禁机”的思路带偏。门禁机上那套方案很多是单摄像头+单目标追踪,甚至直接截帧比对,逻辑简单。课堂考勤一旦上多人,方案的复杂度不在识别本身,而在怎么把同框的人区分开、怎么跨帧保持身份稳定。选型时把这两点放在第一位,比纠结特征模型准确率高0.几个点更重要。
2.3 一个buffalo_l包就包住整个识别链路
InsightFace的buffalo_l模型包可以理解成三合一套装:det_10g负责检测人脸框和五个关键点,关键点用来做对齐,w600k_r50把对齐后的人脸压缩成512维特征向量。这个结构最大的价值是免掉手工对齐环节——很多人写人脸识别代码时,在“裁剪后有没有把眼睛鼻子对齐”这一步消耗大量时间,而insightface的app.get()一次调用就把检测和对齐做完了,拿到的embedding可以直接做余弦相似度比对。
选这个模型还有一个务实的理由:onnxruntime推理。buffalo_l发布时就带onnx格式,不需要装PyTorch或TensorFlow,一台不带GPU的普通机器也能用CPU跑起来。课堂考勤系统的部署环境往往是教室里的老旧电脑,能跑CPU推理、能在低配机器上不卡死,比模型精度更值钱。
2.4 整体链路:摄像头到考勤表的六个环节
这套系统的完整链路是:摄像头采集视频帧 → 检测同框多张人脸 → 关键点对齐 → 提取512维特征 → 追踪器对同一个人跨帧保持ID → 与特征库比对并写考勤。前四步是识别本体,后两步是课堂考勤能不能落地的关键。
追踪器不是可选项。没有追踪器时,每一帧都是独立识别,同一个学生在连续几帧里被识别出多次,考勤表里会出现重复签到;更麻烦的是,一个人脸上帧间特征波动稍大,就可能出现前半节课签到了、后半节课又冒出来签一次的情况。加上追踪器之后,系统只对同一个track_id输出一次签到,重复的问题从源头被掐掉。
3. 环境与数据集:先把Python环境装利索,再建人脸特征库
3.1 Python环境与依赖安装:onnxruntime和insightface的安装顺序
这个工程对Python版本有要求,建议用3.8到3.10的64位版本,太新的版本部分依赖还没跟上,安装阶段就会卡住。装依赖也有顺序讲究,先把onnxruntime装好,再装insightface,后者会把onnxruntime的版本拉低,顺序反了可能出现奇怪的推理报错。
# 建议新建虚拟环境,避免污染系统Python python -m venv .venv source .venv/bin/activate # Windows 下用 .venv\Scripts\activate pip install numpy opencv-python onnxruntime insightface这一步装完,先用下面这段代码验证环境是否正常。很多人装完直接跑大程序,报错了不知道是哪一层的问题,先验证模型能加载,后面排错会省很多时间。
import insightface import cv2 app = insightface.app.FaceAnalysis( name='buffalo_l', providers=['CUDAExecutionProvider', 'CPUExecutionProvider'] ) app.prepare(ctx_id=0, det_size=(640, 640), det_thresh=0.4) print("模型加载成功") img = cv2.imread("test.jpg") faces = app.get(img) print(f"检测到 {len(faces)} 张人脸")参数说明:providers里CUDAExecutionProvider在前面,有GPU会优先用GPU,没有会自动回退CPU;det_size是检测分辨率,640在速度和精度之间比较均衡;det_thresh是检测置信度阈值,默认0.5,课堂场景我会先降到0.4,后面避坑章节会细说。
3.2 数据集准备:每个学生20到30张照片怎么拍
特征库的质量直接决定考勤准确率,比模型选哪个更关键。我的经验是每个学生收20到30张照片,覆盖三个变化维度:角度变化,正脸、左右各15度;表情变化,正常、微笑、张嘴;光照变化,教室灯下、窗边自然光、稍微背光。照片像素不要低于200x200,人脸占比尽量大。
拍摄方式不需要专业设备,手机在后置摄像头下拍就行,但有一个硬性要求:照片必须在座位上拍。注册照片如果是证件照那种纯正面、均匀光照的,课堂实拍时教室光线和角度一偏,特征马上对不上。这就是很多人“注册时好好的,一到教室就认不出来”的根本原因。
目录结构按学号分文件夹,方便批量提取:
student_photos/ ├── 2023001/ │ ├── 001.jpg │ ├── 002.jpg │ └── ... ├── 2023002/ │ └── ...3.3 批量提取特征:把照片文件夹变成npy特征库
特征库构建脚本是整个系统最基础的部分。它的输入是student_photos文件夹,输出是一个npy特征矩阵和一个txt标签文件,两者行数一一对应。注意对同一个学生的多张照片特征做平均,这能显著降低单张照片的光照噪声。
from pathlib import Path import cv2 import numpy as np import insightface from collections import defaultdict app = insightface.app.FaceAnalysis(name='buffalo_l') app.prepare(ctx_id=0, det_size=(640, 640), det_thresh=0.3) emb_by_label = defaultdict(list) for photo_path in sorted(Path('student_photos').glob('*/*.jpg')): label = photo_path.parent.name img = cv2.imread(str(photo_path)) faces = app.get(img) if not faces: print(f"跳过: {photo_path} 未检测到人脸") continue # 一张照片里可能带出背景人脸,取面积最大的一张 target = max(faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1])) emb = target.embedding / np.linalg.norm(target.embedding) emb_by_label[label].append(emb) embedding_list, label_list = [], [] for label, embs in emb_by_label.items(): avg = np.mean(embs, axis=0) avg /= np.linalg.norm(avg) embedding_list.append(avg) label_list.append(label) np.save('face_db.npy', np.array(embedding_list)) with open('labels.txt', 'w', encoding='utf-8') as fp: fp.write('\n'.join(label_list)) print(f"共入库 {len(label_list)} 名学生")逻辑说明:第16行取面积最大的人脸,是为了排除照片背景里误检到的其他人;第17行做了L2归一化,把人脸特征向量变成单位长度,后续比对用余弦距离才公平。第24到27行按学号平均特征再归一化,这一步是注册阶段的降噪,比直接用单张照片的特征稳定得多。
3.4 特征库的L2归一化和阈值初始值
特征库存好之后,比对逻辑就是用当前帧的人脸特征和库里的每一条特征做点积,取最大值作为相似度。因为所有特征都做了L2归一化,点积就是余弦相似度,数值范围在负一到一之间。
阈值初始值我建议设0.5,然后根据实测调整。0.5偏保守,宁可漏掉也不误判;想提高签到覆盖率就往0.45降,但误识别的风险会上升。阈值这个参数就是典型的“玄学”,不能拍脑袋定死,后面避坑章节会讲一套校准方法。这里先记住一个原则:阈值校准必须用课堂实拍的数据,不能用注册照片自测。
4. 多人实时识别:视频流、追踪器、考勤落库三件套
4.1 摄像头读取与识别分离:别让采集线程卡住识别线程
很多人写视频识别程序时,直接在循环里read一帧识别一帧,这样做在1280x720分辨率下勉强能跑,一到1080p就开始卡。卡的原因不是识别慢,而是摄像头读取是阻塞的,read操作等帧、识别操作等CPU,两件事互相拖后腿。
常规做法是开一个线程专门读摄像头,把最新帧放进队列,主线程只做识别。注意队列要限长,丢弃旧帧,保证每次拿到的都是接近当前时刻的帧,否则延迟越来越大。
import threading import queue import cv2 frame_queue = queue.Queue(maxsize=2) def capture_worker(): cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while True: ret, frame = cap.read() if not ret: break if frame_queue.full(): frame_queue.get() # 丢旧帧,保持低延迟 frame_queue.put(frame) threading.Thread(target=capture_worker, daemon=True).start()参数说明:maxsize=2表示队列里最多留2帧,满了就丢最旧的,这是控制延迟的关键;分辨率设1280x720是性能和画质的折中,1080p会让后排人脸更清楚,但CPU推理时间明显变长。
4.2 同框多人与IOU追踪:一个学生只认一个ID
识别主循环要处理三件事:检测同框所有学生、和特征库比对、更新追踪器状态。追踪器的作用是跨帧保持身份,我做一个简化的IOU追踪器,先定义追踪对象,再用检测框的IOU做帧间匹配。
class FaceTrack: def __init__(self, track_id, bbox, embedding): self.track_id = track_id self.bbox = bbox self.embedding = embedding self.student_id = None self.confirm_count = 0 self.miss_count = 0 def iou(a, b): ax1, ay1, ax2, ay2 = a bx1, by1, bx2, by2 = b ix = max(0, min(ax2, bx2) - max(ax1, bx1)) iy = max(0, min(ay2, by2) - max(ay1, by1)) inter = ix * iy union = (ax2 - ax1) * (ay2 - ay1) + (bx2 - bx1) * (by2 - by1) - inter return inter / max(union, 1e-6)匹配逻辑是这样的:检测到的人脸框,和现有所有track的bbox计算IOU,IOU大于0.5就算同一个人的新位置,更新track的bbox和embedding,连续3帧识别到同一个学号才确认签到。IOU小于0.5的检测结果就新建一个track。这个方案在固定摄像头下够用,如果教室有走动或者摄像头晃动频繁,可以换成Bytetrack,效果更好。
主循环代码如下:
import time import numpy as np face_db = np.load('face_db.npy') label_list = open('labels.txt', encoding='utf-8').read().splitlines() def match_student(emb, threshold=0.5): sims = face_db @ emb idx = int(np.argmax(sims)) if sims[idx] < threshold: return None return label_list[idx] tracks = [] next_id = 0 sample_interval = 0.3 last_check = 0.0 while True: try: frame = frame_queue.get_nowait() except queue.Empty: continue now = time.time() if now - last_check < sample_interval: continue last_check = now faces = app.get(frame) for face in faces: if face.det_score < 0.3: continue bbox = face.bbox.astype(int) emb = face.embedding / np.linalg.norm(face.embedding) track = None for t in tracks: if iou(t.bbox, bbox) > 0.5: track = t break if track is None: track = FaceTrack(next_id, bbox, emb) next_id += 1 tracks.append(track) else: track.bbox = bbox track.embedding = emb student_id = match_student(emb, threshold=0.5) if student_id and student_id == track.student_id: track.confirm_count += 1 elif student_id: track.student_id = student_id track.confirm_count = 1 if track.confirm_count >= 3 and not track.confirmed: track.confirmed = True write_attendance(student_id, course_id)参数说明:sample_interval=0.3表示每0.3秒做一次检测,摄像头30fps时相当于每秒抽3帧,CPU压力大幅下降;det_score过滤掉模糊小脸;连续3帧确认是防止单帧误识别导致错签到,这是考勤系统里最重要的一道保险。
4.3 考勤业务逻辑:正常、迟到、缺勤怎么判定
识别只是前半场,后半场是考勤规则。课堂考勤最常见的规则是:上课前10分钟和上课后15分钟内签到算正常,之后算迟到,课程结束还没签到的补成缺勤。这些判断放在write_attendance函数里做。
签到窗口的参数要放在配置文件里,不要写死在代码里。不同学校对迟到的时间定义不一样,有的按上课铃,有的按教师到岗时间,做成配置项后调起来很方便。我这里用一个简单的时间判断:
import datetime late_deadline = datetime.datetime.now().replace(hour=9, minute=15, second=0) def write_attendance(student_id, course_id): now = datetime.datetime.now() status = "正常" if now <= late_deadline else "迟到" save_to_db(student_id, course_id, status, now)4.4 考勤落库:SQLite的UNIQUE约束是重复签到的后悔药
考勤数据用SQLite存就够了,不需要上MySQL。三张表就能撑起整个系统:students学生表、courses课程表、attendance考勤表。attendance表里加UNIQUE约束,同一学生同一课程只能存在一条记录,这是防重复签到的兜底方案。
CREATE TABLE IF NOT EXISTS students ( student_id TEXT PRIMARY KEY, name TEXT NOT NULL, class_name TEXT ); CREATE TABLE IF NOT EXISTS courses ( course_id TEXT PRIMARY KEY, course_name TEXT NOT NULL, semester TEXT ); CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course_id TEXT NOT NULL, status TEXT NOT NULL DEFAULT '正常', check_time DATETIME NOT NULL, UNIQUE (student_id, course_id) );写入时用INSERT OR IGNORE,配合UNIQUE约束,就算追踪器没写好导致同一学生重复识别,第二次写入也会被自动忽略。这是整个系统里最便宜的“后悔药”,强烈建议加上。
5. 避坑:多人课堂考勤最常见的5个翻车现场
5.1 翻车一:五人同框只识别出三个人
现象:镜头里明明坐着五个学生,检测结果只框出三个,后排两个人被漏掉。 原因:det_thresh默认是0.5,后排人脸尺寸小、像素低,检测置信度通常只有0.3到0.45,全部被阈值过滤了。 解决:把det_thresh降到0.3,同时在上文主循环里用face.det_score >= 0.3做二次判断。降阈值会带进来背景误检,但配合“取最大人脸”和关键点过滤,误检率在可控范围。血泪经验:课堂场景优先保召回,多检一张背景图比漏一个学生好处理。
5.2 翻车二:同一个学生一节课签到了四次
现象:考勤表里一个学生出现多条记录,状态还各不相同。 原因:没有追踪器,或者追踪器IOU匹配阈值太严,同一人的检测框在帧间断开了,被当成两个不同的人。 解决:确认机制加数据库约束双保险。追踪器连续3帧确认才写入,attendance表再用UNIQUE约束兜底。我见过只靠代码判断的版本,一漏就废,加约束之后至少数据不会烂。
5.3 翻车三:相似度阈值卡在玄学区
现象:阈值设0.5,后排学生很多识别不上;降到0.4,前排学生开始频繁误判成另一个人。 原因:注册照片是顺光拍的,教室实拍是顶光加侧光,同一人的特征偏移量比想象的大。阈值本身没有标准答案,它由数据分布决定。 解决:不要拍脑袋定阈值。先跑一节测试课,把每次识别得到的最高相似度记录下来,分别统计“正确匹配的分数”和“错误匹配的分数”,阈值设在两组数据分布中间的间隙上。调完阈值把错分样本的图片存到单独的文件夹,留着下次校准用。
5.4 翻车四:后排人脸太小,特征提取全是噪声
现象:后排学生的人脸框只有30x40像素,提取出的特征和谁都不像。 原因:摄像头分辨率不够,或者det_size设得太小。1080p摄像头下,最后一排人脸可能只有40像素宽,勉强能检测但特征质量极差。 解决:摄像头分辨率至少1080p,安装位置尽量往前移。det_size从640提到960能改善小脸特征,但CPU推理时间会涨三倍,需权衡。最实用的兜底方案:考勤允许学生靠近摄像头刷脸,或者把后排座位和前排分组拍摄。
5.5 翻车五:CPU机器上卡成PPT,GPU又装不上
现象:教室电脑是旧办公机,跑起来一秒一帧,考勤结束名单还没出全。 原因:每帧全量检测加特征提取,CPU推理太慢。 解决:采样间隔从0.3秒调到0.5秒,配合追踪器对中间帧做位置预测。识别线程降到10fps之后,考勤准确率几乎不受影响,因为人坐在位置上不动,0.5秒采样绰绰有余。如果机器有NVIDIA GPU,把providers里换成CUDAExecutionProvider,推理时间能降到原来的五分之一。
6. 验证与回滚:用一节45分钟课堂录像把考勤表跑出来
6.1 离线录像批量识别:拿视频文件当摄像头用
上线前一定要做一次离线验证。方法很简单,把视频采集这一行换成读录像文件,其他逻辑完全不动。这样能用同一套代码反复测试不同光线、不同座次下的考勤效果,不占用教室时间。
cap = cv2.VideoCapture("lecture_recording.mp4") # 后续代码与实时识别完全一致,只是 frame 来源从摄像头换成视频文件我一般会录三节不同时间段的课做测试:上午逆光、下午侧光、阴天灯光全开。这三段录像能暴露绝大部分光线问题,比在实验室里自测可靠得多。离线跑完,把生成的考勤表和实际点名表逐行比对,重点查漏报和误报,整理成一份问题清单再改参数。
6.2 结果核对与人工兜底:给低置信度识别留一张“后悔票”
最后一排学生识别分数普遍偏低,这是物理限制,靠调参解决不了。我的做法是给系统加一个待确认机制:识别相似度低于阈值但高于阈值减0.1的人脸,不直接丢弃,把裁剪下来的人脸图保存到to_check目录,文件名带时间戳和摄像头编号。课后人工扫一眼这个目录,几十张图一分钟就能看完,把漏掉的学生手动补录进考勤表。
这个机制的工程价值很大,它把系统的错误从“无法挽回”变成“可控可查”。我现在每次调整识别阈值,都会把当天的to_check目录截图留档,下次调参时翻出来对比,比凭感觉调稳定得多。课堂考勤这种东西,一次错签比十次漏签更麻烦,宁可让人工兜底,也不要让系统自作主张。
另外有个习惯值得分享:每次换教室,先录5分钟测试段跑一遍识别,看看座位和光线变化对相似度分数的影响,再决定当天阈值是用0.45还是0.5。希望帮到你。
本文还有配套的精品资源,点击获取