ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

人脸识别签到系统毕业设计:深度学习特征提取与活体检测完整方案

人脸识别签到系统毕业设计:深度学习特征提取与活体检测完整方案 简介基于深度学习的人脸识别签到系统是一份面向计算机专业毕业设计的高分项目源码包评审分为99分代码完整且可运行适合正在准备毕设、课程设计或期末大作业的本科学生也适合需要项目实战练习的开发者参考。资源共27个文件涵盖Python源码、HTML模板、SQLite数据库、DAT模型数据、CSS样式、配置文件及说明文档压缩包约101.48MB。其中源码包含主程序、API接口、模型构建与工具函数前端页面支持登录、添加用户、编辑用户、签到记录等场景数据库与迁移文件便于快速初始化和二次开发人脸识别模型和字体文件则为深度学习功能提供支撑。配套的README与文档说明可帮助小白理清整体架构和启动流程节省环境搭建与排错时间。目前已有159人学习下载适合作为毕业设计完整方案直接借鉴或在此基础上扩展。1. 为什么深度学习人脸识别签到成了毕设里的“标准答案”人脸识别签到系统几乎是计算机类毕业设计里最稳妥的题目之一算法上有深度学习CNN可展开工程上有摄像头采集、模型推理、数据落库一条完整链路答辩时还能现场跑演示。标题里这套“源码文档说明”的交付物对应的正是从开题到答辩都有东西可看、可讲、可验证的完整方案。我不止一次看到同学拿OpenCV级联分类器加直方图匹配做人脸签到在实验室日光灯下勉强能看换到教室门口就翻车。换成深度学习特征编码后同一个人在不同光照、角度下都能稳定认出多数场景识别率能到95%以上。这套方案适合正在做毕设的本科生也适合临时接了人脸门禁需求、手上没有现成经验的工程师。下面按技术选型、代码骨架、踩坑记录到论文写作的顺序把这个系统完整拆一遍。2. 从OpenCV直方图到深度学习特征人脸签到系统的技术选型这一章解决的是“为什么必须上深度学习”的问题。如果你去问上两届的学长会听到不少用OpenCV自带的LBPH特征做签到翻车的经历。先说清楚传统方案死在哪再讲深度学习方案的核心思路最后把整个系统的模块骨架搭出来后面几章的代码才有地方挂。2.1 传统方案为什么在教室门口翻车光照、姿态与人脸库规模很多教程会带你走到这一步pip install opencv-contrib-python然后调用cv2.face.LBPHFaceRecognizer_create()对每个人拍几张照片训练运行的时候算直方图距离。这套方案在单人、正面、固定光照的门禁场景下确实能跑但搬到教室里三个问题直接暴露。第一个是光照敏感。LBP特征对整体灰度变化有一定鲁棒性但教室靠窗和靠门的位置光照差异极大逆光时半边脸是黑的局部二值模式算出来的分布会整体偏移。第二个是姿态敏感只要人脸偏转超过30度直方图分布就大变样同学低头看手机签到系统直接认不出。第三个是库规模问题班里三四十个人注册进去之后类间距离和类内距离开始重叠误识别率明显上升。我见过最典型的翻车现场是同学把LBPH的阈值调到0.35结果班里两个体型相近的男生站在摄像头前互相被认成对方。原因就是LBP特征在轮廓相似时区分度不够它本质上是在描述纹理不是在描述“这个人是谁”。所以从那以后我给毕设的固定建议是传统OpenCV方案只用来做入口检测不要用来做身份识别。2.2 FaceNet / ArcFace的度量学习把一张脸压成一个向量深度学习人脸识别的核心思路是把“判断两张脸是不是同一个人”转成“比较两个向量的距离”。FaceNet是这个思路的代表作它用CNN把一张人脸图映射到128维向量空间训练时用三元组损失拉近同一个人的向量、推开不同人的向量。ArcFace在这个基础上改了分类损失给角度加margin让类间更分散特征维度通常是512。对毕设来说你完全不需要自己训练这些模型。直接用预训练权重把一张对齐后的人脸图喂进去拿到的embedding向量就是“人脸指纹”。判断是不是同一个人就计算当前向量和库里每个向量的欧氏距离或余弦距离距离小于阈值就算匹配上。这里有一个需要理解的细节这类模型训练时分类层输出的维度是人脸ID数但推理时要摘掉最后一层分类头取倒数第二层的特征向量。这个向量才是对“这张脸长什么样”的压缩表示。很多人看模型结构看得一头雾水其实就是没想明白“分类头只存在于训练阶段”这件事。方案特征维度光照鲁棒性姿态鲁棒性训练成本毕设工作量OpenCV LBPH直方图低低不需要小FaceNet128维中高中高需大数据集中ArcFace512维高高需GPU集群中偏大对毕设而言最划算的路径是先用FaceNet思路的现成库跑通全流程再根据导师要求决定要不要换成ArcFace重训。2.3 签到系统骨架检测→对齐→编码→比对→写库不管用哪套模型整个签到系统的数据流都是一条固定链路。摄像头取帧后先做人脸检测找到画面里的人脸框接着用5个关键点左右眼、鼻尖、左右嘴角做仿射变换把脸扶正归一化到固定尺寸然后送进CNN取特征向量再和注册库里所有向量比对取最小距离判断身份最后把学号、时间写入考勤数据库。检测环节最常见的选择是MTCNN、RetinaFace或者OpenCV自带的YuNet。对齐环节用的是关键点仿射变换这一步能明显提升编码器的鲁棒性同一张脸歪着头和正着脸最后拿到的向量会接近很多。编码环节是整条链路里计算量最大的部分CPU上单张图可能需要几百毫秒。比对环节本质上是暴力遍历加距离计算几十个人的库耗时可以忽略。写库环节反而是毕设里最容易被忽略但最能体现工程素养的部分。一个完整的模块划分是这样的摄像头帧 → 人脸检测 → 关键点对齐 → 归一化人脸图 → CNN特征向量 → 与库中特征比对 → 距离小于阈值 → 写入考勤表。下一章就按这个链路给出可以直接跑的代码。3. 从零搭起签到系统数据准备、模型调用与签到记录落库这一章是全文的作业核心。我会按“先采集数据、再生成特征库、再写签到判定、最后落库”的顺序贴代码每一段都带参数说明。你把这四段拼起来就是一个能运行的最小签到系统。3.1 用OpenCV采集人脸样本摄像头实时截帧与数据清洗采集阶段的目标是为每个同学攒下20到30张不同角度的人脸图。直接跑一个脚本打开摄像头检测到人脸就裁下来存盘按学号分目录。import cv2 import os save_dir dataset/20210001 # 以学号命名一人一个目录 os.makedirs(save_dir, exist_okTrue) cap cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) face_cascade cv2.CascadeClassifier(haarcascade_frontalface_default.xml) count 0 while count 30: ret, frame cap.read() if not ret: continue gray cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) faces face_cascade.detectMultiScale(gray, scaleFactor1.1, minNeighbors5) for (x, y, w, h) in faces: face frame[y:y h, x:x w] face cv2.resize(face, (160, 160)) cv2.imwrite(f{save_dir}/{count:03d}.jpg, face) count 1 print(f已采集 {count}/30) cv2.imshow(capture, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()这段代码先固定摄像头分辨率到640x480避免默认1080p导致USB带宽不足而黑屏。detectMultiScale的scaleFactor1.1表示每轮检测窗口缩小10%数值越小检测越精细但越慢minNeighbors5表示候选框至少被5个邻居确认才算人脸用来过滤误检。采集时有一个比数量更重要的细节让同学左右转头、抬头、低头各保持几秒保证样本里有角度变化。如果30张全是死板的正脸后面生成的模板会很脆弱。采集完花两分钟人工过一遍把闭眼、拖影、手挡住半张脸的照片删掉这个习惯能省下后面大量的调阈值时间。装OpenCV的时候如果遇到cv2导入报错多半是pip install opencv-python和opencv-contrib-python装混了卸载重装其中一个即可。3.2 加载预训练模型做特征编码把每张脸变成一个128维向量最常见的做法是用face_recognition库它封装了dlib的HOG人脸检测器和ResNet特征编码器几行代码就能把一张图变成128维向量。先把上一步采集的样本全部编码对同一个人的所有向量取平均作为该同学的注册模板。import face_recognition import numpy as np import os known_encodings {} # 学号 - 平均特征向量 for student_id in os.listdir(dataset): stu_dir os.path.join(dataset, student_id) if not os.path.isdir(stu_dir): continue encodings [] for img_name in os.listdir(stu_dir): img_path os.path.join(stu_dir, img_name) image face_recognition.load_image_file(img_path) face_locations face_recognition.face_locations(image) if face_locations: enc face_recognition.face_encodings(image, face_locations)[0] encodings.append(enc) if encodings: known_encodings[student_id] np.mean(encodings, axis0) np.save(known_encodings.npy, known_encodings, allow_pickleTrue)代码的关键在np.mean(encodings, axis0)。同一个人的多张照片算出的向量会有轻微波动取平均等于把噪声抹掉比只存单张图的向量要稳。这里是在注册阶段速度慢可以接受如果某些照片face_locations为空最常见的原因是脸太小或者侧脸超过90度直接跳过就好。这里提一个环境配置的坑face_recognition依赖dlibdlib需要本地编译。Windows下直接pip install dlib大概率报错需要先装好Visual Studio的C生成工具和CMake再执行安装。VSCode里刷红色波浪线不代表装错了先到终端确认import face_recognition能通过再回来调代码。另外注意如果你在用face_recognition自带的检测器时发现小脸经常漏检可以给face_encodings传modelcnn参数但推理阶段不建议开CPU上单张图耗时可能从几十毫秒涨到几百毫秒。3.3 距离判定与签到逻辑阈值调节和重复签到防刷特征库生成后主程序每帧做一次“编码比对”。比对就是对每个注册向量算欧氏距离取最小值再判断是否低于阈值。import face_recognition import numpy as np import cv2 from datetime import datetime import sqlite3 known np.load(known_encodings.npy, allow_pickleTrue).item() ENCODING_THRESHOLD 0.5 # 欧氏距离阈值越小越严格 def match_face(encoding): 返回 (学号, 距离)未匹配时学号为 None best_id, best_dist None, float(inf) for student_id, known_enc in known.items(): dist np.linalg.norm(encoding - known_enc) if dist best_dist: best_dist, best_id dist, student_id if best_dist ENCODING_THRESHOLD: return best_id, best_dist return None, best_distface_recognition官方给的参考阈值是0.6但那是针对大规模人脸库的经验值。教室签到这种场景0.45到0.55之间比较稳。0.6太宽松隔壁宿舍长得像的同学可能被误识别成同一个人0.4以下又会让同一个人低头、大笑时被判成陌生人。我的习惯是注册完先跑一遍全班正脸测试把最大类内距离和最小类间距离都打印出来阈值取两者的中间值。识别成功后进入签到逻辑这里必须处理“重复签到”的问题。def sign_in(student_id): now datetime.now() today now.strftime(%Y-%m-%d) conn sqlite3.connect(attendance.db) cur conn.cursor() cur.execute( SELECT id FROM attendance WHERE student_id? AND sign_date?, (student_id, today), ) if cur.fetchone(): conn.close() return False, 今日已签到 cur.execute( INSERT INTO attendance (student_id, sign_time, sign_date) VALUES (?, ?, ?), (student_id, now.strftime(%H:%M:%S), today), ) conn.commit() conn.close() return True, 签到成功这里用查数据库的方式判断当天是否已签到比在程序里维护一个内存set更可靠。程序一重启内存里的记录就没了数据库不会丢。即使如此后面建表时我仍然会加唯一约束双保险。3.4 把签到结果写进SQLite考勤表结构设计与事务处理数据库选SQLite是毕设场景下的最优解。CSV在连续写入时容易遇到文件占用问题而且按日期筛选要全量读文件SQLite是单文件数据库支持事务、唯一约束和SQL查询答辩演示时拷走一个.db文件就够了。CREATE TABLE IF NOT EXISTS attendance ( id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, student_name TEXT, sign_time TEXT NOT NULL, sign_date TEXT NOT NULL, UNIQUE (student_id, sign_date) );student_id保留字符串类型而不是整数是为了兼容学号可能带字母前缀的情况。student_name在这里是冗余字段正式做可以拆成学生表和考勤表两张表用外键关联但毕设演示阶段直接冗余一个名字字段写界面时少一次联表查询更省事。写入操作建议用带事务的批量接口方便以后扩展“补录签到记录”的功能。INSERT OR IGNORE会在唯一约束冲突时直接跳过不会抛异常中断程序。def add_records(records): conn sqlite3.connect(attendance.db) try: cur conn.cursor() cur.executemany( INSERT OR IGNORE INTO attendance(student_id, sign_time, sign_date) VALUES(?,?,?), records, ) conn.commit() except Exception as e: conn.rollback() raise e finally: conn.close()executemany批量插入比循环单条execute快一个数量级rollback()保证任何一条插入失败时之前的部分写入不会残留半截数据。到这一步最小可用系统已经跑通了。接下来要面对的是各种能把人逼疯的玄学问题第四章全是我踩过的坑。4. 人脸签到系统避坑指南5个让新手当场翻车的经典场景这一章不写原理只写现象、原因和解决办法。每一条都是我看见过或者自己踩过的按“现象→原因→解决”的结构写方便你到时候直接照着排查。4.1 现象摄像头画面卡死——分辨率与帧率参数的玄学跑起来前几秒画面正常半分钟后整个窗口冻住程序没崩但read()一直返回False。在教室用USB HUB连接摄像头时尤其频繁。原因是OpenCV默认的摄像头缓冲区只有1到2帧主线程做检测和编码太慢消费不过来缓冲区被占满后驱动就不投递新帧了。另外Windows上部分摄像头默认请求1920x1080的MJPEG流USB 2.0带宽撑不住这种持续传输。解决方法是固定采集参数并且主动降分辨率。cap cv2.VideoCapture(0, cv2.CAP_DSHOW) # Windows下强制走DirectShow cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) cap.set(cv2.CAP_PROP_FPS, 30) cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)CAP_PROP_BUFFERSIZE1是关键告诉驱动只保留最新一帧处理不过来就丢旧帧保证画面永远在走。识别链路宁可降到5帧每秒也不要让画面卡死这是演示现场最直接的体验分。4.2 现象同一个人经常识别失败——阈值调教与注册样本质量同学站在摄像头前系统一会儿认出人一会儿提示未注册同一个位置10次识别只有6次成功。原因通常是两个叠加阈值设得离类内距离太近以及注册样本全是正经正脸。模板学到的只是“正脸长相”一旦低头、大笑或者侧脸超过30度当前向量就跳出阈值球。解决办法分两步。先重新采集让同学在自然状态下左右转头、抬头、大笑凑够20到30张重新生成平均向量。再调阈值最稳的方法不是拍脑袋而是写一小段测试脚本把班里最像的两个人互相比对的距离打印出来阈值必须明显小于这个距离的一半。放宽到0.55甚至0.6可以接受但前提是确认误识别风险可控。注意调阈值之前先确认注册图片质量。模糊样本生成的平均向量本身就是糊的阈值怎么调都没用这种时候删掉重采比重写判定逻辑管用得多。4.3 现象教室灯一关画面全黑——光照预处理与硬件底限白天一切正常傍晚开灯也能识别一旦关了灯屏幕上整张脸黑成一团检测框都出不来。原因是弱光下人脸对比度太低关键点定位和特征编码同时劣化摄像头自动增益又把噪点放大了。解决方法是保留颜色信息的同时做亮度增强。常见做法是转到YCrCb色彩空间只对Y亮度通道做直方图均衡化再合并回去转回BGR。def preprocess(frame): ycrcb cv2.cvtColor(frame, cv2.COLOR_BGR2YCrCb) y, cr, cb cv2.split(ycrcb) y_eq cv2.equalizeHist(y) ycrcb_eq cv2.merge([y_eq, cr, cb]) return cv2.cvtColor(ycrcb_eq, cv2.COLOR_YCrCb2BGR)只均衡亮度通道不会破坏色彩比整图灰度均衡更自然。但要说实话软件预处理只能救到一定程度教室关了灯还想识别最终解决方案是换带红外补光的USB摄像头。我在这个坑上耗过一个晚上最后悟出来的经验是弱光场景的后悔药多数在硬件算法救不了物理。4.4 现象用一张打印照片就能骗过系统——活体检测缺失同学拿手机里存储的照片往摄像头前一放识别框亮起签到成功。整个链路只做“像不像”的比对根本没有验证屏幕前是不是真人。这是静态人脸识别场景的通病也是答辩评委最爱挑的刺。解决思路是先加一层眨眼检测。用68个面部关键点里的眼睛区域6个点计算眼睛纵横比EAR连续若干帧EAR低于阈值再恢复正常记为一次眨眼。签到动作要求2秒内连续两次眨眼照片就过不了这一关。def eye_aspect_ratio(eye): A np.linalg.norm(eye[1] - eye[5]) B np.linalg.norm(eye[2] - eye[4]) C np.linalg.norm(eye[0] - eye[3]) return (A B) / (2.0 * C)EAR一般在0.25到0.35之间闭眼时掉到0.2以下。完整实现放在第六章。这里提醒一点加了活体检测后页面提示要写“请眨眼”而不是直接弹出“识别失败”否则用户会以为系统坏了。4.5 现象识别耗时2秒被老师吐槽——向量检索优化与线程分离点“签到”后画面明显卡顿从按下到跳出结果要2秒。很多人误以为慢在全库遍历比对其实几十个人算欧氏距离是微秒级的事。真正慢的是检测器前向推理和编码器前向推理CPU上这两步加起来就要一两秒。每帧都全流程跑自然卡。解决思路是抽帧加线程分离。主线程只取帧和显示每隔几帧才送一帧进检测检测到人脸后把裁剪图丢进队列给编码线程比对结果异步回传。队列设置最大长度处理不过来时直接丢帧。face_queue queue.Queue(maxsize2) def encode_worker(): while True: face face_queue.get() enc face_recognition.face_encodings(face)[0] result_queue.put(enc) # 主循环里每隔N帧往里丢一次队列满就丢弃这一帧“丢帧保流畅”的思路在答辩现场非常关键。现场环境比实验室复杂哪怕识别结果出得慢画面实时在动观感就好非常多画面一卡评委的第一印象就坏了。我把识别前的人脸图压到112x112单次编码耗时会明显下降代价是远距离小脸可能识别率变差这个取舍要自己权衡。5. 从功能到高分毕设源码文档怎么组织才算“有工程味”代码能跑只是及格线。毕设评分看的从来不只是“能不能识别”而是源码结构、文档完整度和答辩演示。这一章讲怎么把一套能跑的代码组织成让评委觉得“像个工程”的交付物。5.1 模块化拆分config / detector / encoder / database / ui 五件套评委最先看的不是算法是目录结构和入口文件。2000行的main.py和结构清晰的模块化工程第一印象完全不同。我最常用的组织方式是把职责拆成五个模块加三个脚本。face_sign/ ├── config.py # 所有阈值、分辨率、路径集中管理 ├── detector.py # 摄像头取帧 人脸检测 对齐预处理 ├── encoder.py # 特征编码、向量比对 ├── database.py # SQLite读写、签到记录查询 ├── ui.py # Tkinter签到界面 ├── collect_data.py # 采集新同学人脸样本 ├── register_student.py # 采集完生成特征模板 ├── sign_in.py # 主程序入口 ├── dataset/ # 按学号分目录的原始人脸图片 ├── known_encodings.npy # 注册特征库 └── attendance.db # 考勤数据库config单独放的意义在于答辩现场调阈值不用改代码打开config改一个常量重跑就行。detector、encoder、database三个模块之间只通过参数传递数据不互相import这样哪一环出问题能立刻定位。入口文件sign_in.py只负责组合这些模块逻辑总量控制在100行以内评委一眼能看懂你的分层思路。5.2 文档的“黑匣子”写法从研究背景到实验结果的自洽链条论文或说明文档最容易犯的毛病是“我用了某某模型所以很厉害”的空话。高分写法是把技术选型做成对比表说清楚为什么不用传统方案把实验设计成可复现的表格记录同一个同学在几种光照条件下各识别10次的结果再在分析里明确指出失败样本集中在哪个环节比如侧脸超过60度时漏检这比单纯报一个99%的识别率可信得多。文档里最该花时间的是实验表格。一段典型的实验结果可以长这样场景识别10次成功平均识别耗时失败原因统计白天自然光100.8s无傍晚室内灯91.1s1次侧脸角度过大弱光加红外补光101.0s无手机照片攻击20.6s活体检测未启用时误识别最后一行“照片攻击失败2次”不要删掉这个诚实反而能在答辩时变成加分点顺势引出你在活体检测上做的改进。评委最怕的是学生把系统吹得完美一个追问就露馅。5.3 答辩前必测的3个演示场景注册新脸、多人并发、认错人回滚演示场景一现场注册新脸。随机叫一位评委从采集界面到生成特征模板再到成功签到整个流程要控制在60秒以内。注册时让评委转头边操作边解释“多角度样本能让模板更稳定”这是最自然的算法展示。操作超过两分钟的话说明交互设计太绕回去改界面。演示场景二两人同框并发签到。很多实现只写了单张脸的处理逻辑两人同框时只取faces[0]另一个人直接被忽略。答辩现场这个场景被点到的概率极高必须先测。正确写法是在主循环里遍历所有检测框分别识别。faces detector.detect(frame) for box in faces: face_img crop_and_align(frame, box) enc encoder.encode(face_img) sid, dist matcher.match(enc) if sid: db.sign_in(sid)演示场景三认错人回滚。答辩现场光线复杂误识别在所难免。万一系统把A识别成B写进了考勤你要能当场打开管理功能删除记录并让B重新签到。SQLite里的删除操作只有一行但必须提前测过。DELETE FROM attendance WHERE student_id? AND sign_date?;删除后唯一约束里不再有该学号和日期的组合新签到可以正常写入不会出现“已签到不能重复签”的死锁。这三个场景全部走通答辩演示的基本盘就稳了。6. 给签到系统加一道活体防线眨眼检测与照片攻击防御人脸识别签到被问得最多的问题永远是“拿张照片能不能签”。第四章提过简单的EAR眨眼检测这一章把它补完整。直接把校验逻辑插在“识别成功之后、写库之前”的位置不满足连续两次眨眼就不落库。def verify_blink(eye_landmarks, frames_needed2): blink_count 0 eye_closed False for frame_landmarks in eye_landmarks: ear_left eye_aspect_ratio(frame_landmarks[0:6]) ear_right eye_aspect_ratio(frame_landmarks[6:12]) ear (ear_left ear_right) / 2.0 if ear 0.2 and not eye_closed: eye_closed True elif ear 0.25 and eye_closed: blink_count 1 eye_closed False return blink_count frames_needed注意这里用了状态切换而不是单纯数低值帧数因为照片不会产生完整的“闭眼再睁开”状态变化。frames_needed2要求两秒内完成两次眨眼这个参数对正常人约1到2秒对演示来说节奏刚好。界面提示用“请眨眼”识别成功后活体校验不通过就停在等待状态不要直接判失败。我最早做签到系统的时候只跑通了识别就把活体检测当彩蛋留着没上。结果演示当天被一张打印照片当场打脸从那以后我把活体检测放到和识别同等重要的位置。这套方案做完不管是毕设还是实际人脸门禁场景至少照片攻击这条路是堵上了。希望帮到你。本文还有配套的精品资源点击获取
返回列表