☰
基于FaceNet与MTCNN的人脸识别相似性匹配毕设方案
2026/10/13 15:29:13 网站建设 项目流程

简介:这是一份基于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 张不同角度、不同光照条件下的照片。自建数据的优势是论文里能写“实验数据来源于真实采集环境”,但缺点是数量少、分布不均匀,个别志愿者可能只愿意给十几张。

第二种是使用公开数据集做验证,比如 LFW(Labeled 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 matplotlib

facenet-pytorch 里自带 MTCNN 的预训练权重,首次运行时会自动下载。FaceNet 的预训练权重同样由这个库管理,调用时指定pretrained='vggface2'即可加载在 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_all=True, # 返回图片中所有人脸,而不是只返回最大的一张 post_process=False, # 返回 0-255 范围的原始图像,不做归一化 min_face_size=40, # 小于 40x40 像素的人脸直接忽略 device='cuda' # 有 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_all=True是关键配置。默认情况下 MTCNN 只返回图片中“最像人脸”的那一张,但毕设演示场景里经常要处理合影或多人同框,必须设置为返回全部。post_process=False也很重要,它让返回值保持原始像素范围,方便后续可视化;如果设成 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(pretrained='vggface2').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 embeddings

InceptionResnetV1(pretrained='vggface2')加载的是 Inception ResNet v1 结构,输出层是 512 维还是 128 维取决于模型构造参数。facenet-pytorch 的默认实现里,InceptionResnetV1类在classify=True时输出 512 维分类向量,classify=False时输出 128 维嵌入向量。这里必须用默认的classify=False,否则后面所有距离计算维度都对不上。

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_k=3, threshold=0.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(key=lambda x: x[1], reverse=True) 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_pickle=True).item() matches = match_face(query_emb, face_db) print(f"最相似的人脸: {matches[0][0]},相似度: {matches[0][1]:.4f}")

threshold=0.8只是一个初始猜测值,最终阈值必须用验证集重新标定。这里的逻辑是:相似度大于等于阈值的才认为是库里的人,否则判为未知。毕设答辩时,“未知人拒识”这个功能非常加分,因为它体现了系统不只是做分类,还具备开集识别能力。

注意np.load(..., allow_pickle=True)加载的是字典对象。如果 face_db 很大(几万张向量),numpy 字典的加载速度会变慢,到第 6 章会讲换成专用向量存储的优化方案。

5. 毕设实战中的五个高频坑:现象、原因与解决办法

5.1 MTCNN 死活检测不到人脸:min_face_size 与图像金字塔

现象:一张清晰的正脸照,MTCNN 返回None,打印置信度列表为空。

原因:min_face_size=40是 MTCNN 的初始检测窗口大小,它决定了对多小的人脸敏感。如果照片里人脸像素宽度在 30 到 50 之间,min_face_size=40时检测窗口可能正好落在人脸的缝隙里,导致候选框全部被后续网络否决。另一个原因是图片过大或过小时,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 或 8;MTCNN 和 FaceNet 的 device 都设为cuda,但不要同时开两个流。对推理速度要求高时,把模型转成半精度:

model = model.half() # 转为 FP16 face_tensor = face_tensor.half() # 输入也要对应转 emb = model(face_tensor.unsqueeze(0)).float() # 输出再转回 FP32

FP16 在大多数 GPU 上能把推理时间缩短 30% 到 50%,人脸识别任务对精度损失不敏感,毕设完全可以用。

5.5 自定义数据微调时损失震荡不收敛

现象:用自建人脸库微调 FaceNet,triplet 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, axis=1) 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 += 1

last_faces里保存的是上一轮检测到的人脸框坐标和身份标签。这里的代价是快速转头时会有最多 4 帧的延迟,但人脸识别演示场景下完全能接受。答辩时如果有人问“为什么偶尔会延迟”,这个跳帧设计本身就是话术点,解释起来不丢分。

最后说一个我自己养成的习惯:每次调完参数,把当前数据集下的最佳阈值、模型输入尺寸、损失曲线截图单独存一个目录。毕设临近答辩那几天,你不可能靠记忆重建实验参数,而评审最喜欢问的就是“你这个阈值是怎么定出来的”。有截图有数据,当场就能打开讲清楚。这些细节比模型本身的准确率更能体现一个人有没有做工程的经验和习惯。希望这套方案能帮你的毕设少走一段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询