简介这是一份基于FaceNet架构的人脸检测识别与相似性匹配完整毕业设计项目适合计算机视觉方向本科生、研究生及人脸识别入门开发者使用。资源围绕人脸向量化核心思路展开先检测图片中人脸再提取512维特征向量存入MySQL数据库同时提供POST接口上传新图片即可检索并返回数据库中相似人脸及其信息形成从入库到查询的闭环实践方案。压缩包共33个文件体积约3.2MB主要包含Python脚本、模型封装模块、测试图片及SQL数据库文件等其中11个py文件覆盖人脸检测、特征提取、数据库读写和API接口等核心逻辑配套requirements.txt便于快速复现运行环境也包含多组jpg/png测试图与npy特征数据可直接验证效果。目前已有467人学习下载适合用于课程设计、毕业论文参考或人脸比对原型开发。通过阅读源码与目录结构可学习到FaceNet模型调用、人脸对齐预处理、向量相似度检索及Web接口封装等完整技术链路。1. 基于 FaceNet 的人脸检测识别与相似性匹配为什么这套组合最适合毕业设计人脸识别方向做毕业设计最尴尬的时刻不是模型跑不通而是导师问“你这个系统除了能认出我还能做什么”。用 FaceNet 做核心配 MTCNN 做检测最后落在“人脸相似性匹配”上恰好能把识别、检索、验证三件事全串起来既能回答“这个人是谁”也能回答“这两张脸有多像”还能做“图库里有几张脸和这张最接近”。这个组合的工程边界很清晰——检测归检测特征提取归特征提取匹配归匹配每一层都能单独调试也都能单独写进论文里当一章。这套方案适合数据集规模不大、算力有限、但需要完整演示效果的场景。FaceNet 是预训练好的嵌入模型不需要你在自己的小数据集上从头训只需要微调或者直接拿来做特征提取器。而相似性匹配天然自带“可视化”输入两张照片输出一个相似度分数这个交互过程比单纯输出一个类别名字更有说服力。下面从原理、工程搭建、代码实现到调参踩坑按一条可复现的路线走一遍。2. FaceNet 和 MTCNN 的分工为什么检测和识别要拆成两步2.1 FaceNet 的“最后一公里”从分类任务到嵌入向量FaceNet 的核心思路不是训练一个分类器而是训练一个映射函数把一张人脸图像映射到一个 128 维的欧氏空间里。在这个空间里同一个人的两张脸距离近不同人的两张脸距离远。训练时用三元组损失每一组数据包含锚点、正样本、负样本目标是把锚点和正样本的距离拉近、和负样本的距离推远并且让类间距离比类内距离大出一个边距。这里有一个对毕设特别重要的点FaceNet 不负责“找脸”。你给它一张包含背景的整图它也会输出一个向量但这个向量可能是背景、身体、人脸混合物的特征没有任何判别意义。所以工程上必须先做检测把人脸裁出来再送进 FaceNet。很多同学在毕设里翻车问题不在 FaceNet 本身而是把整张图直接喂进去了。实际使用中预训练 FaceNet 模型输出的 128 维向量已经做了 L2 归一化向量的模长为 1。这意味着你可以直接用欧氏距离比较两张脸的相似度——距离越小越相似。原论文里推荐的判断阈值在 1.1 左右但这只是 LFW 数据集上的经验值换到自己的数据集上必须重新标定后面第 5 章会专门讲。2.2 为什么检测环节选 MTCNN 而不是 OpenCV 的 Haar 级联MTCNN 在毕设里的人脸检测场景几乎是唯一合理选择。它输出两样东西人脸边界框和五个人脸关键点左眼、右眼、鼻尖、左嘴角、右嘴角。关键点的价值在于可以做人脸对齐——把眼睛旋转到水平位置、把脸缩放到统一尺寸。对齐操作对 FaceNet 的识别准确率影响极大同一个人的脸歪了 10 度欧氏距离可能就比不同人的距离还大。对比 OpenCV 自带的 Haar 级联检测器它只给出边界框不给关键点检测率在侧脸、暗光、遮挡场景下明显偏低。而 MTCNN 是三级联网络先用浅层网络快速扫描候选框再用更深的网络逐步精修最后还会做边界框回归和关键点定位。虽然 MTCNN 在手机端实时性一般但在毕设场景下跑离线图片或视频足够了。2.3 相似度度量的两种姿势欧氏距离和余弦相似度FaceNet 原论文明确建议用欧氏距离因为训练时直接优化的是欧氏距离这和模型的训练目标一致。但工程上有不少人习惯用余弦相似度做法是先把两个向量做 L2 归一化再点乘得到的结果是 [-1, 1] 区间内的分数越大越相似。由于 FaceNet 输出的向量本身已经 L2 归一化余弦相似度和欧氏距离在数学上是等价的只是数值方向相反余弦相似度 0.8 大致对应欧氏距离 0.632。问题在于不同预训练权重对归一化的处理不完全一致有的权重在最后一层做了归一化有的没做。稳妥的做法是自己在代码里再归一化一次然后统一用欧氏距离作为指标同时把余弦相似度作为辅助展示。毕设答辩时给出两个指标比只给一个更有解释空间。3. 搭建毕业设计工程目录结构、数据准备与依赖安装3.1 目录结构与模块划分FaceNet MTCNN 的毕设工程建议按功能拆成四个目录数据、模型权重、核心代码、输出结果。我一般会这样组织face_project/ ├── data/ │ ├── raw/ # 原始采集照片 │ ├── cropped/ # 检测并对齐后的人脸 │ └── embeddings/ # 预计算的特征向量 npy 文件 ├── models/ │ └── facenet/ # 预训练权重文件 ├── src/ │ ├── detect.py # MTCNN 检测与对齐 │ ├── embed.py # FaceNet 特征提取 │ ├── match.py # 相似度匹配 │ └── utils.py # 工具函数 ├── outputs/ │ └── match_results/ # 匹配结果可视化 └── main.py # 入口脚本这个结构的用意是让每一层都能单独跑通detect.py可以单独执行看检测效果embed.py可以单独批量提取特征match.py负责最终的查询匹配。写毕业论文时每个脚本对应一个章节的实验工作量看起来是实打实的。3.2 人脸数据库的准备自建与公开数据集两条路径毕设通常有两种数据路线。第一种是自建人脸库找 20 到 30 个志愿者每人采集 30 到 50 张不同角度、不同光照条件下的照片。自建数据的优势是论文里能写“实验数据来源于真实采集环境”但缺点是数量少、分布不均匀个别志愿者可能只愿意给十几张。第二种是使用公开数据集做验证比如 LFWLabeled Faces in the Wild包含 1 万多人、超过 1.3 万张网络采集的人脸图片。LFW 的好处是有一个标准的pairs.txt协议里面预先定义了几千组“同一个人/不同人”的配对直接用这个协议跑一遍就能得到可对比的准确率指标。毕设里常见做法是“自建数据做演示LFW 做量化评估”两条腿走路。3.3 依赖安装与预训练权重落位Python 环境建议 Python 3.8 以上版本。核心依赖是 PyTorch 和 facenet-pytorch 这个库它把 MTCNN 和 FaceNet 的推理封装得很干净。安装命令pip install facenet-pytorch torch torchvision pillow numpy matplotlibfacenet-pytorch 里自带 MTCNN 的预训练权重首次运行时会自动下载。FaceNet 的预训练权重同样由这个库管理调用时指定pretrainedvggface2即可加载在 VGGFace2 数据集上训练的模型。注意facenet-pytorch 并不包含 FaceNet 原始论文里基于 CASIA 数据集训练的那套权重而是社区的复现版本输出维度同样是 128 维。对于毕设来说够用但论文里最好写明用的是 VGGFace2 预训练版本避免评审误以为你复现了原论文的训练过程。4. 核心代码实战从检测到相似度匹配的完整链路4.1 用 MTCNN 完成人脸检测与对齐的完整流程第一步是把原始图片里的人脸检测出来并用关键点做仿射变换对齐。facenet-pytorch 的 MTCNN 类内置了对齐功能关键参数是keep_all和post_process。# detect.py from facenet_pytorch import MTCNN from PIL import Image # 初始化 MTCNN开启关键点对齐 mtcnn MTCNN( keep_allTrue, # 返回图片中所有人脸而不是只返回最大的一张 post_processFalse, # 返回 0-255 范围的原始图像不做归一化 min_face_size40, # 小于 40x40 像素的人脸直接忽略 devicecuda # 有 GPU 用 cuda没有就改成 cpu ) img Image.open(data/raw/zhang3.jpg) boxes, probs, landmarks mtcnn.detect(img) # 输出每个检测框的置信度便于排查漏检 print(f检测到 {len(boxes)} 张人脸) for i, (box, prob) in enumerate(zip(boxes, probs)): print(f第 {i} 张人脸 置信度: {prob:.4f})keep_allTrue是关键配置。默认情况下 MTCNN 只返回图片中“最像人脸”的那一张但毕设演示场景里经常要处理合影或多人同框必须设置为返回全部。post_processFalse也很重要它让返回值保持原始像素范围方便后续可视化如果设成 True返回的是归一化后的张量直接喂给 FaceNet 也行但没法用来画框。对齐动作发生在这个类的内部——它会根据五个人脸关键点计算仿射变换矩阵把人脸旋转到标准姿态。这意味着你不需要自己写对齐代码但你要知道对齐发生了否则可能出现“同一张图每次跑出来的脸位置都略有不同”的困惑。4.2 提取 FaceNet 特征并建立人脸库索引检测到人脸之后下一步是把每张对齐后的人脸转成 128 维向量。这一步要走批量处理不要一张张循环否则速度会慢到怀疑人生。批量提取前先要把所有检测到的人脸裁剪成统一尺寸MTCNN 的输出可以直接送进 FaceNet。# embed.py import torch from facenet_pytorch import InceptionResnetV1 import numpy as np from PIL import Image # 加载在 VGGFace2 上预训练的 FaceNet 模型 model InceptionResnetV1(pretrainedvggface2).eval() model model.to(cuda) def extract_embedding(face_tensor): 输入 MTCNN 对齐后的人脸张量输出 128 维向量 with torch.no_grad(): emb model(face_tensor.unsqueeze(0)) # 增加 batch 维度 return emb.cpu().numpy().flatten() # 批量处理示例 def build_embedding_db(image_paths, mtcnn, model): 遍历所有人脸图片提取特征并保存为字典 embeddings {} for path in image_paths: img Image.open(path) # MTCNN 返回对齐后的人脸张量已经归一化到 [-1, 1] faces mtcnn(img) if faces is None: print(f跳过 {path}未检测到人脸) continue # faces 的形状是 [N, 3, 160, 160]N 是检测到的人脸数 for i, face in enumerate(faces): face face.to(cuda) emb extract_embedding(face) key f{path}_face{i} embeddings[key] emb np.save(data/embeddings/face_db.npy, embeddings) return embeddingsInceptionResnetV1(pretrainedvggface2)加载的是 Inception ResNet v1 结构输出层是 512 维还是 128 维取决于模型构造参数。facenet-pytorch 的默认实现里InceptionResnetV1类在classifyTrue时输出 512 维分类向量classifyFalse时输出 128 维嵌入向量。这里必须用默认的classifyFalse否则后面所有距离计算维度都对不上。mtcnn(img)这一步会把对齐后的人脸缩放到 160x160并归一化到 [-1, 1] 区间正好符合 FaceNet 的输入要求。也就是说MTCNN 输出的张量可以直接喂进 FaceNet中间不要再加额外的标准化操作。4.3 相似性匹配从查询到返回 Top-K 结果人脸库建立好之后查询流程就只剩三步对查询图片做检测对齐、提取 embedding、和库里所有向量算距离。距离计算推荐用scipy.spatial.distance或者直接 numpy 广播两种方式都够快。# match.py import numpy as np def cosine_similarity(a, b): 计算两个向量的余弦相似度 a_norm a / np.linalg.norm(a) b_norm b / np.linalg.norm(b) return np.dot(a_norm, b_norm) def match_face(query_emb, face_db, top_k3, threshold0.8): 在特征库中查找最相似的 top_k 张人脸 query_emb: 查询人脸的 128 维向量 face_db: 人脸库字典 {name: embedding} threshold: 相似度阈值低于此值视为未知身份 similarities [] for name, db_emb in face_db.items(): score cosine_similarity(query_emb, db_emb) similarities.append((name, score)) similarities.sort(keylambda x: x[1], reverseTrue) results [s for s in similarities if s[1] threshold] if not results: return [(unknown, 0.0)] return results[:top_k] # 使用示例 query_emb extract_embedding(mtcnn(Image.open(data/raw/test.jpg)).to(cuda)) face_db np.load(data/embeddings/face_db.npy, allow_pickleTrue).item() matches match_face(query_emb, face_db) print(f最相似的人脸: {matches[0][0]}相似度: {matches[0][1]:.4f})threshold0.8只是一个初始猜测值最终阈值必须用验证集重新标定。这里的逻辑是相似度大于等于阈值的才认为是库里的人否则判为未知。毕设答辩时“未知人拒识”这个功能非常加分因为它体现了系统不只是做分类还具备开集识别能力。注意np.load(..., allow_pickleTrue)加载的是字典对象。如果 face_db 很大几万张向量numpy 字典的加载速度会变慢到第 6 章会讲换成专用向量存储的优化方案。5. 毕设实战中的五个高频坑现象、原因与解决办法5.1 MTCNN 死活检测不到人脸min_face_size 与图像金字塔现象一张清晰的正脸照MTCNN 返回None打印置信度列表为空。原因min_face_size40是 MTCNN 的初始检测窗口大小它决定了对多小的人脸敏感。如果照片里人脸像素宽度在 30 到 50 之间min_face_size40时检测窗口可能正好落在人脸的缝隙里导致候选框全部被后续网络否决。另一个原因是图片过大或过小时MTCNN 对图像金字塔层数的控制不当金子塔顶层直接越过了目标尺寸。解决把min_face_size调到 20 再试。如果还是不行手动把图片缩放到统一宽度后重试。我一般会写一个预处理函数将输入图片的短边缩放到 800 像素长边不超过 1200这样金字塔搜索范围更稳定。5.2 同一个人两次提取的向量相差很大对齐与预处理现象同一个人的两张不同照片计算出的欧氏距离达到 1.5比不同人的平均距离还大匹配直接失败。原因绝大多数情况是对齐没生效。MTCNN 的detect方法检测到的关键点若置信度低仿射变换会偏离导致人脸在 160x160 的输入框内位置偏移几个像素。FaceNet 对空间位置非常敏感偏移 5 个像素就可能造成 0.3 以上的距离波动。解决检查mtcnn(img)返回的张量本身把张量转成图片保存出来肉眼查看对齐结果。如果发现部分人脸没有对齐可以改用mtcnn.detect()拿关键点坐标自己做cv2.warpAffine对齐再手动裁剪。注意不要使用 MTCNN 内部对齐后再重新缩放两次——对齐和缩放尽量一次完成避免二次插值带来的信息损失。5.3 阈值拍脑袋设置导致识别准确率虚高或虚低现象阈值设 0.9大量真人进不来系统看起来“很保守”阈值设 0.6谁都能通过误识率感人。原因阈值和数据集分布强相关。不同人的脸部相似度基线不一样同卵双胞胎的类间距离可能小于普通人的类内距离。直接用 LFW 上常用的 0.8 余弦阈值等于拿别人的尺子量自己的数据。解决从库里拿 20 个“同人对”和 20 个“异人对”分别计算相似度画出两个分布的直方图取交叉点作为阈值。实际操作是给定一个候选阈值列表从 0.5 到 0.95 每隔 0.05 步进算每一档的准确率和误识率选择等错误率最低的那一档。5.4 GPU 显存不足与推理卡顿batch 与数据类型现象用 GPU 跑批量提取时CUDA out of memory报错改成 CPU 跑一张图要 3 秒。原因FaceNet 推理本身不重但mtcnn(img)在 GPU 上运行时会占额外显存因为 MTCNN 三级网络都会在 GPU 上创建中间特征图。如果 batch 一次喂 64 张图显存很容易爆。解决把 batch size 降到 16 或 8MTCNN 和 FaceNet 的 device 都设为cuda但不要同时开两个流。对推理速度要求高时把模型转成半精度model model.half() # 转为 FP16 face_tensor face_tensor.half() # 输入也要对应转 emb model(face_tensor.unsqueeze(0)).float() # 输出再转回 FP32FP16 在大多数 GPU 上能把推理时间缩短 30% 到 50%人脸识别任务对精度损失不敏感毕设完全可以用。5.5 自定义数据微调时损失震荡不收敛现象用自建人脸库微调 FaceNettriplet loss在 0.5 附近反复震荡准确率上不去。原因最常见问题是没有做难样本挖掘。随机抽三元组绝大多数组合是“简单对”——正样本离锚点近、负样本离锚点远梯度很小模型学不到东西。另一个原因是你把全连接层一起微调了破坏了预训练模型的语义空间。解决毕设场景不建议从零微调。保留预训练权重只训练一个小的 embedding 调整层或者干脆不训练直接拿预训练向量做匹配——后者在小型数据集上的表现往往比微调更好。如果一定要微调使用在线难样本挖掘策略每批次内挑距离最远的正样本和距离最近的负样本构成三元组。6. 进阶技巧从“能跑”到“能答辩”6.1 用 LFW 的 pairs.txt 跑一遍协议级验证毕设评审最在意的量化指标是准确率。LFW 数据集自带pairs.txt每行格式“人名1 人名2 是否为同一人”。写一个评估脚本遍历所有配对分别提取两张人脸的特征做比对统计阈值从 0.5 到 0.95 变化时的准确率# evaluate_lfw.py def evaluate_lfw(pairs_path, face_db_path): 按 LFW 协议计算准确率 with open(pairs_path) as f: pairs [line.strip().split() for line in f if line.strip()] correct 0 total len(pairs) for pair in pairs: name1, name2, label pair[0], pair[1], int(pair[2]) emb1 get_embedding(name1) # 实际是从预建索引中查 emb2 get_embedding(name2) sim cosine_similarity(emb1, emb2) pred 1 if sim 0.8 else 0 correct (pred label) return correct / total协议级验证的意义在于你的系统可以和过去十几年公开发表的人脸识别方案直接对比准确率这在论文的“实验结果”章节里是非常硬核的材料。哪怕最终准确率是 92%比 SOTA 低不少但实验流程规范评审挑不出毛病。6.2 预处理加速把 embedding 缓存到向量文件每次查询都重新遍历 numpy 字典做全量距离计算在几千张图时没问题到几万张图就明显卡顿。入门级优化是把 embedding 写成二进制向量文件读取时用np.fromfile加载成二维矩阵距离计算全部向量化。# 向量化矩阵距离计算 embs np.stack(list(face_db.values())) # [N, 128] names list(face_db.keys()) dists np.linalg.norm(embs - query_emb, axis1) top_idx np.argsort(dists)[:top_k]这段代码把几十万次的循环压缩成一次矩阵运算小数据集上的耗时从几百毫秒降到几毫秒。如果想更进一步可以引入近似最近邻索引但毕设阶段没有必要向量化已经足够。6.3 从静态图片到视频帧流做一个简单的实时演示毕设答辩现场静态图片演示的冲击力远不如摄像头实时识别。常见做法是打开摄像头对每一帧调用 MTCNN FaceNet然后做距离匹配。但逐帧做全流程CPU 上会掉到 5 帧以下观感很差。我一般会用一个“跳帧 缓存”的策略每 5 帧做一次完整检测中间帧只复用最近一次检测结果。检测到的人脸框位置在相邻帧变化不大直接沿用上一帧的 bounding box匹配结果也沿用。这样帧率能稳定在 15 帧以上视觉效果流畅代码复杂度增加很少。# 视频推理时的跳帧逻辑 frame_count 0 last_faces None for frame in video_stream: if frame_count % 5 0: # 完整检测 特征提取 last_faces detect_and_match(frame) else: # 直接画上一轮的检测结果 draw_boxes(frame, last_faces) frame_count 1last_faces里保存的是上一轮检测到的人脸框坐标和身份标签。这里的代价是快速转头时会有最多 4 帧的延迟但人脸识别演示场景下完全能接受。答辩时如果有人问“为什么偶尔会延迟”这个跳帧设计本身就是话术点解释起来不丢分。最后说一个我自己养成的习惯每次调完参数把当前数据集下的最佳阈值、模型输入尺寸、损失曲线截图单独存一个目录。毕设临近答辩那几天你不可能靠记忆重建实验参数而评审最喜欢问的就是“你这个阈值是怎么定出来的”。有截图有数据当场就能打开讲清楚。这些细节比模型本身的准确率更能体现一个人有没有做工程的经验和习惯。希望这套方案能帮你的毕设少走一段弯路。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?