首页
社区
课程
招聘
[原创]看懂身份验证中的活体检测(三):静默活体检测
发表于: 2026-7-30 16:34 350

[原创]看懂身份验证中的活体检测(三):静默活体检测

2026-7-30 16:34
350

之前讲了动作活体要用户配合“表演”:眨眼、摇头、张嘴。三色活体又多了一位灯光师:屏幕要依次发出不同颜色。

本篇拆解的静默活体链把这两件事都拿掉了:它直接观察当前帧的人脸区域,再输出一个外观分类分数。

所谓“静默”,只是交互方式更安静。本文这份实现回答的仍是:当前这块普通人脸图像,在模型看来更接近哪一侧。

前两篇分别讲了动作活体如何使用 Face 的时序特征,以及三色活体如何计算人脸区域的光照响应

这一篇只沿另一条线往下走:SilentLiveEnginelibLiveness.so 与 ncnn 如何把一帧人脸 crop变成静默活体分。

本文只解释检测机制与防御边界,不提供攻击复现;FaceAISDK 仅作为公开项目样本,不作漏洞结论。

1. 先看全图:一帧人脸怎样变成静默活体分

图片描述

这张图的前半段负责准备输入,后半段才是本文重点:native 怎样裁出人脸、让模型打分并把结果送回 Java。下面只用最少代码接上前两篇,再把篇幅留给 libLiveness.so

2. 节点①:引擎和模型怎样就位

状态机创建时,只要活体检测没有关闭,就会创建 SilentLiveEngine。包装层随后加载 so、读取本地配置并让 native 加载模型:

[F3:101] if (livenessType != FaceLivenessType.NONE) { // 活体检测未关闭时准备静默引擎
[F3:102]     val engine = SilentLiveEngine(context.assets) // 把 App 资源交给包装层
[F3:104]     engine.init() // 开始加载 so、配置和模型
[SEJ1:Component.<init>] System.loadLibrary(LiveDetector.tag) // tag 固定为 "Liveness",加载 libLiveness.so
[SEJ1:LiveDetector.loadModel] val configs = parseConfig(assetManager) // 解析 assets/liveness/config.json
[SEJ1:LiveDetector.loadModel] return nativeLoadModel(assetManager, configs) // 把资源管理器和配置列表交给 JNI/native

这里知道一件事就够了:模型随 SDK 资源在端侧加载,config.json 决定 native 加载哪个模型以及怎样准备输入。

3. 节点②—③:从前两篇接过当前帧和人脸框

CameraX 取帧、Bitmap 转换、ML Kit 生成 Face 以及多脸时选择最大脸的完整过程,已经在第一篇:动作检测中展开;第二篇:三色 / 炫彩 / RGB 检测也复用了同一条输入链,本篇不再重复贴代码。

没看过前文只需记住:Face 是 ML Kit 的人脸检测结果,不是活体结论;boundingBox 只标出脸在当前 Bitmap 中的位置。

无脸、距离和多脸检查都发生在静默模型打分之前。

4. 节点④—⑤:Java 怎样把当前帧送进 libLiveness.so

前两篇留下的是当前 Bitmap 和 Face.boundingBox。Java 侧只做三步:包装脸框、转换图像、调用引擎。

[F5:200-F5:205] SilentFaceBox box = new SilentFaceBox(left, top, right, bottom, 0.0f); // 把 boundingBox 包装成 native 可读的脸框
[F5:207-F5:208] byte[] nv21 = LibYuv.bitmapToNv21(fullBmp, width, height); // 把当前整帧 Bitmap 转成 NV21
[F5:210-F5:211] float raw = state.OooOOOo.detectLive(nv21, width, height, 0, box); // 输入整帧、尺寸、方向值和脸框

包装层只校验 NV21 长度并展开脸框,随即进入 JNI:

[SEJ1:LiveDetector.detect] if (width * height * 3 / 2 != yuv.size) throw IllegalArgumentException("Invalid yuv data") // 只校验 NV21 长度
[SEJ1:LiveDetector.detect] return nativeDetectYuv(yuv, width, height, orientation, left, top, right, bottom) // 进入 JNI 并返回分数

进入 libLiveness.so 的输入只有四类:整帧 NV21、帧尺寸、方向值和脸框边界

5. 节点⑥—⑧:libLiveness.so 里发生了什么

以下地址化伪代码由作者根据当前 ARM64 libLiveness.so 样本的反汇编结果整理,不是上游 C++,地址也只绑定当前样本。

native 读到哪些模型资产

assets/liveness/ 中有两组 ncnn 模型;关键配置按原字段重排后是:

[
  {"name":"liveness_model_1","scale":2.7,"width":80,"height":80}, // 较小观察范围
  {"name":"liveness_model_2","scale":4.0,"width":80,"height":80}  // 较大观察范围
]

两个模型都会把各自的 crop 缩放为 80×80 后再推理。

一次调用怎样从 NV21 走到分数

[N2:0x1698AC] nativeDetectYuv(nv21, w, h, angle, left, top, right, bottom) // native 接收单帧 NV21、尺寸、方向和人脸框
[N2:0x169960] NV21_to_RGB(nv21, w, h, rgb) // 把 NV21 转成连续 RGB 颜色缓冲
[N2:0x16999C] swap(rgb.R, rgb.B) // 逐像素交换第 1、3 通道,把 RGB 缓冲改成 BGR
[N2:0x169D84] maxSafeScale = min((frameW - 1) / faceW, (frameH - 1) / faceH) // 算出不越出画面的最大扩边倍数
[N2:0x169E60] effectiveScale = min(config.scale, maxSafeScale) // 目标尺度过大时,以画面边界允许值为准
[N2:0x169F08] Mat::from_pixels_crop_resize(bgr, PIXEL_BGR, cropRect, config.width, config.height) // 裁出扩边脸区并缩放到配置尺寸
[N2:0x16A048] extractor = net.create_extractor() // 为当前模型创建一次 ncnn 推理 extractor
[N2:0x16A07C] extractor.input("data", mat) // 把处理后的人脸 crop 放入名为 data 的输入
[N2:0x16A0AC] extractor.extract("softmax", out) // 读取名为 softmax 的模型输出
[N2:0x16A0B4] live_value = out[1] // 只读取输出数组的第二个元素
[N2:0x169D98] score_sum += live_value // 把当前模型的第二个输出累加进总分
[N2:0x16A140] return score_sum / model_count // 用累加值除以参与推理的模型数量并返回平均值

这位于总图的节点⑥ native 预处理层、节点⑦模型层和节点⑧聚合层

这段 native 代码可以压成三步:

  1. **换颜色格式:**NV21 → RGB → BGR。
  2. **裁人脸区域:**按脸框扩边,靠近画面边缘时自动缩小范围。
  3. **计算分数:**crop 送入 ncnn,每个模型取 out[1],最后求平均。

crop 到底是什么

crop 不是一种新图片格式,也不是模型的神秘中间结果。它就是根据人脸框,从整帧画面中裁出来的模型输入区域

crop 画面里保留什么
2.7 人脸占比更大,更集中于脸部
4.0 保留更多头发、轮廓、颈部和周围背景

两个 crop 都以脸框中心为基准;靠近画面边缘时,实际观察范围会缩小。

模型真正读的是哪些人脸信号,又怎样算出静默分

先看“画面怎样变成一个分数”:

模型读入的是两块人脸 crop 的像素,不是姓名、身份证号、身份模板或真实深度图;每个模型最终输出 3 个 Softmax 类别值。

它不是用固定 RGB 阈值判断肤色,而是可能从 crop 中综合观察这些线索:

信号 画面里可能看到什么 不能误读成
色泽与光照 颜色分布、明暗过渡、屏幕发光、材质反光 肤色深浅等于真假
纹理 皮肤连续性、打印网点、屏幕像素、摩尔纹、压缩痕迹 当前权重一定依赖某一种纹理
立体外观 鼻梁、眼窝、面颊的轮廓、阴影与遮挡 测得真实物理深度
区域一致性 脸、头发、耳朵、颈部、边缘和背景的衔接 证明画面来自可信摄像头
成像痕迹 扭曲、异常光影、重采样、边缘重复 某一种痕迹就能单独决定真假

现有模型结构与开源测试支持把 out[1] 解释为真人/活体方向。上面的 native 代码已经给出打分算法,换成公式就是:

静默分 =(模型 1 当前 crop 的真人类别值 + 模型 2 当前 crop 的真人类别值)÷ 2
模型 三分类输出示意 代码实际取值
model_1 [0.12, 0.78, 0.10] out[1] = 0.78
model_2 [0.08, 0.86, 0.06] out[1] = 0.86
最终静默分 (0.78 + 0.86) ÷ 2 0.82

0.82 只是计算示意,不是实测结果,也不等于“系统有 82% 把握证明镜头前是真人”。

6. 节点⑨—⑩:静默分怎样进入最终判断

模型返回后,Java 保存三位小数静默分,再交给上层 Demo:

[F5:213] state.OooOOo0 = Math.round(raw * 1000f) / 1000f; // 四舍五入后保存为三位小数静默分
[F5:820] callback.onLivenessDetected(OooOOo0, bitmap); // SDK 把静默分和当前 Bitmap 交给上层
[F1:102] public void onLivenessDetected(float livenessValue, Bitmap bitmap) { // Demo 接收最终活体回调
[F1:104]     if (livenessValue > 0.75) { // Demo 用自己的实现常量消费这个静默分
[F1:111]         finishFaceVerify(10, R.string.liveness_detection_done, livenessValue); // 把结果和静默分交给完成流程
[F1]         } // 结束 Demo 的结果判断
[F1]     } // 结束最终活体回调

OooOOo0 是静默活体类别分,不是身份分、三色 gainScore 或校准概率;回调不再推理,只把它交给业务层。

三色链路可回看第二篇

7. 从三个商业样本看静默活体的现实形态

FaceAISDK 把一个端侧单帧模型讲得很清楚,但它还不能代表完整商业认证系统。基于作者对三个匿名 Android 商业身份核验样本的代码与运行链路观察,至少可以看到三种不同组合:

脱敏样本 观察到的检测形态 给第三篇的启发
样本 A 端侧分类和多任务反伪模型被放进多帧、光照响应状态机 单帧模型分可能只是流程中的一个状态信号
样本 B RGB 反伪模型之外,还出现纹理统计、多帧一致性和可选近红外(NIR)路径 商业检测会把模型、传统图像特征和多模态能力组合起来
样本 C 端侧负责人脸质量、流程与素材采集,服务端继续结合风险信号和身份比对裁决 端侧检测通过不等于整条认证链已经通过

**“静默”首先描述用户体验,而不是统一的算法名称。**三个样本共同说明:本文的端侧单帧模型只可能是商业认证系统的一层,其他实现还可能组合多帧、可选多模态、设备风险、云端反伪和身份比对;这些是方向性观察,不代表所有厂商。

8. 这一层在防御体系中的位置

问题 结论
它补了什么 无需用户动作或屏幕发光,也能为当前人脸 crop 输出一条外观分类分数
它没覆盖什么 不证明身份、真实深度、采集来源或摄像头到 SDK 的链路完整性
防御方怎样使用 分层记录输入质量、静默分、其他检测分、最终结果、失败原因和版本

9. 小结

静默活体之所以“静默”,是因为用户不必完成动作,屏幕也不必为这一层打三色光;不是因为模型已经越过图像,直接知道了“真人是谁、画面从哪里来”。

从这份代码证据看,它的完整路径是:

当前帧与人脸框 → NV21 与 SilentFaceBoxlibLiveness.so → crop / resize → ncnn → 累加 out[1] 并平均 → 三位小数 OooOOo0onLivenessDetected → Demo 业务判断。

把这条链路读准确,防御方才能既不低估静默活体,也不把一个单帧外观分类分数抬成整条身份可信结论。

下一篇,我们把动作、三色 / RGB 与静默三层重新放回同一条画面信任链,讨论一个更贴近现实的问题:为什么 AI 生成视频可能同时挑战这三层活体防护?

这是前三篇的综合收尾,只讲三层共享的信任假设与防御意义,不做攻击复现。


代码索引说明:文中的 [F*][SEJ1][N2] 标记来自作者对 FaceAISDK 公开流程、包装层和当前 ARM64 样本的防御侧整理,只用于定位流程证据;本文不包含攻击实现、复现路径或攻击样本。


传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!

收藏
免费 4
打赏
分享
最新回复 (5)
雪    币: 1950
活跃值: (3847)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
2
cy
2026-7-30 16:54
0
雪    币: 351
活跃值: (7799)
能力值: ( LV3,RANK:30 )
在线值:
发帖
回帖
粉丝
3
太强了,心里有个疑问,人脸失败这个结果太含糊了,是怎么排除 
2026-7-30 17:04
0
雪    币: 89
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
4
感谢分享,很巧妙的设计
2026-7-30 19:50
0
雪    币: 1730
活跃值: (2848)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
5
叽里咕噜说啥呢 ,直接让ai一顿乱改
2026-7-30 20:06
0
雪    币: 226
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
6
New对象处 太强了,心里有个疑问,人脸失败这个结果太含糊了,是怎么排除 [em_004]
人脸失败是最终结果,就像你的环境被检测一样,还是要拆开来看,所以我把这个Demo拆分开3块来讲的,人脸验证的成功与否就是在这三块的结果汇总。
5天前
0
游客
登录 | 注册 方可回帖
返回