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 6GBPyCharm 里配置的是 Python 3.9 虚拟环境。传统算法和 ArcFace 都在同一台机器、同一份数据划分下运行确保结果可以横向对比。有一点需要提前说明对于 ArcFace我没有在 ORL 上重新训练而是用预训练模型做迁移特征提取只训练了后面的相似度比对逻辑。这其实也是实际项目里最常见的用法因为大部分人没有足够的人脸训练数据直接使用公开预训练模型再把特征库换成自己的目标人物库就能达到不错的识别效果。2. 环境准备PyCharm Python 模型依赖怎么搭2.1 用 Anaconda 创建独立环境避免依赖地狱人脸识别相关依赖之间很容易出现版本冲突尤其是 NumPy、OpenCV、TensorFlow 这几个包互相踩版本的情况我在 Windows 上遇到太多次了。所以第一步不是直接打开 PyCharm 写代码而是先创建一个独立的 conda 环境。conda create -n face_rec python3.9 -y conda activate face_recPython 3.9 是一个兼容性比较好的版本mtcnn、opencv-contrib-python、onnxruntime 在这些版本下都有现成的 wheel 包。创建完环境之后在 PyCharm 的 Settings 里找到 Project Interpreter选择这个 conda 环境后续安装的包就能直接在 PyCharm 里识别到了。这里要提一句不需要去折腾那些来历不明的“激活”方式JetBrains 官方社区版对本次实验完全够用配置好解释器之后写代码、跑脚本、看调试输出都没有问题。如果团队有教育授权或者公司统一采购了专业版也可以正常激活但不要使用破解补丁或盗版授权尤其是涉及公司项目时风险很大。2.2 安装 opencv、mtcnn、onnxruntime 的版本组合依赖安装命令如下我踩过版本坑之后锁定的组合是pip install numpy1.24.3 pip install opencv-contrib-python4.8.1.78 pip install mtcnn0.1.1 pip install onnxruntime1.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, size112): src np.array([ keypoints[left_eye], keypoints[right_eye], keypoints[nose], keypoints[mouth_left], keypoints[mouth_right] ], dtypenp.float32) dst np.array([ REFERENCE_LANDMARKS[left_eye], REFERENCE_LANDMARKS[right_eye], REFERENCE_LANDMARKS[nose], REFERENCE_LANDMARKS[mouth_left], REFERENCE_LANDMARKS[mouth_right] ], dtypenp.float32) matrix, _ cv2.estimateAffinePartial2D(src, dst) aligned cv2.warpAffine(img_bgr, matrix, (size, size), flagscv2.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, providersproviders) 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), axis0) 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, axis0)识别时对测试图片提取特征后和特征库里的每一个人都计算余弦相似度得分最高的标签就是识别结果。如果最高分低于某个阈值就判定为“未注册人员”这是开集识别的基础逻辑。4. 传统算法对比组LBPH / Eigenfaces / Fisherfaces 怎么实现4.1 数据预处理和标签组织传统方法不需要对齐也不需要特征库它们是直接把整张人脸图作为训练样本交给 OpenCV 里的人脸识别器训练。ORL 数据集本身就是灰度图尺寸为 112×92非常标准。为了减少光线影响我给每张图都做了一步直方图均衡化然后统一转成 uint8 数组。def load_samples(person_dirs, train_count7): 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( radius2, neighbors8, grid_x8, grid_y8 ) 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_components80 ) 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约 18msFisherfaces3996.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-python4.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_size20)另一个容易忽略的问题是输入给 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 字符显示乱码这不影响结果但是会影响日志阅读。可以在运行配置里加一个环境变量PYTHONIOENCODINGutf-8输出会清爽很多。如果调试时发现某一个测试样本始终识别错误不要盲目调阈值先把检测框和对齐结果可视化很多问题其实出在前面环节而不是模型本身。我自己的经验是任何新项目第一次跑通之前都不要直接上完整数据集。先用 5 个人、每个人 2 张图把整条链路跑通再扩到全量数据这样能省下大量排错时间。人脸识别的坑大部分不在算法公式而在数据格式、图像通道、坐标变换这些细节上把这些细节处理好后面就很顺了。
返回列表