ARTICLE DETAIL

资讯详情

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

静默活体检测项目实战 算法原理【附代码】

静默活体检测项目实战  算法原理【附代码】 1. 什么是静默活体检测静默活体检测就是在用户不做任何动作、无感知的情况下判断镜头前那张脸是真人还是假体。不需要眨眼、张嘴、摇头一帧图像就能给出真假结论。2. 为什么需要它因为假脸太容易做了。打印照片、手机屏幕翻拍、硅胶面具、3D 头模都能骗过普通人脸识别。刷脸支付、门禁考勤、远程开户这些场景没有活体这道闸识别再准也是给别人开的门。配合式活体让你眨眨眼体验差、耗时久还会被视频注入绕过静默活体用户零配合、一帧出结果是线上主流形态。3. 算法效果模型FLOPs参数量端上单帧耗时FPR1e-5 时 TPR基线 MobileFaceNet0.224G0.991M——剪枝主干 V10.081G0.414M——剪枝主干 V20.081G0.435M——端侧融合模型84M≈0.42M20 ms97.8%实测机型速度单帧麒麟 990 5G19ms麒麟 99023ms骁龙 84524ms麒麟 81025msRK328890ms。简单说四毛钱的参数量0.4M、八千万次浮点运算、手机上一帧 20 毫秒、万分之一误识率下还能抓住 97.8% 的真人。这个量级才敢往门禁机和 APP 里塞。下面是两张示例图的实际判定结果红框假脸、蓝框真脸框上带置信度模型FLOPs参数量端上单帧耗时FPR1e-5 时 TPR基线 MobileFaceNet0.224G0.991M——剪枝主干 V10.081G0.414M——剪枝主干 V20.081G0.435M——端侧融合模型开源84M≈0.42M20 ms97.8%高精度模型未开源162M—40 ms99.7%实测机型速度单帧麒麟 990 5G19ms麒麟 99023ms骁龙 84524ms麒麟 81025msRK328890ms。简单说四毛钱的参数量0.4M、八千万次浮点运算、手机上一帧 20 毫秒、万分之一误识率下还能抓住 97.8% 的真人。这个量级才敢往门禁机和 APP 里塞。下面是两张示例图的实际判定结果红框假脸、蓝框真脸框上带置信度算法架构整体思路用一个超轻量的分类网络做主干训练时额外挂一个频谱回归的小分支当老师逼着主干学频域特征训练完把老师扔掉推理时用同一张脸的不同尺度裁图喂给多个模型概率加和投票出结果。整个网络叫MultiFTNet拆开看就是一条主干 一条临时支路① 主干剪枝后的轻量分类网络输入3×80×80走的是经典的DWDepthwise 可分离卷积 残差块堆叠classMiniFASNet(Module): def__init__(self, keep, embedding_size, conv6_kernel(7, 7), drop_p0.0, num_classes3, img_channel3): ... self.conv1 Conv_block(img_channel, keep[0], kernel(3,3), stride(2,2), padding(1,1)) self.conv2_dw Conv_block(keep[0], keep[1], kernel(3,3), stride(1,1), padding(1,1), groupskeep[1]) self.conv_23 Depth_Wise(c1[0], c2[0], c3[0], kernel(3,3), stride(2,2), padding(1,1), groupskeep[3]) self.conv_3 Residual(c1, c2, c3, num_block4, ...) ...这里最骚的是那个keep列表也就是剪枝后每层保留多少通道的手工配置表keep_dict {1.8M: [32, 32, 103, 103, 64, 13, 13, 64, 26, 26, 64, 13, 13, 64, 52, 52, 64, 231, 231, 128, 154, 154, 128, 52, 52, 128, 26, 26, 128, 52, 52, 128, 26, 26, 128, 26, 26, 128, 308, 308, 128, 26, 26, 128, 26, 26, 128, 512, 512], ...}注意里面103、231、308、13这种一看就不是人手拍的数字——这是剪枝算法搜出来的每层最优通道数不是 32/64/128 这种凑整的美学。剪枝的本质就是原网络里有些通道是打酱油的砍掉它们对精度几乎没影响但算力和参数量实打实降下来。MobileFaceNet 原版 0.224G / 0.991M剪完 0.081G / 0.41M直接砍掉六成算力和一半参数精度还基本不掉。② 辅助分支傅里叶频谱回归FTGenerator支路从主干的conv_4输出128×10×10抽头接三个卷积就把特征翻译成一张频谱图classFTGenerator(nn.Module): def__init__(self, in_channels128, out_channels1): self.ft nn.Sequential( nn.Conv2d(in_channels, 128, kernel_size(3,3), padding1), nn.BatchNorm2d(128), nn.ReLU(inplaceTrue), nn.Conv2d(128, 64, kernel_size(3,3), padding1), nn.BatchNorm2d(64), nn.ReLU(inplaceTrue), nn.Conv2d(64, out_channels, kernel_size(3,3), padding1), nn.BatchNorm2d(out_channels), nn.ReLU(inplaceTrue) )③ 训练/推理双模式这个设计最妙的地方在forward里一行if就完事了defforward(self, x): ... cls self.model.prob(x1) # 分类主分支 if self.training: ft self.FTGenerator(x) # 只有训练时才跑辅助分支 return cls, ft else: return cls # eval 时辅助分支整段不执行简单理解训练的时候我请了个频谱老师盯着主干的中层特征逼它必须能还原出这张脸的频谱等毕业推理了老师直接走人模型文件大小、参数量、推理耗时一点没变但主干已经沾了老师的光学到的特征里天然带着频域信息。这就是所谓的free lunch白嫖提升。损失也极其朴素两个损失各占一半loss_cls self.cls_criterion(embeddings, labels) # 三分类交叉熵 loss_fea self.ft_criterion(feature_map, imgs[1]) # 频谱 MSE loss 0.5 * loss_cls 0.5 * loss_fea④ 为什么频谱能当监督信号真实人脸是一次成像光线打在立体皮肤上反射进镜头高频细节丰富、频谱过渡自然。而翻拍脸是二次成像真脸 → 相机 → 屏幕/纸张 → 相机多出来的这一次转换会丢高频、引入摩尔纹、屏幕像素栅格、纸张纹理甚至反光斑。这些差异在空域里肉眼不一定看得出来但在频域里非常明显。下面这张图是我用工程里那套generate_FT逻辑对示例图实算出来的对比左人脸区域中频谱图右频谱行能量曲线可以明显看到真脸的频谱能量铺得更开、更毛糙翻拍脸的频谱更集中、更干净——二次成像把高频信息抹平了。这就是辅助监督能起作用的物理前提。⑤ 频谱真值怎么来的答案是算出来的不用标defgenerate_FT(image): image cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 转灰度 f np.fft.fft2(image) # 二维傅里叶变换 fshift np.fft.fftshift(f) # 低频移到中心 fimg np.log(np.abs(fshift) 1) # 取幅度 log 压缩动态范围 # 归一化到 [0,1] fimg (fimg - minn 1) / (maxx - minn 1) return fimg这是整套方案最省钱的一点频谱标签是numpy几行代码在线算出来的不需要任何人工标注也不额外占磁盘加在DatasetFolderFT.__getitem__里随 batch 实时生成。相当于免费请了个老师。⑥ 尺寸怎么对上的一个很讲究的小细节频谱真值被 resize 到10×10正好等于主干conv_4输出特征图的空间尺寸。而这个尺寸不是拍脑袋定的defget_kernel(height, width): kernel_size ((height 15) // 16, (width 15) // 16) return kernel_size # 80×80 输入 → kernel (5, 5) # 频谱标签尺寸跟着 kernel 走 conf.ft_height 2 * conf.kernel_size[0] # 10 conf.ft_width 2 * conf.kernel_size[1] # 10主干最后那层conv_6_dw用的是kernel(5,5)、无 padding 的深度卷积把5×5的特征图一把卷成1×1连 GAP 和全连接都省了。输入从 80×80 换成别的尺寸kernel 自动跟着变频谱标签尺寸也跟着变整套代码不用改一行。2.算法流程1. 数据怎么组织三类 多尺度训练集按3 类分目录真人脸 / 翻拍 / 面具头模等再按原图 or 不同 scale 的 patch分成多份├── datasets └── RGB_Images ├── org_1_80x60 # 原图直接 resize │ ├── 0 ├── 1 └── 2 ├── 1_80x80 # 人脸框按 scale1 扩边后裁剪 ├── 2.7_80x80 # scale2.7 └── 4_80x80 # scale4为什么搞这么多种尺度看这张官方给的 patch 示例就懂了scale 小只框住脸看的是皮肤纹理、边缘细节能抓纸张纹理和屏幕栅格scale 大连脖子带背景一圈看的是周围环境比如照片边缘、手持的边框、屏幕的黑边、脖子处的接缝org 整图看全局光照和场景信息。同一张脸用三种视野去看各自训一个模型最后投票。这比死磕单个模型的收益大得多——因为你没法预判攻击者会用什么介质多尺度等于多买几份保险。裁剪逻辑也就二十行核心是以框为中心按 scale 扩边同时保证不越界classCropImage: staticmethod def_get_new_box(src_w, src_h, bbox, scale): x, y, box_w, box_h bbox scale min((src_h-1)/box_h, min((src_w-1)/box_w, scale)) # 防止扩边超出原图 new_width, new_height box_w * scale, box_h * scale center_x, center_y box_w/2 x, box_h/2 y left_top_x, left_top_y center_x - new_width/2, center_y - new_height/2 right_bottom_x, right_bottom_y center_x new_width/2, center_y new_height/2 # 四个方向分别做越界回推 if left_top_x 0: right_bottom_x - left_top_x; left_top_x 0 ... returnint(left_top_x), int(left_top_y), int(right_bottom_x), int(right_bottom_y)2. 训练配置conf.lr 1e-1 conf.milestones [10, 15, 22] # 多阶段降学习率 conf.gamma 0.1 conf.epochs 25 conf.momentum 0.9 conf.batch_size 1024 conf.num_classes 3# 三分类不是二分类 conf.embedding_size 128数据增强走的是经典四件套train_transform trans.Compose([ trans.ToPILImage(), trans.RandomResizedCrop(sizetuple(conf.input_size), scale(0.9, 1.1)), trans.ColorJitter(brightness0.4, contrast0.4, saturation0.4, hue0.1), trans.RandomRotation(10), trans.RandomHorizontalFlip(), trans.ToTensor() ])注意RandomResizedCrop的 scale 是(0.9, 1.1)——只做很小的尺度扰动不像分类任务那样 0.08~1.0 大砍大裁。因为活体检测靠的是纹理和频域线索裁太狠会把这些线索破坏掉。3. 推理多模型软投票for model_name in os.listdir(model_dir): h_input, w_input, model_type, scale parse_model_name(model_name) param {org_img: image, bbox: image_bbox, scale: scale, out_w: w_input, out_h: h_input, crop: True} if scale isNone: # 模型名以 org_ 开头 → 直接用整图 param[crop] False img image_cropper.crop(**param) prediction model_test.predict(img, os.path.join(model_dir, model_name)) label np.argmax(prediction) value prediction[0][label] / 2# 除以模型个数 平均概率模型名自带全部元信息靠parse_model_name一行解析加模型不用改代码往目录里丢文件就行defparse_model_name(model_name): info model_name.split(_)[0:-1] h_input, w_input info[-1].split(x) # 4_0_0_80x80_MiniFASNetV1SE.pth → 80, 80 model_type model_name.split(.pth)[0].split(_)[-1] # → MiniFASNetV1SE if info[0] org: scale None else: scale float(info[0]) # → 4.0 returnint(h_input), int(w_input), model_type, scale投票方式是softmax 概率逐元素相加再取 argmax本质是软投票集成。多个模型各有各的视野偏好一个犹豫的时候另一个能补上鲁棒性比单模型好一大截。业务侧还能在此基础上再加阈值置信度高于阈值才判真脸宁可拒真也不放伪门禁场景通常就是这么调的。3.算法运行结果下面是整套流程跑起来的终端输出检测 → 多尺度裁剪 → 频谱生成 / 训练 / 推理三段4.总结整套方案其实就三板斧主干够小剪枝把 MobileFaceNet 砍到 0.081G / 0.41M端上单帧 20ms这是能落地的前提训练够聪明挂一个傅里叶频谱辅助分支当免费老师逼主干学频域差异训完老师走人推理零开销推理够稳多尺度 patch 多个模型软投票再配业务阈值宁可拒真也不放伪。从工程角度看它最值得学的不是用了傅里叶变换这个点子而是怎么在不增加部署成本的前提下把额外信息塞进训练过程这个思路。辅助分支、多尺度集成、模型名即配置、尺寸参数联动——全都是成本低、收益稳的工程技巧。联系我们极目视觉团队有十余年算法经验主要提供两类服务课设或论文指导。如果你在课程设计、毕业论文或课题中卡在算法实现、实验设计、系统搭建等环节可以私聊我。图像相关算法定制开发。专注工业生产安全、智慧园区/校园、交通、农业与森林、人脸识别等场景擅长边缘计算下的算法优化与部署。支持 Jetson、RK、算能、高通、海思、地瓜等边缘设备无 NPU 设备也能高效部署可适配 Android、Debian、Ubuntu、Linux、CentOS 等多平台。有课设/论文指导需求或图像算法开发、商务合作欢迎联系并注明来意。
返回列表