
简介一份基于FaceNet与RetinaFace构建的人脸识别管理系统完整项目主要面向毕业设计、课程设计场景适合需要快速搭建人脸识别应用的学生和开发者。项目覆盖人脸检测、特征提取、相似度匹配等核心流程并配有前端界面与后台管理便于直接运行与二次开发。资源包共646个文件约57.39MB以242个PNG图片、172个Java文件、64个Vue组件、34个JS脚本和18个Python脚本为主同时包含PTH模型权重、SQL数据库脚本、XML配置及GIF演示截图等涵盖前端页面、后端逻辑、模型文件与数据初始化结构清晰可按模块阅读理解。目前已有448人学习下载。对于正在筹备毕设或课设的同学可通过本项目掌握RetinaFace的人脸检测框定位与FaceNet特征嵌入原理学习数据增强、阈值设定等实际调参思路并借助完整的代码和演示素材快速整理出项目文档和答辩材料。1. 毕设选型先看这套组合FaceNetRetinaFace 为什么成了默认答案每到毕设季人脸识别方向的题目十有八九绕不开两个名字FaceNet 和 RetinaFace。FaceNet 负责把一张人脸编码成 128 维特征向量RetinaFace 负责在画面里找到人脸并给出五个关键点坐标。这套组合几乎囊括了“人脸识别门禁系统设计”从采集到比对的全部核心链路既能避开从头训练深度模型的巨额时间成本又能在答辩时把检测、对齐、特征匹配每一步都讲清楚。适合选课设或毕设但不想只做个玩具界面的你——不仅跑得通还留得下足够多的技术点用来写论文、画架构图、回答评委提问。2. 先懂再动手RetinaFace 和 FaceNet 的分工与串联逻辑2.1 RetinaFace 输出人脸框和关键点要框更要关键点RetinaFace 是 InsightFace 团队在 2019 年开源的人脸检测器输入一张完整图片输出人脸的边界框、置信度以及五个关键点坐标——右眼、左眼、鼻尖、右嘴角、左嘴角。在人脸识别管理系统的场景里检测框只是过程产物真正值钱的是关键点。因为后续的 FaceNet 对输入图片的裁剪位置和旋转角度非常敏感一张歪着头拍的图直接框出人脸塞进网络特征向量的质量会明显下降。RetinaFace 在结构上属于单阶段检测器主干通常用 ResNet 或 MobileNet加上特征金字塔和上下文模块。它判别能力强主要是因为训练时除了框和关键点回归还引入了像素级的人脸解析和 3D 形状重建作为辅助监督信号。你在毕设里不需要重头训练它但要清楚它的推理输出长什么样。用 insightface 库调用时每个检测结果有kps字段shape 为 5x2顺序就是右眼、左眼、鼻尖、右嘴角、左嘴角。对比 MTCNN 的话RetinaFace 在侧脸、遮挡、暗光下漏检率更低。对后面要做的人脸识别门禁机场景来说漏一帧就可能让识别体验差一截。我一般把检测置信度阈值设在det_thresh0.5附近光线不好的场景再往下探到 0.2。还有一个细节det_size(640, 640)表示把输入图长边压到 640如果画面里的人脸很小建议把det_size提到(1120, 1120)代价是单帧推理时间明显上涨。2.2 FaceNet 把对齐后的人脸压缩成 128 维向量FaceNet 是 Google 2015 年提出的特征嵌入模型核心概念是把一张人脸映射到一个 128 维的欧氏空间里让同一个人的不同照片在空间里离得近不同人的照片离得远。做到这一点的关键是训练时用三元组损失Triplet Loss。拿锚点、正样本、负样本三张图分别过网络希望锚点与正样本的距离比锚点与负样本的距离小出一个固定间隔margin。训练好的 FaceNet 本质上是特征提取器毕设里不用碰训练直接加载预训练权重做推理即可。常见实现是 facenet-pytorch 库里的 InceptionResnetV1预训练于 VGGFace2输入 160x160 RGB 图像输出 512 维特征。注意这个 512 维和原版 FaceNet 的 128 维不是一回事不同实现输出的维度和向量尺度规范都不一样阅读任何参考代码前先确认它用的是哪种模型。这里有个很常见的疑问InsightFace 也提供 ArcFace为什么很多毕设题目点名要 FaceNet我的经验是ArcFace 精度通常更高配合 buffalo_l 包几乎零改动就能跑通但 FaceNet 在论文叙事上有天然优势——它的欧氏距离语义可以直接画特征分布图、做相似度可视化、解释“人脸的语义空间”。ArcFace 的余弦边界偏判别式讲清楚需要额外篇幅。如果导师希望你从损失函数角度推导FaceNet 更好展开如果只要一个能跑的成品ArcFace 更省事。毕设选 FaceNet很多时候是论文好写而不是精度最优。2.3 串联顺序不能反检测 → 对齐 → 裁剪 → 编码把两个模型串起来时顺序决定了精度上限。正确顺序是原图 → RetinaFace 检测出框和关键点 → 用关键点做仿射变换把人脸摆正 → 缩放到 160x160 → 过 FaceNet 编码。中间的对齐环节最容易被跳过很多人直接拿框抠图就用结果换一张光线不同的照片就识别不对。对齐的原理不复杂。RetinaFace 给出的五个关键点坐标是脸在图像里的实际位置预先定义一组“标准脸”关键点坐标用 OpenCV 的estimateAffinePartial2D算出一个变换矩阵把实际关键点映射到标准位置最后对整个图像做 warp。标准五点的经典坐标输出图 160x160是REF_PTS np.array([ [38.2946, 51.6963], # 右眼 [73.5318, 51.6963], # 左眼 [56.0252, 71.7366], # 鼻尖 [41.5493, 103.7189], # 右嘴角 [70.7299, 103.7189] # 左嘴角 ], dtypenp.float32)这套坐标几乎被所有人脸识别算法开源项目沿用直接照抄即可。为什么对齐这么关键因为 FaceNet 训练时见过的人脸基本都经过类似预处理两眼在同一水平线。输入分布偏移一小步输出特征就飘一大步。在人脸识别领域有个共识对齐的好坏对最终准确率的影响常常大于换一个更深的骨干网络。完整链路的五步可以这样理解faces detector.detect(img) # 1. RetinaFace 检测 face choose_main_face(faces) # 2. 选主脸 aligned align(face.kps) # 3. 关键点仿射对齐 emb encoder.encode(aligned) # 4. FaceNet 编码 save_to_db(emb) # 5. 入库或比对上面的encoder.encode在下一篇代码里会展开。两个模型可以分开指定运行设备RetinaFace 放在 CPU 上跑ONNX Runtime速度足够FaceNet 放 GPU反过来也可以。内存允许的前提下给每个模型各建独立的推理会话避免共享线程池互相等待。3. 本地跑通最小闭环Python 实现注册与识别3.1 环境与模型加载提前确认这三件事在敲代码之前先确认三件事能省掉大半天的安装排错时间。第一Python 版本建议 3.9-3.11别用太新或太老的版本insightface 和 onnxruntime 对过新的 Python 支持会有延迟。第二确定跑 CPU 还是 GPU。RetinaFace 的 ONNX 模型在 CPU 上单帧推理约 30-80msFaceNet 约 20-40ms纯 CPU 满足演示需求如果你的电脑有 N 卡再装 CUDA 版 onnxruntime体验会顺很多但别一开始就上 GPU先确认 CPU 链路能通。安装依赖pip install insightface onnxruntime opencv-python numpy pip install facenet-pytorch pillowinsightface会拉取检测模型包默认从 GitHub Release 下载buffalo_l.zip并解压到~/.insightface/models/buffalo_l/。如果下载失败可以让同学把已下载的buffalo_l目录整个拷给你放进对应位置。facenet-pytorch的预训练权重首次加载会从网络下载 VGGFace2 的模型文件放在~/.cache/torch/checkpoints/下耐心等一次就好。测试图片的准备也要认真。找一张清晰的正面照光照均匀、五官完整、无遮挡作为注册样本再准备一张同人不同角度或光照的图片作为识别测试。注意一个高频翻车点OpenCV 默认读进来是 BGRFaceNet 训练时用的是 RGB很多人的识别问题就出在忘记转换通道上。下面的代码里我统一用cv2.imread的 BGR 图传给 RetinaFace再在送入 FaceNet 前转成 RGB。3.2 注册流程检测、对齐、提取特征、落库注册的输入是一张照片输出是一条特征记录。我习惯把每个步骤封装成独立函数方便单独调试。下面这段代码覆盖了注册流程的全部环节import cv2 import numpy as np import sqlite3 import torch from PIL import Image from torchvision import transforms from insightface.app import FaceAnalysis from facenet_pytorch import InceptionResnetV1 # ---- 初始化两个模型 ---- app FaceAnalysis(namebuffalo_l, providers[CUDAExecutionProvider, CPUExecutionProvider]) app.prepare(ctx_id0, det_size(640, 640)) facenet InceptionResnetV1(pretrainedvggface2).eval() # 预处理缩放、转张量、归一化与预训练权重配置一致 transform transforms.Compose([ transforms.Resize((160, 160)), transforms.ToTensor(), transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5]) ]) # 标准人脸关键点坐标对齐目标 REF_PTS np.array([ [38.2946, 51.6963], [73.5318, 51.6963], [56.0252, 71.7366], [41.5493, 103.7189], [70.7299, 103.7189] ], dtypenp.float32) def align_face(img_bgr, face, output_size(160, 160)): RetinaFace 关键点做仿射变换返回对齐后的人脸图 src_pts face.kps.astype(np.float32) M, _ cv2.estimateAffinePartial2D(src_pts, REF_PTS) aligned cv2.warpAffine(img_bgr, M, output_size, flagscv2.INTER_LINEAR) return aligned def extract_feature(aligned_bgr): 送入 FaceNet 前转 RGB输出 L2 归一化特征向量 aligned_rgb cv2.cvtColor(aligned_bgr, cv2.COLOR_BGR2RGB) pil_img Image.fromarray(aligned_rgb) input_tensor transform(pil_img).unsqueeze(0) with torch.no_grad(): emb facenet(input_tensor).cpu().numpy().flatten() return emb / np.linalg.norm(emb) # 再归一化一次保证一致 # ---- 注册一张照片 ---- img_bgr cv2.imread(register_zhangsan.jpg) faces app.get(img_bgr) # insightface 直接吃 BGR 图 if len(faces) 0: print(未检测到人脸请重新拍摄) else: aligned align_face(img_bgr, faces[0]) emb extract_feature(aligned) conn sqlite3.connect(face_db.db) conn.execute(CREATE TABLE IF NOT EXISTS faces (id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, feature BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP)) conn.execute(INSERT INTO faces (name, feature) VALUES (?, ?), (zhangsan, emb.tobytes())) conn.commit() conn.close() print(f注册成功, 特征维度{emb.shape}, f检测置信度{faces[0].det_score:.3f})代码里有几个参数值得说透。det_size(640, 640)控制 RetinaFace 的内部处理分辨率人脸在画面中占比小时可以提到(1120, 1120)但耗时成倍上升。det_thresh没有显式设置时insightface 默认大约在 0.5注册阶段我会临时降到 0.2 提高召回因为宁可多返回几个候选框也不要把用户拒之门外。providers列表里 CUDA 放前面显卡可用时优先 GPU纯 CPU 环境改成[CPUExecutionProvider]并把prepare的ctx_id改成 -1。align_face里的face.kps是 RetinaFace 输出的关键点顺序和REF_PTS一一对应。estimateAffinePartial2D对五点做最小二乘拟合得到一个只允许平移、旋转、缩放、反对称剪切的 2x3 矩阵含义是把这位朋友的眉眼挪到标准位置。warpAffine用矩阵对全图变换统一输出到 160x160。3.3 识别流程向量检索与阈值判定识别就是把新照片的特征拿去和库里所有特征比对。由于特征是归一化向量点积就是余弦相似度。阈值是关键参数同一个人的相似度通常在 0.6-0.8不同人大多落在 0.1-0.4两者有重叠区阈值就放在重叠区靠前的位置。def recognize(img_path, threshold0.55): 与 face_db.db 中所有特征比对返回 (姓名, 相似度) img_bgr cv2.imread(img_path) faces app.get(img_bgr) if len(faces) 0: return None, -1.0 aligned align_face(img_bgr, faces[0]) q_emb extract_feature(aligned) conn sqlite3.connect(face_db.db) rows conn.execute(SELECT name, feature FROM faces).fetchall() conn.close() best_name, best_score None, -1.0 for name, feat_blob in rows: s_emb np.frombuffer(feat_blob, dtypenp.float32) score float(np.dot(q_emb, s_emb)) if score best_score: best_score score best_name name if best_score threshold: return best_name, best_score return None, best_score name, score recognize(test_zhangsan_2.jpg, threshold0.55) print(f识别结果: {name}, 相似度: {score:.3f})这段代码的比对策略是“全表扫描 Python 循环”。几十人的库没问题几百人时每次识别可能要几百毫秒。想提速把全库特征在启动时读成一个大矩阵一次矩阵乘法得到全部相似度这个优化思路在第 5 章的“批量比对卡顿”里展开。暂时先注意一点recognize和注册用的必须是对齐 归一化完全一致的链路否则分数整体偏移阈值就变成玄学。如果注册时做了 L2 归一化而识别时没有或者反过来你调几天阈值都稳定不下来。4. 从脚本到管理系统把识别能力装进毕设作品4.1 系统功能拆解采集、注册、识别、管理四条业务线跑通最小闭环后工作量全在工程侧。一个能上台演示的人脸识别管理系统至少包含四条业务线摄像头采集、人员注册、实时识别、数据管理。模块边界要清晰答辩时导师最爱问的就是每个模块的职责和异常处理。摄像头采集负责从本地摄像头或视频文件读取帧并做抽帧。RetinaFace 对每帧都跑太费算力我一般让检测模块每 5 帧执行一次中间帧沿用上一次检测结果这样视频流能维持合理帧率。注册模块要有交互入口输入姓名、对准摄像头、连续拍 3-5 张照片每张都走“检测-对齐-编码”最终把多张特征的平均值作为模板入库。这里有个细节平均要在归一化之后做平均完再归一化一次避免向量模长漂移。实时识别模块把摄像头帧送入检测器对检测到的每张脸提取特征和库里所有特征比对超过阈值就写一条日志日志包含时间、姓名、相似度、抓拍图路径。数据管理模块则提供人员列表、识别记录查询、删除、导出。四个模块加起来大约 800-1200 行代码对毕设来说是一个合理且充实的工作量不会让人觉得注水。4.2 特征库设计SQLite 存特征向量别用文件名硬扛特征向量本质是 512 维 float32 数组最直觉的存法是“一张图一个 .npy 文件”但管理系统涉及增删改查文件系统很快就会不可控。SQLite 单文件、零配置、支持事务是毕设阶段最合适的选择。表结构我固定用两张核心表-- 人脸特征表 CREATE TABLE IF NOT EXISTS faces ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, feature BLOB NOT NULL, -- float32 特征向量直接存二进制 photo_path TEXT, -- 注册照片存放路径 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 识别日志表 CREATE TABLE IF NOT EXISTS logs ( id INTEGER PRIMARY KEY AUTOINCREMENT, face_id INTEGER, -- 命中的人脸ID未命中为 NULL score REAL, -- 相似度分数 image_path TEXT, -- 抓拍帧保存路径 created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP );用 BLOB 存特征比转成 JSON 文本更紧凑读取时np.frombuffer一下就能还原成向量。photo_path和image_path建议保留管理页面“查看照片”功能要用更重要的是论文里的“数据持久化设计”一节有东西可写。不推荐把整张图片也塞进 SQLite文件目录更直观也更易调试。4.3 界面层怎么选PyQt、Streamlit 还是 Flask管理系统必须有个界面但不用从零写前端。我按三种场景给三个方案只想在答辩时流畅演示Streamlit 是最快路径写 Python 逻辑时自动生成 Web 页面摄像头流用st.image显示按钮交互只改几行希望整个系统看起来像“企业级软件”用 PyQt 做桌面客户端摄像头画面渲染更稳但布局和打包工作量明显上浮想贴近真实门禁场景用 Flask 写后端 API前端页面用简单 HTML 接轮询这样可以把“人脸识别门禁系统设计”讲成前后端分离加边缘执行的完整故事。方案开发速度界面观感摄像头实时性适合场景Streamlit快偏数据看板一般快速跑通、答辩演示PyQt慢可做得很专业好桌面级管理系统Flask HTML中取决于前端功底中前后端分离、门禁对接最关键的一点界面层不该承载识别核心。视频流推到前端后台线程跑推理别把app.get()和facenet()放在 UI 主线程里否则一卡顿整个窗口假死。如果你做的是人脸识别门禁机方向前端还需要一个“门禁控制”区域——用一个布尔变量模拟继电器开关或者走串口给下位机发指令让 STM32 执行舵机开门。见过不少同学卡在最后一步算法跑得通但拿不出一张能说明白系统架构的图。先从上到下把“摄像头 → 识别服务 → 门禁执行器”画清楚再写代码思路会顺很多。5. 避坑RetinaFace FaceNet 在毕设里的五个翻车现场这一章写我跑这套组合时踩过的坑每个都按“现象 → 原因 → 解决”列出来照着排查可以省下大量血泪时间。5.1 显卡显存不足推理卡死甚至闪退现象平时 CPU 跑得好好的装完 CUDA 版 onnxruntime 和 torch 后第一次调用app.get()等了几十秒然后报CUDA out of memory或者程序直接闪退。原因RetinaFace 的特征金字塔在不同尺度上开了大量缓冲区默认配置在 2GB 显存的小显卡上装不下FaceNet 又在 GPU 上占一份显存。很多笔记本是核显加独显共存ctx_id0可能选到了集成显卡显存直接被系统吃掉一半。解决先用 CPU 跑通全流程再优化。设置app.prepare(ctx_id-1)强制走 CPU确认代码逻辑正确确实要用 GPU 时用nvidia-smi看显存余量把det_size降到(512, 512)FaceNet 推理放 CPU、检测放 GPU。显存只剩 1GB 时别勉强。5.2 侧脸和低头漏检注册成功率低现象注册时摄像头对着人脸稍微侧一点头或光线不足faces列表就是空的用户以为系统坏了。原因人脸识别门禁机的使用场景很典型——俯拍或侧面打光。RetinaFace 对极端姿态的召回率没有宣传里那么乐观insightface 默认检测阈值又偏高。解决把检测阈值从默认值降到 0.2-0.3 再跑注册流程并在注册完成后回显“注册照片 关键点连线”用户能直观看到系统真正检测到了。如果某些角度反复失败界面上要明确提示要求转正脸并保持光线均匀而不是让用户傻等。5.3 特征归一化不一致阈值调了多少天都不对现象同一个人两次拍的相似度只有 0.2两个不同人的相似度反而 0.9曲线完全乱套。原因FaceNet 的预训练权重来自多个开源版本有的输出已经 L2 归一化有的没有。注册时和识别时用的预处理不一致特征空间根本不是同一个。解决把“归一化”写死成注册和识别共用的函数emb emb / np.linalg.norm(emb)并保证两条链路的缩放尺寸、均值方差完全一致。最好写一个自检逻辑同一张图注册一次再识别一次分数应接近 1.0如果低于 0.9说明链路没对齐。5.4 只用自己照片验证答辩被问倒现象演示时换了环境光真人识别失败但 PPT 上写着“准确率 99%”。原因整个项目只在自己的三张自拍上调过阈值“99%”是注册照片回代的结果属于典型的数据泄漏。解决至少留出两组测试集——“同人不同光照不同姿态”和“不同人”统计 FAR误接受率和 FRR误拒绝率画出阈值-错误率曲线。再到 LFW 数据集随机抽几百对标注好的 pair 做离线评估这是人脸识别算法验证的标准路径。答辩时拿曲线说话比口头说“效果很好”可信得多。5.5 批量比对卡顿界面像死机现象数据库里注册了两百人每次实时识别要全部扫一遍画面直接掉到一两秒一帧。原因识别模块每次从 SQLite 读全部 BLOB逐个np.frombuffer再调用np.dot全部在 Python 层开销巨大的循环里完成效率极低。解决启动时把所有特征一次性读入内存拼成一个(N, D)的矩阵新图像特征变成(1, D)一次矩阵乘法拿到 N 个相似度再用一个阈值筛掉低分候选。两百人级别够用了到几千人再考虑 faiss 或 hnswlib。前端记得把推理放到后台线程每秒刷新一次结果界面就不会假死。6. 答辩前必做的验证阈值标定与演示脚本设计6.1 先标定阈值不要在演示现场改参数从第 5 章的排查能看出来阈值是整套系统里唯一需要“科学确定”的变量。常见做法是准备 50 对“同人”和 50 对“异人”分别计算相似度画出两条分布直方图阈值放在两分布重叠区中间偏右的位置。具体偏右多少取决于场景门禁系统更怕误接受阈值就高一些考勤系统更怕误拒绝阈值就低一些。一个简单的评估函数可以这样写def evaluate_threshold(pairs, threshold): pairs: list of (feature_a, feature_b, is_same) 返回在给定阈值下的 FAR 与 FRR tp fp tn fn 0 for emb1, emb2, is_same in pairs: score float(np.dot(emb1, emb2)) if is_same: if score threshold: tp 1 else: fn 1 else: if score threshold: fp 1 else: tn 1 far fp / max(fp tn, 1) frr fn / max(tp fn, 1) return far, frr跑完把阈值和 FAR/FRR 对应关系整理成表格放进答辩 PPT这段工作不大但立刻让整个项目有了“评测”的骨架。评委看到的不再是零散的演示截图而是一条完整的验证链条。6.2 演示脚本化的三个细节第一固定摄像机位与光源。别到现场才摆摄像头先在家用同样的环境光把完整流程录一遍视频录好的视频本身就是保底方案。第二现场注册一个人让他离开镜头再换个角度回来验证系统在姿态变化下依然有效。第三如果想把门禁联动展示出来用一个继电器或 GPIO 指示灯代表开锁动作讲清楚“识别成功 → 发送信号 → 执行开门”的链路比只显示一个姓名更有说服力。如果导师提到把系统部署到行空板这类低成本开发板上直接说明 FaceNet RetinaFace 对算力有硬性要求嵌入式端通常要换轻量模型如 MobileFaceNet SCRFD这反倒可以作为论文里“未来工作”的素材。做毕业设计或课程设计做到这个程度该有的检测、对齐、特征提取、特征存储、业务界面、性能度量全都齐了无论导师追问哪一层你都有代码和曲线可以指给他看。我带毕设时总跟学生讲一句话别为“精度不够高”焦虑把“整条链路完整、指标可度量、边界能讲清”当成底线再在这个底线上挑一个点做深比如把阈值标定自动化或者把检测器换成更轻量的版本。希望你按这套思路把 FaceNet RetinaFace 的系统落地成自己的作品答辩顺利也希望这份方案能帮到你。本文还有配套的精品资源点击获取