
1. 从一张合影说起为什么人脸检测和识别要拆成两步做前阵子帮朋友处理一个需求他们手里有一批活动合影想自动把里面出现过的每个人裁出来、归好类方便后续做相册归档。朋友一开始的想法很朴素直接上一个人脸识别模型输入整张图输出“这是谁”。我听完就笑了——这个思路在工程上几乎跑不通因为识别模型只认“一张脸”它不负责告诉你脸在哪。整张合影丢进去模型只会一脸茫然。这就是人脸相关任务里最容易被混淆的一对概念人脸检测Face Detection和人脸识别Face Recognition。前者解决的是“脸在画面的哪个位置”输出的是一个个边界框和关键点坐标后者解决的是“这张脸是谁”输出的是一个身份标签或特征向量。两者是流水线上的上下游关系检测是识别的入口识别是检测的归宿。在众多方案里MTCNN和FaceNet是绕不开的两个经典组合。MTCNN 负责把画面里的人脸框出来、对齐好FaceNet 负责把对齐后的人脸映射成一个高维向量再通过向量距离判断是不是同一个人。这套组合在工业界落地多年稳定、可解释、对硬件要求友好特别适合中小规模的人脸归档、门禁、考勤这类场景。这篇文章我会把这两个模型拆开讲透MTCNN 的三级级联网络到底在干什么、为什么它比传统方法稳、FaceNet 的 128 维或 512 维嵌入向量是怎么训练出来的、三元组损失为什么是它的灵魂、以及把两者串起来时那些文档里不会写的坑。适合有一定深度学习基础、想把这套方案真正跑起来的人也适合刚接触人脸任务、想搞清楚“检测和识别到底差在哪”的读者。2. MTCNN 的三级级联P-Net、R-Net、O-Net 各自在忙什么2.1 为什么不用一个网络一步到位很多人第一次看 MTCNN 会疑惑为什么非要搞三个网络级联用一个网络直接回归边界框不行吗答案藏在“多尺度”和“精度与速度的权衡”里。一张图里人脸大小可能差几十倍远处的人脸可能只有二三十像素近处的人脸可能占满半张图。如果用一个网络在单一尺度上做检测要么漏掉小人脸要么大脸框不准。传统做法是图像金字塔——把图缩放到不同尺寸分别检测但这样计算量爆炸。MTCNN 的思路是先用一个极轻量的网络在图像金字塔上快速筛掉大量背景窗口再用稍重的网络精筛最后用最重的网络做精细回归和关键点定位。这就是级联Cascade的精髓——把计算资源花在越来越少的候选上。P-Net 面对的是几十万个候选窗口所以它必须极快到了 O-Net候选只剩几十个就可以用更复杂的网络做精细活了。2.2 P-Net全卷积网络如何快速筛出候选框P-NetProposal Network是一个全卷积网络输入任意尺寸的图片输出三样东西每个位置的二分类得分是不是人脸、边界框回归量、以及人脸关键点的粗略位置。注意它是全卷积的没有全连接层所以可以接受任意尺寸输入这是它能做图像金字塔检测的前提。实际运行时图像会被缩放到多个尺度每个尺度过一遍 P-Net得到大量候选框。这些框会经过非极大值抑制NMS合并重叠的再根据得分阈值筛掉大部分。P-Net 的阈值通常设得比较低比如 0.6宁可多留一些候选也不能漏。因为后面还有两级网络帮忙精筛P-Net 的任务是“广撒网”。这里有个实操细节P-Net 输出的边界框回归量是相对于锚点anchor的偏移锚点是在特征图上按步长铺的。理解这一点对调试很重要——如果发现框整体偏移往往是锚点生成或回归解码的坐标换算出了问题。2.3 R-Net 与 O-Net从粗筛到精修的两级过滤R-NetRefine Network接收 P-Net 留下的候选框把它们统一缩放到固定尺寸通常是 24x24然后做更严格的二分类和更精细的边界框回归。R-Net 比 P-Net 多了一个全连接层所以它能利用全局信息把那些“长得像人脸但不是”的候选比如窗户、圆形图案筛掉。它的阈值通常设到 0.7 左右。O-NetOutput Network是最后一级输入尺寸 48x48结构更深除了分类和回归还输出 5 个人脸关键点左右眼、鼻尖、左右嘴角。这 5 个关键点非常关键因为后续 FaceNet 需要对齐后的人脸而对齐就靠这 5 个点做仿射变换。O-Net 的阈值一般设到 0.9 以上确保输出的是高质量人脸。三级级联下来一张普通图片的检测时间在 CPU 上大约几十到几百毫秒GPU 上可以做到实时。这个速度在当年是相当能打的即便现在看MTCNN 在精度和速度的平衡上依然有它的位置。2.4 图像金字塔与 NMS两个容易被忽视的工程细节图像金字塔的缩放因子factor通常设 0.709这是原论文的经验值。缩放因子越小金字塔层数越多小人脸召回越好但速度越慢。如果场景里人脸普遍偏大可以适当调大因子减少层数。NMS 的阈值nms_threshold在 P-Net 阶段通常设 0.5R-Net 和 O-Net 阶段设 0.7。这个参数控制的是“多大重叠算同一个框”。设太小会误删相邻的真实人脸设太大会留下大量重复框。我踩过的坑是在密集人群场景下NMS 阈值设 0.5 会把紧挨着的人脸合并掉后来调到 0.7 才正常。还有一个细节是 min_face_size默认 20 像素。如果业务场景里需要检测更小的人脸要把它调小同时金字塔层数会相应增加速度会下降。这个参数和缩放因子是联动的调的时候要一起看。3. FaceNet 的核心把一张脸压缩成一个可比较的向量3.1 从分类到度量学习FaceNet 到底在学什么传统人脸识别做法是训一个分类器每个身份一个类别输出 softmax 概率。这种做法的问题是新增一个人就得重新训练而且类别数一多分类层参数爆炸。FaceNet 换了个思路——它不学“这是第几号人”而是学“怎么把两张脸映射到相近或远离的位置”。这就是度量学习Metric Learning。FaceNet 的输出是一个固定长度的向量叫嵌入embedding常见维度是 128 或 512。训练的目标是同一个人的两张脸嵌入向量的欧氏距离要小不同人的脸距离要大。这样一来识别就变成了向量检索——把待识别人脸编码成向量和底库里所有向量算距离最近的且小于阈值的就是同一个人。新增人员只需要往底库里加一条向量不用重新训练模型。3.2 三元组损失让同类更近、异类更远FaceNet 的灵魂是三元组损失Triplet Loss。一个三元组包含三张图锚点Anchor、正样本Positive同一个人、负样本Negative不同人。损失函数要求锚点到正样本的距离比锚点到负样本的距离小至少一个边界值margin。用大白话说模型每次看到三张脸要努力把“自己和自己”拉近把“自己和别人”推远而且推远的幅度要超过一个门槛。这个 margin 通常设 0.2。margin 设太小类间区分度不够设太大训练容易不收敛。三元组的挑选是训练成败的关键。如果随机选大部分三元组太简单负样本和锚点差太远损失为 0梯度消失训练没进展。所以要用难例挖掘Hard Negative Mining在每个 batch 里找那些“看起来像但其实不是同一个人”的负样本以及“同一个人但差异大”的正样本。这些难例才能提供有效梯度。3.3 嵌入向量的维度选择与归一化处理128 维是原论文的经典配置512 维是后续一些改进版本的配置。维度越高表达能力越强但存储和检索成本也越高。对于底库规模在几万以内的场景128 维完全够用如果底库上百万512 维会更稳。一个必须做的操作是L2 归一化。把嵌入向量除以它的 L2 范数让所有向量落在单位超球面上。这样欧氏距离和余弦距离就等价了而且距离范围被限制在 0 到 2 之间阈值好设。归一化后同一个人的向量距离通常在 0.6 以下不同人通常在 1.0 以上中间那段就是需要靠阈值卡的区域。3.4 对齐为什么是识别的生命线FaceNet 对输入人脸的对齐非常敏感。所谓对齐就是用 MTCNN 输出的 5 个关键点通过仿射变换把人脸旋转、缩放到标准姿态——眼睛水平、人脸居中、尺寸统一通常是 160x160。为什么对齐这么重要因为 FaceNet 是在对齐后的人脸上训练的它学到的特征分布是建立在这个标准姿态上的。如果输入一张歪着的人脸模型提取的特征会偏移距离计算就不准了。我做过对比实验同一张脸对齐前后提取的向量距离能差 0.3 以上足以让识别结果从“正确”变成“错误”。所以千万别跳过对齐这一步直接拿 MTCNN 的框裁出来喂给 FaceNet效果会明显打折。4. 把 MTCNN 和 FaceNet 串起来一条完整的人脸归档流水线4.1 整体流程设计与数据流转整条流水线是这样的输入一张图先过 MTCNN得到若干个人脸框和对应的 5 个关键点对每个人脸框用关键点做仿射变换对齐到 160x160对齐后的人脸送入 FaceNet得到 128 维嵌入向量并做 L2 归一化最后把向量存入底库识别时算距离、比阈值。数据流转中要特别注意坐标的传递。MTCNN 输出的框坐标是相对于原图的对齐时要用原图做裁剪不能拿缩放后的图。我见过有人在对齐时用了金字塔某一层的图结果裁出来的人脸位置全错。记住关键点和框坐标始终以原图为基准。4.2 底库构建与向量检索的工程实现底库就是一堆“身份标签 嵌入向量”的记录。小规模场景几千到几万直接用 NumPy 存矩阵识别时算一个矩阵乘法就能得到所有距离速度足够。规模再大就上 FAISS 这类向量检索库用 IVF 或 HNSW 索引加速。阈值设定是个经验活。我一般先用一批已知同人/异人的样本对画出距离分布直方图找两类分布的交叠区域把阈值卡在交叠区偏保守的一侧。宁可漏认判为未知也别错认把 A 认成 B因为错认的代价通常更高。实际项目里阈值常设在 0.9 到 1.1 之间具体看底库质量和业务容忍度。4.3 一个可运行的代码骨架下面这段代码展示了核心流程省略了模型加载的细节重点看数据怎么流转import numpy as np import cv2 def align_face(img, landmarks, output_size160): # landmarks: 5x2 的数组顺序为左眼、右眼、鼻尖、左嘴角、右嘴角 # 标准参考点基于 160x160 的 ArcFace 风格模板 ref np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [56.0252, 71.7366], [41.5493, 92.3655], [70.7299, 92.2041] ], dtypenp.float32) # 计算相似变换矩阵 tform cv2.estimateAffinePartial2D(landmarks.astype(np.float32), ref)[0] aligned cv2.warpAffine(img, tform, (output_size, output_size)) return aligned def build_embedding(face_img, facenet_model): # face_img: 对齐后的 160x160 RGB 图 face face_img.astype(np.float32) / 255.0 face np.expand_dims(face, axis0) emb facenet_model.predict(face)[0] # L2 归一化 emb emb / np.linalg.norm(emb) return emb def recognize(query_emb, gallery_embs, gallery_labels, threshold1.0): # gallery_embs: N x D 矩阵 dists np.linalg.norm(gallery_embs - query_emb, axis1) idx np.argmin(dists) if dists[idx] threshold: return gallery_labels[idx], dists[idx] return unknown, dists[idx]这段代码里estimateAffinePartial2D用的是相似变换旋转缩放平移不含剪切这对人脸对齐足够且更稳定。参考点的坐标要和 FaceNet 训练时用的模板一致不同实现模板略有差异用错模板会导致对齐偏差。4.4 批量处理时的性能优化思路如果要对成千上万张图做归档逐张跑 MTCNN 会很慢。优化思路有几个一是把 MTCNN 的 P-Net 换成 GPU 版本或者用 ONNX Runtime 加速二是批处理把多张图拼成一个 batch 过 FaceNetGPU 利用率能上去三是并行化用多进程把图片分片处理MTCNN 在 CPU 上跑FaceNet 在 GPU 上跑流水线重叠。我实测下来单张图从检测到出向量CPU 上约 200 毫秒GPU 上能压到 30 毫秒以内。批量处理时FaceNet 的 batch 设 32 或 64 比较合适再大显存吃不消收益也不明显。5. 实测中那些文档不会告诉你的坑5.1 侧脸、遮挡与低质量人脸的应对策略MTCNN 对正脸检测很稳但侧脸超过 45 度、戴口罩遮挡、或者光线极暗时召回会明显下降。这不是模型不行是训练数据分布的问题。应对办法有几个一是降低 O-Net 的阈值宁可多留一些低质量框后续用质量评估筛二是对检测不到的人脸用关键点检测器单独补三是在业务上接受“检测不到就不归档”别硬凑。FaceNet 对遮挡同样敏感。戴口罩的人脸嵌入向量会偏移和没戴口罩的同一人距离可能超过阈值。如果业务场景里遮挡普遍要么用带遮挡数据微调过的模型要么在底库里为同一个人存多张不同状态的照片识别时取最小距离。5.2 阈值调优为什么不能照搬别人的参数网上很多教程直接给一个阈值比如 1.1 或 0.9然后说“这样就行”。千万别照搬。阈值取决于三个因素底库规模、底库质量、业务容忍度。底库越大随机撞脸的概率越高阈值要收紧底库照片越清晰规范同人距离越小阈值可以放宽业务上如果错认代价高阈值要收紧。我的做法是拿一批标注好的测试对同人若干对、异人若干对算距离画 ROC 曲线找等错误率EER对应的阈值再根据业务偏向往保守方向调。这个过程每个项目都要重做一遍没有万能参数。5.3 底库更新与向量漂移问题底库不是建完就不管了。随着时间推移人的外貌会变发型、体重、年龄旧向量和新照片的距离会变大。如果一直用旧底库识别率会慢慢下降。解决办法是定期用新照片更新底库或者为每个人存多张不同时期的照片识别时取最小距离。另一个坑是模型版本漂移。如果中途换了 FaceNet 的权重旧底库的向量和新模型提取的向量不在同一个空间距离计算完全失效。换模型必须重建整个底库这一点务必记住。5.4 隐私与合规工程之外必须考虑的事人脸数据是敏感信息做这类项目必须考虑合规。底库里的向量虽然不可逆理论上但结合其他信息仍可能关联到个人。工程上建议向量加密存储、访问权限最小化、不保留原始人脸图或加密保留、提供删除接口。这些不是技术难点但容易被忽略一旦出问题就是大问题。6. 写在最后一些个人体会这套 MTCNN FaceNet 的组合我从几年前开始用中间换过不少新模型但它在中小规模场景下的稳定性和可解释性一直让我愿意留着它。新模型精度可能高几个点但调参成本、部署复杂度、对硬件的胃口都上去了。如果业务量不大、对精度要求不是极致这套老组合依然是性价比很高的选择。最后分享一个小技巧调试识别问题时先把检测和对齐的结果可视化出来看。很多时候识别不准根源不在 FaceNet而在 MTCNN 框歪了或者对齐点错了。把中间结果画出来问题往往一眼就能定位。这个习惯帮我省了大量瞎调阈值的时间。