27、人脸识别与活体检测:动作活体检测原理、光线活体检测、Android端实现方案
活体检测,说白了就是判断镜头前的是真人还是照片、视频或者面具。我刚开始做人脸识别那会儿,觉得算法能认出人脸就挺牛了。结果客户反馈说,拿张照片就能解锁手机——这脸打得啪啪响。
嗯,从那以后我就明白了:没有人脸活体检测的人脸识别,就是个摆设。
今天咱们聊聊两种主流的活体检测方案:动作活体和光线活体。以及它们在Android端怎么落地。
一、动作活体检测原理
动作活体,就是让用户做几个指定动作——比如眨眼、张嘴、摇头、点头。系统通过分析这些动作,判断是不是真人。
为什么动作能防攻击?你想想看:
- 照片:不会眨眼,不会张嘴
- 视频:虽然能播放动作,但很难精确响应随机指令
- 面具:面部肌肉运动不自然,边缘会有破绽
我在项目中遇到过一种攻击方式:攻击者用手机录了一段用户眨眼的视频,然后在镜头前播放。如果我们的动作指令是固定的(比如总是先眨眼再张嘴),那视频就能蒙混过关。
核心要点:动作指令必须随机生成,且有时间限制。比如随机要求「先张嘴再摇头」,而不是固定顺序。
二、动作活体的技术实现
动作活体检测通常分三步走:
- 人脸检测与关键点定位:找到眼睛、嘴巴、鼻子等68个关键点
- 动作状态判断:根据关键点位置变化判断动作是否完成
- 时序验证:检查动作是否在合理时间内完成,是否有异常帧
举个例子,检测「眨眼」:
- 计算眼睛纵横比(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上实现,通常有两种方式:
- 屏幕闪光法:让屏幕依次显示不同颜色的光,分析反射变化
- 红外光法:利用前置红外摄像头,分析红外反射特性
屏幕闪光法的核心代码逻辑:
// 定义闪光序列:红、绿、蓝、白
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%,交给持续迭代的算法和硬件升级。
公众号:蓝海资料掘金营,微信deep3321