简介:这套基于深度学习的Python人脸静默活体检测项目源码,面向计算机相关专业正在准备毕业设计或期末大作业的学生,也适合需要完整实战项目练习的开发者,主要解决人脸防伪场景中的静默活体识别问题,涉及MTCNN人脸检测与FAS活体判别等核心环节。压缩包共8个文件,大小约10.82MB,包含Python源码、Markdown说明文档、深度学习模型权重、测试图片与演示视频等,各类型文件分工明确,便于对照学习与复现。目前已有72人学习,项目评审分为98分,属于经导师认可的高分设计。源码均经过本地编译调试,保证可运行;配合说明文档、模型和演示视频,使用者能够快速理解从人脸检测、活体判别到结果输出的完整流程,还可参考文档中的环境配置与调试思路,在此基础上进行二次开发或完成课程设计。
1. 人脸静默活体检测项目源码到底在解决什么问题:从一次门禁改造说起
去年做门禁设备升级时,客户最不满意的就是交互式活体检测:用户必须停下来眨眼、左右摇头,高峰期闸机口排成一串。换成静默活体检测方案后,人正常走过去,摄像头抓一帧人脸,几十毫秒内给出“真人/非真人”的判断。标题里的这个项目,核心就是用深度学习算法训练一个二分类模型,输入是单张人脸图,输出是活体概率,附带整套 Python 源码和文档说明。它解决的是打印照片、屏幕翻拍这类低成本攻击的防御问题,同时把交互时间降到零。适合正在做门禁、考勤、账户登录验证的工程师;如果你是刚接触深度学习的人,也可以把它当成一个完整的图像分类工程范例来读。这里有个反直觉的结论:静默方案省掉了用户配合,难度反而更高——因为没有时序动作可依赖,只能从单帧成像差异里找突破口。
2. 静默活体的原理与选型:为什么深度学习是当前主流方案
静默活体检测不是换一个网络结构那么简单,它背后是一套关于“真人和载体在成像上的差异”的假设。要落地这个标题里的源码项目,先得把任务边界、特征来源和模型选型这三件事想清楚。否则训练出来的模型在实验室 AUC 很高,现场一换摄像头就翻车。
2.1 静默活体检测的任务边界与三类常见攻击
静默活体检测的任务定义很简单:在用户不做任何配合动作的前提下,根据摄像头采集到的人脸图像判断它来自真实活体,还是照片、屏幕、面具等载体。相比交互式方案,它没有“眨眼”“转头”这类时序动作可以利用,模型只能从单帧图像的空间特征里找答案。
常见的攻击方式有三类。第一类是打印照片,包括纸质亚光照片和光面照片,特征是打印网点、色偏和纸张表面的反光;第二类是屏幕翻拍,手机或平板展示人脸照片或视频,特征是摩尔纹、像素栅格、屏幕边框和玻璃反光;第三类是纸质裁剪照片,直接把打印照片沿人脸轮廓剪下来挡住摄像头,这类攻击最难防,因为画面里没有人脸以外的边框线索。3D 面具在单目可见光下也属于高难度场景,单纯静默方案很难保证拦住,通常需要近红外或多视角配合。
所以一个合格的静默活体检测项目,本质是在做“细粒度真伪二分类”。模型学到的不是“这是谁”,而是“这张脸的皮肤纹理、反射模式、边缘结构到底来自真人还是来自印刷品或显示屏”。如果你拿到一份标题里这样的源码,第一步不是跑训练,而是先确认它覆盖了哪几类攻击,以及数据里是否包含对应的负样本。
2.2 深度学习相比传统手工特征的优势与代价
在深度学习普及之前,活体检测常用的是 LBP、HOG、SIFT 这类手工特征加 SVM 分类器。它们的思路是把人脸纹理编码成统计直方图,再训练一个分类边界。问题在于,手工特征针对的是“特定攻击的特定痕迹”——比如 LBP 对打印网点敏感,但对屏幕摩尔纹不敏感;换一个摄像头,曝光和 ISP 处理一变,特征分布就漂移,准确率断崖式下跌。
深度学习方案把“特征设计”变成了“特征学习”。网络自己从数据里发现打印照片的色差、屏幕翻拍的栅格、真实皮肤的亚表面散射等规律。它不需要人来定义特征,这有两个直接好处:一是新增攻击类型时,只需要补充数据重新训练,而不是重新发明特征;二是模型会学到一些人肉眼注意不到的微弱线索,比如真实人脸在边缘处的高频细节衰减模式。
代价也很明显。深度学习是个“黑匣子”,可解释性弱,训练结果对数据分布极其敏感。同一个网络,用 A 数据集训练能到 99% 准确率,换到 B 设备采集的数据可能只有 80%。这就是为什么在本文后面我会反复强调数据组织和验证协议,而不是模型结构——对一个活体检测项目来说,数据质量决定了模型的上限,网络结构只是逼近这个上限的手段。
2.3 主干网络选型:边缘设备上的计算约束与默认选择
活体检测模型的部署环境大多是门禁机、考勤机、支付终端这类边缘设备,CPU 算力有限,内存小,还要保证实时性。主干网络的选择直接决定产品能不能跑得动。下表是我在项目里常用的几个备选方案。
| 主干网络 | 参数量级 | CPU 推理速度 | 适用场景 |
|---|---|---|---|
| ResNet18 | 中 | 中 | 服务器端、开发验证、精度优先 |
| MobileNetV2 | 小 | 快 | 边缘设备默认首选 |
| MobileNetV3-Small | 更小 | 更快 | 算力紧张的轻量设备 |
| EfficientNet-Lite | 小到中 | 中 | 追求精度、算力适中的场景 |
我一般默认用 MobileNetV2 作为主干,原因有两个:一是 torchvision 里直接有预训练权重,可以拿 ImageNet 初始化来加速收敛;二是它的倒残差结构在速度和精度之间比较平衡。输入分辨率设为 112x112 或 128x128,过大的输入尺寸会增加延时,过小则会丢掉屏幕摩尔纹这类高频纹理线索。
这里要特别注意:活体检测对高频细节很敏感,输入分辨率不要低于 96。很多新手为了追求速度把输入缩到 64,结果打印照片的网点全糊掉了,模型只能靠颜色猜测,一遇到光线变化就失灵。如果算力真的紧张,优先考虑量化而不是降低分辨率。
2.4 辅助监督设计:为什么补一个深度预测头
分类主干负责输出活体概率,但只有这一个监督信号时,模型容易偷懒——它可能抓到数据集里的某个偶然特征当成“真脸”标志,比如训练样本里活体都是暖色调、假体都是冷色调。为了迫使模型学到更本质的结构差异,常见做法是在主干之上再加一个辅助头,预测人脸深度图。
真实人脸是有三维结构的,鼻梁高、眼眶凹、脸颊平滑;而打印照片和屏幕是平面,深度图近似常数。这个差异非常稳定,不随光照和肤色变化。辅助头用 SmoothL1 损失回归深度图,让网络的中间特征必须携带三维形状信息,分类头再基于这些特征做真伪判断。深度真值不需要深度相机,用 PRNet 或 3DMM 这类人脸重建工具给训练图生成伪标签就行。总损失一般写成:
loss = CrossEntropy(logit, label) + lambda * SmoothL1(depth_pred, depth_gt)
lambda 我一般从 0.1 起步。伪深度标签本身有噪声,权重太大反而会把噪声放大;权重太小又起不到约束作用。数据量越大,辅助头的作用越不明显——如果你的训练集超过五万张,可以只用分类头;如果只有几千张,辅助头就是个很好的正则化器。
3. 组织与清洗数据:这是静默活体检测项目里最能拉开差距的一步
一个完整的人脸静默活体检测项目,源码只是骨架,数据才是血肉。拿到标题里这种“源码+文档说明”的工程包,文档里最先写的往往不是模型结构,而是数据目录怎么组织、标签文件长什么样。这一章把数据准备链路拆开讲透,因为这一步直接决定后面训练和评估有没有意义。
3.1 数据集目录结构与标签文件格式
先看最常见的目录组织方式。项目根目录下放一个 data 文件夹,按正负样本分成 live 和 spoof 两个子目录,每个子目录按采集设备或攻击类型继续分。我用 bash 展示一个可落地的结构:
data/ ├── live/ │ ├── device_a/ │ │ ├── person_001_frame_0001.jpg │ │ └── person_001_frame_0002.jpg │ └── device_b/ ├── spoof/ │ ├── print_photo/ │ ├── screen_replay/ │ └── paper_cutout/ ├── train.csv ├── val.csv └── test.csvCSV 是比文件夹更可靠的标注方式,因为可以同时记录多个属性。我习惯至少保留四个字段:
image_path,label,person_id,attack_typelabel 用 0 表示假体、1 表示活体;person_id 是身份标识;attack_type 记录打印、屏幕、裁剪等攻击来源。person_id 这一列极其重要,后面划分训练集和验证集时必须按它做分组,否则同一张脸既出现在训练集又出现在验证集,评估结果会虚高。
目录树里每个子目录对应什么攻击类型,要在文档说明里写清楚。实际项目中我还会加一个 background 目录,放一些没有人脸的纯场景图作为负样本。门禁设备经常被误触发,模型如果没见过“没有人脸”的输入,就可能把墙上的海报、远处的电视画面判成活体。加少量背景负样本能显著降低现场误报率。
3.2 人脸检测与5点对齐预处理
活体检测模型接收的是对齐后的人脸图,而不是原始摄像头帧。如果不做对齐,同一张脸在画面里可能出现各种位置、角度和尺度,模型不得不用一部分能力去学“位置不变性”,活体判别的能力就被稀释了。对齐的常见做法是先用 RetinaFace 或 MTCNN 检测人脸和 5 个关键点,再做仿射变换。
下面这段代码展示对齐的核心逻辑:
import cv2 import numpy as np # src 是标准人脸模板的 5 点坐标,顺序为左眼、右眼、鼻尖、左嘴角、右嘴角 SRC_TEMPLATE = np.array([ [38.2946, 51.6963], [73.5318, 51.5014], [60.0, 71.7366], [41.0, 92.3655], [70.0, 92.2041] ], dtype=np.float32) def align_face(image, landmarks, size=112): # landmarks 来自检测器输出,与模板顺序保持一致 dst = landmarks.astype(np.float32).reshape(5, 2) # 计算从人脸关键点到标准模板的仿射变换矩阵 M, _ = cv2.estimateAffinePartial2D(dst, SRC_TEMPLATE, method=cv2.LMEDS) aligned = cv2.warpAffine(image, M, (size, size), flags=cv2.INTER_LINEAR) return aligned逻辑说明:estimateAffinePartial2D估计一个只包含旋转、平移和缩放的变换矩阵,把检测到的关键点映射到标准模板位置,再用warpAffine做重采样。LMEDS 方法对关键点噪声有一定鲁棒性,个别点偏移不会让整体对齐崩掉。输出尺寸统一为 112x112,与训练时模型的输入保持一致。
对齐失败的处理容易被忽略。检测器有时会漏检或给出错误的关键点,我的习惯是把这些样本单独记录下来,而不是直接丢弃。如果某个设备的数据连续大量失败,通常说明摄像头安装角度过高或者逆光严重,这是现场问题,不是算法问题,需要反馈给硬件侧。
3.3 数据增强:针对屏幕翻拍与打印照片的人工痕迹
活体检测的数据增强不能只做随机翻转和裁剪,更关键的是模拟现场成像差异。屏幕翻拍的样本经常有摩尔纹,打印照片有网点,普通摄像头传回的图像则可能有运动模糊和压缩伪影。我这里给出一套实战增强组合:
import random import cv2 import numpy as np def liveness_augment(img): # img 是对齐后的彩色人脸图,浮点型,范围 0~1 # 亮度与对比度抖动,模拟现场光照变化 if random.random() < 0.5: alpha = random.uniform(0.7, 1.3) beta = random.uniform(-0.1, 0.1) img = np.clip(img * alpha + beta, 0, 1) # HSV 色彩抖动,模拟不同摄像头白平衡差异 if random.random() < 0.4: hsv = cv2.cvtColor((img * 255).astype(np.uint8), cv2.COLOR_BGR2HSV) hsv[..., 0] = np.clip(hsv[..., 0] + random.randint(-10, 10), 0, 255) hsv[..., 2] = np.clip(hsv[..., 2] + random.randint(-30, 30), 0, 255) img = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR) / 255.0 # 高斯模糊,模拟失焦 if random.random() < 0.15: img = cv2.GaussianBlur(img, (5, 5), random.uniform(0.5, 1.0)) # JPEG 压缩伪影,模拟低码率视频流截图 if random.random() < 0.3: q = random.randint(30, 70) encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), q] result, buf = cv2.imencode(".jpg", (img * 255).astype(np.uint8), encode_param) img = cv2.imdecode(buf, cv2.IMREAD_COLOR) / 255.0 return img.astype(np.float32)参数说明:亮度抖动的 alpha 取 0.7~1.3,模拟暗光到强光的跨度;HSV 的色相变化控制在 ±10,避免把肤色改成绿色;高斯模糊概率只有 0.15,太频繁会让模型把“模糊”学成“假人”;JPEG 质量下限 30,模拟现场视频帧经过压缩后的典型画质。
一个关键注意事项:增强不要只加在活体样本上,假体样本也要同步增强。如果你只对 live 样本做模糊和压缩,模型学到的是“清晰=真、模糊=假”这种错误规律,现场照片一拍就穿帮。增强的目标是让真假两类样本在同一成像域里保持可区分,而不是让它们变得更不同。
4. 训练一个可用的活体检测模型:分类损失与辅助深度监督的组合
数据准备好之后,模型训练反而是一条比较标准的流水线。这一章给出一个可以直接抄走的 PyTorch 训练框架,包括网络结构、损失组合、训练参数和验证指标。你不需要从头实现什么新算法,把工程细节控制住就能得到可用模型。
4.1 网络结构与损失函数设计
整体结构是“主干 + 分类头 + 辅助深度头”。主干用 MobileNetV2 提取特征,分类头输出 2 类 logit,辅助头输出一张低分辨率深度图,后接 Sigmoid 得到深度值。损失函数是分类损失和深度损失的加权和。
类别权重需要根据数据分布调。如果 spoof 样本比 live 多,给 spoof 类一个更高的权重,比如class_weights = [1.0, 1.2],避免模型偏向样本量大的类。深度损失的权重 lambda 前面说过从 0.1 开始。数据量小的项目建议加上辅助头,数据量大时不加也行,辅助头的作用会随数据规模扩大而减弱。
深度图的真值来源要提前确认。如果训练集是从公开数据集拿的,部分数据集已经提供了伪深度图;如果是自采数据,用 PRNet 批量跑一遍就行。伪标签质量不需要非常高,它只是给模型一个“平面与立体”的弱约束,精确度低一点也能起作用。
4.2 PyTorch训练脚本核心片段与参数说明
下面是训练脚本的核心部分,包含模型定义和训练循环:
import math import torch import torch.nn as nn import torchvision.models as models class LivenessNet(nn.Module): def __init__(self, num_classes=2): super().__init__() # 不同 torchvision 版本参数名不同: # 新版本用 weights=None,旧版本用 pretrained=False self.backbone = models.mobilenet_v2(weights=None) feat_dim = self.backbone.classifier[1].in_features self.backbone.classifier = nn.Identity() self.class_head = nn.Sequential( nn.Dropout(0.2), nn.Linear(feat_dim, num_classes) ) self.depth_head = nn.Sequential( nn.Conv2d(feat_dim, 64, 1), nn.ReLU(), nn.Upsample(scale_factor=4, mode="bilinear", align_corners=False), nn.Conv2d(64, 1, 3, padding=1), nn.Sigmoid() ) def forward(self, x): feat = self.backbone(x) feat = feat.mean((2, 3)) # 全局池化,供分类头使用 logit = self.class_head(feat) depth = self.depth_head(self.backbone.features[-1](x)) # 重新取特征图 return logit, depth这段代码的逻辑是:backbone 负责提取特征,classifier被替换成Identity,因为我们要用自己的分类头。分类头先做全局池化再全连接,输出两类 logit;深度头对主干最后一层特征图上采样,输出与输入约 1/4 大小相等的深度图,Sigmoid 把数值压到 0~1 之间。注意depth_head里重新跑了一次 backbone 最后阶段,工程上更高效的做法是把特征图从主路径直接引出来。
训练循环的关键参数如下:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = LivenessNet().to(device) optimizer = torch.optim.AdamW(model.parameters(), lr=1e-3, weight_decay=1e-4) criterion_cls = nn.CrossEntropyLoss(weight=torch.tensor([1.0, 1.2]).to(device)) criterion_depth = nn.SmoothL1Loss() base_lr = 1e-3 epochs = 60 warmup_epochs = 5 for epoch in range(epochs): model.train() total_loss = 0.0 for imgs, labels, depths in train_loader: imgs, labels, depths = imgs.to(device), labels.to(device), depths.to(device) optimizer.zero_grad() logit, depth_pred = model(imgs) loss_cls = criterion_cls(logit, labels) loss_depth = criterion_depth(depth_pred, depths) loss = loss_cls + 0.1 * loss_depth loss.backward() optimizer.step() total_loss += loss.item() # 学习率:前5轮线性warmup,之后余弦退火 if epoch < warmup_epochs: lr = base_lr * (epoch + 1) / warmup_epochs else: lr = base_lr * 0.5 * (1 + math.cos( math.pi * (epoch - warmup_epochs) / (epochs - warmup_epochs))) for g in optimizer.param_groups: g["lr"] = lr print(f"epoch {epoch}, loss {total_loss / len(train_loader):.4f}, lr {lr:.6f}")我常用的一组初始训练参数如下表:
| 参数 | 初始值 | 调整建议 |
|---|---|---|
| 输入尺寸 | 112x112 | 不低于96,优先量化而非降分辨率 |
| batch_size | 64 | 显存不足时减半,学习率同步减半 |
| 初始学习率 | 1e-3 | batch较小时用1e-4更稳 |
| warmup | 5 epochs | 前5轮线性升温,防止初期震荡 |
| 总epochs | 60 | 数据量小可减到30,注意观测验证集 |
| 优化器 | AdamW | weight_decay建议1e-4 |
一个容易被忽略的细节:活体检测训练中正负样本在 batch 里要尽量均衡。如果训练集里 spoof 数量远多于 live,不要直接随机采样,而是在每个 batch 里按比例分别采样,保证每个 batch 都有足够数量的真脸和假脸。否则模型可能在 loss 上看起来收敛得很好,实际只是学会了全体输出“假”。
4.3 验证指标:不要只用Accuracy
准确率在活体检测里是不够用的。现场的正负样本比例极不平衡,每几千次验证里才出现一次攻击,如果模型永远输出“活体”,准确率也高达 99% 以上,但这个模型毫无用处。我习惯至少看下面几个指标:
| 指标 | 含义 | 现场价值 |
|---|---|---|
| Accuracy | 全体样本正确率 | 只作为参考,不用于决策 |
| ROC-AUC | 正负样本区分能力 | 方便横向比较 |
| EER | 等错误率 | FAR与FRR相等时的错误水平 |
| APCER | 攻击样本被接受的比率 | 误拦截攻击的概率 |
| BPCER | 活体样本被拒绝的比率 | 影响用户通过体验 |
验证时设置两个子集:一个是同设备采集的验证集,一个是其他品牌摄像头采集的验证集。模型在同设备上表现好只是及格,跨设备还能保持低 APCER 才能上现场。测试时还要把 attack_type 单独分组统计,比如分别看打印照片、屏幕翻拍、纸质裁剪各自的错误率,哪一类高就去补哪一类的数据。
5. 静默活体检测项目常见的五个坑:现象、原因与解决办法
活体检测项目最大的特点是对数据分布极度敏感,很多问题在训练曲线上一片平静,一到现场就爆雷。下面这五个坑是我做类似项目时反复遇到过的,每一条都按“现象、原因、解决”写清楚,希望能帮你少走弯路。
5.1 训练AUC很高,现场误报却失控:身份泄露导致评估虚高
现象:训练时验证集 AUC 达到 0.99,模型怎么看都完美,但现场上线后活体被频繁拒绝,用户投诉量暴增。
原因:数据集按帧随机划分训练集和验证集时,同一个人的多帧图像同时出现在两边。模型记住了具体的人脸身份,而不是真伪特征。验证集里的活体人脸它全都见过,AUC 自然虚高。
解决:按 person_id 做分组划分,保证同一个人的所有帧只进入训练集或验证集其中之一。用 GroupShuffleSplit 可以一行搞定:
from sklearn.model_selection import GroupShuffleSplit gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(images, labels, groups=person_ids))更严格的做法是按采集 session 划分,而不是只按身份。同一个人在同一台设备、同一天内采集的几十帧高度相似,如果一部分进训练一部分进验证,仍然相当于轻度泄露。按 session 分组会让评估结果更接近真实上线表现。
5.2 打印照片被拦住,屏幕翻拍却漏判:模型记住了边框而非真假
现象:模型对打印照片的检出率很高,但屏幕翻拍的人脸却经常放行。细看之下,训练集里的屏幕翻拍样本大多带着手机边框,模型学到的是“有边框=假”,一旦攻击者把屏幕摆得很近、边框不出现在画面里,模型就失效了。
原因:屏幕翻拍样本的标注噪声和强视觉信号干扰了学习。边框和玻璃反光在图像里太显眼,网络倾向于选择这条“捷径”,而没有真正学习屏幕的像素栅格和摩尔纹。
解决:训练时对 spoof 样本做随机裁剪,裁掉边框区域;验证时单独统计“去边框”子集的准确率,确保模型不是靠边框判断。如果条件允许,采集屏幕翻拍样本时让屏幕尽量填满画面,避免边框成为主要线索。
5.3 换一个摄像头模型就失效:成像域差异比想象中大
现象:实验室用罗技摄像头采集数据训练,AUC 0.99;现场换了某品牌枪机,误报率直接飙到 20%。
原因:不同摄像头传感器的光谱响应、ISP 处理、白平衡和降噪算法完全不同,人脸皮肤在图像里的纹理表现差异很大。深度学习模型对“采集域”非常敏感,同一个真脸在不同摄像头下可能被当成两个不同的域。
解决:增强侧多做色彩抖动和直方图匹配,这是低成本手段;更有效的是拿到目标摄像头的少量现场数据做微调。现场采集几百张活体人脸,打上标签后按 1:10 的比例混入训练集重训,通常能把误报率拉回可用水平。这是我在同类项目里验证过的最可靠路径。
5.4 小batch训练正常,部署后第一帧概率抖动:BN统计漂移
现象:模型在 GPU 上验证正常,导出到 CPU 后前几帧的活体概率忽高忽低,等运行几秒后逐渐稳定。
原因:训练时 batch_size 迫于显存被调到 8 或 16,BatchNorm 的均值和方差统计量在这么小的 batch 上波动很大。训练完成后模型保存的是训练阶段的全局统计量,但小 batch 的估算不够准,部署初期就会不稳定。
解决:尽量把 batch_size 保持在 64 以上,可以缩小输入尺寸来腾出显存,而不是减小 batch。如果算力受限,把主干的 BatchNorm 换成 GroupNorm,它不依赖 batch 统计量,小 batch 下稳定得多。代价是 GroupNorm 在推理时略慢,需要自己权衡。
5.5 量化后掉点严重:分类分支和深度分支的动态范围冲突
现象:FP32 模型 EER 3%,转成 INT8 后 EER 涨到 8%,掉得没法用。
原因:分类头输出的 logit 取值范围较小,深度头输出的深度图经过 Sigmoid 后范围是 0~1,两个分支的动态范围差异大。量化时同一个量化尺度覆盖两个分支,精度自然受损。
解决:先按分支拆分量化范围,对分类头单独使用 per-channel 量化;如果引擎支持,对分类头所在层保留 FP16。还有一种做法是量化后做几轮 QAT(量化感知训练),让网络适应低比特误差。不要指望直接 PTQ 不掉点,活体检测对纹理细节敏感,掉点幅度通常比普通分类任务更明显。
6. 部署与模型更新闭环:导出ONNX、量化与现场回灌
模型训练完成只是项目的一半,部署和迭代才是长期工程。最后分享一个部署与验证的进阶思路。
6.1 ONNX导出与onnxruntime最小推理代码
模型从 PyTorch 导出到 ONNX,可以让部署机不必安装完整的深度学习框架,也方便后续切换推理引擎。导出时建议固定 batch 为 1,以避免某些平台对动态维度兼容不佳:
import torch model.eval() dummy = torch.randn(1, 3, 112, 112) torch.onnx.export( model, dummy, "liveness.onnx", input_names=["input"], output_names=["logit", "depth"], opset_version=13 )导出后用 onnxruntime 验证输出一致性:
import onnxruntime as ort import numpy as np sess = ort.InferenceSession("liveness.onnx", providers=["CPUExecutionProvider"]) input_name = sess.get_inputs()[0].name img = np.random.randn(1, 3, 112, 112).astype(np.float32) logit, depth = sess.run(None, {input_name: img})逻辑说明:logit 是分类输出,取 argmax 或 softmax 后得到活体概率;depth 是辅助头的深度图,部署时可以丢弃,也可以用来做后处理过滤——比如预测深度方差过小的输入大概率是平面载体。推理引擎的选择按部署芯片来,常见的是 ONNX Runtime 或 TensorRT,两者都支持 INT8 量化,量化时机放在导出之后、现场测试之前。
6.2 现场回灌与模型更新的闭环习惯
我自己的习惯是:每次上线后第一周每天抽看误报和漏报样本,按 attack_type 归类,之后每月做一次现场采样回灌训练集。不要以为模型训完就结束了,活体检测面对的攻击手段和现场环境是动态的,新屏幕、新打印机、不同角度光线都会让旧模型逐渐失效。
回灌时注意控制新旧数据比例,一般用旧数据加新数据的 1:5 到 1:10 混合训练,避免模型在新数据上过拟合而遗忘旧场景。每次更新后都要重新跑一遍跨设备验证,确保改动没有破坏对原有攻击类型的防御力。最后把更新后的模型导出 ONNX,替换部署文件并记录版本号,方便线上回退。这套闭环做好,模型的现场表现会随迭代稳步提升,希望帮到你。
本文还有配套的精品资源,点击获取