
简介这套基于深度学习的人脸识别系统压缩包面向具备一定Python基础、希望实践人脸检测与识别技术的开发者与学习者覆盖从数据预处理、模型构建到训练优化、实时识别与API设计的完整流程。压缩包共33个文件其中8个Python脚本承担特征提取、人脸检测、识别及界面交互等核心功能7个fea特征文件与模型文件用于比对与推理另有若干图片用于测试展示3个Markdown文档提供说明与使用指引整体仅3.64MB便于快速下载与部署。系统以卷积神经网络为核心结合MTCNN等人脸检测方法并考虑了轻量化与后处理能够帮助读者理解FaceNet等模型的实际落地方式。当前已有104人学习下载适合作为课程设计、科研入门或企业原型验证的参考起点。1. 先从 FR-system-main 的项目结构看清人脸识别系统的真正骨骼拿到这套“基于深度学习的人脸识别系统”的源码包时先别急着一键运行把 FR-system-main 里的文件关系理清楚更重要。code 目录下同时出现了 face_detection.py、mtcnn、affineTrans.py、featureExtraction.py、face_recognition.py 和 window.py这就是一条完整的人脸识别流水线检测、对齐、提特征、比对、可视化。很多入门做法是直接调 face_recognition 库一行出结果但那样的项目跑完对深度学习理解没有提升而这份项目把每一层都拆成了独立模块想深入 Python 人脸识别实现的话是很合适的对照样本。2. MTCNN 人脸检测与 affineTrans 仿射对齐先把脸找出来、摆正再识别在整个人脸识别流程里检测和对齐的质量决定了后续特征比对能到什么水平。经常遇到的情况是识别模型本身不弱但输入的人脸是歪的、侧着的最后相似度还是不够。这一章从 face_detection.py 和 affineTrans.py 两个文件入手把检测和对齐的工程细节拆开。2.1 face_detection.py 里的 MTCNN 调用与参数选择MTCNN 由 P-Net、R-Net、O-Net 三级网络组成前两级做候选框粗筛最后一级同时输出人脸框和五个关键点。这个结构在 CPU 上也能跑到接近实时的帧率所以很多 Python 人脸识别项目选择它而不是直接上 YOLO。face_detection.py 里典型的初始化方式如下。from mtcnn import MTCNN import cv2 detector MTCNN(steps_threshold[0.6, 0.7, 0.7], min_face_size40) def detect_face(frame_rgb): results detector.detect_faces(frame_rgb) faces [] for face in results: if face[confidence] 0.9: continue x, y, w, h face[box] keypoints face[keypoints] # 五个关键点左右眼、鼻尖、左右嘴角 faces.append((x, y, w, h, keypoints)) return faces frame cv2.imread(demo.jpg) frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) faces detect_face(frame_rgb)这里的 steps_threshold 三个阈值分别对应 P-Net、R-Net、O-Net。第一个阈值放低到 0.6是为了让候选框尽量全后两级阈值设 0.7用来把背景误检压下去。min_face_size 控制最小人脸边长40 表示低于 40 像素的人脸直接不检测。实际使用中如果摄像头画面里人脸忽远忽近可以把它降到 30但误检率也会上升。另外要注意 MTCNN 输入必须是 RGB直接用 cv2.imread 得到的是 BGR少一步 cvtColor检测效果会有明显下降。参数建议值调低/调高的效果steps_threshold[0]0.6调低召回率高误检多steps_threshold[1]0.7调高误检少可能漏正脸steps_threshold[2]0.7对最终置信度影响最大min_face_size40调小能检远距离小脸虚检增多2.2 affineTrans.py 的仿射变换实现逻辑检测到人脸还不够。同一个人的脸左右转头 10 度像素分布就差很多直接提特征会导致同一身份在不同姿态下的特征不一致。affineTrans.py 的目的就是利用五个关键点把脸映射到一个统一坐标空间。import cv2 import numpy as np def align_face(img, keypoints, size160): left_eye np.array(keypoints[left_eye], dtypenp.float32) right_eye np.array(keypoints[right_eye], dtypenp.float32) mouth_left np.array(keypoints[mouth_left], dtypenp.float32) mouth_right np.array(keypoints[mouth_right], dtypenp.float32) eye_center (left_eye right_eye) / 2.0 mouth_center (mouth_left mouth_right) / 2.0 # 原图中的两个锚点双眼中心、嘴部中心 src_tri np.float32([eye_center, mouth_center]) # 标准图中的对应位置使用归一化坐标 dst_tri np.float32([(0.35, 0.35), (0.5, 0.7)]) matrix cv2.estimateAffinePartial2D(src_tri, dst_tri)[0] aligned cv2.warpAffine(img, matrix, (size, size)) return aligned这段代码只用了两个锚点双眼中心点和嘴部中心点。estimateAffinePartial2D 求的是 2x3 的仿射矩阵只包含平移、旋转和等比缩放不引入切变所以不会把脸拉成奇怪的形状。目标坐标 (0.35, 0.35) 和 (0.5, 0.7) 是归一化坐标表示双眼中心落在标准图横向 35%、纵向 35% 的位置嘴落在横向 50%、纵向 70% 的位置。这个比例参考了多数人脸数据集在裁剪时的习惯也接近 FaceNet 输入图的实际分布。如果项目里对齐结果有明显旋转残留常见原因是关键点坐标被重复缩放。例如 detect_faces 的输入已经缩放过但 keypoints 还按原始尺寸处理就会导致锚点偏移。写 affineTrans.py 时最好把输入输出都固定成原始分辨率统一在检测函数里做缩放映射。2.3 越界处理与检测框的过滤warpAffine 在对齐时如果关键点落在图像边缘输出图会出现黑边或裁掉一部分额头。常见做法是对检测框做 padding把框向外扩 20% 再裁剪这一步在 face_detection.py 返回 box 之后、喂给 affineTrans.py 之前完成。另外批量注册人脸照片时要把置信度低于 0.9 的结果丢到一旁重新拍摄否则一张模糊人脸被注册进库后续识别会持续产生误差。还有一个容易被忽略的问题如果 py 文件里混用了 OpenCV 和 matplotlib 的图像坐标shape 顺序会反过来对齐前最好打印一次关键点坐标和图像尺寸确认方向。3. featureExtraction.py 与 face_recognition.py特征向量怎么算、怎么比检测和对齐解决的是“脸在哪里”这一章解决“这是谁”。featureExtraction.py 负责把对齐后的人脸图通过深度学习模型映射成特征向量face_recognition.py 负责把当前向量和注册库比对。3.1 特征提取的网络结构与模型加载方式人脸识别领域主流的做法是用卷积神经网络训练一个分类模型然后在倒数第二层取特征向量。由于项目是 Python 工程featureExtraction.py 里常见的是加载 Keras 的 h5 权重或 TFLite 模型前向传播一次得到一个 128 维或 512 维的 embedding。需要注意加载权重后要确保模型处于推理状态Keras 里不需要额外设置但如果用 PyTorch 的 .pt 权重必须调用 model.eval()。import numpy as np import tensorflow as tf class FeatureExtractor: def __init__(self, model_path, input_size160): self.model tf.keras.models.load_model(model_path) self.input_size input_size def get_embedding(self, aligned_rgb): face aligned_rgb.astype(float32) / 255.0 face (face - 0.5) * 2.0 # 线性映射到 [-1, 1] face np.expand_dims(face, axis0) emb self.model.predict(face, verbose0)[0] norm np.linalg.norm(emb) 1e-8 return emb / norm这段代码里最关键的是最后的 L2 归一化。特征向量被归一化到单位长度之后后面的余弦相似度可以直接用点积计算省掉一次 acos也避免向量模长对相似度造成干扰。输入图的归一化方式必须与训练时一致FaceNet 系列一般用线性归一化到 [-1, 1]OpenFace 则常见减均值除以标准差。如果对齐输出是 160x160x3模型要求 96x96需要先 resize 再送进去直接喂会报 shape 错误。还有一个常见坑加载模型后第一帧推理往往偏慢因为 TensorFlow 在做图初始化不要在性能测试时把这一帧算进去。3.2 face_recognition.py 的相似度度量和阈值判断face_recognition.py 常见的结构是注册阶段把每个人的特征向量存成字典或 numpy 矩阵识别阶段遍历比对。中规模项目里更推荐把所有注册特征拼成一个矩阵一次性算出所有相似度避免 for 循环逐条计算的无谓开销。import numpy as np def recognize(embedding, db_names, db_embs, threshold0.65): # db_embs 形状为 (N, D)每行是一个已归一化的注册特征 scores np.dot(db_embs, embedding) idx int(np.argmax(scores)) if scores[idx] threshold: return db_names[idx], float(scores[idx]) return unknown, float(scores[idx])这个实现的前提是 db_embs 每一行都已经归一化embedding 也已经归一化所以 np.dot 结果就是余弦相似度。threshold 到底设多少取决于误识率和拒识率的权衡。之前用一份公开数据集做过统计0.65 附近通常对应较低的误识率但同一个人的不同照片因为光线差异也可能掉到 0.6 以下如果把阈值降到 0.55识别率会上去陌生人误开的概率也会明显增加。度量方式计算形式同一人典型区间适用场景余弦相似度dot(a, b)0.60 ~ 0.85多数开源 embedding 模型欧氏距离np.linalg.norm(a - b)0.80 ~ 1.40配合未归一化向量归一化欧氏距离先 L2 再算距离0.30 ~ 0.90阈值范围更稳定这里需要说明一点很多 FaceNet 开源权重在评测时用的是欧氏距离阈值大概在 1.1 左右如果代码里已经对向量做了 L2 归一化再算欧氏距离等价于余弦相似度的单调变换但阈值不能照搬。一个有效的验证方法是挑 10 张本人照片和 20 张陌生人照片画出相似度分布把阈值放在两个分布交界偏右的位置。3.3 阈值调优时的数据分析思路调阈值不靠猜。face_recognition.py 里可以临时加一段统计代码把每次比对得到的最高分和真实标签写进 CSV然后用 pandas 统计分布。这样也能发现数据质量问题如果同一个人的注册特征和测试特征相似度只有 0.5问题往往不是阈值而是对齐时关键点偏移或者注册照片和测试照片光照条件差异太大。先看数据分布再决定是调阈值还是换注册照片。4. models 与 images 的工程组织模型权重、注册数据和训练结构拿到 FR-system-main 之后很多人只盯着 code 目录却忽略 models 和 images 这两个目录才是决定系统能不能跑起来的关键。模型权重决定特征表达能力的上限数据组织方式决定注册和识别流程的效率。4.1 models 目录里的权重文件与加载方式项目里有 models 目录说明设计者预留了权重文件的位置。常见做法是放一个检测器权重和一个人脸特征模型权重。检测器可以直接用 mtcnn 库自带权重特征模型则根据训练框架分为几种格式加载方式差异很大。# Keras h5 权重 model tf.keras.models.load_model(models/facenet.h5) # TensorFlow SavedModel 目录 model tf.saved_model.load(models/facenet_savedmodel) # TFLite 权重适合边缘设备 interpreter tf.lite.Interpreter(model_pathmodels/facenet.tflite) interpreter.allocate_tensors()如果是 h5 格式直接 load_model 即可模型结构包含在文件里如果是 pb 或 SavedModel需要知道输入输出张名才能做前向推理TFLite 则适合部署到低算力环境但输入输出的张量顺序要通过 get_input_details 查一次。加载完成后最好先拿一张标准人脸图做单次推理确认输出向量维度。输出维度不对时数据库里的注册特征形状和当前特征形状无法点积face_recognition.py 会直接报错。文件格式常见后缀加载方式典型场景Keras 完整模型.h5keras.models.load_model训练后直接保存SavedModel目录tf.saved_model.load部署到 TensorFlow ServingTFLite.tflitetf.lite.Interpreter边缘设备、低算力环境PyTorch 权重.pttorch.load load_state_dict训练脚本使用较多4.2 images 目录的注册库组织方式images 目录通常按人名分子目录存放注册照片例如 images/zhangsan/001.jpg。这种结构的好处是注册阶段可以直接根据目录名生成标签不用额外维护数据库表。不过照片文件的命名要规范否则后续做数据分析时标签容易乱。一个常用的注册表生成脚本可以这样写import os import csv def build_registry(rootimages, outputregistry.csv): with open(output, w, newline, encodingutf-8) as f: writer csv.writer(f) writer.writerow([path, label]) for label in sorted(os.listdir(root)): label_dir os.path.join(root, label) if not os.path.isdir(label_dir): continue for fname in sorted(os.listdir(label_dir)): if fname.lower().endswith((.jpg, .jpeg, .png)): writer.writerow([os.path.join(label_dir, fname), label])这段脚本遍历 images 下每个子目录把每张照片的绝对路径和人名写入 registry.csv。后面注册特征时按行读取 CSV对每张照片做检测、对齐、提特征最后把 label 和 embedding 存入数据库。参数方面要注意 root 路径必须用绝对路径否则换目录运行脚本会出现文件找不到的问题label 里也不要混入中文之外的特殊符号Windows 下目录名和编码处理容易出错。4.3 从数据分析角度检查注册数据集的质量注册数据集的质量直接决定识别效果。常见问题是某些人照片数量太少或者存在大量重复帧。用 pandas 读一下统计结果很快就能发现问题。import pandas as pd df pd.read_csv(registry.csv) stats df.groupby(label)[path].count().sort_values() print(stats)如果某个人只有 1 张注册照片说明这个人一旦换角度、换光线就会掉到阈值以下如果某个人有 50 张但其中 30 张是从视频里连续抽帧得到的那这些高度相似的样本会让特征向量过拟合到特定姿态。针对前者补拍不同角度的照片针对后者可以按时间间隔抽帧或者先用聚类去掉近重复帧。对检测和对齐过程也可以统计失败率注册阶段检测不到人脸的图片建议直接从库里删掉不要勉强处理。5. window.py 的 GUI 封装把识别流程接到桌面应用上最后是 window.py。这个文件的价值在于把前面几个模块串成一个可交互的界面而不是让人在命令行里手动跑脚本。5.1 摄像头循环与模型加载分离常见错误是在视频循环里初始化特征提取模型。FaceNet 这类模型加载一次要几百毫秒到几秒放在循环里帧率直接归零。正确做法是启动时初始化一次循环里只做推理。extractor FeatureExtractor(models/facenet.h5) detector MTCNN() cap cv2.VideoCapture(0) while True: ok, frame cap.read() if not ok: break faces detect_face(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)) for face in faces: aligned align_face(frame, face[4]) emb extractor.get_embedding(aligned) name, score recognize(emb, names, embeddings) # 绘制检测框和姓名 if cv2.waitKey(1) 0xFF ord(q): breakcap cv2.VideoCapture(0) 打开默认摄像头waitKey(1) 表示每帧等待 1 毫秒并返回按键值按 q 退出。如果是 USB 摄像头有时候 index 不是 0需要换成 1 或 2 逐个试。模型、检测器、注册表都在循环外初始化这是保证实时性的前提。5.2 界面卡顿的来源与规避如果直接把识别放到主线程遇到复杂背景时检测耗时起伏很大画面会一卡一顿。常见做法是用一个队列把摄像头帧交给后台线程处理主线程只负责绘制。队列要控制长度超过两帧就丢旧帧保证显示的是接近实时的结果而不是积压的旧画面。GUI 用的如果还是 Tkinter不要在回调函数里直接跑完整的识别链路回调只负责触发后台线程。5.3 注册与识别模式切换window.py 如果支持录入新面孔要注意注册和识别不能同时修改同一份数据库。我的做法是进入注册模式后停滞当前识别逻辑连续取三帧对齐后人脸计算三个特征向量取两两相似度的中位数只有中位数满足一致性要求才写入 images 目录和注册表。这样能避免拍到一半时脸部移动或遮挡导致注册进一张坏图。实时显示区域画出检测框和相似度分数即可不要把 embedding 数值直接打到屏幕上对展示意义不大。本文还有配套的精品资源点击获取