简介基于Facenet实现人脸检测、识别与人脸相似性匹配的完整毕业设计代码适合计算机视觉方向的学生和开发者用于人脸特征提取、向量比对及检索场景。压缩包共33个文件总大小3.2MB主要包括Python源码、预编译pyc文件、测试图片、Numpy特征矩阵、数据库脚本及说明文档代码注释清晰、目录结构完整便于快速上手。已有467人学习下载。资源集成人脸检测、特征向量入库、基于POST提交图片并返回相似人脸信息的功能内含database.sql建表语句、Facenet核心模型文件以及多组测试样例可帮助读者理解从人脸检测到特征匹配的全流程并作为毕业设计或项目原型直接修改使用。整体体量轻量适合在本地环境快速验证算法效果。1. 基于FaceNet的人脸检测与相似性匹配毕设代码里到底有什么人脸识别毕设最常见的死法是什么不是模型调参而是从检测到特征提取再到相似度比对每一段都各有一套成熟轮子但没人帮你把它们串起来。这份基于FaceNet的毕设完整代码把MTCNN人脸检测、人脸对齐、FaceNet特征提取、欧氏距离相似性匹配串成了一条可运行的流水线。它能解决“给一张人脸判断它是库里哪个人或两张脸相似度多高”这类典型问题适合正在做毕业设计、课程设计以及刚入门人脸识别、想快速搭建原型的人。拿到手不是去看论文而是先跑通再根据你的机器改参数。2. FaceNet原理与Embedding选对模型才能少走弯路2.1 为什么选FaceNet从分类网络到度量学习在讲代码之前先说说这套方案为什么用FaceNet而不是老一套的LBP特征或Softmax分类。传统人脸识别把它当成分类问题最后一层输出每个ID的概率但新增一个ID就要重新训一遍全连接层特征也不具备跨ID的泛化能力。FaceNet改用度量学习核心思想是让同一个人的脸在128维欧氏空间里尽量靠近不同人的脸尽量远离。训练目标不再是分类准确率而是embedding之间的距离关系。这样提取出来的特征向量天然适合做相似性匹配新增人脸库成员时完全不用重训模型。实际中你会发现用Softmax预训练再拿倒数第二层当特征的效果也不差但FaceNet专门在超大规模人脸数据集上优化过三元组损失对光照、姿态、年龄变化的鲁棒性更稳。在毕设场景里你不需要从零训练直接加载预训练权重做推理就能达到实用程度。这也解释了为什么这份代码结构是“检测识别”两段式检测负责把人脸框出来FaceNet负责把框里的人脸变成可比较的向量。2.2 模型结构与预训练权重Inception-ResNet v1还是MobileNetFaceNet的原始主干网络有很多变体毕设工程里最常见的是Inception-ResNet v1和MobileNet。Inception-ResNet v1准确率更高但模型文件几百MBCPU上跑一张图要几百毫秒MobileNet系列更轻量可以在树莓派或普通笔记本上实时跑代价是准确率略低。我一般建议毕设优先用Inception-ResNet v1的预训练权重因为答辩现场很难解释“为了省时间牺牲准确率”而且GPU加速后速度问题并不突出。加载模型的代码在整个工程里非常简单Keras版本大概是这样的from keras.models import load_model # 加载预训练FaceNet模型输入尺寸160x160输出128维embedding model load_model(facenet_keras.h5) print(model.output_shape) # (None, 128)这段代码里facenet_keras.h5是通用预训练文件它的输入张量形状是(None, 160, 160, 3)输出是(None, 128)。如果你拿到的是TensorFlow SavedModel格式就用tf.keras.models.load_model同样处理。加载后建议先打印一遍model.summary()确认底层结构不是被裁剪过的版本因为网上有些压缩后的权重输出维度不是128这会影响后面所有距离计算。2.3 预处理像素归一化与颜色通道顺序FaceNet对输入图像的预处理比普通分类网络更讲究。它不接受RGB图像直接送进去而是要先做一次“白化”把像素值减去均值、除以方差让分布近似标准正态。原始论文在训练时还会做随机裁剪、水平翻转等增强但推理阶段只需要固定尺寸和归一化。代码里通常封装成一个prewhiten函数import numpy as np from PIL import Image def prewhiten(image): # image: 已经resize到(160,160)的ndarraydtypeuint8 image image.astype(np.float32) mean np.mean(image) std np.std(image) # 避免除零 if std 0: std 1 return (image - mean) / std这个函数的参数只有一个读进来且已缩放好的图像数组。注意两点一是先resize再白化不要反过来否则尺寸变化会改变像素分布二是这个归一化是每个样本独立做的不是按整个数据集算均值方差因为FaceNet训练时就是逐样本白化。很多同学直接套用ImageNet的mean[0.485,...]归一化结果特征分布完全不同相似度全乱套。另外颜色通道顺序要按模型要求来。Keras版模型默认输入是RGB如果你用cv2.imread读图它是BGR顺序必须在图片送入模型前用cv2.cvtColor转成RGB否则皮肤和背景颜色反转识别率会掉一大截。这个坑在后面避坑章节还会重点提。3. 人脸检测与对齐MTCNN参数与预处理流程3.1 MTCNN检测、边框与关键点FaceNet本身只负责特征提取不管人脸在哪。当前毕设工程里检测部分基本是MTCNN的天下。MTCNN输出两样东西一个是人脸边框bounding box包括左上角坐标、宽高和置信度另一个是五个关键点左右眼、鼻尖、左右嘴角。后者是我们做人脸对齐的关键。使用现成的mtcnn库检测代码短到令人发指from mtcnn import MTCNN detector MTCNN(min_face_size20, scale_factor0.709, thresholds[0.6, 0.7, 0.7]) result detector.detect_faces(img_rgb) if result: box result[0][box] # [x, y, w, h] keypoints result[0][keypoints] # {left_eye:..., right_eye:..., nose:..., mouth_left:..., mouth_right:...}min_face_size是最小人脸边长像素设太小会漏检远处小脸设太大容易漏掉近距离大脸实际上它是控制图像金字塔的最小尺寸一般设20。scale_factor是每次缩放的比例0.709是原始论文默认值表示金字塔层间尺度差越小检测越慢但小脸检出率越高。thresholds依次是PNet、RNet、ONet的置信度阈值前两级可以宽松些最后一级收紧防止把背景误检成人脸。我见过很多同学直接把detect_faces返回的box拿去裁剪结果人脸是歪的、不全的后面特征提取自然一塌糊涂。正确做法是先拿到五个关键点再做仿射变换把眼睛对准水平这样才能消除人脸姿态差异。3.2 用关键点做对齐仿射变换与裁剪对齐步骤是整套流程里最容易被忽略、又最影响准确率的一环。FaceNet原始论文提到人脸对齐后再输入网络能让准确率明显提升。对齐的实现逻辑是根据左右眼坐标计算旋转角度构造仿射变换矩阵把整幅图旋转使两只眼睛位于同一水平线再按中心点裁剪出统一尺寸。常见实现如下import cv2 import numpy as np def align_face(img, keypoints, size(160, 160)): left_eye keypoints[left_eye] right_eye keypoints[right_eye] # 计算两眼连线与水平方向的夹角 dy right_eye[1] - left_eye[1] dx right_eye[0] - left_eye[0] angle np.degrees(np.arctan2(dy, dx)) # 旋转矩阵围绕图像中心点 center (img.shape[1] // 2, img.shape[0] // 2) rot_mat cv2.getRotationMatrix2D(center, angle, 1.0) rotated cv2.warpAffine(img, rot_mat, (img.shape[1], img.shape[0])) # 用旋转后的关键点或原box裁剪这里按原box对角线扩张后裁剪更稳 box keypoints_to_box(keypoints, margin0.4) x, y, w, h box face rotated[y:yh, x:xw] return cv2.resize(face, size)这里函数有几个参数值得说。size(160,160)是FaceNet的标准输入尺寸不要改成224或128。margin0.4表示在框外额外扩出40%的边距让额头、下巴和耳朵边缘也进去否则裁剪太紧会丢失脸部边缘信息。角度处理要用arctan2而不是arctan否则当眼睛在水平线下方时算出来的角度会反。还有一点旋转会引入黑边如果黑边被裁进去会影响模型建议旋转后用cv2.copyMakeBorder补边。我自己踩过的坑是直接用原图的关键点坐标去裁剪旋转后的图但旋转已经改变了坐标必须重新计算。更稳妥的做法是提前把关键点坐标也用旋转矩阵变换或者干脆先根据关键点计算一个包围盒再旋转。上面的keypoints_to_box示意就是干这个的篇幅有限就不展开完整代码工程里一般都封装好了。3.3 照片批量处理与视频抽帧检测频率与并发毕设里通常有两种使用场景一种是离线给一批照片建人脸库另一种是打开摄像头做实时识别。离线批量处理比较简单遍历所有图片每张都做检测、对齐、提特征。实时场景则要注意抽帧频率因为MTCNN在CPU上每帧可能要100毫秒以上加上FaceNet的特征提取跑不了满帧率。常见做法是每3帧或5帧检测一次检测到人脸后再对这个人脸区域做一次特征提取两次之间用简单的目标跟踪或直接沿用上一个位置这样既保证流畅又不会漏掉关键帧。这里给一个批量建库的流程示意import os import numpy as np face_db {} for file_name in os.listdir(known_faces): path os.path.join(known_faces, file_name) img cv2.imread(path) img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) result detector.detect_faces(img) if not result: print(f{file_name} 未检测到人脸) continue keypoints result[0][keypoints] aligned align_face(img, keypoints) preprocessed prewhiten(aligned) embedding model.predict(np.expand_dims(preprocessed, axis0))[0] # 这里将embedding存储到内存或数据库后面匹配用 face_db[file_name] embedding这个批量流程里np.expand_dims是把单张图增加batch维因为Keras模型默认输入是(batch, 160, 160, 3)。注意model.predict每次调用都有开销如果你有一万张图要建库建议换成model.predict_on_batch或者把所有图拼成一个大张量一次预测否则等待时间会非常感人。4. 相似性匹配与识别从向量距离到人脸库检索4.1 距离度量欧氏距离还是余弦相似度拿到128维embedding后判断两张脸是否同一人本质上就是算两个向量的距离。FaceNet原始论文用的是欧氏距离训练时让同一人的距离小、不同人的距离大。余弦相似度是归一化后的点积在向量已经做过归一化时两者排序结果是等价的但距离阈值不同。使用欧氏距离时阈值取0.8到1.2之间使用余弦相似度时阈值取0.6到0.75之间。范围取决于你的数据分布最好在自己的验证集上标定而不是无脑用论文里的数。工程里最常见的实现是这种def euclidean_distance(emb1, emb2): return np.linalg.norm(emb1 - emb2) def cosine_similarity(emb1, emb2): dot np.dot(emb1, emb2) norm1 np.linalg.norm(emb1) norm2 np.linalg.norm(emb2) return dot / (norm1 * norm2 1e-6)这段代码没什么玄机但我建议统一用欧氏距离因为FaceNet输出没有强制归一化欧氏距离能保留原始尺度信息而余弦相似度忽略模长。虽然理论上模长无关紧要但在实际数据里光照变化可能导致embedding模长有偏移这时候欧氏距离反而更稳定。当然你也可以在提取embedding后先做L2归一化再算欧氏距离效果等同余弦相似度这样阈值好记。4.2 人脸库管理与检索注册和识别的闭环一个完整的识别系统分两步注册时把人脸embedding入库识别时拿未知人脸embedding去和库里所有向量比对取距离最小的那个如果距离小于阈值就判定为库中的人否则判定为陌生人。这一步的代码骨架如下def recognize_face(embedding, face_db, threshold1.0): min_dist float(inf) identity None for name, registered_emb in face_db.items(): dist euclidean_distance(embedding, registered_emb) if dist min_dist: min_dist dist identity name if min_dist threshold: return None, min_dist # 陌生人 return identity, min_dist这里threshold1.0是一个经验起点不是最终值。face_db可以是一个字典更规范的是用向量数据库比如Faiss当人脸库超过几万条时线性比对会变得很慢Faiss可以在几十毫秒内完成百万级的最近邻搜索。但毕设规模一般几百个人字典就够了不要过早引入重型依赖。识别完成后还可以在画面框上叠加名字和相似度分数这部分和OpenCV绑定交互有关。注意OpenCV默认字体不支持中文如果要显示中文需要PIL导入字体否则框上会出现乱码或豆腐块答辩演示时很尴尬。我一般选用PIL.ImageDraw.Draw.text配合中文字体文件绕开OpenCV的硬伤。4.3 阈值标定用验证集找最佳切分点很多人不知道相似度阈值该怎么定直接拍脑袋设0.8结果陌生人全被放进来。阈值不是模型超参数而是数据集上的决策边界应该用一份没有参与训练的人脸验证集来标定。标定方法很简单把真实同人的所有距离和真实不同人的所有距离都算出来画两条分布直方图阈值取两条曲线交点附近。更量化一点是计算每个候选阈值下的TPR和FPR然后选福尔误差最小的点。具体到代码你可以在验证集上跑一个循环def find_best_threshold(distances_same, distances_diff): best_threshold 0.0 best_error 1.0 # 在两个分布范围内枚举阈值 for t in np.linspace(min(distances_diff.min(), distances_same.min()), max(distances_diff.max(), distances_same.max()), 200): false_reject (distances_same t).mean() # 本来相同却被判异 false_accept (distances_diff t).mean() # 本来不同却被判同 error (false_reject false_accept) / 2 if error best_error: best_error error best_threshold t return best_threshold, best_error参数说明distances_same是同一人两张照片的匹配距离数组distances_diff是不同人的匹配距离数组。linspace里枚举了200个候选阈值你也可以改成100或1000粒度越细找到的阈值越接近最优但计算量线性增加。注意这里用均值误差作为目标如果你的场景更偏重“认假”或“拒真”可以给两类错误加权重比如门禁系统更不想放陌生人进去就可以给false_accept更高权重。5. 避坑指南训练与部署的五个高频问题排查在这一章我把在实际跑这类毕设工程时遇到最多的五个问题按现象、原因、解决的顺序列出来每个都是踩过之后花不少时间才绕过去的。5.1 现象CPU上检测极慢视频画面卡成PPT现象是在没有NVIDIA GPU的普通笔记本上用默认参数跑MTCNN检测1080p视频帧单帧检测时间就超过300毫秒再加上FaceNet特征提取整体一帧超过1秒视频画面基本卡成PPT。原因是MTCNN要构建图像金字塔在每层都跑一次卷积网络默认的scale_factor0.709意味着从原图到最小脸之间会生成很多层1080p下计算量非常大FaceNet的Inception-ResNet v1本身也是个重网络两者叠加自然吃不消。解决办法有三条路。第一把视频帧先缩放到640x480甚至更小再送入检测检测完成后用缩放比例还原边框坐标这一步能把MTCNN耗时降到100毫秒左右。第二隔帧检测每3帧或5帧检测一次中间帧沿用上一次的人脸框只对新出现的区域做特征提取这样能避免重复检测。第三调整scale_factor把它从0.709改成0.8金字塔层数变少速度变快但要注意小脸检出率会略微下降。实测下来缩放加跳帧的组合可以把实时视频从1FPS提升到15FPS左右对于毕设演示已经足够流畅。5.2 现象用cv2读图后识别率暴跌现象是同样一个人两张光线正常的照片算出来的欧氏距离竟然超过2.0甚至比不同人的距离还大直接导致识别错误。原因几乎都是颜色通道顺序搞反了。OpenCV的cv2.imread默认返回BGR顺序对图像做任何操作后如果不转成RGB就喂给模型模型看到的图片红色变成蓝色、蓝色变成红色FaceNet在预训练时使用的分布完全被打乱特征向量自然乱套。更隐蔽的是MTCNN内部也可能默认输入RGB如果你把BGR图直接传给detect_faces人脸的边框和关键点本身就会偏移错误从源头就开始了。解决办法很简单在项目里写一个统一的load_image函数里面固定用cv2.imread读取后立刻cv2.cvtColor(img, cv2.COLOR_BGR2RGB)所有调用方都走这个函数不允许其他方式读图。这样颜色顺序问题只在函数内部出现一次后面全局安全。我习惯把这个函数放在utils.py里并在文件头部加一行醒目的注释防止后来人绕过。5.3 现象模型加载报错提示结构不匹配现象是执行load_model(facenet_keras.h5)时抛出ValueError: Unknown layer或者Layer count mismatch甚至直接提示文件不存在但手动看文件又存在。原因主要有三个一是HDF5文件本身损坏或下载不完整很多资源源文件被截断模型结构信息残缺二是模型文件和当前Keras/TensorFlow版本不兼容新版本改了很多层实现老权重自然对应不上三是混用了keras和tf.keras两个库的序列化格式有差异用keras.models.load_model加载tf.keras保存的文件就会报错。解决办法先检查文件大小完整的高质量预训练权重最少也有90MB以上如果只有几十MB很可能是截断过的其次检查层名用model.get_layer(index)或model.summary()看第一层是什么如果是InputLayer且shape正确说明模型加载成功了最后统一使用tf.keras.models.load_model并且在Python端确认import tensorflow as tf和import keras没有出现在同一次会话里。如果这些检查都做了还报错就换一个来源之前有个同学换了五个文件才好原因是第五个文件才是真正匹配他环境的。5.4 现象同一人的两张照片距离反而很大现象是预处理、对齐、特征提取都按流程做了但同一人两张不同光照的照片欧氏距离达到1.5以上识别时被判成陌生人。原因往往出在质量过滤上。MTCNN定位关键点时有置信度如果人脸是侧脸、闭眼、低头状态五个关键点的位置会偏差很大仿射变换后眼睛不在水平线上或者裁剪框把半个耳朵切掉这种低质量对齐喂进FaceNetembedding就会飘。另一个原因是图像模糊尤其在摄像头场景下手稍微一晃整张脸都是糊的模糊图的embedding和清晰图的相距甚远。解决方法是加入质量门禁首先看检测结果的置信度低于0.9的就丢弃其次看左右眼距离如果两眼水平距离小于人脸框宽度的0.25说明脸太小或关键点挤在一起也丢最后在送入模型前对裁剪好的图做Laplacian方差判断方差值低于阈值就判定为模糊提示用户靠近或保持稳定。这套门禁加进去后同人距离能稳定降到0.8以下不同人距离一般能到1.2以上分得开多了。5.5 现象训练自己的数据时loss不下降现象是有人不甘心直接加载预训练想用自己的几千张照片微调模型但训练了十几个epochloss就是纹丝不动甚至越训越高。原因最典型的是数据量远不够triplet loss需要构建海量三元组几千张图只有几百个身份每个身份才几张照片能挖出的有效三元组太少网络根本学不出稳定规律。其次是三元组质量差随机采样生成的三元组大多是简单样本正样本太靠近、负样本太远模型不需要改变就能满足约束梯度信号很弱。解决办法不要从头训加载预训练权重后只微调最后几层或添加一个小型分类头学习率调到1e-4以下用批量内困难负样本挖掘来构造三元组也就是每个batch内只选那些距离接近的负样本参与loss计算。但在毕设场景我不建议走到这一步直接拿通用权重标定阈值足够应付答辩而且通用权重训练数据量比你大得多泛化能力更强。6. 进阶验证人脸识别效果的三个实用技巧到了这一步代码已经能跑通但答辩或演示时最怕“现场翻车”。我积累的三个技巧能让你在有限数据下把效果做扎实。第一个技巧是建立一份固定的“成对验证表”。在项目目录里放两个文件夹一个放十个人的标准照另一个放不同光线、不同角度的测试照用一份脚本自动计算所有测试照与标准照的匹配距离并输出一张表。这样每次改动预处理或阈值跑一遍就能看到整体变化而不是靠肉眼试一两张图。这个验证脚本可以做成命令行参数把阈值和距离写进CSV归档答辩时拿出来就是一张清晰的数据表格。第二个技巧是给识别结果加一个“二次确认”逻辑。单张图的embedding偶发性偏差很大尤其在摄像头推近推远时。常见做法是连续取5帧每帧提取embedding并和库中比对若连续3帧判定为同一人才输出最终结果否则继续等待。这个操作能把随机误检压掉一大截。代码上只需要维护一个长度为5的滑动窗口统计最近5帧的判定结果。第三个技巧是把特征提取封装成服务而不是在UI线程里直接跑模型。以Flask为例启动服务时加载模型一次后续每个请求都复用同一个session避免每次请求重新加载权重。这样在毕设答辩现场即使没有GPU也能让首次识别耗时不超过1秒。我之前带过一个同学他坚持要在笔记本上实时跑后来改成预加载模型加隔帧检测才终于从“卡到没法看”变成“能流畅演示”。从那以后我每次拿到这类工程第一件事就是先看模型加载和图像预处理函数是不是干净再谈后续调优。希望帮到你也祝你答辩顺利。本文还有配套的精品资源点击获取