27、人脸识别与活体检测:动作活体检测原理、光线活体检测、Android端实现方案

活体检测,说白了就是判断镜头前的是真人还是照片、视频或者面具。我刚开始做人脸识别那会儿,觉得算法能认出人脸就挺牛了。结果客户反馈说,拿张照片就能解锁手机——这脸打得啪啪响。

嗯,从那以后我就明白了:没有人脸活体检测的人脸识别,就是个摆设

今天咱们聊聊两种主流的活体检测方案:动作活体光线活体。以及它们在Android端怎么落地。

一、动作活体检测原理

动作活体,就是让用户做几个指定动作——比如眨眼、张嘴、摇头、点头。系统通过分析这些动作,判断是不是真人。

为什么动作能防攻击?你想想看:

  • 照片:不会眨眼,不会张嘴
  • 视频:虽然能播放动作,但很难精确响应随机指令
  • 面具:面部肌肉运动不自然,边缘会有破绽

我在项目中遇到过一种攻击方式:攻击者用手机录了一段用户眨眼的视频,然后在镜头前播放。如果我们的动作指令是固定的(比如总是先眨眼再张嘴),那视频就能蒙混过关。

核心要点:动作指令必须随机生成,且有时间限制。比如随机要求「先张嘴再摇头」,而不是固定顺序。

二、动作活体的技术实现

动作活体检测通常分三步走:

  1. 人脸检测与关键点定位:找到眼睛、嘴巴、鼻子等68个关键点
  2. 动作状态判断:根据关键点位置变化判断动作是否完成
  3. 时序验证:检查动作是否在合理时间内完成,是否有异常帧

举个例子,检测「眨眼」:

  • 计算眼睛纵横比(EAR,Eye Aspect Ratio)
  • 当EAR从正常值(约0.25)降到0.15以下,再回到正常值,算一次眨眼
  • 整个过程必须在0.2~0.5秒内完成

我个人习惯:在EAR计算时,取左右眼的平均值。因为有些人习惯单眼眨眼,单眼数据容易误判。

三、光线活体检测原理

光线活体,是另一种思路。它不依赖用户配合做动作,而是通过分析人脸在不同光照下的反射特性来判断真伪。

原理其实很简单:

  • 真人皮肤:有微弱的透光性,光线照射时会有皮下散射
  • 照片/屏幕:表面光滑,反射强烈,没有散射效果
  • 面具:材质均匀,反射模式与真人不同

我记得有一次测试,一个高仿真的硅胶面具在动作活体下通过了检测——因为它能模拟眨眼和张嘴。但光线活体一照,立马现原形。因为硅胶的透光率和真人皮肤完全不一样。

注意:光线活体需要额外的光源硬件(比如红外补光灯或屏幕闪光),不是所有手机都支持。如果你的App要兼容低端机,建议以动作活体为主,光线活体作为增强方案。

四、Android端实现方案

在Android上实现活体检测,我建议分三个层级:

层级 方案 适用场景 精度
基础 CameraX + ML Kit 快速集成,不需要太高安全性 中等
进阶 Camera2 + 自研算法 需要定制动作逻辑 较高
专业 厂商SDK(如商汤、旷视) 金融、支付等高安全场景

方案一:CameraX + ML Kit(快速集成)

如果你赶时间,这个方案最省事。Google官方提供了现成的活体检测API。

// 初始化活体检测器
val detector = FaceDetection.getClient(
    FaceDetectorOptions.Builder()
        .setPerformanceMode(FaceDetectorOptions.PERFORMANCE_MODE_FAST)
        .setLandmarkMode(FaceDetectorOptions.LANDMARK_MODE_ALL)
        .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL)
        .build()
)

// 在CameraX的ImageAnalysis中处理每一帧
val analyzer = ImageAnalysis.Analyzer { imageProxy ->
    val bitmap = imageProxy.toBitmap()
    val inputImage = InputImage.fromBitmap(bitmap, 0)
    
    detector.detect(inputImage)
        .addOnSuccessListener { faces ->
            for (face in faces) {
                // 检查左眼是否睁开
                val leftEyeOpen = face.leftEyeOpenProbability > 0.5
                // 检查右眼是否睁开
                val rightEyeOpen = face.rightEyeOpenProbability > 0.5
                // 检查是否在微笑
                val smiling = face.smilingProbability > 0.8
                
                // 根据这些状态判断动作是否完成
            }
        }
        .addOnCompleteListener { imageProxy.close() }
}

避坑指南:ML Kit的活体检测只提供了眼睛状态和微笑检测,没有摇头、点头等动作。如果你需要这些,得自己算头部姿态角(Pitch/Yaw/Roll)。

方案二:自研动作活体检测

如果你需要更灵活的动作控制,我建议自己写检测逻辑。核心是计算头部姿态角:

// 使用OpenCV的solvePnP计算头部姿态
fun calculateHeadPose(landmarks: Array<Point3f>): HeadPose {
    // 2D图像点(从人脸关键点获取)
    val imagePoints = arrayOf(
        Point2f(landmarks[30].x, landmarks[30].y), // 鼻尖
        Point2f(landmarks[8].x, landmarks[8].y),   // 下巴
        Point2f(landmarks[36].x, landmarks[36].y), // 左眼左角
        Point2f(landmarks[45].x, landmarks[45].y), // 右眼右角
        Point2f(landmarks[48].x, landmarks[48].y), // 左嘴角
        Point2f(landmarks[54].x, landmarks[54].y)  // 右嘴角
    )
    
    // 3D模型点(标准人脸模型)
    val modelPoints = arrayOf(
        Point3f(0.0f, 0.0f, 0.0f),      // 鼻尖
        Point3f(0.0f, -330.0f, -65.0f), // 下巴
        Point3f(-225.0f, 170.0f, -135.0f), // 左眼左角
        Point3f(225.0f, 170.0f, -135.0f),  // 右眼右角
        Point3f(-150.0f, -150.0f, -125.0f), // 左嘴角
        Point3f(150.0f, -150.0f, -125.0f)   // 右嘴角
    )
    
    // 调用solvePnP得到旋转向量
    val rvec = MatOfDouble()
    val tvec = MatOfDouble()
    Calib3d.solvePnP(
        MatOfPoint3f(*modelPoints),
        MatOfPoint2f(*imagePoints),
        cameraMatrix,
        distCoeffs,
        rvec,
        tvec
    )
    
    // 将旋转向量转为欧拉角
    val rotMat = Mat(3, 3, CvType.CV_64FC1)
    Calib3d.Rodrigues(rvec, rotMat)
    
    return extractEulerAngles(rotMat)
}

有了头部姿态角,判断动作就简单了:

  • 点头:Pitch角从0°变到-15°再回到0°
  • 摇头:Yaw角从0°变到±30°再回到0°
  • 歪头:Roll角从0°变到±20°再回到0°

方案三:光线活体检测实现

光线活体在Android上实现,通常有两种方式:

  1. 屏幕闪光法:让屏幕依次显示不同颜色的光,分析反射变化
  2. 红外光法:利用前置红外摄像头,分析红外反射特性

屏幕闪光法的核心代码逻辑:

// 定义闪光序列:红、绿、蓝、白
val flashColors = intArrayOf(
    Color.RED, Color.GREEN, Color.BLUE, Color.WHITE
)

// 采集每一帧的反射数据
fun captureReflectionData(
    image: Bitmap,
    faceRect: Rect
): FloatArray {
    val facePixels = IntArray(
        faceRect.width() * faceRect.height()
    )
    image.getPixels(
        facePixels,
        0,
        faceRect.width(),
        faceRect.left,
        faceRect.top,
        faceRect.width(),
        faceRect.height()
    )
    
    // 计算人脸区域的平均亮度
    var totalBrightness = 0f
    for (pixel in facePixels) {
        val r = Color.red(pixel)
        val g = Color.green(pixel)
        val b = Color.blue(pixel)
        totalBrightness += (r + g + b) / 3f
    }
    
    return floatArrayOf(
        totalBrightness / facePixels.size
    )
}

// 判断是否为真人:真人皮肤在不同光线下反射曲线平滑
// 照片/屏幕反射曲线有突变
fun isLiveByReflection(
    reflectionData: List<FloatArray>
): Boolean {
    // 计算反射曲线的平滑度
    var smoothness = 0f
    for (i in 1 until reflectionData.size) {
        val diff = abs(
            reflectionData[i][0] - reflectionData[i-1][0]
        )
        smoothness += diff
    }
    
    // 平滑度低于阈值,认为是真人
    return smoothness < REFLECTION_SMOOTHNESS_THRESHOLD
}

我曾经踩过一个坑:屏幕闪光法在强光环境下效果很差。因为环境光淹没了屏幕发出的光,反射数据全是噪声。后来我加了一个环境光传感器检测,如果环境光太强,就自动切换到动作活体模式。

五、综合方案对比与选型建议

说了这么多,到底选哪种方案?我根据项目经验给个建议:

场景 推荐方案 原因
App解锁、登录 动作活体(眨眼+微笑) 用户无感,速度快
支付、转账 动作活体(摇头+张嘴)+ 光线活体 双重验证,安全性高
门禁、考勤 光线活体(红外方案) 无需用户配合,适合批量场景
远程开户 动作活体(随机3个动作)+ 光线活体 金融级安全要求

最后说一句:没有绝对安全的活体检测。攻击技术也在不断进化。我见过用3D打印面具绕过动作活体的案例,也见过用高刷屏视频绕过光线活体的。所以,多模态融合才是王道——把动作、光线、甚至声音结合起来,让攻击者无从下手。

总结一句话:动作活体靠「动」,光线活体靠「光」。两者结合,基本能挡住99%的常见攻击。剩下的1%,交给持续迭代的算法和硬件升级。

人脸活体检测方案全景图 摄像头输入帧 动作活体检测 光线活体检测 人脸检测 + 68关键点定位 动作状态判断(EAR/头部姿态角) 时序验证 + 随机指令 屏幕闪光 / 红外光照射 反射数据采集与分析 反射曲线平滑度判断 活体检测结果

公众号:蓝海资料掘金营,微信deep3321