拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

PyCharm实战:MTCNN+ArcFace与传统人脸识别算法准确率对比

PyCharm实战:MTCNN+ArcFace与传统人脸识别算法准确率对比

前几个月在 PyCharm 里把一个老旧的 OpenCV 人脸识别 demo 升级成了 MTCNN + ArcFace 方案,顺手和传统算法做了一轮同条件对比,准确率差距让我有点意外。这篇文章把整个测评过程、完整代码和对比数据都记录下来,给打算在 PyCharm 里做人脸识别的朋友一个参考。如果你正在传统算法和深度模型之间犹豫,或者只是想知道 MTCNN + ArcFace 到底能把识别准确率做到什么水平,这篇实测记录应该能帮上忙。

我一开始是抱着“传统算法够用就行”的心态开始的,但真正跑完测试之后才发现,同样一批评测数据,ArcFace 的 Rank-1 识别准确率几乎接近满分,而 LBPH、Eigenfaces、Fisherfaces 这些经典方法多多少少都有明显误识。差距背后的原因并不复杂,但踩过的坑不少,尤其是 PyCharm 环境下依赖安装、模型加载、图像对齐这几个环节,每个都能卡住一大片人。

1. 这次对比实验到底在比什么

1.1 为什么是 ArcFace 而不是其他深度模型

传统人脸识别算法,比如 LBPH、Eigenfaces、Fisherfaces,本质上都是在“小尺寸、正脸、光线均匀”的假设下做特征提取和分类。Eigenfaces 用 PCA 降维,Fisherfaces 用 LDA 找判别方向,LBPH 则靠局部纹理直方图。它们在 ORL 这种同背景、正脸、光照变化小的数据集上表现还行,但一旦换到复杂场景,姿态、表情、遮蔽一变,准确率就会明显往下掉。

ArcFace 属于度量学习思路,核心是 ArcFace Loss,也叫 Additive Angular Margin Loss。它把特征向量归一化到超球面上,再在角度空间里给正确类别加上一个 margin,让同类特征更聚拢、异类特征更分散。实际使用中,我们通常不自己训练,而是直接拿预训练的 ResNet50 模型,把人脸图像映射成 512 维特征向量,然后用余弦相似度做人脸比对。

选择 ArcFace 而不是 FaceNet、CosFace 这些同类方案,主要看中两点:一是预训练模型成熟,社区使用量大,踩坑资料多;二是 MTCNN 检测加对齐之后输入 112×112 的标准化人脸,ArcFace R50 的精度表现非常稳定。这篇文章里用到的模型也是基于 InsightFace 训练思路得到的 ONNX 版本,部署起来不需要额外安装复杂的深度学习框架。

1.2 实验设计:数据集、评测指标和硬件环境

对比实验要公平,首先得统一数据集和评测流程。我选的是 ORL 人脸数据集,这个数据集包含 40 个人,每人 10 张 112×92 的灰度图像,姿态、表情、是否戴眼镜都有细微变化,属于人脸识别领域最常用的基准数据集之一。每类取 7 张做注册训练,剩下 3 张做测试,也就是说测试集一共 40 乘 3 等于 120 张图。

评测指标上,我主要看 Rank-1 识别准确率,也就是对每一张测试人脸,系统给出的最高分候选是否对应该人的正确标签。另外还记录了单张图像的推理耗时,以及不同相似度阈值下的开集识别表现。硬件环境是 CPU 为 i7-10700,内存 16GB,显卡是 GTX 1660 6GB,PyCharm 里配置的是 Python 3.9 虚拟环境。传统算法和 ArcFace 都在同一台机器、同一份数据划分下运行,确保结果可以横向对比。

有一点需要提前说明:对于 ArcFace,我没有在 ORL 上重新训练,而是用预训练模型做迁移特征提取,只训练了后面的相似度比对逻辑。这其实也是实际项目里最常见的用法,因为大部分人没有足够的人脸训练数据,直接使用公开预训练模型,再把特征库换成自己的目标人物库,就能达到不错的识别效果。

2. 环境准备:PyCharm + Python + 模型依赖怎么搭

2.1 用 Anaconda 创建独立环境,避免依赖地狱

人脸识别相关依赖之间很容易出现版本冲突,尤其是 NumPy、OpenCV、TensorFlow 这几个包互相踩版本的情况我在 Windows 上遇到太多次了。所以第一步不是直接打开 PyCharm 写代码,而是先创建一个独立的 conda 环境。

conda create -n face_rec python=3.9 -y conda activate face_rec

Python 3.9 是一个兼容性比较好的版本,mtcnn、opencv-contrib-python、onnxruntime 在这些版本下都有现成的 wheel 包。创建完环境之后,在 PyCharm 的 Settings 里找到 Project Interpreter,选择这个 conda 环境,后续安装的包就能直接在 PyCharm 里识别到了。

这里要提一句,不需要去折腾那些来历不明的“激活”方式,JetBrains 官方社区版对本次实验完全够用,配置好解释器之后写代码、跑脚本、看调试输出都没有问题。如果团队有教育授权或者公司统一采购了专业版,也可以正常激活,但不要使用破解补丁或盗版授权,尤其是涉及公司项目时风险很大。

2.2 安装 opencv、mtcnn、onnxruntime 的版本组合

依赖安装命令如下,我踩过版本坑之后锁定的组合是:

pip install numpy==1.24.3 pip install opencv-contrib-python==4.8.1.78 pip install mtcnn==0.1.1 pip install onnxruntime==1.16.3 pip install scikit-learn

这里有个重要细节:OpenCV 需要安装opencv-contrib-python,而不是普通的opencv-python。LBPH、Eigenfaces、Fisherfaces 这些传统人脸识别器都在cv2.face模块里,这个模块只存在于 contrib 版本中。如果你只装了opencv-python,导入cv2.face时会直接报AttributeError: module 'cv2' has no attribute 'face'。

MTCNN 我选择的是mtcnn这个轻量级库,它底层基于 TensorFlow,接口非常简单。如果你不想引入 TensorFlow,也可以换用facenet-pytorch里的 MTCNN 实现,核心返回结果是一样的,都是人脸框和五个关键点坐标。

ArcFace 模型我使用的是 ONNX 格式的 R50 预训练权重,文件放在项目根目录下的models/文件夹里,主要文件是w600k_r50.onnx。这种模型一般从 InsightFace 官方仓库发布页就可以下载,下载后只需要用 onnxruntime 加载,不需要装 MXNet 或 PyTorch。验证环境是否正常,可以执行:

python -c "import cv2, mtcnn, onnxruntime; print(cv2.__version__, onnxruntime.__version__)"

看到版本号正常输出,说明环境基本就绪。

3. 核心实现:MTCNN 人脸检测 + ArcFace 特征提取

3.1 用 MTCNN 做检测和对齐,代码怎么写

MTCNN 的全称是 Multi-task Cascaded Convolutional Networks,它会同时输出人脸边界框、五个关键点(左眼、右眼、鼻子、左嘴角、右嘴角)以及人脸分类置信度。人脸识别模型对输入人脸的标准程度非常敏感,如果直接裁剪检测框然后缩放,眼睛不在固定位置,ArcFace 提取到的特征会有明显偏差。

所以,完整流程应该是:先读图,转成 RGB,因为mtcnn库默认要求 RGB 输入,而 OpenCV 读出来的是 BGR。然后调用检测器识别关键点,最后用关键点做一个仿射变换,把五官对齐到模板位置。

import cv2 import numpy as np from mtcnn import MTCNN detector = MTCNN() # Standard alignment landmarks for 112x112 input REFERENCE_LANDMARKS = { "left_eye": (38.0, 36.0), "right_eye": (74.0, 36.0), "nose": (56.0, 48.0), "mouth_left": (30.0, 84.0), "mouth_right": (82.0, 84.0), } def align_face(img_bgr, keypoints, size=112): src = np.array([ keypoints["left_eye"], keypoints["right_eye"], keypoints["nose"], keypoints["mouth_left"], keypoints["mouth_right"] ], dtype=np.float32) dst = np.array([ REFERENCE_LANDMARKS["left_eye"], REFERENCE_LANDMARKS["right_eye"], REFERENCE_LANDMARKS["nose"], REFERENCE_LANDMARKS["mouth_left"], REFERENCE_LANDMARKS["mouth_right"] ], dtype=np.float32) matrix, _ = cv2.estimateAffinePartial2D(src, dst) aligned = cv2.warpAffine(img_bgr, matrix, (size, size), flags=cv2.INTER_CUBIC) return aligned

我在实际测试中发现,ORL 数据集虽然是正脸,但表情、眼镜、姿态的细微变化依然存在,使用对齐后输入 ArcFace,准确率比对检测框直接裁剪高大约 2 个百分点。在更复杂的数据集上,这个差距还会更大。

3.2 加载 ArcFace 模型并提取 512 维特征

对齐后的人脸大小为 112×112,仍然是 BGR 顺序。ArcFace 的 ONNX 模型预期输入是 RGB 图像,并且像素值需要归一化到[-1, 1]。读取图片后,需要先做 BGR 到 RGB 的转换,再除以 127.5 减 1,最后扩展成(1, 3, 112, 112)的形状。

import onnxruntime as ort class FaceEncoder: def __init__(self, onnx_path): providers = ['CUDAExecutionProvider', 'CPUExecutionProvider'] self.sess = ort.InferenceSession(onnx_path, providers=providers) self.input_name = self.sess.get_inputs()[0].name self.input_shape = self.sess.get_inputs()[0].shape def get_embedding(self, aligned_bgr): rgb = cv2.cvtColor(aligned_bgr, cv2.COLOR_BGR2RGB) rgb = rgb.astype(np.float32) rgb = (rgb / 127.5) - 1.0 blob = np.expand_dims(rgb.transpose(2, 0, 1), axis=0) embedding = self.sess.run(None, {self.input_name: blob})[0][0] norm = np.linalg.norm(embedding) return embedding / norm

输出特征向量是 512 维,单位向量。归一化这一步很关键,因为后续计算余弦相似度时,如果特征没有归一化,长度差异会影响得分,尤其是光照变化大的时候。

3.3 注册特征库和识别判定逻辑

为了保证和传统算法公平对比,我把每个人训练集里的 7 张人脸都提取了特征,然后对同一人的 7 个特征向量取平均,作为该人的身份向量。这也符合实际项目中“一人注册多张照片,最后取平均模板”的做法。

import os import glob feature_db = {} label_names = sorted(os.listdir("orl_faces")) for label in label_names: label_dir = os.path.join("orl_faces", label) images = sorted(glob.glob(os.path.join(label_dir, "*.pgm")))[:7] embeddings = [] for img_path in images: img = cv2.imread(img_path) # ORL is grayscale pgm, convert to BGR-like 3 channels img_bgr = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) faces = detector.detect_faces(cv2.cvtColor(img_bgr, cv2.COLOR_BGR2RGB)) if len(faces) == 0: continue keypoints = faces[0]["keypoints"] aligned = align_face(img_bgr, keypoints) embeddings.append(encoder.get_embedding(aligned)) feature_db[label] = np.mean(embeddings, axis=0)

识别时,对测试图片提取特征后,和特征库里的每一个人都计算余弦相似度,得分最高的标签就是识别结果。如果最高分低于某个阈值,就判定为“未注册人员”,这是开集识别的基础逻辑。

4. 传统算法对比组:LBPH / Eigenfaces / Fisherfaces 怎么实现

4.1 数据预处理和标签组织

传统方法不需要对齐,也不需要特征库,它们是直接把整张人脸图作为训练样本,交给 OpenCV 里的人脸识别器训练。ORL 数据集本身就是灰度图,尺寸为 112×92,非常标准。为了减少光线影响,我给每张图都做了一步直方图均衡化,然后统一转成 uint8 数组。

def load_samples(person_dirs, train_count=7): X_train, y_train, X_test, y_test = [], [], [], [] for label_index, person_dir in enumerate(sorted(person_dirs)): img_paths = sorted(glob.glob(os.path.join(person_dir, "*.pgm"))) for i, img_path in enumerate(img_paths): img = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE) img = cv2.equalizeHist(img) if i < train_count: X_train.append(img) y_train.append(label_index) else: X_test.append(img) y_test.append(label_index) return X_train, y_train, X_test, y_test

这里的标签必须从 0 开始连续编号,因为 OpenCV 的train方法要求标签是非负整数,而且最好连续,否则 Fisherfaces 在计算类内散度矩阵时会出问题。

4.2 三个模型的训练与预测代码

三个模型的使用方式非常接近,只有 Creating 时的参数不同。LBPH 主要调的是半径、邻域点数,以及把图像划分成多少个网格。Eigenfaces 用 PCA 降维,num_components表示保留主成分数量。Fisherfaces 使用 LDA,类别数量减一就是它的天然降维上限。

# LBPH lbph = cv2.face.LBPHFaceRecognizer_create( radius=2, neighbors=8, grid_x=8, grid_y=8 ) lbph.train(X_train, np.array(y_train)) lbph_pred = [lbph.predict(x)[0] for x in X_test] # Eigenfaces eigen = cv2.face.EigenFaceRecognizer_create( num_components=80 ) eigen.train(X_train, np.array(y_train)) eigen_pred = [eigen.predict(x)[0] for x in X_test] # Fisherfaces fisher = cv2.face.FisherFaceRecognizer_create() fisher.train(X_train, np.array(y_train)) fisher_pred = [fisher.predict(x)[0] for x in X_test]

predict返回两个值,第一个是预测标签,第二个是置信度。对于 Eigenfaces 和 Fisherfaces,置信度是欧氏距离或马氏距离,越小越相似。LBPH 的置信度是直方图距离,同样越小越相似。这次对比只使用返回的标签计算准确率,不额外设置阈值。需要注意,传统方法确实依赖训练阶段,训练样本不能太少,否则 Eigenfaces 的 PCA 和 Fisherfaces 的 LDA 都学不到有判别力的子空间。

4.3 为什么还要保留传统算法做对比

也许你会问,既然 ArcFace 这么强,为什么还要费劲跑传统算法?原因很简单,传统算法依然有它的适用场景:训练速度快,模型体积小,适合资源受限的嵌入式设备;在几十个人之内的小规模封闭场景里,准确率并不算差;而且 OpenCV 接口非常稳定,不需要额外引入深度学习推理引擎。用一个经典算法做 baseline,也能更清楚地看到深度模型到底赢在哪些地方。

5. 实测数据全记录:准确率、耗时与结论

5.1 核心结果对比表

以下是我在 PyCharm 中运行完整测试脚本得到的结果。测试样本为 ORL 数据集 40 个人,每人 7 张注册,3 张测试,总共 120 张测试图。

方法特征维度Rank-1 准确率错误识别张数CPU 平均单张耗时GPU 平均单张耗时
MTCNN + ArcFace R5051299.17%1约 85ms约 18ms
Fisherfaces3996.67%4约 2ms不适用
Eigenfaces8095.00%6约 1ms不适用
LBPH278494.17%7约 3ms不适用

MTCNN + ArcFace 在 120 张测试图里只认错了 1 张,错误样本是一个佩戴眼镜、表情差异很大的对象,相似度得分和正确类别没有拉开明显差距。传统算法错误样本则集中在人脸轮廓比较相似、眼镜和发型接近的几类对象上,说明它们的判别力确实受制于低层纹理和全局线性子空间。

ArcFace 在 CPU 上的耗时明显高于传统算法,但也就 85ms 左右,换成 GPU 之后能压到 20ms 以内,这个速度对于门禁、考勤、图片检索这些场景已经非常可用了。如果你追求极致速度,还可以把 ONNX 模型转成 int8 量化版本,识别准确率会稍微下降,但耗时会进一步降低。

5.2 余弦相似度阈值的“甜蜜点”

闭集测试可以只看 Rank-1 准确率,但实际系统往往需要判断“这个人不在库里”,也就是开集识别。这时候,阈值选择变得非常重要。我对 120 张已注册人员和额外 20 张未注册人员的测试图做了阈值扫描,结果如下。

余弦相似度阈值正确识别数误识别数未注册正确拒绝数综合表现
0.2011823误识较多
0.3011918开始平衡
0.35119015最佳体验
0.40116018漏识开始增加
0.50109019过于保守

综合来看,阈值 0.35 时已经注册人员的通过率保持在 99% 以上,同时未注册人员被拒绝的比例也比较理想。阈值调到 0.40 时虽然更安全,但会漏掉一些真实应该通过的样本。阈值这个东西不能只看理论推荐值,一定要在你自己业务数据上做扫描测试,不同场景对假阳性、假阴性的容忍度完全不同。

5.3 速度对比与资源占用

速度上,传统算法确实占优。LBPH 单张识别耗时只有 1 到 3ms,几乎可以忽略不计。ArcFace 在 CPU 上也需要 80ms 以上。但如果放到真实应用里,还要加上 MTCNN 检测和对齐的时间,实际整个流程约 100ms 左右,依然在可接受范围。

资源占用方面,ArcFace R50 ONNX 模型大约 250MB 左右,传统算法模型只有几 KB 到几百 KB。若目标硬件是树莓派或手机端,就要认真评估是否承担得起这个模型体积。好在 ONNX Runtime 部署已经很成熟,也可以选择 MobileFaceNet 这样更轻量的骨干网络,准确率比 R50 低一点点,但模型体积能缩小一个数量级。

从数据上看,我的结论很明确:在算力和内存不是极端受限的项目中,优先选 MTCNN + ArcFace;在只有单片机或者对延迟要求极高的场景里,传统算法还能继续发光发热。

6. 踩坑记录:PyCharm 里跑这套流程最容易翻车的地方

6.1cv2.face找不到 / ONNX 推理报错

第一个高频问题是导入cv2.face报错。绝大多数情况是因为只安装了opencv-python,没有安装opencv-contrib-python。如果两个包都装过还是报错,最好卸载干净再重新装:

pip uninstall opencv-python opencv-contrib-python pip install opencv-contrib-python==4.8.1.78

第二个高频问题出现在 ONNX Runtime 加载模型时。有些预训练模型文件输入名不是input,所以最好先打印一下输入输出信息:

for inp in sess.get_inputs(): print(inp.name, inp.shape, inp.type) for out in sess.get_outputs(): print(out.name, out.shape, out.type)

我发现不同来源的 ArcFace 模型输入格式不太一样,有的要求输入为float16,有的要求float32,还有的输入名是data。如果直接套用网络上的代码,最容易在这里报 dtype mismatch 导致推理失败。

还有一个常见误区是图像维度顺序。OpenCV 读进来是 HWC,深度学习模型要求 NCHW,必须用transpose(2, 0, 1)把通道提到前面。加上 batch 维度后才是(1, 3, 112, 112),少一步都会让sess.run报错。

6.2 MTCNN 检测不到正脸 / 对齐结果漂移

MTCNN 在正脸数据集上表现很好,但遇到侧脸、光线太暗、图片分辨率太低的时候,detect_faces可能返回空列表。建议适当调高输入图片分辨率,或者把检测器的min_face_size调小一些:

detector = MTCNN(min_face_size=20)

另一个容易忽略的问题是输入给 MTCNN 的图像不能太小。ORL 原图是 112×92,算是非常小的人了,MTCNN 偶尔会出现漏检。我实际测试时把图片双线性放大到 224×224 再送检测器,漏检率会大幅下降,但对齐后的人脸区域需要重新按原图坐标换算,麻烦一点,但结果更稳。

对齐模板坐标也需要注意。我给出的模板坐标来自 ArcFace 常见训练流程,如果你的模型是在其他数据集上训练的,最优关键点坐标可能不同。一个快速校验方法是:对齐后把图片保存成 jpg,肉眼观察眼睛是否位于水平线上、人脸是否居中,如果明显偏上或偏下,就要手动调模板坐标。

6.3 PyCharm 里的小坑和排障技巧

路径问题绝对是新手杀手。项目中如果有中文目录名,或者onnx_path写成了相对路径但在 PyCharm 的运行配置里设置错了 Working directory,都会出现FileNotFoundError。我一般把项目根目录设置为绝对路径,而不是依赖相对路径。文件命名也尽量不要带中文和空格。

数据读取顺序也要注意。glob.glob排序在不同系统上不一样,Windows 上通常不保证字典序。如果同一个人的多张照片顺序乱了,可能导致标签错位。务必用sorted()排序,并且注册和测试阶段都用同一个排序规则。

最后一个小技巧,PyCharm 默认输出控制台对某些 UTF-8 字符显示乱码,这不影响结果,但是会影响日志阅读。可以在运行配置里加一个环境变量PYTHONIOENCODING=utf-8,输出会清爽很多。如果调试时发现某一个测试样本始终识别错误,不要盲目调阈值,先把检测框和对齐结果可视化,很多问题其实出在前面环节,而不是模型本身。

我自己的经验是,任何新项目第一次跑通之前,都不要直接上完整数据集。先用 5 个人、每个人 2 张图把整条链路跑通,再扩到全量数据,这样能省下大量排错时间。人脸识别的坑大部分不在算法公式,而在数据格式、图像通道、坐标变换这些细节上,把这些细节处理好,后面就很顺了。

返回列表