Android实现活体人脸识别检测(附带源码)

一、项目介绍

随着移动端安全需求的提升,单纯的“人脸检测”逐渐难以满足支付、身份验证等场景的抗欺骗需求。活体检测(Liveness Detection) 能有效区分真人和照片、视频、面具等攻击手段,保障系统安全。

本项目目标:

  1. 实时打开摄像头,在人脸进入画面时进行活体检测;

  2. 当系统识别为“活体”后,自动进行人脸比对或登录流程;

  3. 支持 Android 5.0+,兼容 Camera2 与 CameraX;

  4. CPU+GPU 混合加速,保证 >15 FPS;

  5. UI 友好:检测过程中提示“眨眨眼睛”“张张嘴”等动作;

  6. 离线优先:优先采用本地 SDK,网络降级调用云端 API。

典型场景:

  • 金融开户/支付;

  • 门禁通行;

  • 考试监控;

  • 社交认证。


二、相关技术与知识

  1. 人脸检测与关键点定位

    • 使用轻量级本地 SDK(如 ArcSoft FaceEngine、Face++ 本地版、人脸识别开源库 MNNFace)

    • 检测人脸框、68/106 关键点,获取眨眼、张嘴等动作特征

  2. 活体检测算法

    • 动作活体:引导用户眨眼、张嘴、点头等,以检测动态特征

    • 纹理活体:分析皮肤光照/频谱差异,检测照片屏幕反光

    • 深度活体:基于双目/结构光深度估计

  3. Camera2 / CameraX

    • 实时预览帧回调,获取 YUV→RGB 或 Bitmap 用于检测

    • 保证回调线程快速返回,耗时检测在子线程执行

  4. 本地 vs 云端

    • 本地 SDK:无流量消耗、延迟低,但需集成 .so 库

    • 云端 API(Face++、Baidu AI):无需本地库,依赖网络、流量与延迟

  5. 异步与性能优化

    • 使用 HandlerThreadExecutorService 串行处理每帧检测

    • 对关键点检测设阈值跳帧,避免每帧都全量检测

  6. UI 与用户引导

    • 使用 SurfaceView / TextureView 显示预览

    • 在画面上绘制人脸框与检测提示

    • 引导用户完成动作,并在超时后失败


三、实现思路

  1. 引入本地 SDK

    • app/libs/ 放入 libarcsoft_face_engine.soarcsoft_face_engine.jar

    • build.gradle 中配置 jniLibs.srcDirs

  2. 布局设计

    • activity_main.xml

      • 顶部 TextureView(或 SurfaceView)承载相机预览

      • 半透明 OverlayView 在上层绘制人脸框与文本提示

    • 控件整合到 MainActivity.java 注释中

  3. 权限管理

    • 申请 CAMERAWRITE_EXTERNAL_STORAGERECORD_AUDIO(若需语音提示)

  4. 摄像头预览

    • 使用 Camera2 API 打开后置摄像头

    • 将预览输出到 TextureView,并在 onImageAvailable() 获取 Image 进行 YUV→RGB 转换

  5. 人脸与活体检测

    • 初始化 AFR_FSDKEngine(人脸识别)、AFT_FSDKEngine(人脸检测)与 ARL_FSDKEngine(活体)实例

    • 在子线程中:

      1. 调用人脸检测接口获取 faceInfo[]

      2. 调用关键点检测接口获取 landmarks

      3. 根据 LANDMARKS 跟踪眨眼/张嘴:动态活体

      4. 调用纹理活体接口分析帧灰度分布

    • 合并结果,判断“活体通过”

  6. 流程控制

    • 应用启动后进入“活体检测”模式

    • 用户按提示完成动作(如眨眼两次),检测通过后回调主线程提示成功

    • 超时或多次失败后提示“检测失败,请重试”

四、完整代码

// ==============================================
// 文件: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 结束 ===========================
*/

五、方法解读

  1. 引擎初始化

    • FaceEngine.init() 按需开启人脸检测、特征、活体模块;

    • 本例使用纹理活体(ASF_LIVENESS)与动作活体检测(眨眼、张嘴)自动结合;

  2. Camera2 预览流

    • 打开后置摄像头,将预览输出到 TextureView

    • 使用 ImageReader 获取 YUV Image,转换为 NV21 byte[];

    • 通过 HandlerThread 串行处理每帧,避免并发冲突;

  3. 活体检测流程

    • detectFaces(...) 得到 FaceInfo[]

    • detectLiveness(...) 得到对应 LivenessInfo[],其中 isLive() 表示活体概率

    • 可根据实际需求调整:仅当连续 N 帧活体通过后才最终判定;

  4. UI 绘制

    • 自定义 OverlayViewonDraw() 中绘制人脸框和“LIVE/FAKE”文字,实时反馈;

    • 手势提示、进度条、文字提示可进一步增强体验;

  5. 生命周期与资源释放

    • onDestroy() 中关闭 Camera2 会话和 HandlerThread,并 unInit() FaceEngine;

    • 必须清理本地 .so 占用的资源,避免崩溃;


六、项目总结

  • 优势

    1. 完全离线:无需网络,适合高安全场景;

    2. 实时高 FPS:本地 C/C++ 实现加速,保证流畅;

    3. 模块化:人脸检测、特征、活体分离,可灵活升级;

  • 注意事项

    • 授权:ArcSoft SDK 需在官网申请 AppId/SDKKey;

    • 性能调优:必要时降低人脸检测频率(如每 200 ms);

    • 多摄像头支持:可扩展前置/后置切换;

  • 扩展方向

    1. 深度活体:结合结构光或双目摄像头实现更强抗攻击;

    2. 动作引导:随机引导用户执行不同动作,防止机密视频播放攻击;

    3. 人脸比对:检测通过后与本地/远程库比对身份;

    4. 语音提示:结合 TextToSpeech 语音引导用户

    5. Jetpack Compose:在 Compose 中将 TextureView 包装为 AndroidView 实现同等效果。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值