之前实现了一个单人脸识别的功能,逻辑是一旦检测到人脸就进行特征提取、搜索、显示UI、状态重置。后来觉得这样的操作只适用于单人脸通行的场景。对于门禁打卡这种情况,如果一个一个来,在人多的情况下用户体验就不太好了。于是我就想设计一种多人脸识别的方案,最直接的想法是每一帧都做特征提取,和前一帧的特征进行比对判断画面中的人脸是否依然存在,但是仔细一想,这种设计是个大坑:
- 人脸特征提取一次要100-500ms,要是每帧都特征提取,估计要卡成ppt。
- 人脸模糊、部分遮挡等情况提取会出错,导致部分帧不能记录。
后来想到了另一种方案:
在人脸识别的过程中,由于相机预览画面的连续性,人脸的位置也是连续的,因此我们可以根据画面的连续性为每个人脸分配一个唯一标识符(trackID)
,那我们就可以针对性地对每个人脸进行操作。
一、人脸识别中上下帧相同人脸判断(人脸跟踪)的实现
1、什么是trackID
在人脸识别的过程中,每一个人脸在进入画面后,其运动都是连续的,因此我们可以通过人脸位置的变化情况大致判断上下帧的两个人脸框是否是同一个人,并为其添加一个唯一标识符,我们称它为trackID
。
2、trackID的作用
在人脸识别中,特征解析和活体判断是很耗时的操作,因此,若有了trackID
,我们可为每个人脸的特征解析和活体判断做指定尝试的次数以达到理想的识别效果。
3、如何判断上下帧的两个人脸框是否属于同一个人
一般情况下,我们可以通过上下帧的重合度来判断两个人脸框是否属于同一个人,那么重合度的依据是什么?这里提供一种思路:通过距离和大小比例判断。若当前帧和上一帧的所有人脸框中最接近的那个人脸框的中心距离小于某个值validDistance
(该值越小,对人脸框的接近程度要求越高)且两者边长比小于某个值validRatio
,则我们认为这两个框属于同一个人脸。由于人脸框基本都是接近正方形的矩形,我们可以将其当做正方形处理,使用其内切圆的关系便于说明。
首先列出以下可能情况
若我们将validDistance
的值设为较大人脸框内切圆的半径的一半,validRatio
的值设为2,则根据上图,我们可以判定:
- case 0, case 1, case 2, case 5, case 8不是同一个人
- case 3, case 4, case 7 是同一个人
- case 6 是临界情况,自行决定
例如以下情况:
红色框代表前一帧人脸框,绿色框代表当前帧人脸框。那么我们可以判断得:
- 前一帧的1号人脸框在当前帧消失
- 前一帧的2号人脸框和当前帧的4号人脸框对应
- 前一帧的3号人脸框和当前帧的5号人脸框对应
- 6号人脸框是当前帧出现的新的人脸框
代码实现:
//当前trackID
private int currentTrackId = 0;
//前一帧的trackID列表
private List<Integer> formerTrackIdList = new ArrayList<>();
//当前帧的trackID列表
private List<Integer> currentTrackIdList = new ArrayList<>();
//前一帧的人脸框列表
private List<Rect> formerFaceRectList = new ArrayList<>();
/**
* 刷新trackId
*
* @param ftFaceList 传入的人脸列表
*/
private void refreshTrackId(List<FaceInfo> ftFaceList) {
currentTrackIdList.clear();
//每项预先填充-1
for (int i = 0; i < ftFaceList.size(); i++) {
currentTrackIdList.add(-1);
}
//前一次无人脸现在有人脸,填充新增TrackId
if (formerTrackIdList.size() == 0) {
for (int i = 0; i < ftFaceList.size(); i++) {
currentTrackIdList.set(i, ++currentTrackId);
}
} else {
//前后都有人脸,对于每一个人脸框
for (int i = 0; i < ftFaceList.size(); i++) {
//遍历上一次人脸框
int minDistance = Integer.MAX_VALUE;
int minDistanceIndex = -1;
for (int j = 0; j < formerFaceRectList.size(); j++) {
//获取最近的人脸框距离以及人脸框下标
int distance = TrackUtil.getDistance(formerFaceRectList.get(j), ftFaceList.get(i).getRect());
if (distance < minDistance) {
minDistance = distance;
minDistanceIndex = j;
}
}
//若这两个Rect距离小于两者最大人脸框宽度的1/4,认为是同一个人脸
if (minDistanceIndex != -1 && minDistance < (Math.max(ftFaceList.get(i).getRect().width(), formerFaceRectList.get(minDistanceIndex).width()) >> 2)) {
currentTrackIdList.set(i, formerTrackIdList.get(minDistanceIndex));
}
}
}
//上一次人脸框不存在此人脸,增加trackID并分配
for (int i = 0; i < currentTrackIdList.size(); i++) {
if (currentTrackIdList.get(i) == -1) {
currentTrackIdList.set(i, ++currentTrackId);
}
}
formerTrackIdList.clear();
formerFaceRectList.clear();
for (int i = 0; i < ftFaceList.size(); i++) {
formerFaceRectList.add(new Rect(ftFaceList.get(i).getRect()));
formerTrackIdList.add(currentTrackIdList.get(i));
}
}
public static int getDistance(Rect rect1, Rect rect2) {
int r1Cx = rect1.centerX();
int r1Cy = rect1.centerY();
int r2Cx = rect2.centerX();
int r2Cy = rect2.centerY();
return (int) Math.sqrt(Math.pow(r1Cy - r2Cy, 2) + Math.pow(r1Cx - r2Cx, 2));
}
若有更佳方案,希望能评论区留言
二、人脸识别过程优化
1. 人脸检测
人脸检测是人脸识别的基础。可以通过控制环境光线、提升相机硬件、使用WDR技术
等方法来提升画面质量以提升人脸检测的速度,以及设置人脸检测角度为单方向
的方式以提升人脸检测速度。下图是使用WDR技术前后的图像效果比对。
2. 活体检测
活体检测是一种安全性验证,其结果并不影响人脸特征提取,因此可和人脸特征提取并行。该项相对于人脸检测是异步操作。对于n:m识别的场景,需要多人脸活体识别,因此可以使用trackID配合图片模式的活体检测方式实现异i步活体检测。
3. 特征提取
在特征提取后才可进行人脸搜索。该项可与活体检测并行,若特征提取成功后活体检测结果为未知,则等待活体检测结果,否则可根据活体检测结果做对应操作。该项相对于人脸检测是异步操作。以下是活体检测和特征提取串行执行和并行执行的流程图(...
表示其它流程,和本步骤的说明关联不大)
4. 人脸搜索
人脸搜索建议在服务器端完成,在1000人以下耗时极短,若人脸库过大,可通过多引擎多线程比对。
单线程与多线程的搜索方式比对
5.活体检测模式的选择
在1:n的场景下,可以使用视频流模式的活体检测。 在n:m的场景下,可以通过异步图片模式活体检测。
6.重试机制
大角度、模糊人脸等因素对活体检测和特征提取的影响较大,可通过添加重试机制来确保活体和特征提取顺利进行。以下以活体检测为例介绍重试机制流程(...
表示其它流程,和本步骤的说明关联不大)。
7.使用引擎池和线程池
对于多人脸情况下的耗时操作,我们先将其做为一个模块思考,例如在做异步特征提取时,可以通过特征解析引擎池
和特征解析线程池
实现并发异步特征解析,在画面中有多个人脸的情况下,使用该方案可以大幅提升画面中有多个人脸的情况下的识别速度,但是对设备性能要求较高。(异步图片模式活体检测方案同理)
注意:
若CPU核心数较少,不建议开过多线程,因为核心数不足的情况下,并发过多的线程还是等待CPU调度,对速度的提升没有任何帮助。
网友评论