简介:这份资源是面向高校学生与Python开发者的毕业设计级项目源码,主题为基于深度学习的人脸识别签到系统,适合作为课程设计、毕设参考或人脸识别入门实战案例。压缩包共27个文件,约102.26MB,以8个Python脚本为核心,配合7个HTML模板、4个数据文件及CSS、mako、ttc字体、ini配置、sqlite数据库等,覆盖后端逻辑、前端页面、模型文件与数据库迁移模块。项目已内置管理员初始化与数据库升级命令,初始账户为学号000000、密码666666,便于快速跑通完整签到流程。目前已有4467人学习下载,说明其参考价值较高。读者可从中获取人脸注册与识别、签到记录管理、用户增删改查、模板渲染与静态资源组织等完整实现思路,并借助requirements依赖清单与README说明快速搭建运行环境,适合需要完整项目结构、排错参考与二次开发基础的学习者。
1. 从一张打卡照片说起:人脸识别签到到底难在哪
很多团队第一次做人脸识别签到,都会掉进同一个坑:拿开源模型在实验室的正面光照图上跑出 99% 的准确率,兴冲冲部署到真实签到场景,结果员工侧脸走过、逆光进门、戴口罩抬头,识别率直接腰斩。问题不在模型不够深,而在于签到这件事本身是一个「活体 + 检索 + 业务闭环」的系统工程,不是单张图片的分类题。
基于深度学习的人脸识别签到系统,核心要解决三件事:一是把摄像头里抓到的人脸框出来并对齐,二是把这张脸映射成一个特征向量,三是在底库里做 1:N 检索并判定「是不是同一个人、是不是本人、要不要放行」。它适合谁?适合要做考勤、门禁、会议签到、工地实名核验的开发者,也适合想拿一个完整 CV 落地项目练手的人。这篇笔记按「先立住原理、再动手复现、最后讲坑」的顺序走,代码能抄,参数能改,边界也说清楚。
2. 人脸识别签到系统的技术选型:检测、对齐、特征、检索四段怎么搭
在动手写代码之前,选型决定了后面 80% 的返工量。人脸识别签到系统常见做法是拆成四段流水线:人脸检测、关键点对齐、特征提取、向量检索。每一段都有「轻量快」和「精度高」两条路线,选错了要么卡在帧率上,要么卡在误识率上。
2.1 检测环节:为什么我一般不用最重的模型
检测负责从整帧画面里找出人脸框。常见方案有基于锚框的检测器和无锚框的检测器,前者对小脸更稳,后者速度快、部署简单。签到场景的特点是摄像头位置固定、人脸尺度变化不大,所以没必要上最重的骨干网络。
我一般会选输入分辨率 320×320 或 640×640 的轻量检测模型,理由是签到是实时流,单帧推理超过 50ms 就会让画面卡顿,用户体验直接崩。检测阈值(置信度)建议从 0.5 起步,如果漏检多就降到 0.4,但要注意误检会跟着涨。下面是一个用常见推理框架加载检测模型的最小骨架:
import cv2 import numpy as np # 初始化人脸检测器,模型文件按实际路径替换 detector = cv2.FaceDetectorYN.create( model="face_detection_yunet.onnx", # 检测模型 config="", input_size=(320, 320), # 与训练分辨率对齐 score_threshold=0.5, # 置信度阈值,漏检多就降到 0.4 nms_threshold=0.3, # 非极大值抑制,重叠框去重 top_k=50 # 单帧最多保留的人脸数 ) def detect_faces(frame): h, w = frame.shape[:2] detector.setInputSize((w, h)) # 每帧更新输入尺寸 _, faces = detector.detect(frame) # faces: [x, y, w, h, 5关键点..., score] if faces is None: return [] return faces逻辑说明:setInputSize必须每帧调用,因为摄像头分辨率可能和初始化时不一致,忘了这步会出现框位置整体偏移的玄学问题。score_threshold和nms_threshold是两个最常调的参数,前者管「找不找得到」,后者管「同一个脸会不会出两个框」。参数说明:top_k设太大会拖慢后处理,签到场景一帧里通常不超过 10 张脸,50 足够。
2.2 对齐环节:五个关键点决定特征质量
检测出来的框是歪的,直接送进特征模型,识别率会明显下降。对齐就是用眼睛、鼻尖、嘴角这些关键点做仿射变换,把脸摆正。常见做法是用 5 点对齐,把双眼拉到水平、人脸缩放到固定尺寸(如 112×112)。
def align_face(frame, face, output_size=(112, 112)): # face 后 10 位是 5 个关键点 (x1,y1,...,x5,y5) landmarks = face[4:14].reshape(5, 2).astype(np.float32) # 标准 112x112 人脸的关键点参考位置 dst = np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtype=np.float32) # 计算相似变换矩阵并做仿射变换 M, _ = cv2.estimateAffinePartial2D(landmarks, dst) aligned = cv2.warpAffine(frame, M, output_size, borderValue=0.0) return aligned逻辑说明:estimateAffinePartial2D只估计旋转、缩放、平移,不估计剪切,这对人脸对齐足够且更稳。参数说明:output_size必须和特征模型训练时的输入一致,用 112×112 的模型却喂 96×96,特征会系统性偏移。对齐后建议做一次像素归一化,把 0-255 映射到 -1 到 1 或 0 到 1,具体看特征模型要求。
2.3 特征与检索:1:N 比对才是签到的核心
特征提取把对齐后的人脸变成一个固定长度向量,常见是 128 维或 512 维。签到是 1:N 检索,不是 1:1 验证,所以真正决定体验的是检索环节。底库有几百人时,直接算余弦相似度就够;上千人再考虑向量索引。
def cosine_similarity(a, b): a = a / np.linalg.norm(a) b = b / np.linalg.norm(b) return float(np.dot(a, b)) def recognize(embedding, gallery, threshold=0.38): # gallery: {name: embedding} best_name, best_score = "unknown", -1.0 for name, emb in gallery.items(): score = cosine_similarity(embedding, emb) if score > best_score: best_name, best_score = name, score if best_score < threshold: return "unknown", best_score return best_name, best_score逻辑说明:先归一化再点积,等价于余弦相似度,避免模长干扰。参数说明:阈值是签到系统最敏感的旋钮,常见余弦阈值在 0.35 到 0.45 之间,设高了本人被拒(体验差),设低了陌生人混进(安全差)。建议先用一批真实样本画一张「本人分数分布」和「陌生人分数分布」的直方图,取两条曲线交叉点附近的值。
3. 从零跑通签到闭环:底库构建、实时识别、签到落库
选型定了,接下来把整条链路串起来。这一章给的是能直接跑的最小闭环:先建底库,再做实时识别,最后把签到结果落库并去重。每一步都给出可抄的代码和参数解释。
3.1 底库构建:注册照片怎么采、特征怎么存
底库质量决定上限。常见做法是每人采 3 到 5 张不同角度、不同光照的照片,分别提特征后取平均,比单张更稳。注册时一定要做质量过滤,模糊、过暗、人脸太小的直接拒收。
import os import pickle def build_gallery(register_dir, extract_fn, min_face_size=80): gallery = {} for person in os.listdir(register_dir): person_dir = os.path.join(register_dir, person) if not os.path.isdir(person_dir): continue embs = [] for img_name in os.listdir(person_dir): img = cv2.imread(os.path.join(person_dir, img_name)) faces = detect_faces(img) if len(faces) != 1: continue # 注册图必须恰好一张脸 x, y, w, h = faces[0][:4] if min(w, h) < min_face_size: continue # 人脸太小,特征不可靠 aligned = align_face(img, faces[0]) embs.append(extract_fn(aligned)) if embs: # 多张特征取平均后再归一化 mean_emb = np.mean(embs, axis=0) gallery[person] = mean_emb / np.linalg.norm(mean_emb) with open("gallery.pkl", "wb") as f: pickle.dump(gallery, f) return gallery逻辑说明:len(faces) != 1这步过滤掉多人合影和漏检图,注册阶段宁可少收也不能脏收。参数说明:min_face_size设 80 是经验值,低于这个尺寸提出来的特征区分度明显下降。取平均前不做归一化、取平均后再归一化,顺序别反。
3.2 实时识别主循环:抽帧、跟踪、防抖
签到系统不能每帧都做识别,否则算力吃不消,还会因为单帧抖动导致结果跳变。常见做法是每隔 N 帧识别一次,中间用跟踪补位,并对连续结果做投票防抖。
import time from collections import deque, Counter def run_signin(camera_id, gallery, extract_fn, threshold=0.38, detect_interval=5, vote_window=5): cap = cv2.VideoCapture(camera_id) frame_idx = 0 history = deque(maxlen=vote_window) # 最近几次识别结果 last_result = ("unknown", 0.0) while True: ok, frame = cap.read() if not ok: break frame_idx += 1 if frame_idx % detect_interval != 0: continue # 跳帧,降低算力占用 faces = detect_faces(frame) for face in faces: aligned = align_face(frame, face) emb = extract_fn(aligned) name, score = recognize(emb, gallery, threshold) history.append(name) # 投票:窗口内出现次数最多的名字才算数 if len(history) == vote_window: name_vote, cnt = Counter(history).most_common(1)[0] if cnt >= vote_window - 1 and name_vote != "unknown": last_result = (name_vote, score) # 这里可把 last_result 交给落库模块 cap.release() return last_result逻辑说明:detect_interval控制识别频率,5 表示每 5 帧识别一次,摄像头 30fps 时约每秒 6 次,足够签到用。vote_window是防抖窗口,要求窗口内几乎一致才确认,避免单帧误识别触发签到。参数说明:detect_interval调小更灵敏但更吃 CPU,调大更省但响应变慢;vote_window调大更稳但确认更慢,签到场景 5 比较平衡。
3.3 签到落库与去重:别让同一个人刷出一屏记录
识别对了不代表业务对了。同一个人站在摄像头前,每秒可能被识别 6 次,如果每次都写库,考勤表会爆炸。常见做法是按「人 + 时间窗」去重,比如同一人 5 分钟内只记一次。
import sqlite3 import time def init_db(path="signin.db"): conn = sqlite3.connect(path) conn.execute(""" CREATE TABLE IF NOT EXISTS records ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, score REAL NOT NULL, ts INTEGER NOT NULL ) """) conn.commit() return conn def sign_in(conn, name, score, cooldown=300): now = int(time.time()) # 查询该人最近一条记录,判断是否在冷却期内 row = conn.execute( "SELECT ts FROM records WHERE name=? ORDER BY ts DESC LIMIT 1", (name,) ).fetchone() if row and now - row[0] < cooldown: return False # 冷却期内,忽略 conn.execute( "INSERT INTO records (name, score, ts) VALUES (?, ?, ?)", (name, score, now) ) conn.commit() return True逻辑说明:先查最近一条再决定是否插入,避免重复签到。参数说明:cooldown设 300 秒适合考勤,会议签到可以设 60 秒,门禁可以设 10 秒。注意score也存下来,方便后期排查误识。
4. 避坑与排查:签到系统上线后最常翻车的五件事
实验室能跑和现场能用是两回事。下面五条是我和身边同行踩过的血泪经验,每条按「现象 → 原因 → 解决」写,照着排查能省很多时间。
4.1 现象:白天好好的,傍晚逆光就集体识别失败
原因:摄像头逆光时人脸区域过暗,检测框还在但特征质量崩了,相似度整体下移,全部落到阈值以下。解决:一是调整摄像头安装位置,避免正对窗户或强光源;二是开启摄像头自带的宽动态(WDR)功能;三是在对齐后加一步直方图均衡或 Gamma 校正,把暗部提亮。注意 Gamma 校正的参数要小步调,调猛了会引入噪声。
4.2 现象:同一个人一会儿识别成 A,一会儿识别成 B
原因:底库里 A 和 B 长得像,或者注册照片质量差导致特征区分度不够,加上单帧识别没有防抖,结果在两人之间跳。解决:先检查底库,把相似度高的两人特征拿出来单独比对,如果本人和他人分数差距小于 0.05,说明底库需要补采;再确认防抖投票窗口是否生效,窗口太小等于没做。
4.3 现象:戴口罩后识别率断崖式下跌
原因:多数特征模型训练时口罩样本少,遮挡下半张脸后特征偏移严重。解决:一是换用对遮挡更鲁棒的模型;二是注册时补采戴口罩的照片进底库;三是把阈值适当下调,但下调会带来误识风险,必须配合活体检测一起用。这里没有后悔药,遮挡场景本质是信息缺失,只能靠数据和阈值折中。
4.4 现象:底库加到两千人后,识别一次要等两三秒
原因:1:N 检索是线性扫描,底库越大越慢,两千人乘上每次余弦计算,单次识别就上百毫秒,再叠加检测和特征提取,整体就卡了。解决:把线性扫描换成向量索引,常见做法是先做一次粗筛(如聚类或哈希)再精算;或者把特征维度从 512 降到 128,速度能快数倍,代价是精度略降。参数上,索引的召回率要单独测,别只看速度。
4.5 现象:照片对着摄像头也能签到成功
原因:只做了识别没做活体检测,攻击者拿一张打印照片或手机屏幕就能骗过系统。解决:加活体检测,常见有动作配合(眨眼、转头)和静默活体(基于纹理、反射)两条路线。签到场景推荐静默活体,体验好;安全性要求高的场合用动作配合。注意活体检测本身也有误拒,阈值要单独调,别和识别阈值混在一起。
5. 把签到系统做扎实:阈值标定、活体联动与灰度上线
前面把闭环跑通了,这一章讲怎么让它从「能跑」变成「敢用」。核心是三件事:用数据标定阈值、把活体和识别串起来、灰度上线观察真实指标。
先说阈值标定。很多人拍脑袋设 0.38,其实应该用数据说话。准备两组样本:一组是底库内本人的照片(每人多张),一组是底库外陌生人的照片。分别算最高相似度,画出两条分布曲线。理想情况下两条曲线分离明显,阈值取在两条曲线交叉点略偏本人一侧,这样误拒可控、误识也低。如果两条曲线大量重叠,说明底库质量或模型不行,调阈值救不了,得回去补数据或换模型。
def calibrate_threshold(genuine_scores, impostor_scores): # genuine_scores: 本人比对分数列表 # impostor_scores: 陌生人比对分数列表 import numpy as np g = np.array(genuine_scores) i = np.array(impostor_scores) # 简单策略:取本人分布的 5% 分位和陌生人分布的 95% 分位的中点 low = np.percentile(g, 5) high = np.percentile(i, 95) return (low + high) / 2逻辑说明:用分位数而不是均值,是因为分布往往有长尾,均值会被极端值带偏。参数说明:本人取 5% 分位意味着允许 5% 的误拒,陌生人取 95% 分位意味着允许 5% 的误识,这两个比例按业务安全等级调整。安全要求高的场合,把陌生人分位提到 99%。
再说活体联动。活体和识别不能各跑各的,否则会出现「活体过了但识别错人」或「识别对了但活体误拒」的割裂。常见做法是串行:先活体,过了再识别;或者并行:两个都过才放行。串行体验差但省算力,并行体验好但算力翻倍。签到场景我一般用串行,因为签到对延迟不敏感,对准确更敏感。
最后是灰度上线。别一上来就全量替换旧考勤,先选一个点位跑一周,重点看三个指标:误拒率(本人被拒的比例)、误识率(陌生人通过的比例)、平均识别耗时。误拒率高于 5% 就要查底库和光照,误识率高于 0.1% 就要查阈值和活体。这三个指标稳定了再铺开。
有个习惯我保持了几年:每次调完阈值或换模型,都把当天的识别日志抽样 100 条人工核对一遍,看有没有系统性偏差。这个动作很笨,但能抓到很多指标看不出来的问题,比如某个角度的人脸总是识别失败。做视觉落地,指标是仪表盘,抽样才是听诊器。希望帮到你。
本文还有配套的精品资源,点击获取