简介:这是一套基于Python深度学习的实验室自动签到与监控系统完整项目,面向高校计算机、人工智能、物联网等专业学生及教师,适用于课程设计、毕业设计或项目演示,覆盖人脸识别签到、实时监控告警、签到记录管理等核心环节。压缩包共包含379个文件,主体为318个Python源文件,另有UI界面文件、已训练模型权重pth、配置文件、IPython Notebook、PyInstaller打包的exe以及虚拟环境批处理脚本等,整体仅3.8MB,轻量但结构清晰,部署文档齐全,适合对照源码逐步复现。目前已有109人学习下载,可作为课程设计或毕业设计的直接参考。项目属于高分通过导师审核的实战源码,测试运行稳定,可直接用于毕设或课设完整方案,也可针对业务场景二次扩展——无论是快速落地演示还是钻研深度学习工程化细节,都能从中获得可复用的代码与思路。
1. 实验室自动签到与监控系统,先想清楚要识别谁、在岗多久、异常怎么报警
实验楼的考勤一直是个低技术含量却高管理成本的事。手写签到表补签成风,刷卡机一台只能管一扇门,指纹机在实验课前后排长队,而真正的需求其实有两层:第一层是“这个时段谁来过、待了多久、有没有按时离开”,第二层是“无人的深夜里,还有人滞留或闯入实验区”。两者放在一起做,就是标题里自动签到与监控系统真正要解决的问题。
这套系统的思路并不复杂:摄像头负责看,深度学习模型负责把画面转成“人”和“身份”,业务逻辑负责把身份落成一条签到记录或一条告警。但落地时有几个容易被忽略的分水岭:人脸检测与人脸识别是两条不同的技术链路;录制视频里的人脸和现场直播流里的人脸,阈值要分开调;签到数据能不能在断电重启后完整补录,比模型精度更影响口碑。
接下来的内容按从理论到工程推进,先拆识别链路与选型,再给一套在普通办公电脑上就能跑通的 Python 环境与最小代码,然后补齐数据库、摄像头采集、告警这类工程骨架,最后放一个上线前必须做的验证方法。整篇按“先让模型跑出一个人脸框和一条特征向量,再谈系统”的顺序来写。
2. 签到为什么用深度学习:识别链路拆解与模型选型
2.1 人脸检测不是人脸识别,别把两步混成一步
第一次做这类项目的人最容易踩的坑,是把“画面里有没有脸”和“这张脸是谁”当成同一个问题。检测(detection)解决的是位置问题:输入一帧 640×480 的图像,输出若干个人脸框和置信度,常见做法是 RetinaFace、YOLOv5 的人脸变体,或者 OpenCV 自带的 DNN 人脸检测器。识别(recognition)解决的是身份问题:把检测出的那一小块人脸区域归一化到 112×112 之类的固定尺寸,再送进特征提取网络,输出一个高维向量。
把两步分清楚,工程上的收益很直接:检测模型可以带活体属性(比如是否戴眼镜、人脸角度),识别模型对图像质量不敏感;检测帧率和识别帧率也可以分开调。系统空闲时段把检测频率降到每秒 2 帧,识别只在检测到新面孔时才触发,整体负载能降一半以上。反过来,如果用一个端到端模型同时输出“框、身份、活体分数”,短期看着省事,真到部署阶段会发现每个子任务的阈值耦合在一起,很难单独调。
选型上我一般会按“检测用轻量的、识别用预训练好的开源权重”的原则,而不是自己做训练。微软的 InsightFace 里带了一组部署友好的模型包,RetinaFace 负责检测,ArcFace/MobileFaceNet 负责提取 512 维特征。依赖只有 onnxruntime,不需要在部署机上再配一套训练框架。这一点对实验室这种需要拷贝给师弟师妹的交付场景尤其重要。
| 方案 | 采集设备成本 | 非接触程度 | 防代签能力 | 与监控系统融合度 |
|---|---|---|---|---|
| IC 卡 | 低 | 差,要刷卡 | 代刷无法杜绝 | 差,需另设门禁 |
| 指纹 | 中 | 一般 | 较强 | 差,需专属设备 |
| 二维码 | 低 | 好 | 截屏可代扫 | 差 |
| 人脸识别 | 中(摄像头即可) | 最好 | 中等,需活体检测 | 强,监控画面复用 |
2.2 用特征向量而不是分类标签,距离阈值是入场券
分类式人脸的常规套路是 Softmax 输出“这个人是谁”,改成新成员就得重训网络。签到场景天然属于“人员会变”的情况——每学期有新的助教、新的课题组学生,训练一轮成本远高于特征比对的成本。所以实际采用的都是度量学习路线:识别网络输出的既不是编号也不是姓名,而是一个 512 维特征向量,系统里预先存好每个人的向量,对比时算余弦相似度或欧式距离。
对实验室场景,我通常把默认阈值设在 0.45 到 0.55 之间(余弦相似度,越高越相似)。这个区间比做门禁的单位更宽松,因为实验室内光线固定、没有逆光,而且签到并不需要绝对防冒用,只要能把漏签率压下来就好。阈值设置有一个基本规律:降低阈值会减少漏检(防止明明在场却没记上),但会增加误识别(把 A 记成 B);升高阈值反之。实际调参时不要只看平均值的准确率,要看两位长相接近的学生之间会不会互相串,这一般要用你自己的现场照片跑一遍比对矩阵才能确认。
比对过程本身不存在模型推理,就是点乘,所以签到系统在人数不多的情况下瓶颈几乎永远不在模型,而在摄像头解码和磁盘写入。这个特性决定了后端可以用很轻的架构,一台带有集成显卡的普通工控机就能支撑 4 路摄像头的并发比对。
2.3 监控模块是行为事件识别,先定事件再选模型
实验室监控里“有人没走”、“深夜闯入”、“长时间无人在岗”,这三类需求背后对应着不同的识别策略。单纯的目标检测只能回答“画面里现在有没有人”,回答不了“这个人趴在桌上一动不动已经 20 分钟了”。后一种情况需要的是动作识别,常规做法是姿态估计 + 时间序列判断:先用轻量级姿态估计模型(如 MediaPipe BlazePose)提取人手、头、躯干的关键点坐标,再按帧序列统计关键点位移。位移小于阈值的时间累计超过设定时长,就触发“疑似晕倒或长时间不动”的事件。
也可以完全跳过姿态模型,用目标框中心点变化做简化:每隔 10 秒记录一次检测框中心坐标,连续 12 次中心点距离小于 8 像素,就认为人长时间未移动。这个方法看起来粗糙,却足够稳定,CPU 占用极低,适合实验室夜间值守这种不需要精细动作判断的场合。真需要识别“翻窗”这种特定动作,再上姿态估计模型,否则不要让高开销的模型常驻内存。
事件定义是这类系统设计中最先要做的事。我会把所有要监控的行为列成一张表,每条记录包含触发条件、优先级、响应动作。举两个例子:“夜间 23:00 后出现人脸”对应高优先级,动作是弹窗 + 短信通知;“早晨 8:30 前无人到达指定机位”对应一般优先级,动作是记录到日报。把这些规则写进配置文件,比把所有逻辑堆在代码里更容易维护,也方便管理员自己调整。
3. Python 深度学习环境配置:从 Miniconda 到 PyTorch 的最小可跑链路
3.1 用 conda 固定 Python 版本,避免系统和项目互相污染
部署文档压缩包解压后的第一件事,就是解读环境依赖。这类项目最常见的形态是 Python 3.9/3.10 + PyTorch 2.x + onnxruntime + OpenCV + InsightFace 组合。我建议直接用 Miniconda 创建独立环境,不让项目依赖混进系统 Python。系统自带 Python 往往被其他工具占用,强行升级 pip 里的包容易把系统脚本弄坏,conda 环境则把这种风险完全隔开。
创建环境的命令如下:
conda create -n lab_attendance python=3.10 -y conda activate lab_attendance pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install opencv-python onnxruntime insightface fastapi uvicorn pymysql这里 torch 和 torchvision 指定了 CUDA 11.8 的预编译版本,因为实验室显卡驱动的兼容性比最新版更可控;如果你的机器只是 CPU 跑,把第二个 pip 命令换成pip install torch torchvision不带索引地址即可。onnxruntime 是 InsightFace 模型包的推理后端,不需要额外装 CUDA 库,它自带 CPU/GPU 两种执行提供程序。最后的 fastapi 和 uvicorn 用于把签到能力封装成 HTTP 接口,方便 Web 端展示考勤记录。
这段命令背后的选型理由:签到的检测与识别推理用 onnxruntime 跑,训练或微调才用到 PyTorch。InsightFace 加载模型后可以直接输出检测框、关键点、特征向量,不用自己拼多个模型。如果你之后打算从头训练一个人脸识别模型,再回到 PyTorch 的 DataLoader 和训练循环,但那是第二步。
3.2 最小的人脸签到代码:检测、特征、比对
环境装好后的第一步,不是直接抄整个项目,而是用一个最小脚本验证“读图 → 检测 → 提特征 → 比对”四步是否通。下面的代码用 InsightFace 完成检测与特征提取:
import cv2 import numpy as np from insightface.app import FaceAnalysis # 加载检测+识别模型,buffalo_l 是官方预训练包 app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) # 读取一张含人脸的现场照片 img = cv2.imread("lab_photo.jpg") faces = app.get(img) for face in faces: # bbox 是 [x1, y1, x2, y2] 的整数坐标,det_score 是检测置信度 bbox = face.bbox.astype(int) det_score = face.det_score # normed_embedding 是单位化的512维向量,直接用产品化部署特征库 emb = face.normed_embedding print(f"bbox={bbox.tolist()}, score={det_score:.3f}, emb_dim={emb.shape}") # 把特征向量存成 .npy,后面做比对时直接加载 np.save(f"feat_{face.face_id if hasattr(face, 'face_id') else 0}.npy", emb)这段脚本验证了三件事:模型能否在当前机器上加载,摄像头或照片中的人脸能否被检出,特征向量能否被导出。det_size=(640, 640)是检测分辨率,太小会漏掉远处人脸,太大则推理耗时急剧上升;实验室室内监控通常 640 够用。ctx_id=0只在有可用 GPU 时生效,providers=['CPUExecutionProvider']则强制走 CPU。二者保留其一即可,我是故意留了两行方便你切换验证环境。
人脸比对时,加载库中所有人的特征向量,计算余弦相似度:
known_embs = [np.load(f) for f in glob.glob("feat_*.npy")] query_emb = np.load("query_feat.npy") sims = [np.dot(q, k) for k in known_embs] # 单位向量的点积即余弦相似度 best_idx = int(np.argmax(sims)) print("best_similarity=", round(sims[best_idx], 3))如果最高相似度高于预设阈值(比如 0.45),就认为签到成功;否则提示“未登记人员”。这里要说明一个工程细节:normed_embedding已经做过单位化,所以直接用点积表示余弦值,不需要再调cosine_similarity。阈值千万不要写死进代码,放进配置文件或环境变量,后面调参时只改一处。
3.3 VSCode 里接环境与 CUDA 不匹配的三个常见报错
代码写完后,最常卡住人的是 VSCode 的解释器没有指向 conda 环境,导致 import 的包都是另一个环境的。在 VSCode 里按Ctrl+Shift+P,输入Python: Select Interpreter,选择lab_attendance环境即可。命令行直接运行python inference.py一般没问题,但用 F5 调试时会走到默认解释器,这一点务必提前确认。
排查环境问题时,我一般先区分三类报错:第一类是一启动就报ModuleNotFoundError,说明包没有装进当前环境;第二类是报No module named 'torch'但明明装过,多半是解释器选错;第三类是程序运行时提示 CUDA 相关错误——这种最隐蔽,因为 Python 层面不会报语法错误,而是在模型加载时崩溃。常见的处理方式如下表:
| 报错现象 | 原因 | 处理方式 |
|---|---|---|
ImportError: No module named 'onnxruntime' | 解释器没切到 conda 环境 | 重选解释器,或执行pip list看包属不属于当前环境 |
CUDA error: no kernel image is available for execution on the device | 显卡驱动太旧,与预编译的 cu118 不匹配 | 把推理切到 CPU;或重装对应驱动版本 |
cv2.VideoCapture(0) 打开黑屏 | 摄像头是 USB 且被其他进程占用 | 杀掉占用进程,或改用cv2.CAP_DSHOW后接cv2.VideoCapture(0, cv2.CAP_DSHOW) |
还有一个和深度学习无关但要提醒的点:实验室多个摄像头同时长时间连接时,Windows 下容易遇到VideoCapture对象不稳定,表现为偶发黑帧。处理方式是给采集线程设置连续读 3 帧、取最近 1 帧的策略,不要用read()一次就更新一次界面,否则识别结果会闪跳。这个主题的完整工程化方案,下一步就会涉及。
4. 自动签到与监控系统的工程实现:从人脸识别到行为告警
4.1 数据表设计:人员表、签到表、告警表
识别链路跑通后,系统就变成一个常规的信息管理工程。数据层我一般设计三张核心表:人员表 storage、签到记录表 attendance、告警表 alert。人员表存学生/教职工的学号、姓名、角色,以及一条主特征向量(可以为二进制或 Base64 字符串)。特征向量直接存库比存文件更省事,迁移时不会出现文件丢路径的问题。
CREATE TABLE person ( id INT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) UNIQUE NOT NULL COMMENT '学号/工号', name VARCHAR(50) NOT NULL, role TINYINT DEFAULT 1 COMMENT '1=学生 2=教师 3=管理员', feature BLOB COMMENT '512维特征向量的npy二进制', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE attendance ( id INT PRIMARY KEY AUTO_INCREMENT, person_id INT NOT NULL, event_type TINYINT NOT NULL COMMENT '1=签到 2=签退', captured_at DATETIME NOT NULL COMMENT '识别时间', camera_id VARCHAR(20) DEFAULT 'cam_01', snap_path VARCHAR(255) COMMENT '抓拍帧保存路径', INDEX idx_captured (captured_at), INDEX idx_person (person_id) ); CREATE TABLE alert ( id INT PRIMARY KEY AUTO_INCREMENT, alert_type VARCHAR(50) NOT NULL COMMENT '夜间闯入/长时间无人/离岗超时', detail TEXT, occurred_at DATETIME NOT NULL, is_handled TINYINT DEFAULT 0, INDEX idx_occurred (occurred_at) );字段类型的选择上,feature用 BLOB 是因为特征向量本质是二进制文件;snap_path保存抓拍帧路径而不是图片二进制,方便后续在浏览器里直接通过静态目录预览;camera_id预留是为了后期扩展多摄像头,避免在代码里写死来源。所有时间字段统一用 DATETIME,不引入时区概念,因为实验室系统不出本地网,时区只会增加排错成本。
4.2 摄像头采集与识别解耦:队列 + 异步任务
直接在主线程里循环读帧并调用模型,识别期间新的画面帧会堆积在摄像头缓冲区,时间一长就会出现“画面延时 5 秒”的假死现象。更稳妥的做法是把摄像头读取和推理拆开:采集线程只负责读帧并将最新帧放入一个定长队列;推理线程负责从队列取帧,做人脸检测、特征比对、写库。队列用 Python 标准库queue.Queue即可,长度控制在 4~8 帧,超过就丢弃最旧的帧。
import queue import threading import cv2 frame_q = queue.Queue(maxsize=8) def capture_loop(camera_id=0): cap = cv2.VideoCapture(camera_id, cv2.CAP_DSHOW) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1280) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 720) while True: ok, frame = cap.read() if not ok: continue if frame_q.full(): try: frame_q.get_nowait() # 丢弃最旧帧,保持实时性 except queue.Empty: pass frame_q.put(frame) def infer_loop(): app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) while True: frame = frame_q.get() faces = app.get(frame) # faces 非空时,进入特征比对、写考勤、抓拍保存这段逻辑的关键是maxsize=8与满时丢弃策略。监控图像的实时性比完整性重要,少一帧不影响识别结果,但队列阻塞导致推理线程卡住,会直接拖垮整条链路。CAP_PROP_FRAME_WIDTH设置为 1280×720 兼顾了远处小目标的检出率与推理耗时,不需要盲目上 2K;分辨率翻倍带来的检测精度提升有限,耗时却接近翻倍。
后台保存抓拍帧时,需要控制磁盘占用。单帧 JPG 大约 200~400KB,如果每次识别都保存,一天下来轻松超过 20GB。常见做法是只在“新事件”时存档——比如该人当天第一次出现、或触发告警,普通签到只写记录不存帧。这样日志依然可追溯,磁盘压力却只有原来的十分之一。
4.3 到岗统计与异常告警怎么算
签到记录写进attendance表后,业务层要解决两个统计问题:一是某人某天在岗时长怎么算,二是异常事件怎么定义。
在岗时长的计算口径影响结果很大。有人 9:01 签到、18:00 签退,中间外出 2 小时,按首末次时间相减得 9 小时,这显然虚高。诚实的计算方法是把当天该人的所有签到、签退记录按时间排序,相邻记录配对成“在岗区间”,再累加。由于人脸签退不一定可靠(人走时不刷脸),我一般按“连续 30 分钟检测不到该人”作为一次签退事件,这样算出的时长更接近真实。
对实验室考勤系统而言,下面几类规则最常用,优先级从高到低排列,高优先级触发时可以直接打断低优先级的流程:
| 事件名称 | 触发条件 | 响应动作 |
|---|---|---|
| 夜间闯入 | 当天时间在 23:00~次日 6:00,检测到未授权人脸 | 立即保存抓拍帧 + 短信通知管理员 |
| 长时间无人 | 日间工作时段内,连续 4 小时无人脸出现 | 写入日报,不实时打扰 |
| 离岗超时 | 已签到人员连续 60 分钟未再检测到 | 记录一条离岗日志 |
这里的判断逻辑全部在 SQL 或规则引擎层完成。比如离岗超时用一条查询即可算出:
SELECT person_id, MAX(captured_at) AS last_seen FROM attendance WHERE captured_at > NOW() - INTERVAL 60 MINUTE GROUP BY person_id;如果某人的last_seen超过 60 分钟,就判定离岗。这个写法简单高效,不需要维护内存里的状态字典,适合重启后能完整恢复现场。多摄像头时把camera_id加进 GROUP BY,可以精确到“人从这个摄像头视野走到了另一个视野”,避免同一个人的连续检测被误判成两个新事件。
4.4 部署要点:进程守护与资源限制
这种系统的生产部署不像上层 Web 应用那么复杂,常见做法是:一台普通办公机装好 Python 环境,模型常驻内存,用 Supervisor 守护采集+推理进程,对外提供一个 FastAPI 接口给 Web 端查询。不需要容器化,因为 onnxruntime 与摄像头驱动都依赖宿主机硬件,Docker 反而增加了一层设备映射的复杂度。
FastAPI 服务端的关键参数是uvicorn.run(app, host="0.0.0.0", port=8000, workers=1),这里必须用单 worker。多个 worker 会各自加载一份模型,内存翻倍且队列数据各持一份;workers=1再加上异步接口,应对实验室内部几十个并发查询没有问题。下面是 Supervisor 的一个最小配置:
[program:lab_attendance] command=/opt/miniconda3/envs/lab_attendance/bin/python /opt/lab_attendance/src/main.py directory=/opt/lab_attendance autostart=true autorestart=true startsecs=5 startretries=3 redirect_stderr=true stdout_logfile=/var/log/lab_attendance.outautorestart=true的价值在于摄像头偶发断流、程序异常退出时能够自动恢复。如果进程挂了但 Supervisor 没有拉起来,早上到实验室看到的就可能是“门开了但系统一夜无记录”,这种故障的代价比一次识别失败大得多。另外建议在main.py里注册atexit回调,把退出原因写进日志,排查“为什么凌晨 3 点进程重启过”时很有用。
5. 签到准确率验证与活体检测:上线前先做这两件事
5.1 用一段带标注的现场视频算 Top1 准确率
模型在自己机器上跑得动,不代表放进真实实验室就好用。我会在目标摄像头位置录一段 3~5 分钟的视频,覆盖不同时刻的自然光、人员走动、一米外与三米外的人脸,然后人工标注每帧里出现的是谁,再用下面的脚本统计 Top1 准确率。
import json, glob import numpy as np from insightface.app import FaceAnalysis app = FaceAnalysis(name='buffalo_l', providers=['CPUExecutionProvider']) app.prepare(ctx_id=0, det_size=(640, 640)) with open("video_annotation.json", "r") as f: ground_truth = json.load(f) # {"frame_0001.jpg": "zhang", ...} correct = 0 total = 0 for img_path, label in ground_truth.items(): img = cv2.imread(img_path) faces = app.get(img) if len(faces) == 0: continue # 视频里确实没人,跳过该帧 # 这里用最大人脸框作为主要识别目标 best_face = max(faces, key=lambda x: x.bbox[2] - x.bbox[0]) emb = best_face.normed_embedding sims = [float(np.dot(emb, k)) for k in known_embs] pred = names[int(np.argmax(sims))] total += 1 if pred == label: correct += 1 print(f"top1_accuracy over {total} frames: {correct / max(total, 1):.2%}")运行后如果准确率低于 90%,先调检测分辨率,再看角度。监控摄像头位置太高,人脸俯仰角过大,特征提取效果会明显下降,这时把摄像头调整到与人脸平齐或略高的位置,比换更强的模型更有效。还建议记录每个人员的最低相似度,输出一张表——相似度最低的那个人的数值,就是系统在真实光线下的脆弱点所在,比一个整体准确率更有诊断价值。
5.2 活体检测:别让一张照片把系统打穿
单靠静态人脸比对,一张打印照片就能通过签到。实验室场景最常见的攻击就是学生把手机照片放在摄像头前。最轻量的防护是“动作活体”:识别到人脸后,要求人员完成一次指定动作(眨眼或左右转头),在 3 秒内检测到关键点变化即通过。InsightFace/MediaPipe 都能输出眼部关键点,不必额外引入深度相机硬件。
动作活体的实现成本很低,只依赖一个人脸关键点检测模型,每秒推理一次即可。它带来的一个副作用是签到耗时增加 2~3 秒,在实验课高峰期门口会排一小队。所以常见做法是区分时段:白天松散时段不启用活体,只在夜间告警和重要实验设备开机前要求活体验证。多一个开关,比让管理员一直忍受体验损失要好。
帧缓冲本身也是个值得调整的运维参数。摄像头采集线程的frame_q.maxsize调大后可以缓冲更多帧,避免推理慢时丢帧;但调太大会导致识别结果延迟到几秒后,体验很差。一般建议按“推理一帧耗时的 5 倍”来设:推理 80ms 就设 maxsize=8,100ms 就设 10。先测单帧推理耗时,再定队列长度的做法,会让在线调试少走很多弯路。
本文还有配套的精品资源,点击获取