简介:这是一份面向计算机、人工智能及相关专业毕业设计的人脸识别签到系统完整项目资料,尤其适合需要快速搭建可演示原型、撰写设计文档或完成课程设计的学生。资源覆盖从人脸数据采集、模型训练、特征提取到签到记录管理的全流程,由Python后端逻辑、前端页面模板、SQLite数据库、模型权重及配置文件共同构成,共38个文件,主要类型为py脚本、html页面、dat权重、master配置和readme说明,压缩包约203MB,目录结构清晰,便于按模块理解与二次开发。目前已有109人学习下载。借助该资料可直接运行基础版本,观察人脸注册、识别比对、签到数据持久化等核心环节的具体实现;通过阅读py源码与html模板,还能了解模型加载、请求处理、页面交互与数据库表设计等工程细节。requirements依赖清单与README可帮助快速搭建运行环境,同时可作为毕业设计说明书的功能模块划分参考,对理清深度学习项目从数据到落地的工程化流程很有价值。
1. 教室门口的点名靠人喊、公司考勤靠刷工卡,基于深度学习的人脸识别签到系统就是把这事自动化
教室点名靠人喊、递纸条,公司考勤靠刷工卡,慢不说,还挡不住代签。基于深度学习的人脸识别签到系统的思路很直接:摄像头拍到人脸,模型把这张脸压缩成特征向量,再和注册库里的人脸特征做比对,比对通过就自动写入一条签到记录。这个课题每年都有大量毕设参考项目在网上流传,可一旦自己动手就会发现,最花时间的根本不是训练模型,而是把环境配通、把识别阈值调准、把签到判重做对。换句话说,这套方案的核心价值不在“深度学习”四个字本身,而在怎么把识别结果稳定地变成一条可信的考勤记录。如果你是正在做毕设的学生,或者第一次接触人脸识别的开发者,这篇笔记按“选型 → 注册 → 识别 → 排错 → 交付”的顺序,讲一条能在两周内跑通的落地路径。
2. 技术选型:ArcFace、FaceNet 与 OpenCV 传统方法,毕设到底选哪个
2.1 从摄像头到签到记录:人脸识别系统的完整数据链路
一个完整的人脸识别签到系统,数据链路是固定的:摄像头拿到视频帧,先做人脸检测,找到画面里哪几个位置有人脸;再做人脸对齐,用关键点把歪头、侧脸校正到标准姿态;然后做人脸特征提取,把一张人脸压缩成一个固定长度的向量;接下来拿这个向量和注册库里所有向量做比对,找到最像的那个;最后把比对结果和签到业务接起来,写入考勤记录。
这条链路里,检测、对齐、特征提取三个环节在现在的开源生态里都已经是“拿来即用”,不需要你自己设计网络、采集训练数据。毕业设计真正要写的不是模型,而是调度逻辑和签到业务。所以先别急着搭 PyTorch 训练代码,把这条链路的每个环节都跑通,比什么都重要。
特别要注意对齐这一步。很多人只听说过检测和识别,忽略对齐,结果侧脸照片识别率一塌糊涂。常见做法是让推理引擎输出 5 个关键点(左眼、右眼、鼻尖、左嘴角、右嘴角),再做仿射变换把眼睛拉到同一水平线。InsightFace 的 FaceAnalysis 在内部已经做了这件事,这也是我推荐选它的原因之一。
2.2 模型选型对比:ArcFace、FaceNet 与 OpenCV 传统方法
结论放前面:毕设做这个题,首选 ArcFace,也就是 InsightFace 项目里那套基于 margin loss 的识别模型。
| 维度 | ArcFace(InsightFace) | FaceNet | OpenCV LBPH/Eigenfaces |
|---|---|---|---|
| 特征维度 | 512 | 128 | 无固定向量 |
| 预训练权重 | 官方一键下载 | 社区权重质量不齐 | 无需权重 |
| 光照鲁棒性 | 好 | 较好 | 差,换光就崩 |
| 工程生态 | 自带检测+对齐+识别 | 需另配检测器和对齐 | OpenCV 内置,最简单 |
| 答辩友好度 | 高,能讲 margin loss | 中 | 低,追问就露馅 |
ArcFace 的过人之处在于角度间隔损失,它在归一化特征上给正确类别加一个角度余量,让同类特征在超球面上聚得更紧,异类分得更开。实际效果就是:512 维特征做余弦比对,正常光照下同一个人的相似度轻松到 0.6 以上,不同人通常在 0.3 以下,阈值很好选。
FaceNet 是 triplet loss 路线的代表,特征只有 128 维,速度不错,但官方权重多年没有大更新,社区流传的模型质量参差不齐,而且它本身不带检测器,你还得再找一个人脸检测和对齐方案,链路长了一截。真要用,也一定要把对齐做好,否则效果比 ArcFace 差不少。
OpenCV 自带的 LBPH、Eigenfaces 属于传统方法,不用下载任何模型就能跑,新手很容易图省事选它。但它的特征表达对光照极其敏感,教室窗边、走廊灯下完全两种效果,老师多问两句“深度学习体现在哪”就答不上来。做毕设选题,别在这上面省事。市面上还有一些封装得特别彻底的现成人脸识别工具,安装即用,能刷脸能签到,但内部是个黑匣子,答辩讲不出深度,也不适合当核心实现。
顺带说一句编程语言的选择:Python 在深度学习生态里最顺手,模型调用、图像处理、界面骨架都有现成库,毕设阶段就用 Python,别用 C++ 折磨自己。以后要嵌入式部署,再考虑把推理迁移到 C++ 端。
2.3 Ubuntu 20.04 与 Windows 下的深度学习环境配置:PyTorch 与 ONNX Runtime 的搭配
装环境是第一个劝退点,很多人在 conda 里把包装乱了,最后全部推到重来。我一般会建一个全新环境,Python 固定用 3.10,不要用系统默认 Python。
conda create -n face_sign python=3.10 -y conda activate face_sign pip install torch torchvision pip install insightface onnxruntime-gpu opencv-python numpy PyQt5先解释一下为什么这样装。insightface 负责人脸检测、关键点和特征提取,onnxruntime 负责跑 ONNX 格式的推理。这里其实不需要 PyTorch 参与推理,纯 ONNX 跑更快更稳,装 torch 只是为了有些调试代码和别的依赖会用到。如果你的电脑没有 NVIDIA 显卡,就把 onnxruntime-gpu 换成 onnxruntime,torch 装 CPU 版,识别照常能跑,只是速度慢一些。
GPU 版的一个重要注意点:onnxruntime-gpu 对 CUDA 和 cuDNN 版本有对应关系要求,装完必须检查推理引擎是否真正加载了 GPU。很多人的毕设翻车点就在这里——错装了不匹配的 onnxruntime-gpu,运行时报 CUDA 加载失败,程序又没报错,静默回退到 CPU,性能反而比纯 CPU 更差。安装后马上执行一行检查代码:
import onnxruntime as ort print(ort.get_available_providers())看到CUDAExecutionProvider在列表里才算成功。这个检查动作应该写进环境搭建清单里,别嫌麻烦。
模型权重方面,InsightFace 首次运行时会自动下载预训练模型包,如果网络不稳定下到一半失败,重跑命令即可,它支持断点续传。下载完成后本地会有一个模型目录,后续离线也能用。这个模型包自带检测网络和识别网络,等于把链路的前半段一次性配齐了。
3. 注册阶段:用 InsightFace 把人脸照片变成 512 维特征库
3.1 注册照片采集脚本:命名规范与多角度原则
签到系统要先知道“谁是谁”。注册阶段干两件事:采集人脸照片,提取特征入库存。这个流程一般离线进行,找一台带摄像头的笔记本就能完成。
我建议每个参与者采集 3 张照片:正面一张,头向左偏约 20 度一张,向右偏约 20 度一张。为什么要多角度?因为现场摄像头不会恰好拍到完美正面,多存几张再取平均,特征更稳。采集时保证顺光,别让窗户在身后,背景里不要有其他人脸出没。
import cv2 import os STUDENT_ID = "2021001_zhangsan" # 改成 学号_姓名,中间用下划线 SAVE_DIR = "dataset/register" os.makedirs(SAVE_DIR, exist_ok=True) cap = cv2.VideoCapture(0) count = 0 while True: ret, frame = cap.read() if not ret: break cv2.imshow("face capture: press s to save, q to quit", frame) key = cv2.waitKey(1) & 0xFF if key == ord("s"): filename = os.path.join(SAVE_DIR, f"{STUDENT_ID}_{count}.jpg") cv2.imwrite(filename, frame) count += 1 print("saved:", filename) elif key == ord("q"): break cap.release() cv2.destroyAllWindows()这个脚本把学号姓名写成了固定常量,而不是每按一次就输入一次,好处是批量采集多人时分批执行,命令不用反复改。按s存一张,按q退出。窗口里能看到取景画面,方便人把脸对准。
提示:命名规则直接决定后面的特征归组,学号_姓名之间用下划线分隔,文件名里不要再出现空格和中文符号。
3.2 用 InsightFace 批量提取人脸特征:核心代码与参数说明
照片采完以后,批量提取特征并保存成一个特征库文件。这个环节是整个系统里“深度学习”浓度最高的一段代码,但写起来反而不长:
import os import numpy as np import cv2 import insightface from insightface.app import FaceAnalysis # 注册阶段用 CPU 即可,离线批量处理不需要 GPU app = FaceAnalysis(name="buffalo_l", providers=["CPUExecutionProvider"]) app.prepare(ctx_id=0, det_size=(640, 640)) register_dir = "dataset/register" feature_dict = {} for file in os.listdir(register_dir): path = os.path.join(register_dir, file) if not file.lower().endswith((".jpg", ".jpeg", ".png")): continue img = cv2.imread(path) faces = app.get(img) if len(faces) == 0: print(file, "no face, skip") continue # 多人同框时,按框面积取最大的人脸,避免把路人当主角 if len(faces) > 1: faces = sorted( faces, key=lambda f: (f.bbox[2] - f.bbox[0]) * (f.bbox[3] - f.bbox[1]), reverse=True, ) # normed_embedding 是 L2 归一化后的 512 维向量 emb = faces[0].normed_embedding # 文件名形如 2021001_zhangsan_0.jpg,按学号_姓名归组 key = "_".join(file.split("_")[:2]) feature_dict.setdefault(key, []).append(emb) print(key, "embedding dim:", emb.shape) # 每人多张照片取平均,再归一化一次,特征更稳 names, embs = [], [] for key, vecs in feature_dict.items(): mean_emb = np.mean(np.vstack(vecs), axis=0) mean_emb = mean_emb / np.linalg.norm(mean_emb) names.append(key) embs.append(mean_emb) np.savez_compressed("face_db.npz", names=np.array(names), embs=np.vstack(embs)) print("saved face_db.npz, total:", len(names))脚本逻辑分四步:遍历注册目录;对每张图调用app.get做检测;取最大人脸框的特征;按学号_姓名归组并平均。这里的参数值得多说几句。
det_size=(640, 640)是送入检测网络的输入分辨率,分辨率越大越能检出小脸、远脸,耗时也随之增加。注册阶段没有实时要求,用 640 稳住;现场识别如果嫌慢,可以降到 (320, 320)。name="buffalo_l"是 InsightFace 的预训练模型包名,包含检测和识别两部分,首次运行会自动下载权重。
多人同框时按 bbox 面积排序选最大的人脸,这条逻辑很关键。注册照片背景里走过一个人,或者同学凑过来看屏幕,都可能造成同框,不排序就会把特征存到路人脸上。每人多张照片取平均,是新手里值得做但容易忽略的一步,平均后再归一化一次,现场识别对姿态变化的容忍度会明显提高。
3.3 特征库存储选型:npz、SQLite 与 Faiss 怎么选
特征提取完要落盘。最短路径是存成 npz 压缩文件,里面就两个数组:names(学号_姓名字符串列表)和embs(N×512 特征矩阵)。加载时np.load一下,直接和摄像头实时特征做矩阵乘法,代码最少、加载最快。10 到 20 个人的毕设演示,用 npz 完全够。
想显得更工程化,可以把学生信息放进 SQLite 表,特征向量以 BLOB 形式存储在列里,签到写入也走 SQLite,整体更统一。缺点是每次匹配要把全部特征从库里读出来再反序列化,人少没问题,人多就需要索引了。
再往上可以提 Faiss 这类向量检索库,百万向量毫秒级返回。但毕设不需要真接,答辩老师问“如果全校一万人都注册了,你还这么比对吗”,你能答出“可以接 Faiss 建立索引做 ANN 检索”就已经拿到分了。
我的默认组合是:特征库用 npz,学生名单放 SQLite。演示稳、代码少、答辩也能讲清楚。如果时间充裕,再把特征也挪进 SQLite,工作量多几十行,但系统完整性会更强。
4. 签到阶段:实时识别、相似度阈值与 SQLite 考勤记录
4.1 实时摄像头识别循环:检测、提特征、算相似度的最小代码
注册库建好之后,进入核心环节:连续从摄像头取帧,对人脸做检测、特征提取、比对,在视频上画出名字。这一步是可运行的完整脚本:
import cv2 import numpy as np import insightface from insightface.app import FaceAnalysis # 现场识别优先尝试 GPU,失败会自动回退 CPU app = FaceAnalysis( name="buffalo_l", providers=["CUDAExecutionProvider", "CPUExecutionProvider"], ) app.prepare(ctx_id=0, det_size=(640, 640)) data = np.load("face_db.npz", allow_pickle=True) names = data["names"] embs = data["embs"] # 形状 (N, 512),已经归一化 def match(embedding, thresh=0.5): # 归一化后做点积,就是余弦相似度 scores = embs @ embedding idx = int(np.argmax(scores)) if scores[idx] >= thresh: return names[idx], float(scores[idx]) return "unknown", float(scores[idx]) cap = cv2.VideoCapture(0) frame_count = 0 while True: ret, frame = cap.read() if not ret: break frame_count += 1 # 跳帧:每 3 帧只识别一次,保证画面流畅 if frame_count % 3 != 0: cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break continue faces = app.get(frame) for face in faces: name, score = match(face.normed_embedding) if name == "unknown": continue bbox = face.bbox.astype(int) cv2.rectangle(frame, (bbox[0], bbox[1]), (bbox[2], bbox[3]), (0, 255, 0), 2) cv2.putText(frame, f"{name} {score:.3f}", (bbox[0], bbox[1] - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.8, (0, 255, 0), 2) cv2.imshow("attendance", frame) if cv2.waitKey(1) & 0xFF == ord("q"): break cap.release() cv2.destroyAllWindows()这段代码的核心是match函数。embs @ embedding是矩阵乘法,因为特征全部做过 L2 归一化,点积就等于余弦相似度。分数最高的注册人就是最像的人,分数达不到阈值就一律算 unknown。0.5 只是一个初始值,不同光线、不同摄像头下需要按下一章的方法校准。
跳帧逻辑看似不起眼,却是流畅度的关键。每 3 帧做一次识别,另外两帧直接显示,CPU 占用明显下降,画面观感反而更顺。人脸不会在两三帧之间变成另一个人,识别频率不需要太高。
这里要提醒:app.get(frame)每调用一次会跑完整的检测+识别网络,默认配置下普通 CPU 耗时约 200 到 500 毫秒,GPU 上几十毫秒。如果发现程序一卡一卡,先别怀疑电脑,检查是不是没有跳帧、没有缩检测分辨率。
4.2 签到判重与迟到判定:SQLite 表结构与插入逻辑
识别出是谁之后,不能直接往数据库里插记录。教室里一个人会在画面里出现几十秒到几分钟,不判重的话一条签到会被写成好几条,考勤统计直接没法看。常见做法是用时间窗口去重:同一人在同一门课最近 5 分钟内已签到,就不再重复写。
import sqlite3 from datetime import datetime, timedelta conn = sqlite3.connect("attendance.db") cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS students ( id TEXT PRIMARY KEY, name TEXT NOT NULL ) """) cur.execute(""" CREATE TABLE IF NOT EXISTS attendance ( record_id INTEGER PRIMARY KEY AUTOINCREMENT, student_id TEXT NOT NULL, course_id TEXT NOT NULL, check_time TEXT NOT NULL, status TEXT NOT NULL ) """) conn.commit() def do_checkin(student_id, course_id, within_minutes=5): # 先查最近几分钟内是否已经签过 threshold_time = datetime.now() - timedelta(minutes=within_minutes) cur.execute(""" SELECT record_id FROM attendance WHERE student_id = ? AND course_id = ? AND check_time >= ? LIMIT 1 """, (student_id, course_id, threshold_time.strftime("%Y-%m-%d %H:%M:%S"))) if cur.fetchone(): return False, "already checked in" # 迟到判定:拿签到时间与课程开始时间比较 now = datetime.now() current = now.strftime("%H:%M") course_start = "08:30" status = "on_time" if current <= course_start else "late" cur.execute(""" INSERT INTO attendance(student_id, course_id, check_time, status) VALUES (?, ?, ?, ?) """, (student_id, course_id, now.strftime("%Y-%m-%d %H:%M:%S"), status)) conn.commit() return True, status表结构只有两张最核心的:students存学号和姓名,attendance存每次签到记录。第一次运行就执行CREATE TABLE IF NOT EXISTS,应用崩溃重启不会丢表。
do_checkin的两个细节值得说明。时间比较用 Python 的 datetime 而不是 SQLite 的时间函数,这样不同操作系统上行为一致。迟到判定拿签到时间和课程开始时间做字符串比较,前提是两者都是 HH:MM 格式;课程跨天或者有多个时段时,要先把开始时间拼上日期,这里从简。
课程 id 和签到学生 id 在完整系统里应该由界面传入,而不是识别出来就硬编码。但毕设第一个版本,先把写入链路打通最重要。到课率统计也简单,一条 SQL 就能出结果:按课程分组、统计去重学生数。
4.3 用 PyQt5 搭最小演示界面:摄像头画面与签到状态一屏显示
命令行里跑识别脚本,老师看不到“系统”的样子。PyQt5 是给这套系统补一张皮的最快方式,界面只需要三个元素:摄像头画面、识别结果文本、签到按钮。
import cv2 import numpy as np from PyQt5 import QtGui, QtWidgets, QtCore class CheckinUI(QtWidgets.QWidget): def __init__(self, app, names, embs): super().__init__() self.app = app # FaceAnalysis 实例 self.names = names self.embs = embs self.video = QtWidgets.QLabel("camera preview") self.state = QtWidgets.QLabel("waiting...") self.btn = QtWidgets.QPushButton("check in") layout = QtWidgets.QVBoxLayout(self) layout.addWidget(self.video) layout.addWidget(self.state) layout.addWidget(self.btn) self.cap = cv2.VideoCapture(0) self.timer = QtCore.QTimer(self) self.timer.timeout.connect(self.on_frame) self.timer.start(30) def on_frame(self): ret, frame = self.cap.read() if not ret: return # 简化版:直接复用 4.1 的 match 逻辑,每帧都识别 results = [] for face in self.app.get(frame): scores = self.embs @ face.normed_embedding idx = int(np.argmax(scores)) if scores[idx] > 0.5: results.append(f"{self.names[idx]} {scores[idx]:.3f}") self.state.setText("; ".join(results) if results else "no one") rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = rgb.shape qt_img = QtGui.QImage(rgb.data, w, h, ch * w, QtGui.QImage.Format_RGB888) self.video.setPixmap(QtGui.QPixmap.fromImage(qt_img))这段代码把识别逻辑直接放进QTimer回调,每 30 毫秒触发一次。人脸少的时候没问题,人脸多了 UI 会卡,因为识别是同步的。专业做法是开QThread在工作线程里跑识别,主线程只负责显示最新一帧;毕设演示若只面对一个人,用这个简化版足够。
签到按钮的回调里调用do_checkin,识别出的学号姓名作为参数传进去,返回成功后把界面状态改成类似“张三 已签到 08:31”。按钮和识别逻辑通过实例变量连接,比在回调里重新读摄像头要干净。界面不需要漂亮,演示时让老师一眼看到“摄像头画面 + 识别结果 + 签到成功状态”这三件事就够。
5. 最容易翻车的 5 个坑:环境、阈值、卡顿、光线与答辩现场排查
这一章是血泪经验。很多毕设程序在我这里跑了一遍,翻车点几乎都集中在下面这五类。每一条都按现象、原因、解决的顺序写,可以直接当排查手册用。
5.1 GPU 版 PyTorch 装了却还在用 CPU 推理
现象:环境里明明装了 GPU 版 PyTorch,torch.cuda.is_available()也返回 True,但识别程序跑起来风扇不转,帧率很低,看起来像在裸奔。
原因通常有两个。一是 onnxruntime 装的是 CPU 版,而推理路径主要走 ONNX;二是 onnxruntime-gpu 装上了,但 CUDA/cuDNN 版本不匹配,初始化失败后静默回退。后一种更隐蔽,代码不报错,就是慢。
解决:先打印可用 provider 列表,确认CUDAExecutionProvider在里面;只有 CPU 就卸载重装 onnxruntime-gpu;如果列表里有 CUDA 也有 CPU,要把 CUDAExecutionProvider 放在 providers 列表第一位,让推理引擎优先选 GPU。
import onnxruntime as ort print(ort.get_available_providers())重装后还是没有 CUDAExecutionProvider,就去查 onnxruntime-gpu 的版本说明,找到对应 CUDA 版本的安装包。这个顺序能解决九成“装了 GPU 却用 CPU”的问题。
5.2 相似度阈值方向搞反,同一个人也提示签到失败
现象:同一个人反复识别,时成功时失败;陌生人的脸偶尔也会被签进去,或者干脆全部返回 unknown。
原因:最常见的还不是阈值设得不好,而是把相似度和距离搞反了。InsightFace 返回的是 L2 归一化向量,点积即相似度,越大越像;而很多文档里的 cosine distance 是 1 减去相似度,越小越像。拿余弦距离的阈值去卡相似度,结果必然全乱。
解决:把阈值当成参数而不是常量,用一段校准脚本打印注册人自身的相似度分布和与其他人的最大相似度分布。
for name, emb in zip(names, embs): all_scores = embs @ emb self_score = float(all_scores.max()) other_score = float(np.delete( all_scores, int(all_scores.argmax())).max()) print(name, "self:", round(self_score, 3), "other_max:", round(other_score, 3))看输出里 self 和 other 两组数。如果 self 全部在 0.6 往上,other 全部在 0.4 以下,阈值取中间 0.5 很稳;如果两分布有交叠,说明注册照片质量太差,或者现场光源和注册光源差异太大,先去改进采集,不要硬调阈值。玄学调参会越调越乱,有分布数据才好找交界。
5.3 现场识别一卡一卡,摄像头画面变成幻灯片
现象:摄像头画面一顿一顿,人一移动就拖影,识别结果半天才刷新一次。
原因:每一帧都调app.get,而且用默认 640 分辨率检测。CPU 机器上处理一帧要几百毫秒,一秒钟只来得及处理两三帧,观感就是幻灯片。
解决:三步走。第一,识别和显示解耦,每 3 帧只识别 1 帧;第二,把检测输入改小,app.prepare(det_size=(320, 320)),或者先把画面缩到宽 640 再识别;第三,把识别放到后台线程,UI 只负责显示。三招都用了还卡,就改成按键签到模式——人对着摄像头按一下才识别一次,帧率不再是瓶颈。对毕设演示来说,按键模式反而更稳定,老师不需要看实时高帧率,只需要看到“识别成功”这个结果。
5.4 逆光、侧脸、戴眼镜:注册照和现场照差太多
现象:白天教室窗边逆光,或者同学侧着坐,系统直接认不出,注册的时候明明好好的。
原因:识别模型的鲁棒性不是无限的。模型能容忍一定姿态和光照变化,但注册阶段只拍了一张正面照片,现场角度偏移超过 30 度,或者脸上有大片阴影,特征就跑到阈值边界外了。
解决:注册时一人多角度 3 张,取平均特征;现场识别前检查face.det_score,低于 0.5 就提示“请正对摄像头”,不要硬匹配;戴眼镜的同学注册时戴眼镜、现场摘了,或者反过来,建议注册时多采一组,因为眼镜对特征的影响比想象中大。特别提醒:口罩会把下半张脸的关键特征挡住,毕设系统不要承诺“戴口罩也能签”。老师问起来,老实说这是已知限制,比现场翻车好。
5.5 答辩现场模型加载慢、摄像头被占、演示黑屏
现象:答辩那天,双击启动程序后转圈十几秒才出画面;或者摄像头指示灯亮着但窗口黑屏;或者直接报错找不到文件。
原因:模型首次加载要初始化 ONNX Runtime 和分配内存,本来就慢;摄像头被其他软件占用后VideoCapture(0)打开失败,程序不报错但画面黑屏;代码里用了绝对路径,换个电脑就崩。这类问题跟算法无关,纯粹是现场工程准备不足。
解决:程序启动时先做一次空图预热,把模型加载时间前置到界面出现之前,后面调用会明显变快;视频源路径全部改成相对路径,用os.path.dirname(os.path.abspath(__file__))拼路径;答辩前录一段 20 秒的演示视频,代码里把VideoCapture(0)换成VideoCapture("demo.mp4"),摄像头出问题时立刻切换视频源。这是我的后悔药方案,救过不止一次。
提示:答辩前一晚把“删掉模型缓存后冷启动”完整跑一遍,能过滤掉一半以上的现场故障。
6. 交付之前:离线部署、活体检测和识别成功率自测
6.1 离线部署与活体检测:让系统能在一台普通电脑上跑起来
毕设交付时,程序要能在没有 GPU 的笔记本上跑。推理全部走 ONNX Runtime,模型文件放在程序目录下,启动时不依赖 PyTorch,这个取舍会让部署体积和启动速度都变得可控。老师问“怎么部署到教室那台旧电脑上”,也能答得清楚。
活体检测是这个题目绕不开的追问点:拿一张打印照片能不能签到?最小成本方案是引入静默活体模型,输入一张 RGB 图输出真脸概率;再简单一点,可以要求先完成一次眨眼动作再放行,用关键点估计眼睛开合度。如果时间不够,答辩时主动说一句“当前版本未包含活体检测,这是已知限制”,也好过被当场戳穿。这一层做完,系统才算从“人脸识别 demo”变成“签到产品原型”,后续还能往课堂状态检测、门禁机终端这类方向延伸。
6.2 用一张测试表把“能跑”变成“鲁棒”:识别成功率自测方法
答辩开始前,给系统做一次量化测试。邀请 10 位同学,每人注册 3 张照片,然后按场景各测 20 次,记录成功率,结果填成类似下面的表:
| 测试场景 | 测试人数 | 成功次数 | 成功率 |
|---|---|---|---|
| 正对摄像头,距离约 1 米 | 10 | 20/20 | 100% |
| 头部偏转约 30 度 | 10 | 18/20 | 90% |
| 逆光环境 | 10 | 16/20 | 80% |
| 连续 10 分钟不重复签到 | 10 | 20/20 | 100% |
| 未注册人脸误识率 | 5 | 0/20 | 0% |
这张表的价值在于,它把“系统还行”变成“正对摄像头成功率 100%,逆光 80%,未注册人脸误识为 0”这样可被评估的事实。老师问鲁棒性,直接把表递过去,比说任何漂亮话都有说服力。我第一次做同类课题,最后悔的就是把时间全耗在调模型上,没留一天做测试,答辩时被问“你这系统到底多可靠”只能含糊过去。后来补了这张表,演示也从“能跑”变成了“可信”。如果你也正在做这个方向,希望这篇笔记能帮你少踩几个坑。希望帮到你。
本文还有配套的精品资源,点击获取