ARTICLE DETAIL

资讯详情

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

MTCNN与FaceNet人脸识别系统搭建:从检测对齐到特征比对的完整实践

MTCNN与FaceNet人脸识别系统搭建:从检测对齐到特征比对的完整实践 简介这是一份基于MTCNN与FaceNet搭建的人脸检测与人脸识别系统毕业设计完整代码面向计算机视觉方向的学生和开发者适合用来学习从人脸检测、对齐到身份识别的完整落地流程。压缩包以ZIP格式打包共83个文件核心是28个Python源码文件并包含相应的.pyc编译文件、模型权重与评估数据npy/npz、配置XML、图片、字体等辅助资源整体大小约95.58MB。目前已有312人学习下载。代码不仅实现了MTCNN的P-Net、R-Net、O-Net三级联检测结构还结合FaceNet三线性损失训练完成高精度人脸识别项目内附带了数据采集与预处理脚本、人脸对齐工具、训练与评估逻辑、实时视频检测demo并给出训练好的模型文件便于直接运行与二次开发。目录划分清晰从数据处理到模型部署均有对应的源码和说明适合作为毕业设计或企业级项目雏形参考能帮助读者在复现中系统性掌握人脸识别系统的架构设计与调优思路。1. 用 MTCNN 和 FaceNet 搭建人脸识别系统毕业设计也能以骨架见真章很多人一听到用 MTCNN 和 FaceNet 搭建人脸识别系统第一反应是又要自己训练模型实际上完全不需要。公开预训练权重足以支撑一个能演示的完整闭环真正的技术难点在检测对齐、特征入库和识别阈值的选择上。MTCNN 负责把人脸从复杂背景里找出来顺便给出五个人脸关键点FaceNet 负责把对齐后的人脸映射成 128 或 512 维特征向量。检测、对齐、特征提取、比对四步各自独立既适合答辩时讲原理也适合把代码拆成模块做功能扩展。这套方案在 CPU 上做单人识别能到 10-15 帧是一套能真正跑起来而不是只写进论文的系统。2. MTCNN 人脸检测的参数调节与对齐实现2.1 P-Net、R-Net、O-Net 三级网络为什么比单模型更省资源MTCNN 的核心思想是把“找候选框”和“精修人脸”拆到三级网络里。P-Net 是轻量全卷积网络输入 12x12在图像金字塔的每一层上快速滑窗输出大量候选框和粗关键点R-Net 输入 24x24把上一级的候选框缩放到统一尺寸筛选掉明显不是人脸的区域O-Net 输入 48x48结构更深除了输出最终的人脸框和置信度还会回归五个人脸关键点。三级网络逐步过滤所以前面的计算量越小后面需要精修的候选越少。这也是它比直接用 YOLO 做检测更省算力的原因尤其适合 CPU 推理的毕设场景。使用facenet_pytorch中的 MTCNN 实现一行代码就能初始化检测器from facenet_pytorch import MTCNN import cv2 detector MTCNN( image_size160, # FaceNet 要求输入 160x160 margin0, # 是否把检测框向外扩 min_face_size20, # 小于该尺寸的人脸直接丢弃 thresholds[0.6, 0.7, 0.7], # 三级网络各自的置信度阈值 factor0.709, # 图像金字塔每次缩小的比例 keep_allFalse, # 是否返回所有人脸 devicecpu ) img cv2.imread(demo.jpg) img_rgb cv2.cvtColor(img, cv2.COLOR_BGR2RGB) boxes, confidence, landmarks detector.detect(img_rgb, landmarksTrue) print(人脸框数量:, len(boxes)) print(第一个框坐标:, boxes[0]) print(第一个人关键点:, landmarks[0])detect返回的boxes形状是 Nx4顺序是左上角 x、左上角 y、右下角 x、右下角 ylandmarks形状是 Nx5x2五个关键点固定为左眼、右眼、鼻尖、左嘴角、右嘴角。上面的参数里image_size只影响后续送入 FaceNet 的尺寸margin如果改成 20会在检测框四周各扩 20 像素把人脸周边的头发和下巴也包进来但它同时会改变五官在整张图里的比例所以我一般保持为 0交由后续对齐步骤控制。参数调节经验可以看下面这张表场景min_face_sizefactorthresholds[0]keep_all办公室单人靠近摄像头200.7090.6False教室多人人脸较小100.80.4True门口门禁逆光严重150.70.5False安防摄像头画面大而远80.850.3True调参时注意两个方向min_face_size越小能检出的小脸越多但候选框数量暴涨CPU 耗时可能翻倍factor越接近 1金字塔层数越多检测越完整同样也更慢。thresholds[0]是最关键的一级它控制 P-Net 是否值得把候选框传给 R-Net如果现场漏检多先降它而不是先把三个阈值全部调低否则背景里的衣服、手掌会被当成脸。2.2 人脸对齐五个关键点才是识别前的最后一步FaceNet 对输入的要求不是“是人脸就行”而是“眼睛大概在同一水平线、人脸居中且尺度一致”。如果不做对齐直接拉伸填充同一个人的侧脸和正脸 embedding 会有明显偏差阈值怎么调都容易误识。MTCNN 在 O-Net 阶段输出的五个关键点正好用来做相似变换。常见做法是把两只眼睛连线的角度当作旋转角再按“目标眼睛距离/实际眼睛距离”计算缩放系数最后把图像仿射到 160x160 的中心区域import numpy as np import cv2 def align_face(img, landmark, output_size(160, 160)): left_eye landmark[0] right_eye landmark[1] # 计算旋转角度和缩放比例 dx right_eye[0] - left_eye[0] dy right_eye[1] - left_eye[1] angle np.degrees(np.arctan2(dy, dx)) actual_dist np.sqrt(dx * dx dy * dy) desired_dist 60.0 scale desired_dist / actual_dist # 以两眼中心为基准做旋转缩放 center ((left_eye[0] right_eye[0]) / 2.0, (left_eye[1] right_eye[1]) / 2.0) m cv2.getRotationMatrix2D(center, angle, scale) # 把眼心平移到图像中心偏上 m[0, 2] output_size[0] / 2.0 - center[0] m[1, 2] output_size[1] / 2.0 - center[1] aligned cv2.warpAffine(img, m, output_size) return aligneddesired_dist一般取 50 到 70 之间太小会让整张脸占满画面下巴被裁掉太大会让脸变小背景比例增加两者都会压低识别准确率。平移目标放在output_size / 2是因为 FaceNet 预训练模型在 160x160 输入上人脸中心应大致位于 H/2、W/2 附近。estimateAffinePartial2D也可以做同样的事但直接用眼睛距离计算更直观答辩时也更容易说清楚。对齐之后建议顺手保存一张aligned.jpg一方面方便排查预处理是否正确另一方面在论文的“系统流程图”里可以直接作为中间结果展示。提示如果注册图来自不同相机建议把对齐后的 160x160 图像统一保存为 PNG避免 JPEG 压缩造成人脸纹理差异。2.3 多人脸场景keep_all 与最大脸选择keep_allFalse时MTCNN 会默认选择置信度最高的那一张脸在单人系统里最省事。但考勤机和课堂点名经常出现前后排同时出现人脸的情况此时应该把keep_all打开拿到所有人脸框后自己决定如何排序。boxes, confidence, landmarks detector.detect(img_rgb, landmarksTrue) if boxes is None: print(未检测到人脸) else: # 按置信度降序保留前 5 个人脸 order np.argsort(confidence)[::-1][:5] for i in order: aligned align_face(img_rgb, landmarks[i]) print(人脸, i, 置信度, confidence[i], 对齐图尺寸, aligned.shape)这里有一个容易踩的坑MTCNN 返回的置信度在多人脸时并不一定等于“谁离摄像头近”大角度侧脸的置信度可能比小尺寸正脸低如果系统以“靠前的人为主”建议排序时结合框面积来判断也就是(x2-x1)*(y2-y1)。我做课堂点名时通常先用面积过滤掉太远的小脸再按置信度选人。3. FaceNet 人脸识别从像素到可比较的向量3.1 Embedding 和三元组损失没训练过也能讲清原理FaceNet 不是把图片“分类”成某个人而是把每张对齐后的人脸映射成一条高维向量。训练时模型同时看三张图锚点脸、正样本同一个人、负样本不同的人三元组损失把锚点和正样本的距离往小压把锚点和负样本的距离往大推当两者距离相差超过设定间隔时才算满足要求。最终模型学到的是“同一个人在任何角度、光线、表情下都落到特征空间相近位置不同人则分散”。毕设代码里一般不会自己训练 FaceNet而是加载预训练权重。facenet_pytorch库内置了 Inception ResNet v1 和 v2 的权重入口比较常见的是在 VGGFace2 数据集上训练的版本。from facenet_pytorch import InceptionResnetV1 import torch model InceptionResnetV1( classifyFalse, # False 表示输出 embedding pretrainedvggface2, # 加载公开预训练权重 devicecpu ).eval() dummy torch.randn(1, 3, 160, 160) with torch.no_grad(): emb model(dummy) print(输出特征维度:, emb.shape)classifyFalse代表模型最后一层是 embedding 层而不是 Softmax 分类层。默认输出维度是 512而不是 FaceNet 经典论文里的 128。如果想要 128 维需要做一次线性投影或直接使用模型的 embedding 层再接全连接。不过对于毕设系统512 维并不影响距离计算只要注册和识别用同一个模型就行。答辩时被问到“为什么用 512”可以主动说明这是 InceptionResnetV1 的默认结构论文里按 128 维训练实现时维度由具体网络决定。3.2 特征向量计算与归一化注册和比对必须使用同一套流程实际系统里注册和识别不能分别写两套代码否则很容易出现“注册时用 BGR识别时用 RGB”这种低级错误。我一般把“图片转 tensor、归一化、得到 embedding、L2 归一化”封装成一个函数。import torch import torch.nn.functional as F import numpy as np from PIL import Image def face_to_embedding(model, face_img_rgb): # face_img_rgb 是经过 MTCNN 对齐的 160x160x3 的 numpy 数组 tensor torch.from_numpy(face_img_rgb).permute(2, 0, 1).float() tensor (tensor - 127.5) / 128.0 # 归一化到 [-1, 1]和训练时一致 tensor tensor.unsqueeze(0).to(model.device) with torch.no_grad(): emb model(tensor)[0] emb F.normalize(emb, dim0) # L2 归一化 return emb.cpu().numpy()归一化到 [-1, 1] 的值来自facenet_pytorch内部的数据预处理约定自己训练模型时不一定相同。L2 归一化之后欧氏距离和余弦距离有换算关系可以直接用np.dot(emb1, emb2)作为相似度比余弦距离更直观的是用欧氏距离小于阈值认为是同一人大于阈值判为陌生人。3.3 注册库和识别库共用一份“人 - 向量”表结构先看注册侧的代码def register_person(name, image_path): img_rgb cv2.cvtColor(cv2.imread(image_path), cv2.COLOR_BGR2RGB) boxes, conf, landmarks detector.detect(img_rgb, landmarksTrue) if boxes is None: raise ValueError(f{image_path} 中没有人脸) idx np.argmax(conf) aligned align_face(img_rgb, landmarks[idx]) emb face_to_embedding(model, aligned) add_person(name, emb) # 把 512 维 float32 数组保存到数据库 return emb这里必须用同一份align_face和face_to_embedding不要识别时为了提高速度换一个简化版本。add_person实现见下一章它会把embedding.astype(np.float32).tobytes()写入 SQLite。识别侧逻辑是查库而非遍历图像库def recognize(embedding, threshold0.7): min_dist float(inf) matched_name None for person_id, name, db_emb in load_all(): dist np.linalg.norm(embedding - db_emb) if dist min_dist: min_dist dist matched_name name if min_dist threshold: return matched_name, min_dist return unknown, min_dist这段逻辑简单但已经足够演示。人脸数量超过上千时线性扫描会变慢届时要换成faiss或hnswlib这类向量检索这也是“边缘人脸识别大量数据”场景里的常见选型。毕设演示通常只有几十人线性扫描无压力答辩老师更关心你有没有意识到这个瓶颈。3.4 阈值到底设多少用实际采集的数据说话不要在论文里拍脑袋写“阈值取 0.7”。正确做法是拿 10 个人的 60 张照片生成所有“同一个人的照片对”和“不同人的照片对”统计两个距离分布然后选定阈值。下面给出一个简单的离线统计脚本import itertools final_embeddings [...] # 每组两张或以上的人脸向量 labels [...] # 每个人对应的 label pos_dist [] neg_dist [] for (i1, e1), (i2, e2) in itertools.combinations(zip(labels, final_embeddings), 2): d np.linalg.norm(np.array(e1) - np.array(e2)) if labels[i1] labels[i2]: pos_dist.append(d) else: neg_dist.append(d)然后画两条分布直方图阈值取两个分布交叠区域中间值。如果两条分布完全重叠说明你的对齐或者图像质量有问题这时候调阈值是没用的先回头检查 MTCNN 对齐是否稳定。距离分布直方图比单独用一个准确率指标更能说明问题因为毕业设计的演示环境通常只有十几个人如果每个人只注册一张正脸测试时光线一变正脸之间的距离可能比不同人之间还要大。要解决这个问题注册阶段让同学正对镜头、侧对镜头、低头三种姿态各拍一张分别生成向量识别时取这 N 个向量中被比对者的最小距离能显著提高系统的鲁棒性。4. 把 MTCNN 和 FaceNet 串成可演示的完整系统4.1 代码结构检测、编码、数据库、主循环四个文件毕设最容易犯的错误是把所有逻辑塞进一个 Python 文件等到要加摄像头功能时发现改一处要动所有代码。一个可维护的“完整代码”骨架应是这样face_system/ ├── detector.py # MTCNN 初始化、align_face ├── encoder.py # face_to_embedding、recognize ├── database.py # SQLite 的 add_person、load_all ├── register.py # 命令行注册脚本 ├── webcam_demo.py # 摄像头识别主循环 └── requirements.txt # torch, facenet-pytorch, opencv-pythonrequirements.txt里固定大版本例如torch2.0、facenet-pytorch2.5、opencv-python4.8。不要只写一句pip install facenet-pytorch硬件环境不同时可能把依赖装乱。每个文件只做一件事webcam_demo.py里不要直接建表也不要去写 SQL业务逻辑和数据访问分离答辩时老师问代码组织也更好答。依赖关系有顺序detector.py不依赖数据库database.py不依赖检测。这样在调试摄像头时可以临时把database.py替换成内存字典方便快速验证光照和角度。4.2 用 SQLite 保存特征向量注册和识别共用一份人脸数据库建表语句放在database.py的最前面第一次运行时自动创建CREATE TABLE IF NOT EXISTS person ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, embedding BLOB NOT NULL, create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP );embedding字段用 BLOB 保存向量而不是把向量转成字符串后用 TEXT 保存后者在读取时要多做一次解析且容易产生浮点精度丢失。写入和读取代码import sqlite3, numpy as np DB_PATH faces.db def get_conn(): conn sqlite3.connect(DB_PATH) return conn def add_person(name, embedding): conn get_conn() blob embedding.astype(np.float32).tobytes() conn.execute(INSERT INTO person (name, embedding) VALUES (?, ?), (name, blob)) conn.commit() conn.close() def load_all(): conn get_conn() rows conn.execute(SELECT id, name, embedding FROM person).fetchall() conn.close() return [(row[0], row[1], np.frombuffer(row[2], dtypenp.float32)) for row in rows]np.frombuffer默认是只读数组后续如果要对向量做修改需要调用.copy()否则可能出现 “assignment destination is read-only” 报错。数据库文件faces.db建议放到.gitignore里因为特征向量也能间接还原出人脸特征不适合提交到代码仓库。4.3 实时摄像头识别跳帧检测和最近邻匹配怎么配合摄像头场景里MTCNN 每帧都跑会让 CPU 打满。常见做法是每 3 帧或 5 帧调一次检测中间帧直接用上一帧的检测框。下面这个主循环把注册和识别合成一个流程import cv2 from detector import detector, align_face from encoder import face_to_embedding, model from database import load_all cap cv2.VideoCapture(0) people load_all() frame_skip 3 frame_idx 0 last_boxes None last_landmarks None while True: ret, frame cap.read() if not ret: break frame_idx 1 if frame_idx % frame_skip 0: rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) last_boxes, _, last_landmarks detector.detect(rgb, landmarksTrue) if last_boxes is not None: for box, landmark in zip(last_boxes, last_landmarks): aligned align_face(rgb, landmark) emb face_to_embedding(model, aligned) dists [(name, np.linalg.norm(emb - db_emb)) for _, name, db_emb in people] if dists: name, dist min(dists, keylambda x: x[1]) label f{name} {dist:.2f} if dist 0.7 else funknown {dist:.2f} else: label no registered person x1, y1, x2, y2 [int(v) for v in box] cv2.rectangle(frame, (x1, y1), (x2, y2), (0, 255, 0), 2) cv2.putText(frame, label, (x1, y1 - 10), cv2.FONT_HERSHEY_SIMPLEX, 0.6, (0, 255, 0), 2) cv2.imshow(face system, frame) if cv2.waitKey(1) 0xFF ord(q): break cap.release() cv2.destroyAllWindows()frame_skip的取值取决于摄像头帧率和 CPU 负载。30 帧摄像头配四核 CPU跳 3 帧可以接受如果掉到 0.5可以改成跳 5 帧。这里把rgb放在循环内转换检测和识别都用 RGB而显示时用的是原始 BGR 的frame避免 OpenCV 反复切换颜色空间带来的混乱。4.4 演示前必查的五个排错点现象大概率原因检查方式摄像头全绿但无人脸框摄像头默认 BGRMTCNN 需要 RGB打印landmarks是否 None注册时能检到摄像头检不到摄像头分辨率太高小脸低于min_face_size把输入缩放到 640x480同一个人每次识别结果都变未做对齐或对齐时不保存中间图保存aligned.jpg肉眼对比不同人互相串脸阈值设得过高或注册图只有一张正脸每人注册 2 到 3 张不同姿态运行到一半卡住数据库被占用或load_all返回空列表打印people长度还要注意 MTCNN 内部对输入图像做了归一化传入前不需要再手动做直方图均衡如果发现逆光下漏检优先做 gamma 校正而不是直接调低阈值。gamma 校正可以用cv2.LUT实现放在detector.detect之前。5. 毕业设计答辩前用 FAR-FRR 曲线给系统定一个可信的识别阈值5.1 用 sklearn 直接生成 ROC 曲线先构造正负样本对的距离列表然后调用 ROC 曲线找到错误接受率和错误拒绝率交点的阈值。把曲线图保存成 PNG答辩时直接放进 PPT。from sklearn.metrics import roc_curve # positive_distances: 同一个人的 embedding 距离 # negative_distances: 不同人的 embedding 距离 y_true [1] * len(positive_distances) [0] * len(negative_distances) y_score positive_distances negative_distances fpr, tpr, thresholds roc_curve(y_true, y_score, pos_label1) chosen None for f, t, th in zip(fpr, tpr, thresholds): if abs(f - (1 - t)) 0.01: chosen th break print(推荐阈值:, chosen)注意 ROC 曲线用的是距离降序排序阈值方向和分类器标准相反所以在表格里要额外注明“阈值为 0.7 表示距离小于 0.7 时接受”。答辩时最好把 FAR 和 FRR 画成两条随阈值变化的曲线比单条 ROC 更直观。5.2 验证脚本建议覆盖的干扰项干扰类型测试方法可接受标准逆光站在窗边背对窗户录制检测成功率不小于 80%侧脸左右转头 30 度仍能识别且不出现高置信误识遮挡用手遮住额头或戴口罩不报错返回 unknown模糊以极快速度靠近摄像头不闪断不把陌生人认成同一个人多人同框注册 3 人后同时出现在画面每人框和姓名一一对应录制视频后逐帧回放并统计“正确识别帧数 / 应识别帧数”这个数值比单张图片的准确率更能说明系统的工程价值。如果某个干扰项导致系统崩溃优先去调 MTCNN 的参数而不是调识别阈值因为检测一旦在早期漏掉人脸FaceNet 根本没有机会参与判断。把验证脚本和结果截图放进论文附录比空写结论更有说服力。建议在答辩前录制三个不同光线场景的视频片段用脚本逐帧回放并统计识别成功率把曲线和表格打印出来贴到系统设计章节。本文还有配套的精品资源点击获取
返回列表