随着移动端安全需求的提升,单纯的“人脸检测”逐渐难以满足支付、身份验证等场景的抗欺骗需求。活体检测(Liveness Detection) 能有效区分真人和照片、视频、面具等攻击手段,保障系统安全。
本项目目标:
实时打开摄像头,在人脸进入画面时进行活体检测;
当系统识别为“活体”后,自动进行人脸比对或登录流程;
支持 Android 5.0+,兼容 Camera2 与 CameraX;
CPU+GPU 混合加速,保证 >15 FPS;
UI 友好:检测过程中提示“眨眨眼睛”“张张嘴”等动作;
离线优先:优先采用本地 SDK,网络降级调用云端 API。
典型场景:
金融开户/支付;
门禁通行;
考试监控;
社交认证。
人脸检测与关键点定位
使用轻量级本地 SDK(如 ArcSoft FaceEngine、Face++ 本地版、人脸识别开源库 MNNFace)
检测人脸框、68/106 关键点,获取眨眼、张嘴等动作特征
活体检测算法
动作活体:引导用户眨眼、张嘴、点头等,以检测动态特征
纹理活体:分析皮肤光照/频谱差异,检测照片屏幕反光
深度活体:基于双目/结构光深度估计
Camera2 / CameraX
实时预览帧回调,获取 YUV→RGB 或 Bitmap 用于检测
保证回调线程快速返回,耗时检测在子线程执行
本地 vs 云端
本地 SDK:无流量消耗、延迟低,但需集成 .so 库
云端 API(Face++、Baidu AI):无需本地库,依赖网络、流量与延迟
异步与性能优化
使用 HandlerThread
或 ExecutorService
串行处理每帧检测
对关键点检测设阈值跳帧,避免每帧都全量检测
UI 与用户引导
使用 SurfaceView
/ TextureView
显示预览
在画面上绘制人脸框与检测提示
引导用户完成动作,并在超时后失败
引入本地 SDK
在 app/libs/
放入 libarcsoft_face_engine.so
与 arcsoft_face_engine.jar
在 build.gradle
中配置 jniLibs.srcDirs
布局设计
activity_main.xml
:
顶部 TextureView
(或 SurfaceView
)承载相机预览
半透明 OverlayView
在上层绘制人脸框与文本提示
控件整合到 MainActivity.java
注释中
权限管理
申请 CAMERA
、WRITE_EXTERNAL_STORAGE
、RECORD_AUDIO
(若需语音提示)
摄像头预览
使用 Camera2 API 打开后置摄像头
将预览输出到 TextureView
,并在 onImageAvailable()
获取 Image
进行 YUV→RGB 转换
人脸与活体检测
初始化 AFR_FSDKEngine
(人脸识别)、AFT_FSDKEngine
(人脸检测)与 ARL_FSDKEngine
(活体)实例
在子线程中:
调用人脸检测接口获取 faceInfo[]
调用关键点检测接口获取 landmarks
根据 LANDMARKS 跟踪眨眼/张嘴:动态活体
调用纹理活体接口分析帧灰度分布
合并结果,判断“活体通过”
流程控制
应用启动后进入“活体检测”模式
用户按提示完成动作(如眨眼两次),检测通过后回调主线程提示成功
超时或多次失败后提示“检测失败,请重试”
// ==============================================
// 文件:MainActivity.java
// 功能:活体人脸识别检测示例(离线本地 SDK)
// 包含:布局 XML、Manifest、Gradle 配置、一处整合
// ==============================================
package com.example.livenessdemo;
import android.Manifest;
import android.content.pm.PackageManager;
import android.graphics.*;
import android.media.Image;
import android.os.*;
import android.util.Size;
import android.view.*;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.app.ActivityCompat;
import com.arcsoft.face.*;
import java.nio.ByteBuffer;
import java.util.List;
public class MainActivity extends AppCompatActivity {
// --- 本地 SDK 相关常量(请替换为自有 AppId/SDKKey)
private static final String APP_ID = "YourArcSoftAppId";
private static final String SDK_KEY = "YourArcSoftSdkKey";
private static final int REQUEST_CAMERA = 2001;
private TextureView tvPreview;
private OverlayView overlay; // 自定义 View 在上层绘制
private CameraDevice cameraDevice;
private CaptureRequest.Builder previewBuilder;
private CameraCaptureSession previewSession;
private HandlerThread cameraThread;
private Handler cameraHandler;
// FaceEngine 模块
private FaceEngine ftEngine, frEngine, flEngine;
private int ftCode, frCode, flCode;
// 检测线程
private HandlerThread detectThread;
private Handler detectHandler;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 加载布局(整合在注释中)
setContentView(R.layout.activity_main);
// 1. 权限申请
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.CAMERA}, REQUEST_CAMERA);
} else {
init();
}
}
/** 初始化相机和 FaceEngine */
private void init() {
// 2. 初始化 FaceEngine
ftEngine = new FaceEngine();
frEngine = new FaceEngine();
flEngine = new FaceEngine();
// 检测引擎
ftCode = ftEngine.init(this, FaceEngine.ASF_DETECT_MODE_VIDEO,
FaceEngine.ASF_OP_0_ONLY, 16, 5,
FaceEngine.ASF_FACE_DETECT);
// 活体(纹理+动作)
flCode = flEngine.init(this, FaceEngine.ASF_DETECT_MODE_VIDEO,
FaceEngine.ASF_OP_0_ONLY, 16, 5,
FaceEngine.ASF_LIVENESS);
// 特征引擎(可选,用于后续比对)
frCode = frEngine.init(this, FaceEngine.ASF_DETECT_MODE_IMAGE,
FaceEngine.ASF_OP_0_ONLY, 16, 5,
FaceEngine.ASF_FACE_RECOGNITION);
if (ftCode != ErrorInfo.MOK || flCode != ErrorInfo.MOK) {
Toast.makeText(this, "引擎初始化失败", Toast.LENGTH_SHORT).show();
return;
}
// 3. 启动相机预览
tvPreview = findViewById(R.id.tvPreview);
overlay = findViewById(R.id.overlay);
startCameraThread();
openCamera();
startDetectThread();
}
/** 启动相机线程 */
private void startCameraThread() {
cameraThread = new HandlerThread("CameraThread");
cameraThread.start();
cameraHandler = new Handler(cameraThread.getLooper());
}
/** 打开 Camera2,略去详细代码(请根据官方示例实现) */
private void openCamera() {
// … 使用 CameraManager.openCamera(...)
// onOpened 回调中创建预览会话,并在每帧 Image到达时回调 onImageAvailable()
// 将 Image 通过 ImageReader 转 ByteBuffer,然后 post 到 detectHandler
}
/** 启动检测线程 */
private void startDetectThread() {
detectThread = new HandlerThread("DetectThread");
detectThread.start();
detectHandler = new Handler(detectThread.getLooper());
}
/** 每帧到达后调用 */
private void onFrameAvailable(Image image) {
// 转 YUV -> NV21 bytes
ByteBuffer y = image.getPlanes()[0].getBuffer();
ByteBuffer u = image.getPlanes()[1].getBuffer();
ByteBuffer v = image.getPlanes()[2].getBuffer();
int w = image.getWidth(), h = image.getHeight();
byte[] nv21 = new byte[w*h*3/2];
y.get(nv21, 0, w*h);
v.get(nv21, w*h, w*h/4);
u.get(nv21, w*h + w*h/4, w*h/4);
image.close();
detectHandler.post(() -> {
// 1. 人脸检测
List faces = new java.util.ArrayList<>();
int detectRes = ftEngine.detectFaces(nv21, w, h,
FaceEngine.CP_PAF_NV21, faces);
if (detectRes != ErrorInfo.MOK || faces.isEmpty()) {
runOnUiThread(() -> overlay.clear());
return;
}
// 2. 活体检测(纹理 + 动作)
LivenessInfo[] liveness = new LivenessInfo[faces.size()];
flEngine.detectLiveness(nv21, w, h,
FaceEngine.CP_PAF_NV21, faces, liveness);
// 3. 合并结果与 UI 绘制
runOnUiThread(() -> {
overlay.setFaces(faces, liveness);
if (liveness[0].isLive()) {
Toast.makeText(this,
"活体检测通过!", Toast.LENGTH_SHORT).show();
// TODO: 停止检测,进行后续比对或操作
}
});
});
}
/** 自定义 OverlayView 绘制人脸框和活体提示 */
public static class OverlayView extends View {
private List faces;
private LivenessInfo[] liveInfo;
private Paint paint = new Paint();
public OverlayView(Context c, AttributeSet a) {
super(c, a);
paint.setColor(Color.GREEN);
paint.setStyle(Paint.Style.STROKE);
paint.setStrokeWidth(5);
}
public void setFaces(List f, LivenessInfo[] l) {
faces = f; liveInfo = l; invalidate();
}
public void clear() { faces=null; invalidate(); }
@Override protected void onDraw(Canvas c) {
super.onDraw(c);
if (faces==null) return;
for (int i=0;i0
&& g[0]==PackageManager.PERMISSION_GRANTED) {
init();
} else {
Toast.makeText(this,
"摄像头权限被拒绝", Toast.LENGTH_SHORT).show();
}
}
}
/*
=========================== res/layout/activity_main.xml ===========================
=========================== 布局结束 ===========================
*/
/*
=========================== app/build.gradle 关键配置 ===========================
android {
compileSdk 33
defaultConfig {
minSdk 21
targetSdk 33
ndk {
abiFilters "armeabi-v7a","arm64-v8a"
}
externalNativeBuild { cmake { } }
}
sourceSets {
main {
jniLibs.srcDirs 'libs'
}
}
}
dependencies {
implementation 'androidx.appcompat:appcompat:1.5.1'
implementation files('libs/arcsoft_face_engine.jar')
}
=========================== Gradle 结束 ===========================
*/
/*
=========================== AndroidManifest.xml 关键节点 ===========================
=========================== Manifest 结束 ===========================
*/
引擎初始化
FaceEngine.init()
按需开启人脸检测、特征、活体模块;
本例使用纹理活体(ASF_LIVENESS
)与动作活体检测(眨眼、张嘴)自动结合;
Camera2 预览流
打开后置摄像头,将预览输出到 TextureView
;
使用 ImageReader
获取 YUV Image
,转换为 NV21 byte[];
通过 HandlerThread
串行处理每帧,避免并发冲突;
活体检测流程
detectFaces(...)
得到 FaceInfo[]
;
detectLiveness(...)
得到对应 LivenessInfo[]
,其中 isLive()
表示活体概率
可根据实际需求调整:仅当连续 N 帧活体通过后才最终判定;
UI 绘制
自定义 OverlayView
在 onDraw()
中绘制人脸框和“LIVE/FAKE”文字,实时反馈;
手势提示、进度条、文字提示可进一步增强体验;
生命周期与资源释放
在 onDestroy()
中关闭 Camera2 会话和 HandlerThread
,并 unInit()
FaceEngine;
必须清理本地 .so
占用的资源,避免崩溃;
优势
完全离线:无需网络,适合高安全场景;
实时高 FPS:本地 C/C++ 实现加速,保证流畅;
模块化:人脸检测、特征、活体分离,可灵活升级;
注意事项
授权:ArcSoft SDK 需在官网申请 AppId/SDKKey;
性能调优:必要时降低人脸检测频率(如每 200 ms);
多摄像头支持:可扩展前置/后置切换;
扩展方向
深度活体:结合结构光或双目摄像头实现更强抗攻击;
动作引导:随机引导用户执行不同动作,防止机密视频播放攻击;
人脸比对:检测通过后与本地/远程库比对身份;
语音提示:结合 TextToSpeech
语音引导用户
Jetpack Compose:在 Compose 中将 TextureView
包装为 AndroidView
实现同等效果。