在使用虹软人脸识别Android SDK的过程中 ,预览时一般都需要绘制人脸框,但是和PC平台相机应用不同,在Android平台相机进行应用开发还需要考虑前后置相机切换、设备横竖屏切换等情况,因此在人脸识别项目开发过程中,人脸框绘制适配的实现比较困难。针对该问题,本文将通过以下内容介绍解决方法:相机原始帧数据和预览成像画面的关系

人脸框绘制到View上的流程

具体场景适配方案介绍

处理多种场景的情况,实现适配函数

将适配好的人脸框绘制到View上

以下用到的Rect说明:

一、相机原始帧数据和预览成像画面的关系

Android设备一般为手持设备,相机集成在设备上,设备的旋转也会导致相机的旋转,因此成像也会发生旋转,为了解决这一问题,让用户能够看到正常的成像,Android提供了相机预览数据绘制到控件时,设置旋转角度的相关API,开发者可根据Activity的显示方向设置不同的旋转角度,这块内容在以下文章中有介绍:

Android使用Camera2获取预览数据

将预览的YUV数据转换为NV21,再转换为Bitmap并显示到控件上,同时也将该Bitmap转换为相机预览效果的Bitmap显示到控件上,便于了解原始数据和预览画面的关系成像关系

二、人脸框绘制到View上的流程

总体流程总体流程

· 第一步,缩放缩放

· 第二步,旋转

需要根据图像数据和预览画面的旋转角度关系,选择对应的旋转方案

后置摄像头(预览不镜像)

后置摄像头,旋转0度后置摄像头

后置摄像头,旋转90度后置摄像头

后置摄像头,旋转180度后置摄像头

后置摄像头,旋转270度后置摄像头

前置摄像头(预览会镜像)

前置摄像头,旋转0度前置摄像头

前置摄像头,旋转90度前置摄像头

前置摄像头,旋转180度前置摄像头

前置摄像头,旋转270度前置摄像头

三、具体场景下的适配方案介绍

以如下场景为例,介绍人脸框适配方案:

可以看到,在竖屏情况下,原始数据顺时针旋转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 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 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可在[虹软人脸识别开放平台]下载

android 人脸识别边框_【技术分享】虹软人脸识别 - Android Camera实时人脸追踪画框适配...相关推荐

  1. android 人脸识别边框_人脸框抠图如何实现

    最近在尝试做一个人脸识别项目,在对比几款主流人脸识别SDK后,采用了虹软的Arcface SDK,因为它提供了免费版本,并且可以离线使用,接入难度也比较低.项目中有一个需求就是显示检测到的人脸,但是如 ...

  2. android opencv 识别文字_基于SpringBoot的车牌识别系统(附项目地址)

    gitee开源地址 https://gitee.com/admin_yu/yx-image-recognition 介绍 spring boot + maven 实现的车牌识别及训练系统 基于java ...

  3. 基于python,虹软sdk3.0实现的实时人脸识别

    前言: 虹软sdk3.0是目前用过的最方便,效果最好的且免费的离线人脸识别SDK. 提供的编程语音没有python,有大佬用c++代码接口转成python调用的, 我在此基础上完善了一些功能,能够实现 ...

  4. python个人博客搭建说明书_技术分享|利用Python Django一步步搭建个人博客(二)...

    原标题:技术分享|利用Python Django一步步搭建个人博客(二) Hello,欢迎来到我们的"利用Python Django一步步搭建个人博客"系列的第二部分.在第一部分中 ...

  5. 对称加密算法_技术分享丨这是一篇简单的小科普——什么是对称加密算法?(下)...

    大家好~我是贾正经,又到了干货满满的技术分享趴啦~ 上期我们讲解了对称加密算法的小知识,并介绍了国密算法中SM4算法的原理.(上集回顾) 本期带大家了解一下分组密码的五个模式. 分组密码的模式 首先了 ...

  6. mysql优化说出九条_技术分享 | MySQL 优化:为什么 SQL 走索引还那么慢?

    原标题:技术分享 | MySQL 优化:为什么 SQL 走索引还那么慢? 背景 2019-01-11 9:00-10:00 一个 MySQL 数据库把 CPU 打满了. 硬件配置:256G 内存,48 ...

  7. 干涉测量技术的应用_技术分享 | 石化行业测量仪表应用在线答疑

    众所周知,在化工和石化这类流程行业当中,稳定性和持续性是至关重要的生产"命脉",对于生产过程中使用的测量仪表有着极其严格的要求. 不仅所有测量仪表都必须满足严苛的国际标准,如PED ...

  8. 大表与大表join数据倾斜_技术分享|大数据技术初探之Spark数据倾斜调优

    侯亚南 数据技术处 支宸啸 数据技术处 在大数据计算中,我们可能会遇到一个很棘手的问题--数据倾斜,此时spark任务的性能会比预期要差很多:绝大多数task都很快执行完成,但个别task执行极慢或者 ...

  9. java生成sm4算法的对称密钥_技术分享丨这是一篇简单的小科普——什么是对称加密算法?(下)...

    原标题:技术分享丨这是一篇简单的小科普--什么是对称加密算法?(下) 大家好~我是贾正经,又到了干货满满的技术分享趴啦~ 上期我们讲解了对称加密算法的小知识,并介绍了国密算法中SM4算法的原理. 本期 ...

最新文章

  1. confluence中org.apache.tomcat.util.net.NioEndpoint$Acceptor.run Socket accept failed的解决方法
  2. 从堆里找回“丢失”的代码
  3. java static method_java 中static的几种用法
  4. 用户不在sudoers文件中,需要使用命令 sudo npm install 的解决方法
  5. Vue源码学习(三)——数据双向绑定
  6. Centos6.7安装Apache2.4+Mysql5.6+Apache2.4
  7. 推挽与开漏输出详解(转)
  8. 计算机主板cpu的电源接口类型,给力:主板CPU电源的4pin和8pin有什么区别?
  9. Android Framework 音频子系统(02)音频系统框架
  10. Linaro ABE(高级构建环境)构建GNU交叉工具链
  11. 超人能一拳把某个人打出地球吗?
  12. linux使用入门debian,Debian 7.7入门安装与配置
  13. 自开发数据可视化平台
  14. GCN - Semi-Supervised Classification with Graph Convolutional Networks 用图卷积进行半监督节点分类 ICLR 2017
  15. ContraD论文部分翻译与解读(Training GANs with Stronger Augmentations via Contrastive Discriminator)
  16. Java:DateUtils 获取 本上下(周/月)周一周日 最后一天 当月多少天
  17. FME2019试用过程
  18. 智哪儿观察:谁在建博会拿奖拿到手软?凯迪仕
  19. C#底层库--随机数生成器
  20. 关于 nl80211

热门文章

  1. MacX DVD Ripper Pro for Mac(DVD格式转换工具)
  2. Cadence OrCAD原理图中快速查找元件的方法
  3. Jenkins部署发布(基于svn、ant)
  4. python课程教学大纲-Python数据分析课程教学大纲
  5. 【研究生开学季】让你幸福感砰砰砰的宿舍神器
  6. OSChina 周六乱弹 ——胸会压到键盘
  7. Oracle中的instr()函数 详解及应用
  8. 银河麒麟aarch64 编译安装Qt5.9.9
  9. DbHelper-SQL数据库访问助手
  10. Mac OS升级出现报错信息:将安装器信息下载到目标宗卷失败