虹软人脸识别 - Android Camera实时人脸追踪画框适配
在使用虹软人脸识别Android SDK的过程中 ,预览时一般都需要绘制人脸框,但是和PC平台相机应用不同,在Android平台相机进行应用开发还需要考虑前后置相机切换、设备横竖屏切换等情况,因此在人脸识别项目开发过程中,人脸框绘制适配的实现比较困难。针对该问题,本文将通过以下内容介绍解决方法:
- 相机原始帧数据和预览成像画面的关系
- 人脸框绘制到View上的流程
- 具体场景适配方案介绍
- 处理多种场景的情况,实现适配函数
- 将适配好的人脸框绘制到View上
以下用到的Rect说明:
变量名 | 含义 |
---|---|
originalRect | 人脸检测回传的人脸框 |
scaledRect | 基于originalRect缩放后的人脸框 |
drawRect | 最终绘制所需的人脸框 |
一、相机原始帧数据和预览成像画面的关系
Android设备一般为手持设备,相机集成在设备上,设备的旋转也会导致相机的旋转,因此成像也会发生旋转,为了解决这一问题,让用户能够看到正常的成像,Android提供了相机预览数据绘制到控件时,设置旋转角度的相关API,开发者可根据Activity的显示方向设置不同的旋转角度,这块内容在以下文章中有介绍:
- Android使用Camera2获取预览数据
将预览的YUV数据转换为NV21,再转换为Bitmap并显示到控件上,同时也将该Bitmap转换为相机预览效果的Bitmap显示到控件上,便于了解原始数据和预览画面的关系
二、人脸框绘制到View上的流程
总体流程
第一步,缩放
第二步,旋转
需要根据图像数据和预览画面的旋转角度关系,选择对应的旋转方案
-
后置摄像头(预览不镜像)
后置摄像头,旋转0度
后置摄像头,旋转90度
后置摄像头,旋转180度
后置摄像头,旋转270度
-
前置摄像头(预览会镜像)
前置摄像头,旋转0度
前置摄像头,旋转90度
前置摄像头,旋转180度
前置摄像头,旋转270度
三、具体场景下的适配方案介绍
以如下场景为例,介绍人脸框适配方案:
屏幕分辨率 | 相机预览尺寸 | 相机ID | 屏幕朝向 | 原始数据 | 预览效果 |
---|---|---|---|---|---|
1080x1920 | 1280x720 | 后置相机 | 竖屏 |
|
|
可以看到,在竖屏情况下,原始数据顺时针旋转90度并缩放才能达到预览画面的效果,既然图像数据旋转并缩放了,那人脸框也要随着图像旋转并缩放。我们可以先旋转再缩放,也可以先缩放在旋转,这里以先缩放再旋转为例介绍适配的步骤。
第一步,缩放
第二步,旋转
第一步:缩放
假设人脸检测结果的位置信息是originalRect:(left, top, right, bottom)
(相对于1280x720的图像的位置),我们将其放大为相对于1920x1080的图像的位置:
scaledRect:(originalRect.left * 1.5, originalRect.top * 1.5, originalRect.right * 1.5, originalRect.bottom * 1.5)
第二步:旋转
在尺寸修改完成后,我们再将人脸框旋转即可得到目标人脸框,其中旋转的过程如下:
- 获取原始数据和预览画面的旋转角度(以上情况为90度)
- 根据旋转角度将人脸框调整为View需要的人脸框,对于绘制所需的人脸框,我们分析下计算方式:
drawRect.left
绘制所需的Rect的left的值也就是scaledRect
的下边界到图像下边界的距离,也就是1080 - scaledRect.bottom
drawRect.top
绘制所需的Rect的top的值也就是scaledRect
的左边界到图像左边界的距离,也就是scaledRect.left
drawRect.right
绘制所需的Rect的right的值也就是scaledRect
的上边界到图像下边界的距离,也就是1080 - scaledRect.top
drawRect.bottom
绘制所需的Rect的bottom的值也就是scaledRect
的右边界到图像上边界的距离,也就是scaledRect.right
最终得出了旋转角度为90度时绘制所需的drawRect
四、处理多种场景的情况,实现适配函数
通过以上分析,可得出画框时需要用到的绘制参数如下,其中构造函数的最后两个参数是额外添加的,用于特殊场景的手动矫正:
- previewWidth & previewHeight
预览宽高,人脸追踪的人脸框是基于这个尺寸的 -
canvasWidth & canvasHeight
被绘制的控件的宽高,也就是映射后的目标尺寸 -
cameraDisplayOrientation
预览数据和源数据的旋转角度 -
cameraId
相机ID,系统对于前置相机是有做默认镜像处理的,而后置相机则没有 -
isMirror
预览画面是否水平镜像显示,例如我们如果手动设置了再次镜像预览画面,则需要将最终结果也镜像处理 -
mirrorHorizontal
为兼容部分设备使用,将调整后的框水平再次镜像 -
mirrorVertical
为兼容部分设备使用,将调整后的框垂直再次镜像
/** * 创建一个绘制辅助类对象,并且设置绘制相关的参数 * * @param previewWidth 预览宽度 * @param previewHeight 预览高度 * @param canvasWidth 绘制控件的宽度 * @param canvasHeight 绘制控件的高度 * @param cameraDisplayOrientation 旋转角度 * @param cameraId 相机ID * @param isMirror 是否水平镜像显示(若相机是手动镜像显示的,设为true,用于纠正) * @param mirrorHorizontal 为兼容部分设备使用,水平再次镜像 * @param mirrorVertical 为兼容部分设备使用,垂直再次镜像 */ public DrawHelper(int previewWidth, int previewHeight, int canvasWidth, int canvasHeight, int cameraDisplayOrientation, int cameraId, boolean isMirror, boolean mirrorHorizontal, boolean mirrorVertical) { this.previewWidth = previewWidth; this.previewHeight = previewHeight; this.canvasWidth = canvasWidth; this.canvasHeight = canvasHeight; this.cameraDisplayOrientation = cameraDisplayOrientation; this.cameraId = cameraId; this.isMirror = isMirror; this.mirrorHorizontal = mirrorHorizontal; this.mirrorVertical = mirrorVertical; }
人脸框映射的具体实现
/** * 调整人脸框用来绘制 * * @param ftRect FT人脸框 * @return 调整后的需要被绘制到View上的rect */ public Rect adjustRect(Rect ftRect) { // 预览宽高 int previewWidth = this.previewWidth; int previewHeight = this.previewHeight; // 画布的宽高,也就是View的宽高 int canvasWidth = this.canvasWidth; int canvasHeight = this.canvasHeight; // 相机预览显示旋转角度 int cameraDisplayOrientation = this.cameraDisplayOrientation; // 相机Id,前置相机在显示时会默认镜像 int cameraId = this.cameraId; // 是否预览镜像 boolean isMirror = this.isMirror; // 针对于一些特殊场景做额外的人脸框镜像操作, // 比如cameraId为CAMERA_FACING_FRONT的相机打开后没镜像、 // 或cameraId为CAMERA_FACING_BACK的相机打开后镜像 boolean mirrorHorizontal = this.mirrorHorizontal; boolean mirrorVertical = this.mirrorVertical; if (ftRect == null) { return null; } Rect rect = new Rect(ftRect); float horizontalRatio; float verticalRatio; // cameraDisplayOrientation 为0或180,也就是landscape或reverse-landscape时 // 或 // cameraDisplayOrientation 为90或270,也就是portrait或reverse-portrait时 // 分别计算水平缩放比和垂直缩放比 if (cameraDisplayOrientation % 180 == 0) { horizontalRatio = (float) canvasWidth / (float) previewWidth; verticalRatio = (float) canvasHeight / (float) previewHeight; } else { horizontalRatio = (float) canvasHeight / (float) previewWidth; verticalRatio = (float) canvasWidth / (float) previewHeight; } rect.left *= horizontalRatio; rect.right *= horizontalRatio; rect.top *= verticalRatio; rect.bottom *= verticalRatio; Rect newRect = new Rect(); // 关键部分,根据旋转角度以及相机ID对人脸框进行旋转和镜像处理 switch (cameraDisplayOrientation) { case 0: if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { newRect.left = canvasWidth - rect.right; newRect.right = canvasWidth - rect.left; } else { newRect.left = rect.left; newRect.right = rect.right; } newRect.top = rect.top; newRect.bottom = rect.bottom; break; case 90: newRect.right = canvasWidth - rect.top; newRect.left = canvasWidth - rect.bottom; if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { newRect.top = canvasHeight - rect.right; newRect.bottom = canvasHeight - rect.left; } else { newRect.top = rect.left; newRect.bottom = rect.right; } break; case 180: newRect.top = canvasHeight - rect.bottom; newRect.bottom = canvasHeight - rect.top; if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { newRect.left = rect.left; newRect.right = rect.right; } else { newRect.left = canvasWidth - rect.right; newRect.right = canvasWidth - rect.left; } break; case 270: newRect.left = rect.top; newRect.right = rect.bottom; if (cameraId == Camera.CameraInfo.CAMERA_FACING_FRONT) { newRect.top = rect.left; newRect.bottom = rect.right; } else { newRect.top = canvasHeight - rect.right; newRect.bottom = canvasHeight - rect.left; } break; default: break; } /** * isMirror mirrorHorizontal finalIsMirrorHorizontal * true true false * false false false * true false true * false true true * * XOR */ if (isMirror ^ mirrorHorizontal) { int left = newRect.left; int right = newRect.right; newRect.left = canvasWidth - right; newRect.right = canvasWidth - left; } if (mirrorVertical) { int top = newRect.top; int bottom = newRect.bottom; newRect.top = canvasHeight - bottom; newRect.bottom = canvasHeight - top; } return newRect; }
五、将适配好的人脸框绘制到View上
实现一个自定义View
/** * 用于显示人脸信息的控件 */ public class FaceRectView extends View { private static final String TAG = "FaceRectView"; private CopyOnWriteArrayList<DrawInfo> drawInfoList = new CopyOnWriteArrayList<>(); private Paint paint; public FaceRectView(Context context) { this(context, null); } public FaceRectView(Context context, @Nullable AttributeSet attrs) { super(context, attrs); paint = new Paint(); } // 主要的绘制操作 @Override protected void onDraw(Canvas canvas) { super.onDraw(canvas); if (drawInfoList != null && drawInfoList.size() > 0) { for (int i = 0; i < drawInfoList.size(); i++) { DrawHelper.drawFaceRect(canvas, drawInfoList.get(i), 4, paint); } } } // 清空画面中的人脸 public void clearFaceInfo() { drawInfoList.clear(); postInvalidate(); } public void addFaceInfo(DrawInfo faceInfo) { drawInfoList.add(faceInfo); postInvalidate(); } public void addFaceInfo(List<DrawInfo> faceInfoList) { drawInfoList.addAll(faceInfoList); postInvalidate(); } }
绘制的具体操作,画人脸框
/** * 绘制数据信息到view上,若 {@link DrawInfo#getName()} 不为null则绘制 {@link DrawInfo#getName()} * * @param canvas 需要被绘制的view的canvas * @param drawInfo 绘制信息 * @param faceRectThickness 人脸框厚度 * @param paint 画笔 */ public static void drawFaceRect(Canvas canvas, DrawInfo drawInfo, int faceRectThickness, Paint paint) { if (canvas == null || drawInfo == null) { return; } paint.setStyle(Paint.Style.STROKE); paint.setStrokeWidth(faceRectThickness); paint.setColor(drawInfo.getColor()); paint.setAntiAlias(true); Path mPath = new Path(); //左上 Rect rect = drawInfo.getRect(); mPath.moveTo(rect.left, rect.top + rect.height() / 4); mPath.lineTo(rect.left, rect.top); mPath.lineTo(rect.left + rect.width() / 4, rect.top); //右上 mPath.moveTo(rect.right - rect.width() / 4, rect.top); mPath.lineTo(rect.right, rect.top); mPath.lineTo(rect.right, rect.top + rect.height() / 4); //右下 mPath.moveTo(rect.right, rect.bottom - rect.height() / 4); mPath.lineTo(rect.right, rect.bottom); mPath.lineTo(rect.right - rect.width() / 4, rect.bottom); //左下 mPath.moveTo(rect.left + rect.width() / 4, rect.bottom); mPath.lineTo(rect.left, rect.bottom); mPath.lineTo(rect.left, rect.bottom - rect.height() / 4); canvas.drawPath(mPath, paint); // 其中需要注意的是,canvas.drawText函数传入的位置,x是水平方向的起点, // 而 y是 BaseLine,文字会在 BaseLine的上方绘制 if (drawInfo.getName() == null) { paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setTextSize(rect.width() / 8); String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN")) + "," + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNWON" : drawInfo.getAge()) + "," + (drawInfo.getLiveness() == LivenessInfo.ALIVE ? "ALIVE" : (drawInfo.getLiveness() == LivenessInfo.NOT_ALIVE ? "NOT_ALIVE" : "UNKNOWN")); canvas.drawText(str, rect.left, rect.top - 10, paint); } else { paint.setStyle(Paint.Style.FILL_AND_STROKE); paint.setTextSize(rect.width() / 8); canvas.drawText(drawInfo.getName(), rect.left, rect.top - 10, paint); } }
本来自己研究了较长时间,后来发现虹软人脸识别Android Demo中早已给出该适配方案,上述代码也源于官方Demo,通过研读Demo,发现其中还提供了很多其他在接入虹软人脸识别SDK时可能用到的优化策略,如:
1. 通过异步人脸特征提取实现多人脸识别
2. 使用faceId优化识别逻辑
3. 识别时的画框适配方案
4. 打开双摄进行红外活体检测
Android Demo可在虹软人脸识别开放平台下载