简介基于Python的人脸识别系统是一套完整的入门级人脸识别解决方案面向计算机视觉初学者与Python开发者。项目覆盖人脸采集、特征提取、建库到识别比对的完整流程并借助OpenCV、dlib、face_recognition等主流库实现附带tkinter构建的图形界面操作直观配合详细运行说明文档可帮助读者快速理解人脸识别原理并动手实践。资源包共118个文件包含52个Python脚本、人脸样本图片、预训练模型pb/pkl/npy以及配置文件xml、txt等压缩包大小约99.59MB目录结构清晰便于按模块学习与二次开发。已有10411人学习下载热度较高。通过该项目读者不仅能掌握人脸检测、特征编码和相似度比对的实现方法还能学习GUI程序开发与模型部署思路为深入研究深度学习人脸识别打下基础。1. 一个常被误解的方向能跑的 Demo 和能用的系统是两回事用 Python 做人脸识别可以说是计算机视觉里门槛最低、但翻车率也最高的方向之一。不少开发者第一次接触时都会拿 OpenCV 自带的 Haar 级联分类器跑通一个「框出人脸」的 Demo然后觉得事情已经成了。实际上这只是整条链路里最浅的一层检测框只是找到了「脸在哪」真正决定系统能不能用的是后面的对齐、特征提取和比对策略。一套基于 Python 的人脸识别系统从工程角度拆开看是「检测-对齐-特征提取-比对-底库管理」五个环节的串联任何一个环节偷懒最终都会变成识别准确率上的玄学问题。本文面向的是准备做考勤签到、门禁验证、会员识别这类落地任务的开发者。我会从方案选型讲起给出一套基于 OpenCV DNN 检测器加 FaceNet 特征提取的实现路径包含完整可跑的注册和识别代码、参数调整思路以及我实际踩过的几个坑。目标只有一个让你搭建的系统在真实场景下能用而不是只在理想光照的测试图片上自欺欺人。2. 先定方案为什么我弃用 Haar 和 LBPH选了 OpenCV DNN FaceNet2.1 检测器选型Haar 只适合教学真实场景需要 DNNOpenCV 自带的 Haar Cascade 在正面、光线均匀、低分辨率的图片上表现尚可一旦出现侧脸、低头、逆光、遮挡漏检率会迅速上升。它的原理是基于 Haar-like 特征训练级联分类器本质上是在用一堆「矩形特征」做模式匹配对姿态和光照的变化非常敏感。LBPH 检测器同理虽然在纹理特征上比 Haar 鲁棒一些但对模糊和低照度场景依然无能为力。我现在的标准做法是用 OpenCV 的 DNN 模块加载 ResNet SSD 人脸检测模型。这个模型把检测当作目标检测任务来处理输出的是人脸框和置信度对侧脸、部分遮挡、暗光环境的容忍度明显更高。调用方式也简单不需要额外安装大型框架OpenCV 自带 dnn.readNetFromCaffe加载一个 prototxt 和一个 caffemodel 就能用。import cv2 net cv2.dnn.readNetFromCaffe( deploy.prototxt, # 网络结构描述文件 res10_300x300_ssd_iter_140000.caffemodel # 预训练权重 ) def detect_faces(img, conf_threshold0.7): h, w img.shape[:2] blob cv2.dnn.blobFromImage(img, 1.0, (300, 300), (104.0, 177.0, 123.0)) net.setInput(blob) detections net.forward() boxes [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence conf_threshold: x1 int(detections[0, 0, i, 3] * w) y1 int(detections[0, 0, i, 4] * h) x2 int(detections[0, 0, i, 5] * w) y2 int(detections[0, 0, i, 6] * h) boxes.append((x1, y1, x2, y2, confidence)) return boxes这段代码里有几个关键参数需要说明。blobFromImage 中的 300x300 是模型的输入尺寸图像会被缩放后再送入网络缩放意味着小脸的检测能力会下降但换来了推理速度。conf_threshold 是置信度阈值我一般设 0.7在误检和漏检之间取一个平衡点0.5 会多检但误检率高0.85 以上会漏掉不少侧面和低分辨率人脸。2.2 特征提取选型LBPH 是玩具FaceNet 才是能用的底线特征提取是决定识别精度的核心环节。OpenCV 自带的 LBPHRecognizer 和 EigenFaceRecognizer 都是传统方法它们直接在像素灰度空间上做统计对光照变化极度敏感。同一个人的脸只要换个光源方向特征距离可能比两个不同人还远。FaceNet 这类基于深度卷积网络的方法把一张人脸映射到 128 维的欧氏空间。训练目标是让同一个人的特征向量距离小、不同人的距离大所以它对光照、姿态、年龄变化的鲁棒性远强于传统方法。实际使用中因为 FaceNet 模型权重体积小约 90MB 左右、推理速度快CPU 上单张图约 30-80ms一直是中小型项目的常见选择。from tensorflow.keras.models import load_model import numpy as np import cv2 model load_model(facenet_keras.h5) def get_embedding(face_img): # face_img 是已经对齐并缩放到 160x160 的 RGB 图像 face_img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) face_img cv2.resize(face_img, (160, 160)) face_img face_img.astype(float32) # 像素归一化到 [-1, 1] face_img (face_img - 127.5) / 127.5 face_img np.expand_dims(face_img, axis0) embedding model.predict(face_img, verbose0)[0] # 对特征向量做 L2 归一化 embedding embedding / np.linalg.norm(embedding) return embedding这里最容易被忽略的是归一化。FaceNet 模型在训练时输入的像素范围是 [-1, 1]如果直接用 0-255 的原始像素送入特征向量的分布会发生偏移轻则阈值失效重则识别率直线下降。L2 归一化也很重要因为比对时用的是向量之间的距离如果向量长度不统一阈值就没法设成固定值。2.3 比对逻辑与阈值欧氏距离还是余弦相似度阈值定多少特征提取完成之后识别就变成了向量距离计算问题。FaceNet 官方训练时用的比对方式是欧氏距离但实际工程中我一般会换算成余弦相似度因为它在数值上更直观、更容易设定阈值。欧氏距离的优点是符合直觉距离越小越相似。缺点是特征向量的维度是 128 维高维空间里的距离值对阈值设置很不友好不同模型训练出来的距离分布差异较大。余弦相似度的范围固定为 [-1, 1]就算换模型阈值调整的起点也基本一致。from numpy.linalg import norm def face_distance(emb1, emb2): return np.dot(emb1, emb2) # 因为已经做了 L2 归一化内积就是余弦相似度 def is_same_person(emb1, emb2, threshold0.65): return face_distance(emb1, emb2) threshold阈值的设定是整个系统里最需要花时间的参数没有万能值。我通常的做法是采集 50 个不同人的各 10 张照片计算所有同人相似度和异人相似度画出分布曲线取两条曲线交叉点附近的值。实际经验是FaceNet 模型下同人相似度通常在 0.75-0.95 之间异人相似度通常在 0.3-0.6 之间0.65 是一个相对安全的起点。比对方式范围优点缺点欧氏距离0-2模型原生支持阈值跨模型不通用余弦相似度-1 到 1阈值直观易调数值偏大精度感不如距离3. 环境准备从零搭出人脸识别的最小依赖环境3.1 创建虚拟环境与安装依赖我建议使用 Anaconda 或者 venv 管理环境避免不同项目之间的包版本冲突。需要安装的核心依赖并不多OpenCV 负责图像处理和检测推理TensorFlow 或 PyTorch 负责加载 FaceNet 模型NumPy 负责向量运算。如果只是做推理不需要 GPUCPU 版本就够跑。conda create -n face_system python3.9 conda activate face_system pip install opencv-python pip install tensorflow-cpu pip install numpy这里有一个版本上的坑OpenCV 的 DNN 模块对 ONNX 格式的支持在不同版本上差异很大。如果后面打算换 ONNX 模型建议把 opencv-python 固定在 4.5.x 以上。TensorFlow CPU 版本就够用因为 FaceNet 的推理只是单张图像的前向计算CPU 和 GPU 的差距远没有训练时那么悬殊。3.2 模型文件获取与目录结构规划模型文件是整个系统里最难言说的部分。FaceNet 的 Keras 权重在网络上有多处流传来源不一质量也参差不齐。我建议选择能够明确追溯到训练数据集和预处理方式的版本。一个好的判断标准是在 LFW 数据集上报告过准确率的模型一般不会太差。face_system/ ├── models/ │ ├── deploy.prototxt │ ├── res10_300x300_ssd_iter_140000.caffemodel │ └── facenet_keras.h5 ├── datasets/ │ ├── register/ # 注册底库每人一个子文件夹 │ └── test/ # 测试图片用于验证 ├── embeddings/ # 预提取的特征向量缓存 ├── register.py ├── recognize.py └── config.py目录结构按照这个方式组织最大的好处是注册阶段提取的特征可以缓存下来不需要每次启动都重新过一遍模型。对于几十人的底库来说重新提取也就几十秒但几百人的时候差距就明显了。3.3 统一配置入口把阈值、路径、尺寸集中管理实际项目中我吃过亏阈值散落在几个脚本里改一个漏一个最后识别效果异常却找不到原因。现在我把所有关键参数集中到一个配置文件中。# config.py import os # 模型路径 DETECTOR_PROTOTXT models/deploy.prototxt DETECTOR_MODEL models/res10_300x300_ssd_iter_140000.caffemodel FACENET_MODEL models/facenet_keras.h5 # 检测参数 DETECT_CONF_THRESHOLD 0.7 DETECT_INPUT_SIZE 300 # 特征提取参数 EMBEDDING_INPUT_SIZE 160 EMBEDDING_NORM 127.5 # 比对参数 MATCH_THRESHOLD 0.65 # 路径 REGISTER_DIR datasets/register TEST_DIR datasets/test EMBEDDING_DIR embeddings这类集中配置最大的价值是让参数调整变得可追踪。后面在避坑章节里我会提到识别系统的调试往往不是写代码的问题而是参数相互影响的问题。配置集中了排查起来至少能确定变量范围。4. 核心实现注册底库、提取特征、实时识别三块代码4.1 注册脚本把每一张人脸变成特征向量注册阶段要做的事情是遍历底库文件夹对每张图片做检测如果检测到人脸就提取特征向量连同人的标签一起存下来。# register.py import os import cv2 import numpy as np from config import * from tensorflow.keras.models import load_model detector cv2.dnn.readNetFromCaffe(DETECTOR_PROTOTXT, DETECTOR_MODEL) facenet load_model(FACENET_MODEL) def detect_faces(img, conf_thresholdDETECT_CONF_THRESHOLD): h, w img.shape[:2] blob cv2.dnn.blobFromImage(img, 1.0, (DETECT_INPUT_SIZE, DETECT_INPUT_SIZE), (104.0, 177.0, 123.0)) detector.setInput(blob) detections detector.forward() boxes [] for i in range(detections.shape[2]): confidence detections[0, 0, i, 2] if confidence conf_threshold: x1 int(detections[0, 0, i, 3] * w) y1 int(detections[0, 0, i, 4] * h) x2 int(detections[0, 0, i, 5] * w) y2 int(detections[0, 0, i, 6] * h) boxes.append((max(0, x1), max(0, y1), min(w, x2), min(h, y2), confidence)) return boxes def extract_embedding(face_img): face_img cv2.cvtColor(face_img, cv2.COLOR_BGR2RGB) face_img cv2.resize(face_img, (EMBEDDING_INPUT_SIZE, EMBEDDING_INPUT_SIZE)) face_img face_img.astype(float32) face_img (face_img - EMBEDDING_NORM) / EMBEDDING_NORM face_img np.expand_dims(face_img, axis0) emb facenet.predict(face_img, verbose0)[0] return emb / np.linalg.norm(emb) known_embeddings {} for name in os.listdir(REGISTER_DIR): person_dir os.path.join(REGISTER_DIR, name) if not os.path.isdir(person_dir): continue embs [] for img_name in os.listdir(person_dir): img_path os.path.join(person_dir, img_name) img cv2.imread(img_path) if img is None: print(f无法读取: {img_path}) continue boxes detect_faces(img) if len(boxes) 0: print(f未检测到人脸: {img_path}) continue x1, y1, x2, y2, _ boxes[0] face img[y1:y2, x1:x2] emb extract_embedding(face) embs.append(emb) if embs: known_embeddings[name] np.mean(embs, axis0) print(f{name}: 已注册 {len(embs)} 张人脸) np.save(os.path.join(EMBEDDING_DIR, known_embeddings.npy), known_embeddings)这段代码有几个要点需要说明。第一个是只取检测置信度最高的那张脸这在注册阶段是合理的约束因为注册照片应该是一人一张正面照如果注册照片本身有多个人脸后续比对时就会产生干扰。第二个是特征向量的平均策略我用多张照片的特征向量均值作为最终归档向量比单张照片的向量更稳定能抵消表情、轻微姿态变化带来的波动。第三个是 np.save 直接保存字典但字典里包含不同长度的数组加载时需要用 allow_pickleTrue后面在避坑章节会再提。4.2 识别脚本摄像头实时识别与阈值判断识别端的流程是每一帧先做检测对每个检测到的人脸提取特征再和底库里的所有特征做比对取相似度最高且超过阈值的那个作为识别结果。# recognize.py import cv2 import numpy as np from config import * from tensorflow.keras.models import load_model detector cv2.dnn.readNetFromCaffe(DETECTOR_PROTOTXT, DETECTOR_MODEL) facenet load_model(FACENET_MODEL) known_embeddings np.load(os.path.join(EMBEDDING_DIR, known_embeddings.npy), allow_pickleTrue).item() def recognize_face(face_img, thresholdMATCH_THRESHOLD): emb extract_embedding(face_img) best_name unknown best_score -1 for name, known_emb in known_embeddings.items(): score float(np.dot(emb, known_emb)) if score best_score: best_score score best_name name return best_name if best_score threshold else unknown, best_score cap cv2.VideoCapture(0) while True: ret, frame cap.read() if not ret: break boxes detect_faces(frame) for (x1, y1, x2, y2, conf) in boxes: face frame[y1:y2, x1:x2] name, score recognize_face(face) label f{name}: {score:.2f} cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (0, 255, 0), 1) cv2.imshow(Face Recognition, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()实时识别的问题在于性能瓶颈。在 CPU 上每个检测到的人脸都要过一次 FaceNet 模型如果画面里同时出现多个人帧率会明显下降。常见的做法是设定检测间隔比如每 3 帧做一次完整的检测和识别中间几帧直接沿用上一次的结果另一个做法是降低输入画面的分辨率从 1280x720 降到 640x480检测和特征提取的耗时都会大幅降低。4.3 特征比对中容易被忽略的细节均值向量与逐张比对的取舍很多人在这一步会想既然底库里每个人有 5 张照片那我是不是应该拿现场的 embedding 去和这 5 张分别比对取最大值这个思路在逻辑上成立但如果 5 张照片里有某一张特别模糊或者角度奇怪它产生的特征向量和真实人脸偏差很大取最大值反而会引入噪声。我建议的做法是注册阶段对同一个人的多条特征取均值形成一个「均值人脸向量」。原理很简单特征向量空间中同一个人的多次采样会分布在真实向量附近均值能有效抑制噪声。这个技巧在老手看来稀松平常但新手往往会忽略导致识别率忽高忽低。另一个容易被忽略的是底库规模对阈值的影响。底库只有 10 个人时0.6 的阈值可能就足够区分底库有 500 个人时0.6 就可能频繁产生误识。底库越大异人相似度的最大值会越接近同人相似度所以阈值需要随底库规模动态调整。一个稳的做法是定期统计线上误识情况把阈值逐步向上抬直到误识率降到可接受范围。5. 识别系统必踩的坑现象、原因与解决方式5.1 识别率低经常把熟人认成 unknown现象注册阶段用的照片挺清晰但到了实际环境同一个人站在摄像头前经常识别不出来相似度只有 0.5 左右远低于注册时的 0.8。原因注册照片和现场照片之间的差异太大了。光照方向、摄像头角度、面部表情、是否戴眼镜这些因素都会改变特征向量。FaceNet 虽然对姿态有一定鲁棒性但注册照片是顺光大头照现场是逆光侧脸照样会翻车。解决注册阶段就按实际使用场景采集照片。如果系统是装在室内门禁上注册照就选室内灯光下拍的如果是考勤机就让使用者正视摄像头、表情自然。每人的注册照片控制在 3-5 张保证表情和角度有差异。另外识别时的相似度不稳定也有可能是特征向量没有做 L2 归一化检查一下代码里有没有除以范数。5.2 不同人长相相近阈值调到 0.7 还是会误识现象系统把 A 同事识别成了 B 同事而且反复出现。单独看 B 的识别没问题就是 A 的脸总是挂在 B 的名字上。原因有两种可能。第一种是底库里 B 的照片特征质量差可能注册时用的是模糊照片或多张不同角度照片直接取了均值导致特征向量偏离真实分布A 的脸反而离这个劣质向量更近。第二种是两个人的特征空间确实有重叠这在面孔相似的人身上并不罕见。解决先排查底库中每个标签的特征向量是否合理。打印出每个标签的均值向量和所有样本向量的最大距离如果某个标签内部距离特别大说明注册照片彼此差异太大直接取均值会把特征向量拉到一个谁都不像的位置需要剔除离群样本。如果排查后确认是阈值问题就把阈值从 0.7 抬到 0.75 或 0.8同时观察误拒率是否上升。5.3 检测框总是框不准或者人脸出框现象画面上的人脸框偏大或偏小尤其是人脸靠近摄像头时框经常把人脸额头或下巴截掉导致识别率下降。原因ResNet SSD 检测器输出的是归一化坐标乘以原始图像的宽高时没有做边界裁剪。人脸靠近摄像头时框的下沿可能超出图像边界导致截取的区域包含脖子或背景。另外模型训练时对人脸框的标注偏紧直接使用原始框会丢失部分脸部信息。解决对检测框做适度外扩。我一般在检测框的基础上向上扩大 20%向下扩大 10%左右各扩大 15%确保整张脸都落在裁切区域内。这个外扩系数需要根据实际画面调试扩太多会把背景和衣领带进来扩太少又截断五官。def expand_bbox(x1, y1, x2, y2, img_w, img_h, expand_ratio0.2): box_w x2 - x1 box_h y2 - y1 x1 max(0, int(x1 - expand_ratio * box_w)) y1 max(0, int(y1 - expand_ratio * box_h)) x2 min(img_w, int(x2 expand_ratio * box_w)) y2 min(img_h, int(y2 expand_ratio * box_h)) return x1, y1, x2, y25.4 现场有两个人以上时帧率掉到个位数现象摄像头画面里一旦出现多张人脸系统就卡得没法用每帧处理时间超过 200ms。原因每张人脸都要单独做一次 FaceNet 推理。画面里每多一个人就多一次前向计算。在 CPU 上单次 FaceNet 推理约 40ms五个人就是 200ms还要加上检测的开销帧率自然上不去。解决三个层面优化。第一降低检测频率每 3 帧做一次完整识别中间 2 帧沿用上一帧的框第二把输入分辨率降到 640x480检测和特征提取输入尺寸不变但裁剪区域变小计算量下降第三如果同时出现的人脸数超过 3 个只处理置信度最高的前 3 个其余不识别。对门禁和考勤场景来说同时出现在画面里的人数很少超过这个数。5.5 重启系统后加载 npz 报错特征向量全丢了现象程序第一次运行正常重启后 numpy 加载报错提示对象无法识别或者加载出来的字典结构不对。原因np.save 在保存包含对象字典的数组时默认没有启用 pickle 支持。如果直接保存一个 Python 字典对象保存时不会报错加载时如果不加 allow_pickleTrue 就会失败。另一个常见问题是不同版本的 Python 之间 pickle 序列化不兼容换环境后字典加载不出来。解决正规做法是不用 pickle 格式保存特征改用更稳定的存储方式。我一般把特征向量按人名存成独立的 .npy 文件文件名就是标签加载时扫描目录。这样做的好处是人名可以从文件名推断不会因为字典键值错乱而排查半天也方便后面对单个人员的特征做更新和删除。import os import numpy as np # 保存每人一个独立文件 os.makedirs(EMBEDDING_DIR, exist_okTrue) for name, emb in known_embeddings.items(): np.save(os.path.join(EMBEDDING_DIR, f{name}.npy), emb) # 加载扫描目录 known_embeddings {} for fname in os.listdir(EMBEDDING_DIR): if fname.endswith(.npy): name fname.replace(.npy, ) known_embeddings[name] np.load(os.path.join(EMBEDDING_DIR, fname))6. 进阶技巧把识别率从「勉强能用」拉到「稳定好用」6.1 五点关键点对齐一个被很多人跳过的步骤我在前面所有代码里都直接用了检测框裁切的人脸区域这在照片比较正的时候没问题但实际场景中人的头部会有轻微倾斜人脸在画面里未必是正着的。FaceNet 模型对轻微旋转有一定的容忍度但超过一定角度后特征提取受到的影响会明显增大。解决方法是做人脸关键点对齐。用 Dlib 的 68 点模型或者 OpenCV 的 Facemark 模型检测两只眼睛的中心位置通过仿射变换把两只眼睛旋转到水平位置再缩放裁切到 160x160。这一步能把因头部轻微偏转造成的特征偏差降到最低。import cv2 import numpy as np def align_face(img, left_eye, right_eye, output_size160): # 计算旋转角度两只眼睛连线的角度 dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] angle np.degrees(np.arctan2(dy, dx)) # 以左眼为基准构造旋转矩阵 center left_eye rot_mat cv2.getRotationMatrix2D(center, angle, 1.0) # 先旋转 aligned cv2.warpAffine(img, rot_mat, (img.shape[1], img.shape[0])) # 再裁切到以两眼连线中心为基准的正方形区域 eye_center ((left_eye[0] right_eye[0]) // 2, (left_eye[1] right_eye[1]) // 2) # 裁切边长取两眼距离的 2.5 倍保证包含整个脸部 side int(np.linalg.norm(np.array(left_eye) - np.array(right_eye)) * 2.5) x1 max(0, eye_center[0] - side // 2) y1 max(0, eye_center[1] - int(side * 0.4)) face aligned[y1:y1 side, x1:x1 side] return cv2.resize(face, (output_size, output_size))加了对齐之后识别率提升不是一星半点。我在自己的模拟项目里做过对比同样一批现场照片不齐的做法识别率约 88%加了关键点对齐后提升到 94% 以上。代价是要额外加载一个关键点检测模型推理时间增加约 10ms完全值得。6.2 识别置信度低时不要立即拒绝增加重试机制真实摄像头画面里经常出现瞬间模糊、眨眼、转头的情况。如果系统对这些瞬间画面直接判断为 unknown用户体验会非常差。实际场景中一个人走到摄像头前、站定、看镜头中间需要一两秒画面质量是逐渐变好的。我在做门禁类系统时会加入一个「连续确认」机制在连续 3 帧内只要有两帧识别为同一个 ID且相似度超过阈值就确认身份。如果 3 帧里只有一帧识别成功说明特征不稳定继续观察几帧再做结论。vote_dict {} while True: ret, frame cap.read() boxes detect_faces(frame) for (x1, y1, x2, y2, _) in boxes: face frame[y1:y2, x1:x2] name, score recognize_face(face) vote_dict.setdefault(name, 0) vote_dict[name] 1 if vote_dict[name] 2: print(f确认身份: {name}, 置信度 {score:.2f}) vote_dict.clear() # 执行开门/签到等动作 # 每帧清理一次防止累积 if len(vote_dict) 0: for name in list(vote_dict.keys()): if vote_dict[name] 0: del vote_dict[name]这个机制还有另外一个收益能过滤掉「路过画面但并没有面向摄像头」的人。这类人的特征提取结果往往不稳定不太可能在连续两帧都给出同一个高分 ID。6.3 底库的动态更新只加不删是灾难的开始很多系统的底库从上线那天起就再也没更新过随着人员流动识别率每况愈下。一个常见坑是只往里加新员工的照片从不删除离职人员的特征。这会让底库越来越大异人相似度分布随之变化原本设好的阈值开始频繁误识。我现在习惯的做法是给底库设置一个「更新机制」每次识别成功后如果相似度超过 0.85高置信度就用当前帧的特征向量和现有均值做加权平均生成新的均值向量并更新底库。这样底库会随着使用者状态缓慢变化适应发型变化、佩戴眼镜等细微改变。def update_embedding(name, new_emb, alpha0.1): if name not in known_embeddings: known_embeddings[name] new_emb else: old_emb known_embeddings[name] updated (1 - alpha) * old_emb alpha * new_emb known_embeddings[name] updated / np.linalg.norm(updated) np.save(os.path.join(EMBEDDING_DIR, f{name}.npy), known_embeddings[name])alpha 取 0.1 意味着每次更新只移动 10% 的权重防止某一次特征提取异常导致底库向量被带偏。这个机制的边界在于如果识别本身有误会把错误的人脸特征混入某个标签里。所以在实际部署时我会要求识别置信度超过 0.9 才触发更新宁可少更新也不引入脏数据。做这个方向最深的教训就是人脸识别系统的瓶颈从来不在模型而在工程细节。检测框小一点、特征没归一化、阈值拍脑袋定、底库从不维护任何一个环节偷懒都会让系统的体验崩塌。我自己也翻过车曾经因为注册照片用了一张美颜过的自拍导致现场识别率暴跌。后来把注册照片规范成「即时抓拍三连张」这个问题就再没出现过。希望这些踩坑经验能帮你在搭建自己的系统时少走弯路真正把一个能用的系统做出来。本文还有配套的精品资源点击获取