在使用虹軟人臉識別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可在虹軟人臉識別開放平台下載