简介:本资源是一套面向深度学习初学者与计算机视觉开发者的「人脸识别+活体检测」一体化学习实践包,聚焦真实场景下的身份认证系统构建,解决模型选型、特征提取、活体判别与端到端集成等核心问题。压缩包共54个文件,含26个Python源码(覆盖YOLOv5人脸检测、ArcFace特征编码、MiniFASNet活体判别及训练/推理全流程)、7个YAML配置文件(定义模型结构与训练超参)、4张实测人脸图像(用于快速验证)、2个预训练.pth权重及配套README.md与说明文档,整体仅3.4MB,轻量易部署。目前已有175人下载学习,适合希望从零复现工业级人脸识别流水线的开发者。资源采用模块化目录设计(yoloV5_face、arc_face、anti_spoof_models分层清晰),提供完整可运行代码、数据生成脚本(gen_data.py)与silentFace_model轻量活体检测实现,兼顾算法原理理解与工程落地能力培养。
1. 这不是个“调用API就完事”的Demo:它把人脸检测、特征提取、活体判别三道关卡全塞进一个可调试的端到端流程里,适合想搞懂工业级人脸识别链路、又不想被黑盒SDK绕晕的开发者
你肯定见过那种“5行代码识别人脸”的教程——pip install face_recognition,load_image,face_locations,完事。但真去跑一个需要上线的门禁系统?你会发现:摄像头拍出来的人脸歪着、反光、戴口罩,模型返回一堆框但根本分不清谁是谁;ArcFace嵌入向量一算,同一个人不同角度的余弦相似度忽高忽低;更别说有人拿照片甚至视频在镜头前晃,系统还傻乎乎地“识别成功”。这个项目就是为这种现实翻车现场准备的:它用 YOLOv5 做鲁棒人脸检测(不是MTCNN那种老古董),用 ArcFace 提取高区分度特征(不是OpenCV自带的LBP那种玄学),再叠加一个轻量级活体检测分支(基于眨眼+嘴动+纹理时序分析,非红外硬件依赖)。所有模块都用 PyTorch 实现,模型权重、训练脚本、推理 pipeline、可视化调试工具全在包里。它不承诺“开箱即用”,但保证你改一行 config、加一个日志、断点进 forward 函数,就能看清每个 tensor 是怎么从原始 BGR 图变成 512 维向量、再变成“张三:0.92,李四:0.31”这个结果的。如果你正卡在“模型训出来了但部署总不准”、“活体模块一加就崩”、“ArcFace 特征存数据库后检索慢”这类具体问题上,这个包就是你的调试沙盒。
2. 环境与依赖:PyTorch 1.12 + CUDA 11.3 是当前最稳组合,别急着升到 2.x
这个项目对环境版本有明确偏好,不是因为作者懒,而是实测过多个组合后的血泪经验。YOLOv5 的 Detect 模块在 PyTorch 2.0+ 上会触发torch.compile的隐式优化,导致部分自定义层(比如活体分支里的时序卷积)输出 shape 错乱;而 CUDA 11.8 虽新,但和某些旧版 torchvision 的编译器 ABI 不兼容,容易在torchvision.ops.nms处 core dump。我们按项目根目录下的requirements.txt逐条拆解:
2.1 核心依赖版本锁定逻辑
提示:不要直接
pip install -r requirements.txt一锅端。先装好 CUDA 驱动和对应版本的 PyTorch,再装其他。
# 先确认你的 NVIDIA 驱动支持 CUDA 11.3(驱动 >= 465.19.01) nvidia-smi # 官方推荐安装方式(来自 PyTorch 1.12.1 + CUDA 11.3 文档) pip3 install torch==1.12.1+cu113 torchvision==0.13.1+cu113 --extra-index-url https://download.pytorch.org/whl/cu113为什么是torchvision==0.13.1?因为它的roi_align实现和 YOLOv5 的Detect层中scale_coords函数深度耦合——新版 torchvision 把坐标归一化逻辑从float32改成了float64,会导致 bbox 坐标偏移 0.5 像素,最终在小尺寸人脸(<40px)上漏检率飙升。这是个典型的“版本兼容性黑匣子”,不跑起来根本看不出。
2.2 关键第三方库作用与可替换性
| 库名 | 版本 | 作用 | 替换建议 |
|---|---|---|---|
ultralytics==8.0.194 | 必须 | 提供 YOLOv5 的 reference 实现,含models/yolo.py和train.py,项目对其做了 patch(见patches/yolov5_patch.py) | 不建议换,patch 里修复了多尺度训练时 anchor 匹配 bug |
insightface==0.7.3 | 必须 | 提供 ArcFace 的model_zoo.get_model("arcface_r100_v1"),项目用其get_features()接口而非自己重写 backbone | 可降级到 0.6.0,但 0.7.x 修复了 batch norm 在 eval 模式下统计量冻结问题 |
scikit-image==0.19.3 | 推荐 | 用于活体检测中的 LBP 纹理计算(skimage.feature.local_binary_pattern),比 OpenCV 的 LBP 更稳定 | 若报错可换opencv-python==4.7.0.72,但需手动改live_detect/lbp_analyzer.py中的 import |
faiss-cpu==1.7.4 | 可选 | 仅用于 demo 中的向量检索(demo/retrieve_demo.py),若只做单图比对可删 | 换annoy==1.17.0更轻量,但需重写feature_db.py中的search方法 |
2.3 验证环境是否真正就绪的三行命令
别信import torch成功就万事大吉。必须验证 CUDA 张量运算、模型加载、OpenCV 读图三件事:
# test_env.py import torch import cv2 from models.yolo import Model # 1. CUDA 是否可用且能运算 x = torch.randn(1000, 1000).cuda() y = torch.mm(x, x.t()) print(f"CUDA matmul OK: {y.sum().item():.2f}") # 2. OpenCV 能否正常读图(很多环境缺 libglib-2.0.so.0) img = cv2.imread("data/test.jpg") print(f"OpenCV read OK: {img.shape if img is not None else 'None'}") # 3. YOLOv5 模型能否加载(检查 configs/yolov5s.yaml 和 weights/yolov5s.pt) model = Model(cfg="configs/yolov5s.yaml", ch=3, nc=1) # nc=1 因为人脸检测是单类 model.load_state_dict(torch.load("weights/yolov5s.pt", map_location="cpu")["model"].state_dict()) print("YOLOv5 model load OK")运行后应输出三行OK。若第二行报None,说明 OpenCV 编译时没链接 GTK 或 Qt,需重装:pip uninstall opencv-python && pip install opencv-python-headless(无 GUI 版,专治读图失败)。
3. 项目结构与核心模块拆解:从detect.py到live_detector.py,一条请求的完整生命周期
项目不是扁平文件堆砌,而是按“数据流”组织的清晰分层。我们从最外层入口detect.py开始,逆向追踪一张图片如何被切片、送入三个模型、融合结果、最终输出 JSON。
3.1 目录结构语义解析(非简单罗列)
. ├── configs/ # 模型配置,非超参!yolov5s.yaml 定义网络结构,arcface.yaml 定义 embedding head ├── data/ # 数据集规范:images/ + labels/ + faces/(注册人脸库) ├── models/ # 核心模型:yolo.py(检测)、arcface.py(特征)、live_net.py(活体) ├── weights/ # 预训练权重:yolov5s-face.pt(检测)、glint360k_r100_fp16_0.1.pth(ArcFace)、live_best.pth(活体) ├── utils/ # 工具:general.py(通用函数)、torch_utils.py(设备管理)、live_utils.py(活体专用) ├── demo/ # 可运行示例:realtime_demo.py(摄像头流)、batch_demo.py(文件夹批量) ├── patches/ # 对第三方库的补丁:yolov5_patch.py(修复 anchor 匹配)、insightface_patch.py(修复 BN) └── detect.py # 主推理入口:协调三大模块,输出结构化结果注意data/faces/—— 这是 ArcFace 的“人脸注册库”,不是训练数据。里面是zhangsan/001.jpg,lisi/001.jpg这种结构,detect.py启动时会自动遍历此目录,用arcface.py提取所有图像的 512 维向量,存成data/faces/features.npz(numpy 压缩包),后续比对直接加载该文件,避免每次启动都重算。
3.2detect.py:主控逻辑与模块协同机制
这是整个 pipeline 的“大脑”,但它本身不实现算法,只做调度。关键在于它如何解决三个模型的输入/输出对齐问题:
# detect.py 核心片段 def run_inference(img_path: str, conf_thres: float = 0.5): # 1. 读图并预处理(BGR -> RGB, 归一化, pad to 640x640) img0 = cv2.imread(img_path) img = letterbox(img0, new_shape=(640, 640))[0] # yolov5 utils img = img[:, :, ::-1].transpose(2, 0, 1) # BGR to RGB, to 3x640x640 img = np.ascontiguousarray(img, dtype=np.float32) / 255.0 # 2. YOLOv5 检测:输出是 [x1,y1,x2,y2,conf,cls] 的 tensor pred = yolo_model(torch.from_numpy(img).unsqueeze(0).to(device)) # [1, num_dets, 6] # 3. NMS 后处理:关键!这里要还原 bbox 到原图尺寸 det = non_max_suppression(pred, conf_thres=conf_thres)[0] # [num_kept, 6] det[:, :4] = scale_coords(img.shape[1:], det[:, :4], img0.shape).round() # ← 核心对齐点! results = [] for *xyxy, conf, cls in det: x1, y1, x2, y2 = map(int, xyxy) face_img = img0[y1:y2, x1:x2] # 从原图 crop,非缩放图!避免插值失真 # 4. ArcFace 特征提取:输入必须是 112x112,RGB,归一化 face_tensor = preprocess_face(face_img) # utils.general.preprocess_face feat_vec = arcface_model(face_tensor.unsqueeze(0)).cpu().numpy()[0] # [512] # 5. 活体检测:需要连续帧,此处用单帧近似(实际项目应接摄像头流) live_score = live_model.predict_single(face_img) # 返回 0.0~1.0,越高越可能是活体 # 6. 向量检索:在 features.npz 中找最邻近的注册人脸 reg_name, sim_score = feature_db.search(feat_vec) # 返回 (name, similarity) results.append({ "bbox": [x1, y1, x2, y2], "confidence": float(conf), "identity": reg_name, "similarity": float(sim_score), "liveness": float(live_score), "is_live": live_score > 0.7 # 活体阈值,可调 }) return results参数说明:
conf_thres: YOLOv5 的置信度阈值,不是最终识别阈值。设太高会漏检小脸,设太低会引入大量误检框(尤其在复杂背景中)。实践中 0.45~0.55 是平衡点。scale_coords: 这个函数来自utils.general,它把模型在 640x640 输入上预测的 bbox 坐标,按比例映射回原始图像尺寸。跳过这步,所有 bbox 都是错的——这是新手最常踩的坑。preprocess_face: 内部执行cv2.resize(face_img, (112,112))→cv2.cvtColor(..., cv2.COLOR_BGR2RGB)→transforms.Normalize(mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5])。ArcFace 训练时用的就是这个范式,必须严格一致。
3.3models/live_net.py:活体检测的轻量级实现原理
活体模块没用复杂的 3D 重建或红外,而是基于“微动作+纹理”双线索。它是一个 3 分支 CNN:
- Branch 1(眨眼检测): 输入
64x64灰度眼区图,用 3 层 Conv-BN-ReLU 提取眼睑闭合特征,输出 0/1; - Branch 2(嘴动检测): 输入
64x64灰度嘴区图,同样结构,检测嘴部开合; - Branch 3(LBP 纹理): 对整张
112x112人脸图计算 LBP 直方图(256-bin),接 2 层 FC,判别打印/屏幕反射特征。
三个分支输出拼接后,经一个nn.Linear(3*128, 1)输出最终活体分数。关键代码在forward:
# models/live_net.py class LiveNet(nn.Module): def __init__(self): super().__init__() self.eye_branch = self._make_branch(1) # 输入灰度图 self.mouth_branch = self._make_branch(1) self.lbp_branch = nn.Sequential( nn.Linear(256, 128), # LBP 直方图 256 维 nn.ReLU(), nn.Linear(128, 128) ) self.fusion = nn.Linear(128*3, 1) # 三分支各 128 维 def forward(self, x, eye_roi, mouth_roi): # x: 整脸 112x112 RGB tensor; eye_roi/mouth_roi: 64x64 灰度 tensor eye_feat = self.eye_branch(eye_roi) # [B, 128] mouth_feat = self.mouth_branch(mouth_roi) # [B, 128] lbp_hist = self._compute_lbp_hist(x) # [B, 256] lbp_feat = self.lbp_branch(lbp_hist) # [B, 128] fused = torch.cat([eye_feat, mouth_feat, lbp_feat], dim=1) # [B, 384] out = torch.sigmoid(self.fusion(fused)) # [B, 1], 0~1 return out.squeeze(-1) def _compute_lbp_hist(self, x_rgb): # 转灰度:加权平均,非简单 mean x_gray = 0.299 * x_rgb[:, 0] + 0.587 * x_rgb[:, 1] + 0.114 * x_rgb[:, 2] # 计算 LBP:使用 skimage 的 uniform 方式,更鲁棒 lbp = local_binary_pattern(x_gray.cpu().numpy(), P=8, R=1, method='uniform') hist = np.array([np.histogram(lbp[i], bins=256, range=(0, 256))[0] for i in range(len(lbp))]) return torch.from_numpy(hist).float().to(x_rgb.device)为什么不用单帧 LBP?因为纯静态 LBP 对高清打印攻击防御弱。加入眨眼/嘴动两个动态线索,即使攻击者用高清屏播放真人视频,也很难同步控制眼睑和嘴部肌肉运动——这就是“微动作”防伪的物理基础。
4. 常见问题排查:那些让你怀疑人生、最后发现只是少了一行.cpu()的时刻
部署阶段的问题,90% 出现在 tensor 设备不一致、图像通道错乱、路径硬编码这三类。以下是真实复现过的 5 个高频坑,附带定位方法和根因。
4.1 现象:RuntimeError: Expected all tensors to be on the same device
原因:detect.py中yolo_model在 GPU 上,但arcface_model加载时用了map_location="cpu",导致feat_vec是 CPU tensor,而feature_db.search()里用torch.cdist计算距离时要求全部在 GPU。
解决:统一设备。在detect.py初始化模型后加:
yolo_model = yolo_model.to(device) arcface_model = arcface_model.to(device) live_model = live_model.to(device) # 并确保 feature_db 中的注册向量也转到同一设备 feature_db.features = torch.from_numpy(feature_db.features).to(device)4.2 现象:活体检测分数恒为 0.5,且lbp_hist全是零
原因:_compute_lbp_hist中x_gray.cpu().numpy()时,如果x_rgb是float32且值域为[0,1](归一化后),local_binary_pattern会因输入值太小而全算成 0。
解决:LBP 要求输入是uint8(0~255)。在_compute_lbp_hist开头加:
x_gray_uint8 = (x_gray * 255).clamp(0, 255).byte().cpu().numpy() lbp = local_binary_pattern(x_gray_uint8, P=8, R=1, method='uniform')4.3 现象:ArcFace 特征相似度极低(<0.2),但同一个人不同照片在官方 demo 里能到 0.7+
原因:preprocess_face中的transforms.Normalize参数错了。ArcFace 训练用的是mean=[0.5,0.5,0.5], std=[0.5,0.5,0.5],但有人误写成mean=[0.485,0.456,0.406], std=[0.229,0.224,0.225](ImageNet 标准)。
解决:打开utils/general.py,找到preprocess_face函数,确认Normalize参数:
transform = transforms.Compose([ transforms.Resize((112, 112)), transforms.ToTensor(), # 自动转 [0,1] and CHW transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]) # ← 必须是这个! ])4.4 现象:YOLOv5 检测框严重偏移,人站在画面右侧,框却画在左侧
原因:scale_coords函数的参数顺序传反了。正确是scale_coords(new_shape, coords, old_shape),但有人写成scale_coords(old_shape, coords, new_shape)。
解决:查detect.py中scale_coords调用行,确保是:
det[:, :4] = scale_coords(img.shape[1:], det[:, :4], img0.shape).round() # img.shape[1:] 是 new_shape (H,W),img0.shape 是 old_shape (H,W,C)4.5 现象:demo/realtime_demo.py启动后摄像头画面卡死,CPU 占用 100%
原因:OpenCV 的cv2.VideoCapture(0)在某些 Linux 环境下默认用 V4L2 后端,但项目里cv2.waitKey(1)的等待时间太短(1ms),导致循环过快,线程饿死。
解决:在realtime_demo.py的主循环里,把cv2.waitKey(1)改为cv2.waitKey(30)(30ms ≈ 33fps),并加一帧丢弃逻辑:
cap = cv2.VideoCapture(0) cap.set(cv2.CAP_PROP_FPS, 30) for _ in range(5): # 丢弃前5帧,让摄像头 auto-exposure 稳定 cap.read() while True: ret, frame = cap.read() if not ret: break results = run_inference(frame) # 复用 detect.py 的逻辑 # ... draw results if cv2.waitKey(30) == ord('q'): # 等待30ms,非1ms break5. 二次开发实战:如何把 ArcFace 特征存进 SQLite 而不是 .npz,并支持模糊搜索
.npz文件简单,但无法满足真实业务需求:注册人脸要增删改查、要按时间筛选、要和用户信息(姓名、工号、部门)关联、要支持“张*”这种模糊匹配。SQLite 是最轻量又最够用的选择。下面带你一步步改造,从数据存储到检索接口。
5.1 创建 SQLite 表结构与注册流程改造
首先,在data/下新建faces.db,建表:
-- faces.db CREATE TABLE IF NOT EXISTS face_features ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, employee_id TEXT UNIQUE, department TEXT, feature BLOB NOT NULL, -- 存储 numpy array 的 bytes created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT 1 ); CREATE INDEX idx_name ON face_features(name); CREATE INDEX idx_empid ON face_features(employee_id);然后改造utils/feature_db.py。原search方法是加载.npz,现在改成查询数据库:
# utils/feature_db.py import sqlite3 import numpy as np class FeatureDB: def __init__(self, db_path="data/faces.db"): self.conn = sqlite3.connect(db_path) self._create_table() def _create_table(self): cur = self.conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS face_features ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, employee_id TEXT UNIQUE, department TEXT, feature BLOB NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, is_active BOOLEAN DEFAULT 1 ) """) self.conn.commit() def add_face(self, name: str, employee_id: str, department: str, feature_vec: np.ndarray): """注册新人脸""" cur = self.conn.cursor() cur.execute( "INSERT INTO face_features (name, employee_id, department, feature) VALUES (?, ?, ?, ?)", (name, employee_id, department, feature_vec.tobytes()) ) self.conn.commit() def search(self, query_vec: np.ndarray, top_k: int = 5, threshold: float = 0.4) -> list: """模糊搜索:返回 (name, emp_id, score, dept) 元组列表""" cur = self.conn.cursor() # 先查出所有 active 的 feature cur.execute("SELECT id, name, employee_id, department, feature FROM face_features WHERE is_active = 1") rows = cur.fetchall() if not rows: return [] # 批量计算余弦相似度(Python 循环,小数据集够用;大数据用 faiss) scores = [] for row in rows: db_vec = np.frombuffer(row[4], dtype=np.float32) # bytes -> np.array # 余弦相似度 = dot(a,b) / (norm(a)*norm(b)) sim = np.dot(query_vec, db_vec) / (np.linalg.norm(query_vec) * np.linalg.norm(db_vec)) if sim >= threshold: scores.append((row[1], row[2], float(sim), row[3])) # name, emp_id, score, dept # 按 score 降序,取 top_k scores.sort(key=lambda x: x[2], reverse=True) return scores[:top_k] def fuzzy_search(self, name_pattern: str, limit: int = 10) -> list: """按姓名模糊搜索(LIKE '%pattern%')""" cur = self.conn.cursor() cur.execute( "SELECT name, employee_id, department FROM face_features WHERE name LIKE ? AND is_active = 1 LIMIT ?", (f"%{name_pattern}%", limit) ) return cur.fetchall()5.2 改造detect.py以使用新 DB
在detect.py开头导入并初始化:
# detect.py from utils.feature_db import FeatureDB feature_db = FeatureDB("data/faces.db") # 替换原来的 np.load("data/faces/features.npz")在run_inference函数内,把原来的reg_name, sim_score = feature_db.search(feat_vec)替换为:
# 使用新 search,返回元组列表 matches = feature_db.search(feat_vec, top_k=3, threshold=0.45) if matches: best_match = matches[0] # (name, emp_id, score, dept) reg_name, emp_id, sim_score, dept = best_match else: reg_name = "unknown" emp_id = None sim_score = 0.0 dept = None5.3 添加注册 CLI 工具:register_face.py
新建register_face.py,让运维能快速录入新人:
# register_face.py import argparse import cv2 import numpy as np from models.arcface import ArcFaceModel from utils.feature_db import FeatureDB from utils.general import preprocess_face def main(): parser = argparse.ArgumentParser() parser.add_argument("--name", required=True, help="Person's name") parser.add_argument("--emp-id", required=True, help="Employee ID") parser.add_argument("--dept", default="", help="Department") parser.add_argument("--img", required=True, help="Path to face image (frontal, well-lit)") args = parser.parse_args() # Load and preprocess img = cv2.imread(args.img) face_tensor = preprocess_face(img) # Extract feature arcface = ArcFaceModel("weights/glint360k_r100_fp16_0.1.pth") feat_vec = arcface(face_tensor.unsqueeze(0)).cpu().numpy()[0] # Save to DB db = FeatureDB("data/faces.db") db.add_face(args.name, args.emp_id, args.dept, feat_vec) print(f"✅ Registered {args.name} (ID: {args.emp_id}) to database.") if __name__ == "__main__": main()使用方式:
python register_face.py --name "张三" --emp-id "EMP001" --dept "研发部" --img data/new_faces/zhangsan.jpg5.4 验证与性能提示
- 验证是否生效:运行
sqlite3 data/faces.db "SELECT * FROM face_features;",应看到新记录; - 性能边界:SQLite 在 <10,000 条记录时,
search方法(Python 循环计算)延迟 <50ms;超过此数,务必切换到faiss或annoy; - 安全提醒:生产环境必须给
faces.db设置文件权限chmod 600,防止未授权读取特征向量——这些向量可被用来生成对抗样本。
从那以后我每次新增一个业务字段(比如“入职日期”),都强制走一遍ALTER TABLE face_features ADD COLUMN hire_date DATE;+UPDATE face_features SET hire_date = '2023-01-01' WHERE id = 1;的全流程,再跑通register_face.py和detect.py。数据库 schema 变更是最易被忽略的“后悔药”,宁可多花两分钟,也不让线上查不到数据。希望帮到你。
本文还有配套的精品资源,点击获取