在使用虹軟人臉識別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);
}
}複製代碼