
简介基于Python深度学习的实验室自动签到与监控系统是一套面向课程设计与毕业设计场景的完整实现包聚焦人脸识别签到与实验室监控两大核心功能适合计算机相关专业学生用于项目学习、演示与二次开发。资源包共有379个文件以318个py源码文件为主体覆盖训练、推理与界面逻辑同时配有exe可执行程序、pth模型权重、ui界面定义、配置与部署脚本并附带部署文档和说明文档压缩包整体约3.8MB结构简洁方便快速上手。项目已通过导师指导评审答辩分为95分代码经测试运行无误可直接用于课设、毕设展示。已有109人学习下载如需扩展功能也可基于现有模块调整或添加新检测逻辑适合不同基础的学习者借鉴改进。1. 一套能交差的实验室自动签到监控系统把深度学习用在“谁来了、谁没来、在不在岗”很多实验室的到岗统计还停留在纸质签到表或者群里喊一声导师月底想看数据时值班学生得翻半天聊天记录。基于Python深度学习的实验室自动签到与监控系统要解决的就是这个场景摄像头对准实验室门口人来时自动识别身份并记录签到时间人离开后系统判断离岗超时未归就告警。它能让“谁来了、谁没来、谁其实在工位上”这三个问题全自动回答。这套系统最大的卖点是你不用自己训练模型检测和特征提取都用预训练模型只用 Python 把它们组合成一条可落地的链路。本文按我实际搭这套系统的顺序来讲从方案选型、核心识别链路、签到业务逻辑到部署文档里最容易翻车的几个坑最后是交付前的验证方法。新手照做能跑通熟手能直接拿走里面的阈值参数和踩坑记录。2. 系统架构与方案选型为什么用“人脸检测特征比对”而不是让签到机排队2.1 三种签到方案对比校园卡、二维码、人脸识别各自的边界实验室签到最常用的有三种方案先别急着选深度学习把需求盘清楚再定。校园卡/IC卡方案最成熟但代刷严重一个人拿五张卡刷完就走统计出来全是“人在岗”实际工位空着。二维码方案便宜但要求每个人主动掏手机扫和“无感签到”差距很大。人脸识别方案前期要采集样本、调阈值但识别过程不打断实验操作还能承担一部分在岗监控的功能做到“签到和监控共用一路视频流”。从实现成本看人脸识别方案不一定比刷卡贵因为摄像头实验室通常已经有了深度学习部分用的是开源预训练模型不需要买专门的考勤机。它的代价在软件侧环境配置、摄像头光线适配、误识别调优这三件事没做好系统体验会直接从“智能”掉回“智障”。所以我的建议是预算充足且只做签到就刷卡要做“签到在岗监控”就上人脸方案这也是标题里“签到与监控”两个词对应的真实需求。2.2 模块拆分采集端、识别端、业务端、监控端各管一段整套系统按数据流拆成四段每段独立成模块别写成一个 main.py。采集端只负责从摄像头取帧、检测人脸、保存样本识别端负责把视频帧里的人脸变成特征向量并和库里对比出“这个人是谁”业务端管签到表、迟到缺勤判定、重复签到去重监控端管离岗计时、超时告警。四段之间用数据库和文件解耦识别端不直接写数据库只返回 person_id 和置信度业务端拿到结果再判断。我见过不少毕设代码把 OpenCV 的摄像头读取、人脸识别、SQLite 写入、Flask 路由全塞在一个文件里最后改一个参数要全局搜索。这种写法演示没问题但换一台电脑部署时依赖冲突和进程卡死会一起爆发。拆成独立模块还有一个好处识别端可以单独跑一个进程就算 Web 服务崩了摄像头采集和识别不中断。2.3 技术栈选型Python 3.8、OpenCV、MTCNN、FaceNet、Flask SQLite先说明以下选型是“能跑、好交差、有扩展空间”的折中不是性能最优。Python 版本我建议锁 3.8别用最新的 3.12因为不少预训练模型转换工具和旧版依赖在 3.8 上测试得最充分。深度学习这块人脸检测用 MTCNN特征提取用 FaceNet 的 Inception ResNet v1两者都有预训练权重不需要自己准备训练集和 GPU 训练。如果要求更高精度可以把特征模型换成 ArcFace但 ArcFace 的预训练权重质量参差不齐调起来比 FaceNet 费时间。业务后端用 Flask 加 SQLite。SQLite 对单实验室规模完全够用几百个成员、每天几千条签到记录不需要 MySQLWeb 页面用原生 HTML 加 AJAX 定时刷新不引前端框架做到基本的跨浏览器支持部署时少一个 Node 环境就少一个坑。整体依赖尽量控制在 requirements.txt 里能一次装完的程度。3. 从零跑通核心识别链路人脸采集、检测、特征提取与比对3.1 数据准备每人 20~30 张样本按规范建目录很多人一上来就找现成人脸数据集这是错误方向。实验室成员就几十个人样本必须现场采集因为识别对象是固定的一批人不是全网人脸。样本质量直接决定识别效果比模型选型重要得多。我一般要求每人拍 20 到 30 张覆盖正面、左右侧脸、戴不戴眼镜、上午和下午光线每张照片里人脸宽度不低于 80 像素。样本目录按“成员编号_姓名”命名内部按 0.jpg、1.jpg 顺序存方便后续读取。import cv2 import os member_dir dataset/001_zhangsan os.makedirs(member_dir, exist_okTrue) cap cv2.VideoCapture(0) count 0 while count 30: ret, frame cap.read() if not ret: continue # 检测人脸区域保证保存的是正脸且清晰 face_cascade cv2.CascadeClassifier( models/haarcascade_frontalface_default.xml ) gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, 1.1, 5, minSize(80, 80)) for (x, y, w, h) in faces: face frame[y:yh, x:xw] # 只保存宽高都大于 120 像素的人脸过滤过小和模糊样本 if min(w, h) 120: cv2.imwrite(f{member_dir}/{count}.jpg, face) count 1 print(fsaved {count}/30) cv2.imshow(capture, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码用 OpenCV 自带的人脸检测器辅助采集它只负责找脸和裁剪不用来做识别因此存在误检也没关系保存前看一眼窗口画面即可。注意 minSize 参数控制最小人脸尺寸抓小脸会导致保存一堆模糊头像保存的样本最后人工删一遍糊的、闭眼的、只露半张脸的全部清掉这一步偷懒后患无穷。样本质量是后面所有阈值调整的地基。我见过因为样本全是顺光自拍结果系统在逆光环境下把三个人都识别成同一个人不是模型差是样本太单一。所以采集时尽量让成员坐在摄像头常驻的位置模拟真实签到场景而不是各自拿手机自拍发给你。3.2 检测与特征提取MTCNN 找脸FaceNet 提特征的最小推理代码样本采集完成后进入核心链路。检测用 MTCNN它比 OpenCV 的 Haar 检测更稳能对齐人脸关键点FaceNet 对对齐后的输入提取特征更准。特征提取的核心是把一张人脸图片变成一个 128 维向量同一人的不同照片向量距离近不同人的向量距离远。这一步不涉及训练只是调用预训练权重做前向推理。import cv2 import numpy as np from mtcnn import MTCNN from keras_facenet import FaceNet detector MTCNN() embedder FaceNet() def face_embedding(image_path): img cv2.imread(image_path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) faces detector.detect_faces(img) if len(faces) 0: return None x, y, w, h faces[0][box] # 边界修正避免人脸被裁掉一部分 x, y max(0, x), max(0, y) face img[y:yh, x:xw] face cv2.resize(face, (160, 160)) # FaceNet 输入要求像素值在 [-1, 1] 区间 face (face.astype(np.float32) - 127.5) / 128.0 embedding embedder.embeddings(np.expand_dims(face, axis0))[0] return embedding vec face_embedding(dataset/001_zhangsan/0.jpg) print(vec.shape, vec[:5])逻辑不复杂MTCNN 返回人脸框裁剪后缩放到 160×160归一化后传入 FaceNet最后拿到 128 维向量。两个参数值得说归一化公式用 (x-127.5)/128 是 FaceNet 官方预训练模型的标准输入换错公式特征会整体漂移resize 尺寸必须 160keras_facenet 封装内部虽然会检查但你自己变了尺寸输出就不对。第一次跑的时候建议打印 vec.shape 确认是 (128,)不是 (1, 128)。这里要提醒MTCNN 的 detect_faces 返回的人脸框有时候越界尤其是人靠近摄像头边缘时所以代码里 max(0, x) 的修正不能省。另外这个环节是性能瓶颈MTCNN 加 FaceNet 在 CPU 上处理一帧平均要 100 到 300 毫秒所以正式运行时不能每帧都做要抽帧。3.3 比对逻辑余弦相似度比欧氏距离更不容易被向量模长干扰算出特征向量之后剩下的问题是库里有几百个向量怎么判断当前人脸是谁。我一般先归一化所有向量然后算余弦相似度相似度大于阈值就认为命中。不用欧氏距离的原因是人脸向量经过 L2 归一化后余弦和欧氏是单调等价的但余弦的取值天然在 [-1, 1]阈值写法和解释都更直观答辩时也好讲。import numpy as np def cosine_similarity(a, b): a a / (np.linalg.norm(a) 1e-9) b b / (np.linalg.norm(b) 1e-9) return float(np.dot(a, b)) def match_face(query_vec, db_vecs, names, threshold0.75): best_score -1.0 best_name unknown for i, db_vec in enumerate(db_vecs): score cosine_similarity(query_vec, db_vec) if score best_score: best_score score best_name names[i] if best_score threshold: return unknown, best_score return best_name, best_scorethreshold 是最需要调的参数没有固定值。0.75 是 FaceNet 模型在常见数据集上的经验起点实际使用中误识别严重就把阈值调高到 0.80 以上漏识别频繁就把阈值降到 0.70 左右。调试方法是拿 20 个已知身份的人脸各测 10 次画出同类相似度和异类相似度的分布两类有明显分界时阈值取中间值。加 1e-9 是防止零向量除零虽然理论上不会出现但数据库里如果有脏数据它能避免程序崩溃。3.4 入库与增量更新特征向量存库新增成员不用重训很多人一想到人脸识别就以为要重新训练模型这是最大的误区。这套方案里模型是固定死的每个成员只有一个 ID 对应一组特征向量。新增成员时只需要采集样本、提取特征、把向量写进数据库整个过程不需要触碰模型文件。这一点在部署文档里一定要写清楚因为管理员最怕的就是“加个人进去是不是要重新跑训练”。import sqlite3 import pickle def add_member(person_id, name, sample_dir): conn sqlite3.connect(lab_attendance.db) cur conn.cursor() vectors [] for f in os.listdir(sample_dir): vec face_embedding(os.path.join(sample_dir, f)) if vec is not None: vectors.append(vec) mean_vec np.mean(vectors, axis0) # 多个样本取平均更稳定 blob pickle.dumps(mean_vec.astype(np.float32)) cur.execute( INSERT INTO person(person_id, name, feature) VALUES(?,?,?), (person_id, name, blob), ) conn.commit() conn.close()这段代码把同一个人的多张样本特征取平均作为入库向量比单张样本更稳。注意库表设计person 表存 person_id、name、feature 三个字段feature 用 BLOB 类型存 pickle 序列化后的特征向量attendance 表单独存签到记录。不要试图把特征向量按浮点数组存在一张表里SQLite 对 BLOB 的处理最省事。pickle 序列化在 Python 版本间兼容性尚可但换机器部署时最好保持 Python 主版本一致。4. 自动签到业务逻辑与监控告警迟到、缺勤、离岗怎么判定4.1 签到规则设计签到窗口、迟到判定和幂等去重识别出人是谁之后真正让系统“像签到系统”的是业务规则。规则不复杂但漏了任何一条都会在使用时闹笑话。我把常见规则说一遍你按自己实验室的上课或值班时间改设备在签到窗口内识别到某人生成一条签记录签到窗口设置为上课前 15 分钟到上课后 30 分钟窗口内重复识别不生成第二条记录只更新最后活跃时间这就是幂等。from datetime import datetime, timedelta def handle_attendance(person_id, recognized_time): # 签到窗口08:45 - 09:309 点整开课 window_start recognized_time.replace( hour8, minute45, second0, microsecond0 ) window_end recognized_time.replace( hour9, minute30, second0, microsecond0 ) if not (window_start recognized_time window_end): return {action: ignored, reason: out_of_window} cur.execute( SELECT id FROM attendance WHERE person_id? AND date(created_at)date(?), (person_id, recognized_time.isoformat()), ) if cur.fetchone() is not None: return {action: ignored, reason: duplicated} status normal if recognized_time.time() datetime.strptime( 09:00, %H:%M ).time() else late cur.execute( INSERT INTO attendance(person_id, created_at, status) VALUES(?,?,?), (person_id, recognized_time.isoformat(), status), ) return {action: inserted, status: status}判定迟到不要在 SQL 里做用 Python 的 datetime 逻辑写清楚更好读。这段代码里窗口写死了 8:45 和 9:30真实环境最好把作息时间表统一存在 config 里因为实验室可能有多个签到时段。重复签到判断用的是“当天存在记录就忽略”这个粒度适合一天只签一次的场景如果要做早午晚多次签到判断条件要改成“同一时段内存在记录”。4.2 监控事件流视频抽帧、在岗检测、离岗超时告警签到只是前半场监控才是值班人员最在意的部分。监控逻辑可以独立成一个循环线程摄像头持续运行每隔 5 秒抽一帧送入人脸检测检测到有人脸就记录该脸对应的成员为“最近在岗”同时更新该成员的 last_seen 时间。当 last_seen 距离当前时间超过 10 分钟判定为离岗给管理员推送一条告警记录。抽帧间隔别太短CPU 推理跟不上会积压摄像头取帧的内存占用会不断涨。import threading import time last_seen {} def monitor_loop(capture, interval5, timeout600): while True: ret, frame capture.read() if not ret: time.sleep(1) continue if frame_count % interval 0: boxes, names detect_person(frame) for name in names: last_seen[name] time.time() for name, seen in list(last_seen.items()): if time.time() - seen timeout: push_alert(name, absence_timeout) frame_count 1 time.sleep(0.1)capture 建议用独立的 VideoCapture 实例不要和签到线程共用同一个否则 read() 方法会互相抢帧。interval 设为 5 秒是因为人离开工位到再次出现至少需要几十秒5 秒的粒度足够timeout 设为 600 秒意味着连续 10 分钟检测不到这个人就告警。如果实验室有人只是去厕所回来后会重新更新 last_seen不会误报。push_alert 先做成写数据库加控制台打印后面接钉钉或者企业微信机器人是另一个工程不需要在毕设阶段做。4.3 Web 展示与数据落库Flask 提供签到记录和实时状态页识别和监控都在后台跑管理员和导师需要一个查看入口。直接用 Flask 起一个最小服务提供三个接口查询某天的签到记录、查询当前在岗名单、查询今天的迟到缺勤统计。页面用原生 HTML 加 JavaScript 的 setInterval 定时拉取接口不引 Vue 也不引 React这样只要浏览器不是太老都能正常打开跨浏览器支持上少操很多心。from flask import Flask, jsonify, request app Flask(__name__) app.route(/api/records) def api_records(): date request.args.get(date) rows query_attendance(date) return jsonify({code: 0, data: rows}) app.route(/api/status) def api_status(): present [name for name, seen in last_seen.items() if time.time() - seen 600] return jsonify({code: 0, data: {present: present}})两个接口的关键是返回结构固定前端只用 code 和 data 两个字段出错时 code 非 0。Flask 默认的开发服务器够用但摄像头识别线程在 Flask 进程里跑时要注意调试模式会启动两个进程导致摄像头被打开两次。建议 app.run(debugFalse)也别用 Flask 自带的 reloader亲自踩过这个坑的人都知道有多难受。5. 部署与运维常见问题排查环境、依赖、模型文件与开机自启5.1 现象代码在自己电脑上能跑服务器上一启动就报错原因一般是 Python 小版本不一致或依赖版本没锁。最典型的是在自己电脑上单独 pip install 一个个装没问题换机器后用 requirements.txt 重新装发现 opencv-python 装成了最新版底层的 numpy API 变了FaceNet 封装调用时直接抛异常。解决方法是把 requirements.txt 做成完全锁版本格式不是 opencv-python 这种裸库名而是 opencv-python4.8.1.78 这种带上精确版本号。另一点是 Python 3.11 以上跑部分旧版 MTCNN 会报警告不要忽视直接换 Python 3.8。5.2 现象显卡驱动正常但推理速度反而比 CPU 还慢很多人装好 CUDA 和 cuDNN 后满心期待 GPU 加速结果速度没有任何提升。原因很可能是 MTCNN 和 FaceNet 的预训练模型是 TensorFlow 1.x 权重而新版环境里加载的库走的是 CPU fallback。排查方法是打印模型推理日志看有没有类似 “Could not create cuDNN algorithm” 的提示。这不是算法问题是环境匹配问题。如果你的实验室电脑没有 NVIDIA 显卡就直接约定 CPU 跑 5 秒抽帧方案不要在 GPU 上浪费时间。深度学习推理部署的黄金法则是:先确认参考实现用的框架版本而不是先装最新的驱动。5.3 现象摄像头无缘无故被占用程序重启后还是打不开原因通常是上一次运行没有正确释放 VideoCapture 对象或者进程没有退出摄像头设备被残留进程占住。解决分两步代码里把 cap.release() 写在 finally 块里确保异常时也释放部署时用一个哨兵脚本启动前检查是否有残留的 Python 进程有就杀掉再开启。这里不要把每一条 VideoCapture 都做成全局变量越多越容易泄漏。5.4 现象人脸误识别严重戴个口罩就变成另一个人原因多半是样本和场景不匹配。实验室摄像头装在门口逆光拍出来的人脸对比度极低而你训练库里的样本都是顺光自拍特征自然对不上。解决方法是重新采集实测位置的样本采集时加上口罩、眼镜、低头看手机三种变化。另一个原因是阈值太宽松把阈值从 0.70 提到 0.78误识别率会肉眼可见下降。注意这一点要在部署文档里写明因为管理员遇到误识别第一反应是换模型其实换阈值更管用。5.5 现象模型文件放在项目目录里部署时丢失程序不报错却全识别成 unknown模型权重文件是最容易被忽略的一部分。很多人把代码打包上传结果忘了 models/ 目录导致推理结果全是空。部署资料里应该把模型文件单独列出来并在启动脚本里加一个文件存在性检查#!/bin/bash if [ ! -f models/facenet_model.h5 ]; then echo model file missing, check models directory exit 1 fi python main.py这不是大问题但足以让整个现场演示翻车。把这种检查放进启动脚本成本几乎为零收益是去掉一大类“部署后才发现缺文件”的问题。如果你做毕设答辩评委不会看你怎么训练模型但一定会问你“换了机器之后模型参数从哪来”这一步能回答得漂亮。5.6 现象长时间运行后内存持续上涨三天后系统卡死原因大概率是视频帧没有被正确释放。OpenCV 的 read() 每次返回一帧如果这一帧被保存在全局变量里没被覆盖或者检测线程里积压了队列内存就会缓慢上涨。另一个隐蔽原因是 Flask debug 模式反复加载两次模型每次加载都吃几百 MB。解决方法是把识别线程和 Web 线程分开部署识别线程里每处理完一帧就显式 del 帧并且用 deque 限长队列长度超过 10 就直接丢弃最旧的帧。6. 交付前一定要做的验证压测、断网断电、数据备份与换机部署6.1 用模拟视频流做人脸识别压测真实摄像头不能一直来回走动测试我习惯先用录制好的视频文件当作输入源把 cv2.VideoCapture(0) 改成 cv2.VideoCapture(test.mp4)。测试时分别统计同一人连续经过 10 次识别成功率、三人同时入镜时响应耗时、室内光线变化后漏识别率。记录一张测试表成功率达到 90% 以上再上真机。顺手测一下最坏情况摄像头在门口两个人并排走进MTCNN 能否同时框住两个人。框不住的话过道换广角镜头或调低 minSize。6.2 模拟断网、断电、摄像头被拔掉时的行为实验室环境不稳定断电断网都是常态。断网不影响本地识别和签到因为整套系统不依赖云 API但如果你接了钉钉告警机器人断网时告警会失败代码里要加队列网络恢复后补发。摄像头被拔掉时read() 返回 False监控线程不能崩溃要记录错误日志并隔几秒重试等待插回后自动恢复。这一项经常被忽视交付的时候没问题真正用起来三天两头重启印象分直接跌到底。6.3 数据库备份与迁移到新机器的三个步骤SQLite 的优点是单文件迁移很简单。步骤固定三步停止识别进程防止写库复制 lab_attendance.db 文件到新机器把模型目录和 requirements.txt 一起拷过去在干净环境里执行 pip install -r requirements.txt。迁移后先跑一次静默测试识别两个成员确认数据库文件权限和新环境的 numpy 版本没问题。备份频率不用太高一天一次就行签到数据丢了最多补签但特征向量库丢了要重新采集样本非常麻烦。6.4 最后的部署检查清单交付前按这个清单过一遍启动脚本是否能开机自启摄像头供电是否跟电脑同步模型文件是否打包齐全timeout 和 threshold 参数是否写死在配置里日志文件是否有轮转。如果这是毕设项目答辩前把参数调优的过程记录下来尤其是误识别阈值从 0.70 调到 0.78 前后对比这是最有说服力的材料。我自己的教训是第一次演示时忘了把模型权重路径改成绝对路径结果换目录运行直接全盘 unknown从此之后所有路径都用 os.path.join 拼出来再也没翻车。这套系统做到这个程度日常使用是可以在实验室顶上至少一个学期的人力统计工作的。希望帮到你。本文还有配套的精品资源点击获取