zoukankan      html  css  js  c++  java
  • 虹软人脸识别

    在使用虹软人脸识别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)

    第二步:旋转
    在尺寸修改完成后,我们再将人脸框旋转即可得到目标人脸框,其中旋转的过程如下:

      1. 获取原始数据和预览画面的旋转角度(以上情况为90度)
      2. 根据旋转角度将人脸框调整为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可在虹软人脸识别开放平台下载


  • 相关阅读:
    [译]GLUT教程
    [译]GLUT教程
    [译]GLUT教程
    [译]GLUT教程
    [译]GLUT教程
    [译]GLUT教程
    [译]GLUT教程
    表单
    列表、表格与媒体元素
    HTML5基础
  • 原文地址:https://www.cnblogs.com/feishixin123/p/11719237.html
Copyright © 2011-2022 走看看