在使用虹软人脸识别Android SDK的过程当中 ,预览时通常都须要绘制人脸框,可是和PC平台相机应用不一样,在Android平台相机进行应用开发还须要考虑先后置相机切换、设备横竖屏切换等状况,所以在人脸识别项目开发过程当中,人脸框绘制适配的实现比较困难。针对该问题,本文将经过如下内容介绍解决方法:html
如下用到的Rect说明:canvas
变量名 | 含义 |
---|---|
originalRect | 人脸检测回传的人脸框 |
scaledRect | 基于originalRect缩放后的人脸框 |
drawRect | 最终绘制所需的人脸框 |
Android设备通常为手持设备,相机集成在设备上,设备的旋转也会致使相机的旋转,所以成像也会发生旋转,为了解决这一问题,让用户可以看到正常的成像,Android提供了相机预览数据绘制到控件时,设置旋转角度的相关API,开发者可根据Activity的显示方向设置不一样的旋转角度,这块内容在如下文章中有介绍
bash
将预览的YUV数据转换为NV21,再转换为Bitmap并显示到控件上,同时也将该Bitmap转换为相机预览效果的Bitmap显示到控件上,便于了解原始数据和预览画面的关系异步
成像关系ide
整体流程
函数
须要根据图像数据和预览画面的旋转角度关系,选择对应的旋转方案post
后置摄像头,旋转0度 后置摄像头,旋转90度
优化
前置摄像头,旋转0度ui
前置摄像头,旋转90度this
前置摄像头,旋转180度
前置摄像头,旋转270度
以以下场景为例,介绍人脸框适配方案:
屏幕分辨率 | 相机预览尺寸 | 相机ID | 屏幕朝向 | 原始数据 | 预览效果 |
---|---|---|---|---|---|
1080x1920 | 1280x720 | 后置相机 | 竖屏 |
原始数据
|
预览效果
|
第一步,缩放
第二步,旋转
第一步:缩放
假设人脸检测结果的位置信息是originalRect:(left, top, right, bottom)
(相对于1280x720的图像的位置),咱们将其放大为相对于1920x1080的图像的位置:
scaledRect:(originalRect.left * 1.5, originalRect.top * 1.5, originalRect.right * 1.5, originalRect.bottom * 1.5)
第二步:旋转
在尺寸修改完成后,咱们再将人脸框旋转便可获得目标人脸框,其中旋转的过程以下:
drawRect.left
scaledRect
的下边界到图像下边界的距离,也就是1080 - scaledRect.bottom
drawRect.top
scaledRect
的左边界到图像左边界的距离,也就是scaledRect.left
drawRect.right
scaledRect
的上边界到图像下边界的距离,也就是1080 - scaledRect.top
drawRect.bottom
scaledRect
的右边界到图像上边界的距离,也就是scaledRect.right
最终得出了旋转角度为90度时绘制所需的drawRect
经过以上分析,可得出画框时须要用到的绘制参数以下,其中构造函数的最后两个参数是额外添加的,用于特殊场景的手动矫正:
/**
* 建立一个绘制辅助类对象,而且设置绘制相关的参数
*
* @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;
}复制代码
/**
* 用于显示人脸信息的控件
*/
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);
}
}复制代码