虹軟人臉識別 - Android Camera實時人臉追蹤畫框適配

在使用虹軟人臉識別Android SDK的過程當中 ,預覽時通常都須要繪製人臉框,可是和PC平臺相機應用不一樣,在Android平臺相機進行應用開發還須要考慮先後置相機切換、設備橫豎屏切換等狀況,所以在人臉識別項目開發過程當中,人臉框繪製適配的實現比較困難。針對該問題,本文將經過如下內容介紹解決方法:html

  • 相機原始幀數據和預覽成像畫面的關係
  • 人臉框繪製到View上的流程
  • 具體場景適配方案介紹
  • 處理多種場景的狀況,實現適配函數
  • 將適配好的人臉框繪製到View上

如下用到的Rect說明:canvas

變量名 含義
originalRect 人臉檢測回傳的人臉框
scaledRect 基於originalRect縮放後的人臉框
drawRect 最終繪製所需的人臉框

1、相機原始幀數據和預覽成像畫面的關係

Android設備通常爲手持設備,相機集成在設備上,設備的旋轉也會致使相機的旋轉,所以成像也會發生旋轉,爲了解決這一問題,讓用戶可以看到正常的成像,Android提供了相機預覽數據繪製到控件時,設置旋轉角度的相關API,開發者可根據Activity的顯示方向設置不一樣的旋轉角度,這塊內容在如下文章中有介紹
bash

將預覽的YUV數據轉換爲NV21,再轉換爲Bitmap並顯示到控件上,同時也將該Bitmap轉換爲相機預覽效果的Bitmap顯示到控件上,便於瞭解原始數據和預覽畫面的關係異步

                                  成像關係ide

2、人臉框繪製到View上的流程

整體流程
函數

  • 第一步,縮放


  • 第二步,旋轉

    須要根據圖像數據和預覽畫面的旋轉角度關係,選擇對應的旋轉方案post

  • 後置攝像頭(預覽不鏡像)

                                                      後置攝像頭,旋轉0度                                                       後置攝像頭,旋轉90度            優化

                                                       後置攝像頭,旋轉180度

                                                       後置攝像頭,旋轉270度

  • 前置攝像頭(預覽會鏡像)

                                                      前置攝像頭,旋轉0度ui

                                                      前置攝像頭,旋轉90度this

                                                       前置攝像頭,旋轉180度

                                                       前置攝像頭,旋轉270度


3、具體場景下的適配方案介紹

以以下場景爲例,介紹人臉框適配方案:

屏幕分辨率 相機預覽尺寸 相機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


4、處理多種場景的狀況,實現適配函數

經過以上分析,可得出畫框時須要用到的繪製參數以下,其中構造函數的最後兩個參數是額外添加的,用於特殊場景的手動矯正:

  • 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;
    }複製代碼

5、將適配好的人臉框繪製到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可在虹軟人臉識別開放平臺下載
         
相關文章
相關標籤/搜索