简介:这是一份面向深度学习与人脸识别方向学习者的项目代码包,基于CNN等模型实现从人脸检测、特征提取到身份识别的完整流程。资源提供Python源码、特征文件、图像样本与说明文档,共33个文件,8个py脚本覆盖MTCNN检测、特征提取、识别与窗口交互等核心功能,7个fea文件保存预提取的人脸特征,png和jpg图片可用于测试与可视化,md文档帮助梳理项目结构,压缩包整体仅3.64MB,方便下载与本地复现。目前已有104人学习使用。通过阅读代码与运行示例,可理解深度学习人脸识别系统的工程组织方式,掌握人脸对齐、特征比对、相似度计算等关键环节,并可作为课程设计、毕业设计或算法入门实验的参考起点。
1. 基于深度学习的人脸识别,选型比训练更靠前
在学校实验室把开源人脸识别项目跑通后,直接搬到厂区门禁机上,第一天就被两件事打脸:打印照片骗过了活体判断,夜间红外补光下检出率掉到三成。标题里“系统”两个字的分量,远大于“深度学习”。检测、对齐、特征提取、比对、阈值、缓存和外围业务,任何一环没兜住,模型再强也白搭。
这类系统要解决的核心问题,是在复杂光照、姿态变化、遮挡和并发请求下保持可靠的识别供给。常见路径是 MTCNN 做检测对齐,FaceNet 或 ArcFace 提取特征,再封装成对外推理接口,覆盖门禁考勤和中小规模人脸检索。
适合读这篇内容的,是已经跑通某个深度学习人脸识别项目、接下来要处理阈值标定与业务对接的人。下文从选型开始,逐步落到可跑代码和调优参数。
2. 人脸识别链路拆解与深度学习方案选型
实际项目里最常见的提问是“换了一个人脸识别算法之后,准确率反而下降”。直接原因往往是:把不同环节的模型塞进同一个 Python 脚本里调用,却没有看清各自的输入输出约束。一个完整的人脸识别系统至少有四个环节:目标检测、关键点定位与对齐、特征提取、特征比对。
检测环节从图像里框出人脸,常见候选是 MTCNN 和 RetinaFace,输出边界框加五个关键点概率;对齐环节用关键点做仿射变换,把倾斜、侧脸矫正到标准姿态,这一步对后续特征提取的影响经常被低估——同一人的照片在未对齐与已对齐情况下,余弦相似度可能相差 0.1 以上,足以跨过阈值造成误判。特征提取环节把人脸编码成高维向量,比对环节则用余弦相似度或欧氏距离判定是否为同一人。
2.1 识别模型选型:FaceNet、ArcFace 与开源商用模型的取舍
深度学习在这条链路里的“深度”主要体现在特征提取和损失函数设计上。2015 年 FaceNet 用三元组损失让同一人的特征拉近、不同人推远,识别性能大幅超过传统手工特征;之后 SphereFace、CosFace 再到 ArcFace,本质是把训练损失换成带角度间隔的 Softmax 变体,让特征在超球面上类内收紧、类间分散。
| 模型 | 常用主干 | 输出维度 | 特点 | 适用场景 |
|---|---|---|---|---|
| FaceNet | Inception-ResNet v1 | 512 | 实现简单,生态成熟 | 原型验证、中小规模库 |
| ArcFace | ResNet50 / 100 | 512 | 角度间隔,精度上限高 | 门禁考勤、人脸检索 |
| MobileFaceNet | Mobile 风格 | 128 / 512 | 计算量小 | 边缘设备、人脸识别门禁机 |
我一般分三层选型:效果优先选 ArcFace,它在 LFW、AgeDB 等公开基准上的指标一直靠前;速度优先选 MobileFaceNet,适合塞进低算力的嵌入式设备;商业闭源 SDK 是第三种选择,精度通常不差,但按调用量计费,且离线部署时“数据不出本地”的诉求容易被卡住。开源开箱的人脸识别模型中,ArcFace 预训练权重可以直接下载,但下载前要看清训练数据与 License,部分权重只允许学术用途。
2.2 对齐模块的关键点顺序与仿射变换参数
模型换来换去,最容易出错的是五个关键点的顺序。MTCNN 输出的是“左眼、右眼、鼻尖、左嘴角、右嘴角”,而 RetinaFace 在部分开源实现里输出的是“左眼角、右眼角、鼻孔左、鼻孔右、下巴尖”。沿用之前项目里的对齐代码而不检查顺序,仿射矩阵会被算错,喂进特征网络的就是歪脸。
import cv2 import numpy as np # 假设 keypoints 形状为 (5, 2),顺序为 左眼, 右眼, 鼻尖, 左嘴角, 右嘴角 # 如果不是这个顺序,先按该约定重排再使用 src = np.array([ keypoints[0], # 左眼 keypoints[1], # 右眼 keypoints[2] # 鼻尖 ], dtype=np.float32) # 标准人脸模板,来自 ArcFace 的常用坐标 dst = np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366] ], dtype=np.float32) M = cv2.getAffineTransform(src, dst) aligned = cv2.warpAffine(img, M, (112, 112))仿射变换只需要三个不共线的点,选左眼、右眼和鼻尖就足够。warpAffine的目标尺寸要和特征提取网络的输入保持一致,ArcFace 系默认 112×112,FaceNet 的 MTCNN 内部则常缩到 160×160,两者混用会把大量时间浪费在无意义的 bug 上。还有一类隐蔽问题:检测阶段把置信度阈值调低,可以捡回更多模糊人脸,但其中相当比例无法对齐成功,反而拉高特征提取失败率。实践中常用双路召回:第一路高阈值直接对齐,第二路低阈值检出但要求关键点置信度足够,两路汇合后再做质量过滤。
2.3 推理框架边界:PyTorch 导出 ONNX 再部署
模型在服务端跑 PyTorch 没有压力,但换到人脸识别门禁机这类边缘设备上,目标环境不一定有完整 PyTorch 依赖。常见做法是先把模型导出成 ONNX,再借助 ONNX Runtime 推理,体积和依赖都更小。
import torch model = InceptionResnetV1(pretrained='vggface2').eval() dummy = torch.randn(1, 3, 160, 160) torch.onnx.export( model, dummy, "face_encoder.onnx", input_names=["input"], output_names=["embedding"], dynamic_axes={"input": {0: "batch"}, "embedding": {0: "batch"}}, opset_version=17 )dynamic_axes只把 batch 维设为动态,其余维度保持静态,既支持一次处理多张人脸,又避免整张图动态尺寸带来的复杂度。部署前要检查 BatchNorm 是否已经融合,PyTorch 导出的 ONNX 会自动处理,但自定义预处理如果落在模型内部,容易让输入分布偏移。INT8 量化时要特别关注 PReLU 这类算子的兼容性,ReLU 的量化实现比 PReLU 更成熟,换激活函数有时比调量化参数更省事。
3. 用 PyTorch 搭建最小可跑的人脸识别系统
接下来的目标,是在本地把“检测 → 对齐 → 特征提取 → 比对”的闭环跑通,并留下可扩展的接口。以 MTCNN 加 FaceNet 的组合为例,依赖最少、权重最好拿,先跑起来,再考虑替换成 ArcFace 或轻量模型。
3.1 深度学习环境配置:先把依赖版本对齐
深度学习环境配置里最常见的坑是版本漂移。PyTorch 与 CUDA 版本不匹配时,torch.cuda.is_available()会返回 False,程序不报错,但全程用 CPU 跑,推理慢一个数量级。建议直接写死依赖文件。
pip install torch==2.1.2 torchvision==0.16.2 --index-url https://download.pytorch.org/whl/cu118 pip install facenet-pytorch opencv-python numpy pandasfacenet-pytorch 自带 MTCNN 与 Inception-ResNet v1 预训练权重,封装方便,但它的 FaceNet 输出是 512 维,不是原论文里的 128 维,对接下游代码时要注意维度声明。如果部署环境不能联网下载 whl,先在能联网的机器上拉包,再离线安装,同时确认 CUDA 运行时版本与驱动匹配。
| 软件 | 版本建议 | 说明 |
|---|---|---|
| Python | 3.9~3.11 | 过老版本对新版 PyTorch 支持差 |
| CUDA Toolkit | 11.8 | 与 PyTorch cu118 构建对应 |
| PyTorch | 2.1.x | 对动态图和 ONNX 导出支持较完整 |
| opencv-python | 4.x | 注意 BGR/RGB 通道顺序 |
3.2 检测、对齐、特征提取一次完成的推理代码
把链路串成函数,输入图片路径,输出归一化特征向量。这里用 facenet-pytorch 的 MTCNN 完成检测和对齐。
import torch import numpy as np from facenet_pytorch import MTCNN, InceptionResnetV1 from PIL import Image # keep_all=False 表示一张图只保留置信度最高的人脸 device = 'cuda' if torch.cuda.is_available() else 'cpu' mtcnn = MTCNN( image_size=160, margin=0, min_face_size=40, thresholds=[0.7, 0.8, 0.8], device=device ) encoder = InceptionResnetV1(pretrained='vggface2').eval().to(device) def face_embedding(image_path: str) -> np.ndarray: img = Image.open(image_path).convert('RGB') face = mtcnn(img) # 返回 160x160 对齐后的人脸张量 if face is None: raise ValueError('no face detected') with torch.no_grad(): emb = encoder(face.unsqueeze(0).to(device)).cpu().numpy()[0] return emb / np.linalg.norm(emb) # 单位化,后续直接用余弦相似度min_face_size=40表示小于 40 像素的人脸直接丢弃,太低会引入大量噪声检测框。thresholds是 MTCNN 三级网络的置信度阈值,调低能召回更多困难样本,但会带入误检,后续要靠关键点分数兜底。单位化这一步不能省,否则余弦相似度会被向量模长干扰。
3.3 批量建库与比对:从单张图片到全量人脸库
门禁系统的注册阶段,需要把员工照片批量向量化并建立索引。这里用 CSV 记录人员 ID 和特征字节流,匹配时逐行计算余弦相似度。
import pandas as pd from pathlib import Path from scipy.spatial.distance import cosine def build_registry(image_dir: str, output_csv: str): records = [] for img_path in sorted(Path(image_dir).glob('*.jpg')): person_id = img_path.stem.split('_')[0] try: emb = face_embedding(str(img_path)) records.append({'id': person_id, 'path': str(img_path), 'emb': emb.astype(np.float32).tobytes()}) except ValueError: continue pd.DataFrame(records).to_csv(output_csv, index=False) def match(emb: np.ndarray, registry: pd.DataFrame, threshold: float = 0.6): best_id, best_sim = None, -1.0 for row in registry.itertuples(): lib = np.frombuffer(row.emb, dtype=np.float32) sim = 1.0 - cosine(emb, lib) if sim > best_sim: best_sim, best_id = sim, row.id return (best_id if best_sim >= threshold else None), best_sim特征写入 CSV 前转成 bytes,读出来时必须以 float32 还原,丢了 dtype 参数会还原出异常数值。余弦相似度的阈值 0.6 只是临时的,后续要用真实正负样本对做 ROC 校准。人员规模到几千人时逐行遍历还能接受,再往上就要引入 FAISS 或 hnswlib 一类的向量索引。
3.4 视频流抽帧与多目标策略
接入实时摄像头后,不能每帧都跑全链路。25 帧的 1080P 视频流,MTCNN 在 CPU 上单帧就要几十毫秒,GPU 上也只有每秒 20 帧左右的吞吐,再叠加特征提取必然超时。常见做法是抽帧检测,检测命中后再放大裁剪人脸块做对齐与特征提取。
# 每隔 3 帧取 1 帧,小图检测,命中后回原图提取 frame_index = 0 while True: ret, frame = cap.read() if not ret: break if frame_index % 3 != 0: frame_index += 1 continue small = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5) boxes, probs = detect_small(small) # 轻量检测,自封装函数 for box in boxes: x1, y1, x2, y2 = [int(v * 2) for v in box] # 坐标还原到原图 face_block = frame[max(y1, 0):y2, max(x1, 0):x2] # 对 face_block 单独做对齐与特征提取 frame_index += 1小图上检测,原图上裁块,计算量能明显降下来,但要记得坐标按缩放比例还原,裁剪边界做限位。多目标场景还要加质量过滤:人脸面积小于 60×60、亮度方差过低、模糊度评分过高的块直接跳过。这些过滤放在特征提取之前,能省下大量无效计算。
4. 人脸识别模型训练与调优参数配置
如果只做推理,预训练权重够用。但想要在特定人员分布或自有数据上获得更高精度,就得进入微调和训练。这一章的参数配置直接决定模型能不能收敛、收敛到什么精度。
4.1 ArcFace 损失实现与角度间隔选择
ArcFace 给 Softmax 的 logits 叠加一个角度边界,训练时类内特征被压向分类中心,类间特征被推开。最小实现如下:
import torch import torch.nn as nn import torch.nn.functional as F class ArcFaceLoss(nn.Module): def __init__(self, s=64.0, m=0.5): super().__init__() self.s = s self.m = m self.cos_m = torch.cos(torch.tensor(m)) self.sin_m = torch.sin(torch.tensor(m)) def forward(self, cosine, label): # cosine 形状 [N, num_classes],由特征与分类中心归一化点积得到 theta = torch.acos(cosine.clamp(-1.0, 1.0)) target_cos = torch.cos(theta + self.m) one_hot = F.one_hot(label, num_classes=cosine.size(1)).bool() output = torch.where(one_hot, target_cos, cosine) return F.cross_entropy(self.s * output, label)s是特征缩放因子,太小会让 Softmax 梯度被压平,建议从 64 开始;m默认 0.5,增大能拉大类间距,但收敛会变慢。训练时穿插难样本挖掘很有必要,简单样本占比过大时,loss 停在 0.5 附近不再下降,这往往不是学习率问题,而是批次内负样本太容易区分。
4.2 批次内 PK 采样与数据增强配置
Triplet Loss 和 ArcFace 并不冲突,工程里常见两阶段:先用 ArcFace 训到分类收敛,再用 Triplet Loss 精调少量 epoch,针对困难样本再迈一步。Triplet Loss 需要同一个人的多张图出现在同一批次,通常采样 32~64 个 ID,每个 ID 取 4~8 张图。
| 参数 | 推荐范围 | 作用 | 调低或调高的影响 |
|---|---|---|---|
| batch_size | 64~256 | 影响 BN 统计量 | 过小 BN 抖动,过大显存溢出 |
| embedding_size | 512 | 特征向量维度 | 过小区分度差,过大检索变慢 |
| learning_rate | 1e-4~1e-5 | 优化步长 | 过大震荡不收敛,过小收敛极慢 |
| arcface_m | 0.4~0.6 | 角度间隔 | 过大难收敛,过小分不开 |
| triplet_margin | 0.2~0.5 | 距离间隔 | 过大负样本惩罚过重 |
数据增强围绕真实场景分布来设计:随机亮度扰动、随机饱和度扰动、随机遮挡块、随机高斯模糊都有效。不建议随机旋转超过 15 度,对齐模块本身负责纠正姿态,训练数据里塞进大幅侧脸反而会让模型把“侧脸”当作身份特征的一部分。
4.3 预训练权重、Epoch 与早停策略
从零训练人脸识别模型,需要的数据量通常在数十万张以上,中小团队很难凑齐。常见做法是借用公开数据预训练权重,再在自己的业务数据上微调。Epoch 设置要看验证集走势:训练集 loss 一直降,但验证集识别率从第 20 个 epoch 开始不再上升,就该早停并回滚到最优权重。验证指标建议直接用 ROC-AUC 或在 LFW 协议式的测试集上算准确率,比只看 loss 更贴近业务。学习率调制上,前 5 个 epoch 做 warmup,线性升到设定值,之后用余弦退火下降,是比较稳妥的配置。微调时不要把 backbone 冻结时间拉得太长,人脸特征层需要参与训练,角度边界才能生效。
5. 人脸识别系统部署:接口封装与阈值标定
前面的链路是单机 Python 函数,业务方要接入还得吐出一个接口。最直接的做法是把特征提取和比对封装成 REST API,同时解决并发、阈值和跨设备一致性问题。
5.1 FastAPI 封装与全局模型句柄
用 FastAPI 把 embedding 暴露成 HTTP 接口,模型只加载一次,不随请求反复初始化。
from fastapi import FastAPI, UploadFile import numpy as np import cv2 import uvicorn app = FastAPI() @app.on_event("startup") def load_model(): # 全局句柄,避免每个请求重新建模型 app.state.mtcnn = mtcnn app.state.encoder = encoder @app.post("/embedding") async def embedding(file: UploadFile): image_bytes = await file.read() # 处理图片解码失败的情况,imdecode 可能返回 None nparr = np.frombuffer(image_bytes, np.uint8) img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) if img is None: return {"error": "invalid image"} emb = face_embedding_from_ndarray(img) return {"dim": len(emb), "vector": emb.astype(float).tolist()}on_event("startup")里加载模型,后续每个请求复用。输入图片最好由前端先行压缩到长边 640 像素,避免超大图拖慢整个推理线程。并发较高时,把视频流抽帧和 REST 调用解耦,抽帧模块只管产出人脸块,比对服务做批量向量检索。
5.2 用 ROC 曲线校准识别阈值
之前代码里的阈值 0.6 只是占位。真实部署前,需要准备一组同人对和不同人对的相似度分数,用 ROC 曲线挑选业务可接受的平衡点。
from sklearn.metrics import roc_curve, auc import numpy as np y_true = [1] * len(same_sims) + [0] * len(diff_sims) y_score = np.concatenate([same_sims, diff_sims]) fpr, tpr, thresholds = roc_curve(y_true, y_score) # 约登指数所在位置是综合准确率最高的点 idx = np.argmax(tpr - fpr) print("auc=%.4f" % auc(fpr, tpr)) print("threshold=%.4f" % thresholds[idx]) print("fpr=%.4f tpr=%.4f" % (fpr[idx], tpr[idx]))实际选点不要只看约登指数。考勤系统更强调低误识,就往 FPR < 0.1% 的方向找阈值;安防场景更怕漏人,就在 TPR > 97% 的范围内挑最小 FPR。
注意:阈值校准用的正负样本对必须与线上特征提取链路完全一致,任何预处理差异都会让 ROC 曲线失真。
阈值标定后,要用不同摄像头、不同时段的照片做跨域验证,单用训练域内数据选出的点在换环境后往往跑偏。
5.3 换机排查:从通道顺序到特征数值比对
换了个硬件环境识别率骤降,先不要急着重训模型,按下面顺序排查:
| 现象 | 排查路径 | 常见原因 |
|---|---|---|
| 全部识别失败 | 打印单帧图片检查颜色 | BGR/RGB 通道顺序颠倒 |
| 暗光下找不准人脸 | 检查补光后有无过曝区域 | 红外补光白平衡异常 |
| 部分人时好时坏 | 对比新旧设备同图特征差 | 摄像头色温与画面锐度不同 |
一个很有效的定位手段是拿同一张图在本地与边缘设备上分别提取特征,打印前 16 维数值做对比。如果前几位连续性差异明显,基本可以断定预处理链路不一致;如果数值几乎一致但业务表现不同,再去查阈值和注册库。这套顺序在正式压测前就能省下大半天时间。
本文还有配套的精品资源,点击获取