在使用虹軟人臉識別Android SDK的過程當中 ,預覽時通常都須要繪製人臉框,可是和PC平臺相機應用不一樣,在Android平臺相機進行應用開發還須要考慮先後置相機切換、設備橫豎屏切換等狀況,所以在人臉識別項目開發過程當中,人臉框繪製適配的實現比較困難。針對該問題,本文將經過如下內容介紹解決方法:html
如下用到的Rect說明:canvas
變量名 | 含義 |
originalRect | 人臉檢測回傳的人臉框 |
scaledRect | 基於originalRect縮放後的人臉框 |
drawRect | 最終繪製所需的人臉框 |
Android設備通常爲手持設備,相機集成在設備上,設備的旋轉也會致使相機的旋轉,所以成像也會發生旋轉,爲了解決這一問題,讓用戶可以看到正常的成像,Android提供了相機預覽數據繪製到控件時,設置旋轉角度的相關API,開發者可根據Activity的顯示方向設置不一樣的旋轉角度,這塊內容在如下文章中有介紹:異步
整體流程ide
須要根據圖像數據和預覽畫面的旋轉角度關係,選擇對應的旋轉方案 函數
後置攝像頭(預覽不鏡像)post
後置攝像頭,旋轉0度優化
後置攝像頭,旋轉90度this
後置攝像頭,旋轉180度spa
後置攝像頭,旋轉270度3d
前置攝像頭(預覽會鏡像)
前置攝像頭,旋轉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)
第二步:旋轉
在尺寸修改完成後,咱們再將人臉框旋轉便可獲得目標人臉框,其中旋轉的過程以下:
drawRect.left
scaledRect
的下邊界到圖像下邊界的距離,也就是1080 - scaledRect.bottom
drawRect.top
scaledRect
的左邊界到圖像左邊界的距離,也就是scaledRect.left
drawRect.right
scaledRect
的上邊界到圖像下邊界的距離,也就是1080 - scaledRect.top
drawRect.bottom
scaledRect
的右邊界到圖像上邊界的距離,也就是scaledRect.right
最終得出了旋轉角度爲90度時繪製所需的drawRect
4、處理多種場景的狀況,實現適配函數
經過以上分析,可得出畫框時須要用到的繪製參數以下,其中構造函數的最後兩個參數是額外添加的,用於特殊場景的手動矯正:
/** * 建立一個繪製輔助類對象,而且設置繪製相關的參數 * * @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); } }