ARTICLE DETAIL

资讯详情

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

人脸静默活体检测实战:用深度学习防御照片与屏幕攻击

人脸静默活体检测实战:用深度学习防御照片与屏幕攻击 简介基于深度学习的Python人脸静默活体检测算法项目面向计算机相关专业正在完成课程设计、毕业设计或希望积累项目实战经验的学习者。项目包含完整源码与配套文档整体难度适中经导师指导并获评审高分源码可本地编译运行便于直接复现与二次开发。压缩包为zip格式共8个文件约10.82MB主要包含Python脚本、模型权重文件、说明文档、效果图与运行演示视频可辅助理解人脸检测、静默活体判定流程及模型调用方式。资源目录按代码、数据、模型、结果等模块划分结构清晰便于按步骤对照学习附带的运行演示录屏与效果图也能帮助验证算法实际表现。目前已有72人学习下载适合作为人脸识别安全方向的深度学习入门及毕设参考。1. 人脸静默活体检测在防什么一次刷脸攻击是怎么绕过去的你公司刚上线的人脸闸机被一张A4纸打印照片刷开了。这不是段子是我接手过最多的现场。基于深度学习的Python人脸静默活体检测算法做的是在用户完全无感、不需要配合眨眼摇头的前提下从单张或短时图像里把真人脸和照片、屏幕、面具区分开。它是深度学习落地中最容易被低估的一环识别做得再好活体这关被绕过整个系统就是摆设。这套项目源码加文档说明覆盖了从数据组织、模型训练到指标评估的完整链路适合正在做人脸产品防攻击验证、或想评估这条技术方向值不值得投入的工程师。2. 数据、标签与模型选型先想清楚算法在学什么静默活体检测不是一个“换个网络就能跑”的任务。拿到源码先别急着训练把任务形态、攻击类型和评测指标这三件事校准后面才不会白干。2.1 静默活体与配合活体任务形态和评测指标不一样行业内把活体检测分成两类。配合活体Active Liveness是让用户按提示眨眼、张嘴、左右转头系统通过动作是否按指令发生来判断静默活体Silent Liveness则要求用户什么都不用做系统只靠摄像头采集到的人脸图像本身判断面前是真人还是伪造介质。同一个模型、同一份数据这两类任务的难点完全不同。对比项配合活体静默活体用户交互需要按提示做动作无感零配合典型攻击视频重放跟着指令播打印照片、屏幕翻拍、3D面具判定依据动作时序是否符合指令图像纹理、摩尔纹、生理信号单次时延通常 25 秒毫秒级到 1 秒内体验门槛闸机前容易排队顺滑但对算法鲁棒性要求高评测指标上两者差别更大。配合活体现在一般看动作指令完成率而静默活体必须用 ISO 30107-3 里定义的三个指标来评估APCER攻击样本被系统当成真人的比例越低越安全、BPCER真人被系统拒绝的比例越低越好用、ACER两者平均值。很多源码文档里也会贴分类准确率但真实产品评审看的几乎只有 APCER 和 BPCER这一点从选模型到选阈值都在起作用。2.2 攻击类型和公开数据集先搞清楚假脸长什么样静默活体检测本质上是一个伪造人脸识别问题。你要先知道攻击介质会产生什么视觉痕迹才知道模型应该去学什么特征。最常见的几类打印照片A4纸、铜版纸、高光照片纸。放大看有规则半色调网点翻拍时有纸张纹理和镜面反光。屏幕翻拍手机、平板、笔记本屏幕对准摄像头。会出现摩尔纹、像素点阵、屏幕边缘和高光条带。抠图攻击把打印照片的人脸区域抠掉贴在真人脸上或举在脸前。边缘有裁剪痕迹、脸部与周围光照不一致。面具与头模3D打印或硅胶头模表面材质和皮肤反射率不同但单帧静态图像里最难分辨。AI生成/换脸Deepfake 一类的合成人脸高频细节分布和真实传感器噪声不一致。公开数据集方面老牌的 CASIA-FASD 和 NUAA 适合做原型验证MSU-MFSD 的采集设备分辨率较低更接近现实摄像头画质Oulu-NPU 给出了按攻击类型和采集设备划分的协议是评估泛化能力的常用基准SiW 系列则加入了 3D 面具和更大规模的数据。源码项目里数据通常按视频目录组织每条样本是一个视频片段而非单帧标签文件一般是一行一个路径加一个 0/1 标签。2.3 模型选型单帧CNN、频域分解还是rPPG常见方案基本可以归成三条路线对源码项目来说选哪条决定了主干网络和预处理代码的写法。第一条是端到端单帧 CNN 二分类。把对齐后的人脸图直接喂给 ResNet18、MobileNetV3 或 EfficientNet输出真/假 logit。优点是实现最简单、推理最快缺点是模型很容易学到数据集的颜色和背景分布换个摄像头就翻车。第二条是在输入端做频域分解对图像做高通滤波或傅里叶变换把高频分量作为额外通道和 RGB 一起输入。打印照片的网点、屏幕翻拍的摩尔纹都会在高频通道里留下明显痕迹这相当于逼着模型去学攻击介质的物理特征。第三条是 rPPG 生理信号路线从多帧肤色变化里提取心跳信号。打印照片没有血流这一信号是天然的活体指标但需要约 1 秒以上的视频成本和计算量也更高。我一般会建议一套源码项目以轻量 CNN 为主干输入 RGB 加高频残差通道先跑通再往上叠 rPPG 分支。原因很实际MobileNetV3-Small 单帧推理在 CPU 上只有几十毫秒远比堆大模型更容易落地。输入尺寸默认 224×224如果做多通道输入则从 3 通道变成 6 通道训练显存和数据量都会翻倍这一步要在配置里提前预留。3. 读懂源码目录结构、数据加载与模型定义的落地写法拿到一个静默活体检测源码项目不要直接点开 train.py 从头读。先看配置文件和目录结构搞清楚数据从哪来、模型长什么样、指标在哪算再顺着数据的流向去读代码效率高得多。3.1 一份可落地的源码目录六个部分各干什么一个规范的活体检测项目目录结构通常能直接看出作者的工程习惯目录/文件职责说明configs/训练与推理参数yaml 或 json包含数据集路径、batch size、学习率、输入尺寸data/数据集解析与 Loader按视频段组织返回对齐后的人脸帧与标签models/网络结构定义轻量骨干加分类头或多通道输入分支losses/损失函数BCE 或 Focal Lossutils/指标计算与可视化APCER/BPCER/ACER、ROC 曲线、Grad-CAMpreprocess/人脸检测与对齐检测、仿射变换、裁剪到统一尺寸train.py / train.sh训练入口单卡/多卡、混合精度开关inference.py推理与导出单张图片接口、ONNX/TensorRT 导出文档说明一般分三份README 告诉你怎么装环境和跑通训练文档写数据格式和超参部署文档写导出和推理流程。拿到源码先看这三份文档能省掉大半踩坑时间。3.2 数据加载器按视频段抽样别把同段帧塞进训练和验证静默活体数据几乎都是视频采集的。如果把视频拆成单帧做训练验证集里一旦混入同一段视频的相邻帧模型相当于开卷考试准确率虚高到 99% 也说明不了任何问题。正确的做法是按视频段作为样本单元训练和验证在视频级别切分。下面是一份常见的视频段数据加载器# data/fasd_dataset.py import cv2 import numpy as np import torch from torch.utils.data import Dataset class FASDDataset(Dataset): def __init__(self, video_dir, label_file, seq_len8, sample_stride4, img_size224, transformNone): # 每条样本是一个视频片段而不是单帧 self.video_dir video_dir self.samples self._parse(label_file) # [(video_path, label), ...] self.seq_len seq_len # 每段取多少帧 self.sample_stride sample_stride # 每隔多少帧抽一帧 self.img_size img_size self.transform transform def _parse(self, label_file): samples [] for line in open(label_file, r): path, label line.strip().split() samples.append((path, int(label))) return samples def _read_frames(self, video_path): cap cv2.VideoCapture(video_path) total int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 先在原视频上按 stride 粗抽样再均匀取 seq_len 个位置 idxs np.arange(0, total, self.sample_stride) idxs np.linspace(0, len(idxs) - 1, self.seq_len).astype(int) frames [] for i in idxs: cap.set(cv2.CAP_PROP_POS_FRAMES, int(i)) ok, frame cap.read() if ok: frames.append(frame) cap.release() return frames def __getitem__(self, idx): path, label self.samples[idx] video_path f{self.video_dir}/{path} frames self._read_frames(video_path) # 逐帧做人脸检测和对齐工程上会缓存对齐结果避免重复计算 faces [align_face(f, sizeself.img_size) for f in frames] if self.transform: faces [self.transform(f) for f in faces] # 返回 [seq_len, C, H, W] 和视频级标签 return torch.stack(faces), label这段代码做了两件关键的事。第一是按视频段返回样本一个样本是[seq_len, C, H, W]的张量而不是单帧第二是sample_stride控制抽帧间隔避免连续取到动作瞬间的相似帧也用来控制一个片段覆盖的时间跨度。seq_len8表示每段取 8 帧sample_stride4表示每隔 4 帧抽 1 帧约覆盖 32 帧窗口。如果后续要接 LSTM 或 rPPG 分支这个 Loader 不用改造直接换模型读取维度即可。标签是视频级的整段视频为假则所有帧都是假。3.3 模型定义与损失函数轻量主干加上对难样本更狠的损失对于 CPU 或边缘设备部署我通常用 MobileNetV3-Small 作为主干只改最后一层分类头# models/liveness_net.py import torch.nn as nn from torchvision.models import mobilenet_v3_small, MobileNet_V3_Small_Weights class LivenessNet(nn.Module): MobileNetV3-Small 主干输出一个真脸 logit def __init__(self): super().__init__() base mobilenet_v3_small( weightsMobileNet_V3_Small_Weights.IMAGENET1K_V1 ) self.features base.features self.pool nn.AdaptiveAvgPool2d(1) # 把 1000 类分类头换成 128 维 1 维输出的 MLP self.classifier nn.Sequential( nn.Dropout(0.2), nn.Linear(576, 128), nn.BatchNorm1d(128), nn.ReLU(inplaceTrue), nn.Linear(128, 1), ) def forward(self, x): # x: [B, C, H, W]多帧输入在外层做均值池化 x self.features(x) x self.pool(x).flatten(1) return self.classifier(x).squeeze(1)MobileNetV3-Small的 features 输出是 576 维参数量约 2.5M单帧 CPU 推理可以控制在几十毫秒。分类头加了一层BatchNorm1d因为活体检测的训练集通常不大BN 能缓解深层 MLP 的过拟合Dropout(0.2)同样是为了防止分类头把训练集的拍摄环境背下来。输出层是单 logit配合BCEWithLogitsLoss使用。如果训练集中假样本类型多但每种数量少常见做法是把损失换成 Focal Loss让模型集中精力学习那些容易被当成真脸的难样本。这个损失函数在后续训练阶段是默认配置。3.4 指标计算用APCER/BPCER/ACER替换Accuracy源码里指标计算脚本通常是独立文件因为训练时打印 accuracy 意义不大只有 APCER/BPCER 才能反映安全与可用性的真实权衡# utils/metrics.py import numpy as np def compute_acer(scores, labels, thresholdsNone): scores: 模型输出的真脸置信度越高越像真脸 labels: 0攻击1真脸 返回每个阈值下的 APCER / BPCER / ACER if thresholds is None: thresholds np.linspace(scores.min(), scores.max(), 200) results [] for t in thresholds: pred (scores t).astype(int) # 攻击样本被判成真人的比例 apcer ((pred 1) (labels 0)).sum() / max((labels 0).sum(), 1) # 真人样本被判成攻击的比例 bpcer ((pred 0) (labels 1)).sum() / max((labels 1).sum(), 1) results.append((t, apcer, bpcer, (apcer bpcer) / 2)) return np.array(results)这段代码遍历 200 个候选阈值每个阈值下算出一组(APCER, BPCER, ACER)。用的时候注意类别数量攻击样本和真样本数量差距大时必须按各自的类别总数归一化而不是把错误的绝对次数直接相加否则少数类会被多数类淹没。实际产品里APCER 要小到 0.1% 级别才敢给安全部门签字。4. 训练与调参让静默活体模型在真实场景不翻车训练阶段的目标不是让损失降到最低而是让模型在换摄像头、换光线、换攻击方式之后仍然稳定。这一章讲三个最影响结果的环节数据增强、损失函数、阈值选取。4.1 数据增强给真假样本都加摄像头降质防止只学纹理公开数据集里的图像大多是实验室环境下采集的画质干净、光线均匀真实部署时摄像头可能是几十块钱的 USB 头光线忽明忽暗画面还带噪点。如果只用原始数据训练模型会把“清晰”当成活体特征一旦线上画面模糊就直接把真人拒掉。常见做法是在数据加载时叠加摄像头降质增强# 训练阶段 transform transform transforms.Compose([ transforms.RandomResizedCrop(224, scale(0.8, 1.0)), transforms.ColorJitter(brightness0.3, contrast0.3, saturation0.2), transforms.GaussianBlur(3, sigma(0.1, 1.5)), # 模拟摄像头失焦 transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]), ])RandomResizedCrop模拟人脸框检测偏移避免模型依赖固定的裁切位置ColorJitter的 brightness 和 contrast 调到 0.3覆盖不同色温GaussianBlur的 sigma 范围 0.1 到 1.5模拟低端摄像头的失焦效果。要注意的是这些增强对真样本和攻击样本一视同仁因为线上摄像头降质同样作用于两类输入。很多源码项目会在训练后期把增强强度逐步调低让模型在收敛前看到更接近真实分布的数据这是个值得试的细节。4.2 损失函数与难样本挖掘让模型记得住最难的那批假脸活体检测的标签分布很特殊真脸样本多且相似假脸样本少但千奇百怪。如果只用 BCE Loss模型很容易满足于把大多数简单的打印照片判对而对那些仿真的屏幕翻拍掉以轻心。Focal Loss 在这里比 BCE 好用得多# losses/focal_loss.py import torch import torch.nn as nn import torch.nn.functional as F class FocalLoss(nn.Module): def __init__(self, gamma2.0, alpha0.25): super().__init__() self.gamma gamma # 对易分样本的惩罚衰减力度 self.alpha alpha # 正负样本类别权重 def forward(self, logits, targets): bce F.binary_cross_entropy_with_logits( logits, targets, reductionnone ) prob torch.sigmoid(logits) p_t prob * targets (1 - prob) * (1 - targets) # 易分样本 p_t 接近 1权重趋近 0 loss (1 - p_t) ** self.gamma * bce alpha_t self.alpha * targets (1 - self.alpha) * (1 - targets) return (alpha_t * loss).mean()gamma2.0是默认值含义是当某个样本已经被分得很好p_t 接近 1时它的损失权重会以平方级衰减让模型把精力留给那些仍然分错的难样本。alpha0.25是正类权重如果数据集中假样本远多于真样本把这个值往 0.4 调反过来真样本少就降到 0.1 左右。这个损失函数收敛后APCER 往往比 BCE 低一个数量级代价是训练时间略长、BPCER 可能轻微上升需要结合验证集做权衡。4.3 阈值选取线上使用选FAR可控的点不选ACER最小点训练结束后模型输出的是 logit经过 sigmoid 变成 0 到 1 的分数。这个分数本身没有业务意义阈值才是决定系统安全性的关键。很多新手直接取 ACER 最小的阈值但 ACER 最小只代表“平均错误低”不代表安全性可接受。正确做法是在验证集上先定业务允许的 APCER 上限比如金融场景要求 APCER ≤ 0.1%然后在该约束下找 BPCER 最小的阈值阈值APCERBPCERACER说明0.300.20%0.05%0.125%安全但可能拒真人0.450.08%0.12%0.100%ACER 最小但 APCER 仍超标0.600.05%0.35%0.200%满足安全BPCER 上升上表只是示意。实际项目中阈值要在独立的验证集上选取测试集不能参与这个过程否则阈值会被测试集“记住”线上表现失真。阈值定好后推理代码里只用一行score threshold一个超参数决定了整个系统的安全水位。5. 静默活体检测避坑清单四个真实踩过的坑活体检测这个方向数据、指标和部署每个环节都有黑匣子。以下四个坑是我在多个项目里反复遇到过的按“现象、原因、解决”说清楚能帮你省掉至少两周调试时间。5.1 坑一按帧划分数据测试集准确率虚高到 99%现象训练集和验证集分开后验证集准确率直接到 99%模型看起来完美。一部署到现场假脸识别率骤降几乎可用性为零。原因同一个视频的相邻帧高度相似甚至只是压缩编码差异。如果按帧划分数据集同一段视频的相似帧会同时出现在训练集和验证集模型相当于见过“考试原题”它在学帧噪声而不是活体特征。解决训练前把数据按视频目录或人物 ID 划分。验证集里出现的视频训练集里绝不能有同源片段。更严格的做法是按采集设备划分训练集用设备 A 的拍摄数据验证集用设备 B 的拍摄数据这样才能暴露模型的泛化能力。源码项目若没有提供划分协议要自己检查数据文件名的 subject 前缀。5.2 坑二没有人脸对齐模型悄悄去学背景了现象模型在原有测试集上表现正常换一个环境部署后开始大量误判——真人被拒打印照片反而通过。原因人脸检测框在训练集里位置相对固定模型不知不觉把人脸框边缘的留白、背景颜色也当成特征。一旦部署环境的背景完全不同模型输出就失去依据。解决在数据加载前加一个人脸对齐模块用检测框的五个关键点做仿射变换把眼睛对齐到固定坐标再裁剪为统一尺寸。训练和推理必须走同一条对齐链路。工程上建议把对齐结果缓存成预处理文件避免每次训练重复跑检测器。我之前有个项目就是漏了这一步Grad-CAM 可视化一看模型激活区域全在人脸轮廓外的背景上修完之后 APCER 直接掉了一个数量级。5.3 坑三训练集只有打印照片屏幕翻拍轻松绕过现象训练时建模的所有攻击样本都是 A4 纸打印照片测试时拿手机屏幕翻拍一测直接被当成真人放行。原因屏幕翻拍的视觉特征和打印照片完全不同。屏幕有摩尔纹、像素栅格和刷新条纹而模型只见过印刷网点没有见过屏幕纹理自然无法泛化。解决在训练集里补足攻击类型至少要覆盖打印照片和屏幕翻拍两类。如果数据集采集成本高可以用数据增强模拟摩尔纹对图像叠加正弦光栅或周期性噪声再与原始图混合。更稳妥的做法是给模型输入加入高频通道高通滤波后的画面会把两种攻击的周期性纹理都暴露出来降低模型对具体攻击类型的依赖。5.4 坑四只信Accuracy不看失败样本模型学了肤色没学纹理现象一个 ResNet50 模型验证集 accuracy 有 98%但安全评估时 APCER 高达 3%完全无法上线。原因Accuracy 对类别不均衡不敏感。数据集里真脸占八成模型只要把真脸全判对再把少量假脸判错accuracy 也能很好看。更深层的问题是模型的判定依据可能是肤色、亮度这些表层特征而不是攻击介质的纹理缺陷。解决训练时每轮打印 APCER、BPCER 和 ACER不做 accuracy 汇报。评估阶段随机抽一批被误判的攻击样本逐张看是打印反光、屏幕摩尔纹还是边缘裁剪按漏网类型倒推补充数据或增强策略。这是活体检测最容易见效的排查手段比反复换网络结构有用得多。6. 部署验证与进阶把模型接到摄像头数据流上训练完的模型最终要跑在真实数据流里。这里给一个最小推理接口以及从单帧走向多帧的进阶方向。6.1 最小推理接口检测、对齐、推理三行拼起来# inference.py 核心逻辑 def liveness_check(frame, face_box, model, transform, threshold0.5): # 1. 用检测器给出的人脸框做对齐裁剪 aligned align_face(frame, face_box, size224) # 2. 归一化后送到活体模型 x transform(aligned).unsqueeze(0) with torch.no_grad(): score torch.sigmoid(model(x)).item() # 3. score 越高越可能是真脸threshold 由验证集选定 return score, score threshold这段代码可以在摄像头循环里直接调用。注意threshold不要写死在模型文件里要放到配置文件或环境变量中方便上线后调整align_face需要和训练时的对齐代码完全一致包括目标尺寸和填充方式。端侧部署时模型导出为 ONNX 后再转 TensorRT 或 NCNN分辨率降到 160×160CPU 推理延迟能控制在 10 毫秒以内。6.2 从单帧到多帧rPPG与时序融合的进阶方向单帧模型能拦住大部分照片和屏幕攻击但遇到头模或高精度打印时静态纹理特征就不够用了。接下来值得投入的方向是 rPPG 多帧融合从连续人脸区域提取皮肤反射的微小周期性变化计算心跳频率是否存在。这个信号对打印、屏幕、面具都天然免疫因为非活体没有血流。代价是需要约 1 秒的视频片段且对帧率、运动模糊敏感。我踩过的最大教训是只顾刷模型精度忽略了前处理一致性。早先一个项目训练时用 OpenCV 的面部关键点对齐部署时换了个检测库两者输出有十几像素的偏差精度掉得莫名其妙。后来统一成同一套预处理代码才恢复正常。活体检测的很多问题不是网络不行是训练和推理的数据流没对齐。希望这个方向的经验能帮到你少走这些弯路。本文还有配套的精品资源点击获取
返回列表