一、项目介绍
随着移动端安全需求的提升,单纯的“人脸检测”逐渐难以满足支付、身份验证等场景的抗欺骗需求。活体检测(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<FaceInfo> 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<FaceInfo> 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<FaceInfo> 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;i<faces.size();i++) {
FaceInfo fi = faces.get(i);
Rect rect = fi.getRect();
c.drawRect(rect, paint);
String txt = liveInfo[i].isLive()?"LIVE":"FAKE";
paint.setTextSize(50);
c.drawText(txt, rect.left, rect.top-10, paint);
}
}
}
@Override protected void onDestroy() {
super.onDestroy();
// 停止预览
if (previewSession!=null) previewSession.close();
if (cameraDevice!=null) cameraDevice.close();
cameraThread.quitSafely();
// 销毁引擎
ftEngine.unInit();
flEngine.unInit();
frEngine.unInit();
}
@Override public void onRequestPermissionsResult(int rc,
@NonNull String[] p, @NonNull int[] g) {
if (rc==REQUEST_CAMERA
&& g.length>0
&& g[0]==PackageManager.PERMISSION_GRANTED) {
init();
} else {
Toast.makeText(this,
"摄像头权限被拒绝", Toast.LENGTH_SHORT).show();
}
}
}
/*
=========================== res/layout/activity_main.xml ===========================
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<!-- 摄像头预览 -->
<TextureView
android:id="@+id/tvPreview"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
<!-- 人脸框与活体提示 -->
<com.example.livenessdemo.MainActivity.OverlayView
android:id="@+id/overlay"
android:layout_width="match_parent"
android:layout_height="match_parent"/>
</FrameLayout>
=========================== 布局结束 ===========================
*/
/*
=========================== 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 xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.livenessdemo">
<uses-permission android:name="android.permission.CAMERA"/>
<application ...>
<activity android:name=".MainActivity"
android:screenOrientation="portrait">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
</manifest>
=========================== Manifest 结束 ===========================
*/
五、方法解读
-
引擎初始化
-
FaceEngine.init()
按需开启人脸检测、特征、活体模块; -
本例使用纹理活体(
ASF_LIVENESS
)与动作活体检测(眨眼、张嘴)自动结合;
-
-
Camera2 预览流
-
打开后置摄像头,将预览输出到
TextureView
; -
使用
ImageReader
获取 YUVImage
,转换为 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
实现同等效果。
-