errorSampleIdList = new ArrayList<>();
+ if (faceList.isEmpty()) return errorSampleIdList;
+ long startTime = System.currentTimeMillis();
+ // 批量查询人脸模型
+ for (FaceEntity v : faceList) {
+ if (v.getFaceModel() != null && v.getFaceModel().length > 0) continue;
+ byte[] bytes = null;
+ try {
+ bytes = FileUtil.fileToData(new File(v.getFaceFilePath()));
+ } catch (Exception e) {
+ errorSampleIdList.add(v.getSampleId());
+ Log.e(TAG, "registerFaceByDatabase,方法错误,file读取错误error:" + e.getMessage() + "json:" + new Gson().toJson(v), e);
+ continue;
+ }
+ if (bytes == null) {
+ errorSampleIdList.add(v.getSampleId());
+ Log.e(TAG, "registerFaceByDatabase,方法错误" + v.getUserName() + "bytes is null," + "json:" + new Gson().toJson(v));
+ continue;
+ }
+ Boolean isRegister = registerFace(context, bytes, v);
+ if (isRegister) {
+ updateFaceModel(context, v);
+ }
+ }
+ FaceServer faceServer = FaceServer.getInstance();
+ faceServer.init(context);
+ if (errorSampleIdList.isEmpty()) {
+ try {
+ faceServer.registerFaceFeatureInfoListFromDb(faceList);
+ Log.d(TAG, "registerFaceByDatabase,注册全部人脸耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
+ } catch (Exception e) {
+ faceServer.removeFace(-1);
+ Log.e(TAG, "registerFaceByDatabase,人脸引擎批量注册失败使用单个注册");
+ }
+ } else {
+ for (FaceEntity v : faceList) {
+ faceServer.registerFaceFeatureInfoFromDb(v);
+ }
+ Log.d(TAG, "registerFaceByDatabase,注册单个人脸耗时:" + (System.currentTimeMillis() - startTime) + "毫秒");
+ }
+ return errorSampleIdList;
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java
new file mode 100644
index 0000000..1c234e7
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/flutter/config/Constants.java
@@ -0,0 +1,9 @@
+package io.flutter.plugins.facediscern.flutter.config;
+
+
+public class Constants {
+ public static String APP_ID = "FWKhBhtgnUnVrUJXZD3SWqnGYV99AiXP4SS8aNSR6bk3";
+ public static String SDK_KEY = "5SAziGjCHYPY4GdjGY8dTiBABBmuLbiTp7oWsKTcWzMb";
+ public static String ACTIVE_KEY = "U571-11UF-F13U-F43A";
+ public static boolean IS_ACTIVE = false;
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java
new file mode 100644
index 0000000..12cc662
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/DualCameraHelper.java
@@ -0,0 +1,583 @@
+package io.flutter.plugins.facediscern.tool.camera;
+
+import android.graphics.ImageFormat;
+import android.graphics.Point;
+import android.graphics.SurfaceTexture;
+import android.hardware.Camera;
+import android.util.Log;
+import android.view.Surface;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.TextureView;
+import android.view.View;
+
+import io.flutter.plugins.facediscern.tool.camera.callback.CameraListener;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * 打开两个相机的辅助类,和{@link CameraListener}共同使用,获取nv21数据等操作
+ *
+ * 由于IR摄像头和RGB摄像头的默认分辨率可能不同,为了让两者相同,该类做了以下操作:
+ * 1. 获取两者支持的分辨率列表到到静态变量{@link DualCameraHelper#rgbSupportedPreviewSizes}及{@link DualCameraHelper#irSupportedPreviewSizes}中,
+ * 2. 使用{@link DualCameraHelper#getCommonSupportedPreviewSize()}方法取分辨率的交集,
+ * 3. 使用{@link DualCameraHelper#getBestSupportedSize(List, Point)}取最佳分辨率使两个摄像头分辨率尽可能相同
+ */
+public class DualCameraHelper implements Camera.PreviewCallback {
+ private static List rgbSupportedPreviewSizes;
+ private static List irSupportedPreviewSizes;
+ private static final String TAG = "CameraHelper";
+ private Camera mCamera;
+ private int mCameraId;
+ private Point previewViewSize;
+ private View previewDisplayView;
+ private Camera.Size previewSize;
+ private Point specificPreviewSize;
+ private int displayOrientation = 0;
+ private int rotation;
+ private int additionalRotation;
+ private boolean isMirror = false;
+
+ private Integer specificCameraId = null;
+ private CameraListener cameraListener;
+ private static final int MIN_PREVIEW_WIDTH = 720;
+ private static final int MIN_PREVIEW_HEIGHT = 720;
+
+ private DualCameraHelper(Builder builder) {
+ previewDisplayView = builder.previewDisplayView;
+ specificCameraId = builder.specificCameraId;
+ cameraListener = builder.cameraListener;
+ rotation = builder.rotation;
+ additionalRotation = builder.additionalRotation;
+ previewViewSize = builder.previewViewSize;
+ specificPreviewSize = builder.previewSize;
+ if (builder.previewDisplayView instanceof TextureView) {
+ isMirror = builder.isMirror;
+ } else if (isMirror) {
+ throw new RuntimeException("mirror is effective only when the preview is on a textureView");
+ }
+ }
+
+ public void init() {
+ if (previewDisplayView instanceof TextureView) {
+ ((TextureView) this.previewDisplayView).setSurfaceTextureListener(textureListener);
+ } else if (previewDisplayView instanceof SurfaceView) {
+ ((SurfaceView) previewDisplayView).getHolder().addCallback(surfaceCallback);
+ }
+
+ if (isMirror) {
+ previewDisplayView.setScaleX(-1);
+ }
+ }
+
+ public List getCommonSupportedPreviewSize() {
+ /**
+ * irSupportedPreviewSizes 和 rgbSupportedPreviewSizes 为null才去获取,
+ * 不为null就没必要获取了,而且此时有可能该camera已处于打开状态,无法打开camera
+ */
+ if (rgbSupportedPreviewSizes == null) {
+ Camera rgbCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
+ rgbSupportedPreviewSizes = rgbCamera.getParameters().getSupportedPreviewSizes();
+ rgbCamera.release();
+ }
+ try {
+ if (irSupportedPreviewSizes == null) {
+ Camera irCamera = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+ irSupportedPreviewSizes = irCamera.getParameters().getSupportedPreviewSizes();
+ irCamera.release();
+ }
+ } catch (RuntimeException e) {
+ e.printStackTrace();
+ irSupportedPreviewSizes = rgbSupportedPreviewSizes;
+ }
+ List commonPreviewSizes = new ArrayList<>();
+ for (Camera.Size rgbPreviewSize : rgbSupportedPreviewSizes) {
+ if (rgbPreviewSize.width < MIN_PREVIEW_WIDTH || rgbPreviewSize.height < MIN_PREVIEW_HEIGHT) {
+ continue;
+ }
+ for (Camera.Size irPreviewSize : irSupportedPreviewSizes) {
+ if (irPreviewSize.width == rgbPreviewSize.width && irPreviewSize.height == rgbPreviewSize.height) {
+ commonPreviewSizes.add(rgbPreviewSize);
+ }
+ }
+ }
+ return commonPreviewSizes;
+ }
+
+ /**
+ * 回传当前使用的cameraID,若当前没打开相机,回传-1
+ *
+ * @return cameraId,失败回传-1
+ */
+ public int getCurrentOpenedCameraId() {
+ if (mCamera == null) {
+ return -1;
+ }
+ return mCameraId;
+ }
+
+ public void start() {
+ synchronized (this) {
+ if (mCamera != null) {
+ return;
+ }
+ List supportedPreviewSize = getCommonSupportedPreviewSize();
+ //相机数量为2则打开1,1则打开0,相机ID 1为前置,0为后置
+ mCameraId = Camera.getNumberOfCameras() - 1;
+ //若指定了相机ID且该相机存在,则打开指定的相机
+ if (specificCameraId != null && specificCameraId <= mCameraId) {
+ mCameraId = specificCameraId;
+ }
+ //没有相机
+ if (mCameraId == -1) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(new Exception("camera not found"));
+ }
+ return;
+ }
+ if (mCamera == null) {
+ mCamera = Camera.open(mCameraId);
+ }
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ try {
+ Camera.Parameters parameters = mCamera.getParameters();
+ parameters.setPreviewFormat(ImageFormat.NV21);
+ //预览大小设置
+ previewSize = parameters.getPreviewSize();
+ if (supportedPreviewSize != null && supportedPreviewSize.size() > 0) {
+ previewSize = getBestSupportedSize(supportedPreviewSize, previewViewSize);
+ }
+ Log.i(TAG, "start: " + previewSize.width + "x" + previewSize.height);
+ parameters.setPreviewSize(previewSize.width, previewSize.height);
+ //对焦模式设置
+ List supportedFocusModes = parameters.getSupportedFocusModes();
+ if (supportedFocusModes != null && supportedFocusModes.size() > 0) {
+ if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_PICTURE);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_CONTINUOUS_VIDEO);
+ } else if (supportedFocusModes.contains(Camera.Parameters.FOCUS_MODE_AUTO)) {
+ parameters.setFocusMode(Camera.Parameters.FOCUS_MODE_AUTO);
+ }
+ }
+ mCamera.setParameters(parameters);
+ if (previewDisplayView instanceof TextureView) {
+ mCamera.setPreviewTexture(((TextureView) previewDisplayView).getSurfaceTexture());
+ } else {
+ mCamera.setPreviewDisplay(((SurfaceView) previewDisplayView).getHolder());
+ }
+ mCamera.setPreviewCallback(this);
+ mCamera.startPreview();
+ if (cameraListener != null) {
+ cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror);
+ }
+ } catch (Exception e) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(e);
+ }
+ }
+ io.flutter.Log.e(TAG, "相机已打卡!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!");
+ }
+ }
+
+ public void switchCameraId() {
+ mCameraId = 1 - mCameraId;
+ if (specificCameraId != null) {
+ specificCameraId = 1 - specificCameraId;
+ }
+ }
+
+ private int getCameraOri(int rotation) {
+ int degrees = rotation * 90;
+ switch (rotation) {
+ case Surface.ROTATION_0:
+ degrees = 0;
+ break;
+ case Surface.ROTATION_90:
+ degrees = 90;
+ break;
+ case Surface.ROTATION_180:
+ degrees = 180;
+ break;
+ case Surface.ROTATION_270:
+ degrees = 270;
+ break;
+ default:
+ break;
+ }
+ additionalRotation /= 90;
+ additionalRotation *= 90;
+ degrees += additionalRotation;
+ int result;
+ Camera.CameraInfo info = new Camera.CameraInfo();
+ Camera.getCameraInfo(mCameraId, info);
+ if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ result = (info.orientation + degrees) % 360;
+ result = (360 - result) % 360;
+ } else {
+ result = (info.orientation - degrees + 360) % 360;
+ }
+ return result;
+ }
+
+ /**
+ * 停止预览
+ */
+ public void stop() {
+ synchronized (this) {
+ if (mCamera == null) return;
+ mCamera.setPreviewCallback(null);
+ mCamera.stopPreview();
+ }
+ }
+// public void stop() {
+// synchronized (this) {
+// if (mCamera == null) return;
+// mCamera.setPreviewCallback(null);
+// mCamera.stopPreview();
+// mCamera.release();
+// mCamera = null;
+// if (cameraListener != null) {
+// cameraListener.onCameraClosed();
+// }
+// }
+// }
+
+ // 继续预览
+ public void resumePreview() {
+ synchronized (this) {
+ if (mCamera != null) {
+ try {
+ mCamera.startPreview();
+ if (cameraListener != null) {
+ cameraListener.onCameraOpened(mCamera, mCameraId, displayOrientation, isMirror);
+ mCamera.setPreviewCallback(this);
+ }
+ } catch (Exception e) {
+ if (cameraListener != null) {
+ cameraListener.onCameraError(e);
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * 释放操作
+ */
+ public void release() {
+ synchronized (this) {
+ stop();
+ if (cameraListener != null) {
+ cameraListener.onCameraClosed();
+ }
+ mCamera.release();
+ mCamera = null;
+ previewDisplayView = null;
+ specificCameraId = null;
+ cameraListener = null;
+ previewViewSize = null;
+ specificPreviewSize = null;
+ previewSize = null;
+ }
+ }
+
+ public boolean isStopped() {
+ synchronized (this) {
+ return mCamera == null;
+ }
+ }
+
+
+ /**
+ * 获取候选分辨率列表中最接近预览view大小的分辨率
+ *
+ * @param sizes 支持的分辨率
+ * @param previewViewSize 预览view的大小
+ * @return 最接近预览view大小的分辨率
+ */
+ private Camera.Size getBestSupportedSize(List sizes, Point previewViewSize) {
+ if (sizes == null || sizes.size() == 0) {
+ return mCamera.getParameters().getPreviewSize();
+ }
+ Camera.Size[] tempSizes = sizes.toArray(new Camera.Size[0]);
+ Arrays.sort(tempSizes, new Comparator() {
+ @Override
+ public int compare(Camera.Size o1, Camera.Size o2) {
+ if (o1.width > o2.width) {
+ return -1;
+ } else if (o1.width == o2.width) {
+ return o1.height > o2.height ? -1 : 1;
+ } else {
+ return 1;
+ }
+ }
+ });
+ sizes = Arrays.asList(tempSizes);
+
+ Camera.Size bestSize = sizes.get(0);
+ float previewViewRatio;
+ if (previewViewSize != null) {
+ previewViewRatio = (float) previewViewSize.x / (float) previewViewSize.y;
+ } else {
+ previewViewRatio = (float) bestSize.width / (float) bestSize.height;
+ }
+
+ if (previewViewRatio > 1) {
+ previewViewRatio = 1 / previewViewRatio;
+ }
+ boolean isNormalRotate = (additionalRotation % 180 == 0);
+
+ for (Camera.Size s : sizes) {
+ if (specificPreviewSize != null && specificPreviewSize.x == s.width && specificPreviewSize.y == s.height) {
+ return s;
+ }
+ if (isNormalRotate) {
+ if (Math.abs((s.height / (float) s.width) - previewViewRatio) < Math.abs(bestSize.height / (float) bestSize.width - previewViewRatio)) {
+ bestSize = s;
+ }
+ } else {
+ if (Math.abs((s.width / (float) s.height) - previewViewRatio) < Math.abs(bestSize.width / (float) bestSize.height - previewViewRatio)) {
+ bestSize = s;
+ }
+ }
+ }
+ return bestSize;
+ }
+
+ public List getSupportedPreviewSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPreviewSizes();
+ }
+
+ public List getSupportedPictureSizes() {
+ if (mCamera == null) {
+ return null;
+ }
+ return mCamera.getParameters().getSupportedPictureSizes();
+ }
+
+
+ @Override
+ public void onPreviewFrame(byte[] nv21, Camera camera) {
+ if (cameraListener != null) {
+ cameraListener.onPreview(nv21, camera);
+ }
+ }
+
+ private TextureView.SurfaceTextureListener textureListener = new TextureView.SurfaceTextureListener() {
+ @Override
+ public void onSurfaceTextureAvailable(SurfaceTexture surfaceTexture, int width, int height) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewTexture(surfaceTexture);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void onSurfaceTextureSizeChanged(SurfaceTexture surfaceTexture, int width, int height) {
+ Log.i(TAG, "onSurfaceTextureSizeChanged: " + width + " " + height);
+ }
+
+ @Override
+ public boolean onSurfaceTextureDestroyed(SurfaceTexture surfaceTexture) {
+ stop();
+ return false;
+ }
+
+ @Override
+ public void onSurfaceTextureUpdated(SurfaceTexture surfaceTexture) {
+
+ }
+ };
+ private SurfaceHolder.Callback surfaceCallback = new SurfaceHolder.Callback() {
+ @Override
+ public void surfaceCreated(SurfaceHolder holder) {
+// start();
+ if (mCamera != null) {
+ try {
+ mCamera.setPreviewDisplay(holder);
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ @Override
+ public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
+
+ }
+
+ @Override
+ public void surfaceDestroyed(SurfaceHolder holder) {
+ stop();
+ }
+ };
+
+ public void changeDisplayOrientation(int rotation) {
+ if (mCamera != null) {
+ this.rotation = rotation;
+ displayOrientation = getCameraOri(rotation);
+ mCamera.setDisplayOrientation(displayOrientation);
+ if (cameraListener != null) {
+ cameraListener.onCameraConfigurationChanged(mCameraId, displayOrientation);
+ }
+ }
+ }
+
+ public static final class Builder {
+
+ /**
+ * 预览显示的view,目前仅支持surfaceView和textureView
+ */
+ private View previewDisplayView;
+
+ /**
+ * 是否镜像显示,只支持textureView
+ */
+ private boolean isMirror;
+ /**
+ * 指定的相机ID
+ */
+ private Integer specificCameraId;
+ /**
+ * 事件回调
+ */
+ private CameraListener cameraListener;
+ /**
+ * 屏幕的长宽,在选择最佳相机比例时用到
+ */
+ private Point previewViewSize;
+ /**
+ * 传入getWindowManager().getDefaultDisplay().getRotation()的值即可
+ */
+ private int rotation;
+ /**
+ * 指定的预览宽高,若系统支持则会以这个预览宽高进行预览
+ */
+ private Point previewSize;
+
+ /**
+ * 额外的旋转角度(用于适配一些定制设备)
+ */
+ private int additionalRotation;
+
+ public Builder() {
+ }
+
+
+ public Builder previewOn(View val) {
+ if (val instanceof SurfaceView || val instanceof TextureView) {
+ previewDisplayView = val;
+ return this;
+ } else {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ }
+
+
+ public Builder isMirror(boolean val) {
+ isMirror = val;
+ return this;
+ }
+
+ public Builder previewSize(Point val) {
+ previewSize = val;
+ return this;
+ }
+
+ public Builder previewViewSize(Point val) {
+ previewViewSize = val;
+ return this;
+ }
+
+ public Builder rotation(int val) {
+ rotation = val;
+ return this;
+ }
+
+ public Builder additionalRotation(int val) {
+ additionalRotation = val;
+ return this;
+ }
+
+ public Builder specificCameraId(Integer val) {
+ specificCameraId = val;
+ return this;
+ }
+
+ public Builder cameraListener(CameraListener val) {
+ cameraListener = val;
+ return this;
+ }
+
+ public DualCameraHelper build() {
+ if (previewViewSize == null) {
+ Log.e(TAG, "previewViewSize is null, now use default previewSize");
+ }
+ if (cameraListener == null) {
+ Log.e(TAG, "cameraListener is null, callback will not be called");
+ }
+ if (previewDisplayView == null) {
+ throw new RuntimeException("you must preview on a textureView or a surfaceView");
+ }
+ return new DualCameraHelper(this);
+ }
+ }
+
+ /**
+ * 根据设置的额外旋转角度旋转
+ *
+ * @param additionalRotation 额外旋转角度
+ * @return 当前显示旋转角度
+ */
+ public int rotateAdditional(int additionalRotation) {
+ this.additionalRotation = additionalRotation;
+ int cameraOri = getCameraOri(rotation);
+ if (mCamera == null) {
+ start();
+ return cameraOri;
+ }
+ mCamera.setDisplayOrientation(cameraOri);
+ return cameraOri;
+ }
+
+ public void setSpecificPreviewSize(Point specificPreviewSize) {
+ this.specificPreviewSize = specificPreviewSize;
+ }
+
+ public static boolean hasDualCamera() {
+ return Camera.getNumberOfCameras() > 1;
+ }
+
+ public static boolean canOpenDualCamera() {
+ Camera camera0 = null;
+ Camera camera1 = null;
+ boolean can = true;
+ try {
+ camera0 = Camera.open(Camera.CameraInfo.CAMERA_FACING_BACK);
+ camera1 = Camera.open(Camera.CameraInfo.CAMERA_FACING_FRONT);
+ } catch (Exception e) {
+ can = false;
+ }
+ if (camera0 != null) {
+ camera0.release();
+ }
+ if (camera1 != null) {
+ camera1.release();
+ }
+ return can;
+ }
+
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java
new file mode 100644
index 0000000..ccc2ea3
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/callback/CameraListener.java
@@ -0,0 +1,40 @@
+package io.flutter.plugins.facediscern.tool.camera.callback;
+
+import android.hardware.Camera;
+
+
+public interface CameraListener {
+ /**
+ * 当打开时执行
+ * @param camera 相机实例
+ * @param cameraId 相机ID
+ * @param displayOrientation 相机预览旋转角度
+ * @param isMirror 是否镜像显示
+ */
+ void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror);
+
+ /**
+ * 预览数据回调
+ * @param data 预览数据
+ * @param camera 相机实例
+ */
+ void onPreview(byte[] data, Camera camera);
+
+ /**
+ * 当相机关闭时执行
+ */
+ void onCameraClosed();
+
+ /**
+ * 当出现异常时执行
+ * @param e 相机相关异常
+ */
+ void onCameraError(Exception e);
+
+ /**
+ * 属性变化时调用
+ * @param cameraID 相机ID
+ * @param displayOrientation 相机旋转方向
+ */
+ void onCameraConfigurationChanged(int cameraID, int displayOrientation);
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java
new file mode 100644
index 0000000..66041c1
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/camera/config/PreviewConfig.java
@@ -0,0 +1,58 @@
+package io.flutter.plugins.facediscern.tool.camera.config;
+
+import android.hardware.Camera;
+
+public class PreviewConfig {
+ /**
+ * 默认的可见光相机ID
+ */
+ public static final int DEFAULT_RGB_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_BACK;
+ /**
+ * 默认的红外相机ID
+ */
+ public static final int DEFAULT_IR_CAMERA_ID = Camera.CameraInfo.CAMERA_FACING_FRONT;
+
+ private int rgbCameraId;
+ private int irCameraId;
+ private int rgbAdditionalDisplayOrientation;
+ private int irAdditionalDisplayOrientation;
+
+ public PreviewConfig(int rgbCameraId, int irCameraId, int rgbAdditionalDisplayOrientation, int irAdditionalDisplayOrientation) {
+ this.rgbCameraId = rgbCameraId;
+ this.irCameraId = irCameraId;
+ this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation;
+ this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation;
+ }
+
+ public int getRgbCameraId() {
+ return rgbCameraId;
+ }
+
+ public int getIrCameraId() {
+ return irCameraId;
+ }
+
+ public int getRgbAdditionalDisplayOrientation() {
+ return rgbAdditionalDisplayOrientation;
+ }
+
+ public int getIrAdditionalDisplayOrientation() {
+ return irAdditionalDisplayOrientation;
+ }
+
+ public void setRgbCameraId(int rgbCameraId) {
+ this.rgbCameraId = rgbCameraId;
+ }
+
+ public void setIrCameraId(int irCameraId) {
+ this.irCameraId = irCameraId;
+ }
+
+ public void setRgbAdditionalDisplayOrientation(int rgbAdditionalDisplayOrientation) {
+ this.rgbAdditionalDisplayOrientation = rgbAdditionalDisplayOrientation;
+ }
+
+ public void setIrAdditionalDisplayOrientation(int irAdditionalDisplayOrientation) {
+ this.irAdditionalDisplayOrientation = irAdditionalDisplayOrientation;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java
new file mode 100644
index 0000000..92012e0
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/FaceBrodConstant.java
@@ -0,0 +1,7 @@
+package io.flutter.plugins.facediscern.tool.face.constants;
+
+public class FaceBrodConstant {
+ public static String FACE_GLOBAL_INTENT = "accessibility_event";
+ public static final String FACE_GLOBAL_ACTIONS = "face_global_actions";
+ // public static final String FACE_GLOBAL_BROD = "face_global_brod";
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java
new file mode 100644
index 0000000..90e7348
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/LivenessType.java
@@ -0,0 +1,15 @@
+package io.flutter.plugins.facediscern.tool.face.constants;
+
+/**
+ * 活体检测类型
+ */
+public enum LivenessType {
+ /**
+ * RGB活体检测
+ */
+ RGB,
+ /**
+ * 红外活体检测
+ */
+ IR
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java
new file mode 100644
index 0000000..b363e6d
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RecognizeColor.java
@@ -0,0 +1,22 @@
+package io.flutter.plugins.facediscern.tool.face.constants;
+
+import android.graphics.Color;
+
+/**
+ * 识别过程中人脸框的颜色
+ */
+public class RecognizeColor {
+ /**
+ * 未知情况的颜色
+ */
+ public static final int COLOR_UNKNOWN = Color.YELLOW;
+ /**
+ * 成功的颜色
+ */
+ public static final int COLOR_SUCCESS = Color.GREEN;
+ /**
+ * 失败的颜色
+ */
+ public static final int COLOR_FAILED = Color.YELLOW;
+
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java
new file mode 100644
index 0000000..554061e
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestFeatureStatus.java
@@ -0,0 +1,28 @@
+package io.flutter.plugins.facediscern.tool.face.constants;
+
+/**
+ * 人脸识别中可能出现的状态
+ * @author
+ */
+public @interface RequestFeatureStatus {
+ /**
+ * 默认状态
+ */
+ int DEFAULT = -1;
+ /**
+ * 处理中
+ */
+ int SEARCHING = 0;
+ /**
+ * 识别成功
+ */
+ int SUCCEED = 1;
+ /**
+ * 待重试
+ */
+ int TO_RETRY = 2;
+ /**
+ * 识别失败
+ */
+ int FAILED = 3;
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java
new file mode 100644
index 0000000..e4aaad0
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/constants/RequestLivenessStatus.java
@@ -0,0 +1,5 @@
+package io.flutter.plugins.facediscern.tool.face.constants;
+
+public class RequestLivenessStatus {
+ public static final int ANALYZING = 10;
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java
new file mode 100644
index 0000000..81408ef
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceMoveFilter.java
@@ -0,0 +1,95 @@
+package io.flutter.plugins.facediscern.tool.face.filter;
+
+import android.graphics.Rect;
+
+
+import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.LinkedBlockingDeque;
+
+/**
+ * 人脸移动过滤器:
+ * 仅保留在{@link FaceMoveFilter#CHECK_QUEUE_SIZE}帧数内,每一帧人脸的移动大小都小于{@link FaceMoveFilter#movePixels}的人脸
+ */
+public class FaceMoveFilter implements FaceRecognizeFilter {
+ private static final String TAG = "FaceMoveFilter";
+ private Map> facePositionQueueMap = new ConcurrentHashMap<>();
+ private static final int CHECK_QUEUE_SIZE = 5;
+ private double movePixels;
+
+ public FaceMoveFilter(double movePixels) {
+ this.movePixels = movePixels;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ clearFacesNotInPreview(facePreviewInfoList);
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ LinkedBlockingDeque rectDeque = facePositionQueueMap.get(facePreviewInfo.getTrackId());
+ if (rectDeque == null) {
+ rectDeque = new LinkedBlockingDeque<>(CHECK_QUEUE_SIZE);
+ facePositionQueueMap.put(facePreviewInfo.getTrackId(), rectDeque);
+ }
+ if (rectDeque.remainingCapacity() == 0) {
+ rectDeque.removeLast();
+ }
+ rectDeque.push(facePreviewInfo.getFaceInfoRgb().getRect());
+
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+
+ boolean qualityPass = false;
+ if (rectDeque.size() == CHECK_QUEUE_SIZE) {
+ qualityPass = true;
+ Iterator iterator = rectDeque.iterator();
+ Rect previous = iterator.next();
+ while (iterator.hasNext()) {
+ Rect current = iterator.next();
+ double distance = getDistance(current, previous);
+ previous = current;
+ if (distance > movePixels) {
+ qualityPass = false;
+ break;
+ }
+ }
+ }
+ facePreviewInfo.setQualityPass(qualityPass);
+ }
+ }
+
+ private void clearFacesNotInPreview(List facePreviewInfo) {
+ Set trackIdSet = facePositionQueueMap.keySet();
+ for (Integer trackId : trackIdSet) {
+ boolean contains = false;
+ for (FacePreviewInfo previewInfo : facePreviewInfo) {
+ if (previewInfo.getTrackId() == trackId) {
+ contains = true;
+ break;
+ }
+ }
+ if (!contains) {
+ facePositionQueueMap.remove(trackId);
+ }
+ }
+ }
+
+ public static double getDistance(Rect first, Rect second) {
+ int firstX = first.centerX();
+ int firstY = first.centerY();
+
+ int secondX = second.centerX();
+ int secondY = second.centerY();
+
+ int distanceX = secondX - firstX;
+ int distanceY = secondY - firstY;
+
+ return Math.sqrt(distanceX * distanceX + distanceY * distanceY);
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java
new file mode 100644
index 0000000..62163f0
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceRecognizeAreaFilter.java
@@ -0,0 +1,32 @@
+package io.flutter.plugins.facediscern.tool.face.filter;
+
+
+import android.graphics.Rect;
+
+import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸识别区域过滤器:
+ * 仅保留人脸区域在{@link FaceRecognizeAreaFilter#validArea}中的人脸。(基于View位置判断)
+ */
+public class FaceRecognizeAreaFilter implements FaceRecognizeFilter {
+ private static final String TAG = "FaceRecognizeAreaFilter";
+ private Rect validArea;
+
+ public FaceRecognizeAreaFilter(Rect validArea) {
+ this.validArea = validArea;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ facePreviewInfo.setQualityPass(validArea.contains(facePreviewInfo.getRgbTransformedRect()));
+ }
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java
new file mode 100644
index 0000000..adceafa
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/FaceSizeFilter.java
@@ -0,0 +1,41 @@
+package io.flutter.plugins.facediscern.tool.face.filter;
+
+import android.graphics.Rect;
+
+
+import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸尺寸过滤器:
+ * 仅保留人脸宽度大于{@link FaceSizeFilter#horizontalSize},且人脸高度大于{@link FaceSizeFilter#verticalSize}的人脸。
+ */
+public class FaceSizeFilter implements FaceRecognizeFilter {
+ private int horizontalSize;
+ private int verticalSize;
+
+ private static final String TAG = "FaceSizeFilter";
+
+ public FaceSizeFilter(int horizontalSize, int verticalSize) {
+ this.horizontalSize = horizontalSize;
+ this.verticalSize = verticalSize;
+ }
+
+ @Override
+ public void filter(List facePreviewInfoList) {
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ if (facePreviewInfo.getFaceInfoRgb() != null) {
+ Rect rgbRect = facePreviewInfo.getFaceInfoRgb().getRect();
+ Rect irRect = facePreviewInfo.getFaceInfoIr() == null ? null : facePreviewInfo.getFaceInfoIr().getRect();
+ boolean rgbRectValid = rgbRect == null || (rgbRect.width() > horizontalSize && rgbRect.height() > verticalSize);
+ boolean irRectValid = irRect == null || (irRect.width() > horizontalSize && irRect.height() > verticalSize);
+ facePreviewInfo.setQualityPass(rgbRectValid && irRectValid);
+ }
+ }
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java
new file mode 100644
index 0000000..778e1e4
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/filter/callback/FaceRecognizeFilter.java
@@ -0,0 +1,14 @@
+package io.flutter.plugins.facediscern.tool.face.filter.callback;
+
+
+
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+
+import java.util.List;
+
+/**
+ * 人脸识别过滤器,仅保留满足条件的人脸,(只有满足条件的人脸才进行后续的活体检测、人脸识别操作)
+ */
+public interface FaceRecognizeFilter {
+ void filter(List facePreviewInfoList);
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java
new file mode 100644
index 0000000..bf33e23
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceHelper.java
@@ -0,0 +1,1183 @@
+package io.flutter.plugins.facediscern.tool.face.help;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.util.Log;
+
+import androidx.annotation.IntDef;
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.ImageQualitySimilar;
+import com.arcsoft.face.LivenessInfo;
+import com.arcsoft.face.MaskInfo;
+import com.arcsoft.face.enums.ExtractType;
+import com.google.gson.Gson;
+
+import io.flutter.plugins.facediscern.tool.face.constants.LivenessType;
+import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus;
+import io.flutter.plugins.facediscern.tool.face.constants.RequestLivenessStatus;
+import io.flutter.plugins.facediscern.tool.face.filter.FaceMoveFilter;
+import io.flutter.plugins.facediscern.tool.face.filter.FaceRecognizeAreaFilter;
+import io.flutter.plugins.facediscern.tool.face.filter.FaceSizeFilter;
+import io.flutter.plugins.facediscern.tool.face.filter.callback.FaceRecognizeFilter;
+import io.flutter.plugins.facediscern.tool.face.help.callback.FaceListener;
+import io.flutter.plugins.facediscern.tool.face.help.callback.IDualCameraFaceInfoTransformer;
+import io.flutter.plugins.facediscern.tool.face.help.callback.RecognizeCallback;
+import io.flutter.plugins.facediscern.tool.face.model.CompareResult;
+import io.flutter.plugins.facediscern.tool.face.model.FaceEntity;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfiguration;
+import io.flutter.plugins.facediscern.tool.face.model.RecognizeInfo;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.DecimalFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+import io.flutter.plugins.facediscern.tool.face.servcie.FaceServer;
+import io.reactivex.Observable;
+import io.reactivex.Observer;
+import io.reactivex.android.schedulers.AndroidSchedulers;
+import io.reactivex.disposables.CompositeDisposable;
+import io.reactivex.disposables.Disposable;
+
+
+/**
+ * 人脸操作辅助类
+ */
+public class FaceHelper implements FaceListener {
+
+ private static final String TAG = "FaceHelper";
+
+ /**
+ * 识别结果的回调
+ */
+ private RecognizeCallback recognizeCallback;
+
+
+ /**
+ * 用于记录人脸识别过程信息
+ */
+ private ConcurrentHashMap recognizeInfoMap = new ConcurrentHashMap<>();
+
+ private CompositeDisposable getFeatureDelayedDisposables = new CompositeDisposable();
+ private CompositeDisposable delayFaceTaskCompositeDisposable = new CompositeDisposable();
+ /**
+ * 转换方式,用于IR活体检测
+ */
+ private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
+
+ /**
+ * 线程池正在处理任务
+ */
+ private static final int ERROR_BUSY = -1;
+ /**
+ * 特征提取引擎为空
+ */
+ private static final int ERROR_FR_ENGINE_IS_NULL = -2;
+ /**
+ * 活体检测引擎为空
+ */
+ private static final int ERROR_FL_ENGINE_IS_NULL = -3;
+ /**
+ * 人脸追踪引擎
+ */
+ private FaceEngine ftEngine;
+ /**
+ * 口罩检测引擎
+ */
+ private FaceEngine maskEngine;
+ /**
+ * 特征提取引擎
+ */
+ private FaceEngine frEngine;
+ /**
+ * 活体检测引擎
+ */
+ private FaceEngine flEngine;
+
+ private Camera.Size previewSize;
+
+ private List faceInfoList = new CopyOnWriteArrayList<>();
+ private List maskInfoList = new CopyOnWriteArrayList<>();
+ /**
+ * 特征提取线程池
+ */
+ private ExecutorService frExecutor;
+ /**
+ * 活体检测线程池
+ */
+ private ExecutorService flExecutor;
+ /**
+ * 特征提取线程队列
+ */
+ private LinkedBlockingQueue frThreadQueue;
+ /**
+ * 活体检测线程队列
+ */
+ private LinkedBlockingQueue flThreadQueue;
+
+ private FaceRectTransformer rgbFaceRectTransformer;
+ private FaceRectTransformer irFaceRectTransformer;
+ /**
+ * 控制可识别区域(相对于View),若未设置,则是全部区域
+ */
+ private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+ private List faceRecognizeFilterList = new ArrayList<>();
+ /**
+ * 上次应用退出时,记录的该App检测过的人脸数了
+ */
+ private int trackedFaceCount = 0;
+ /**
+ * 本次打开引擎后的最大faceId
+ */
+ private int currentMaxFaceId = 0;
+
+ /**
+ * 是否只检测活体
+ */
+ private boolean onlyDetectLiveness;
+
+ /**
+ * 识别的配置项
+ */
+ private RecognizeConfiguration recognizeConfiguration;
+ private List currentTrackIdList = new ArrayList<>();
+ private List facePreviewInfoList = new ArrayList<>();
+
+ private Disposable timerDisposable;
+
+ private FaceHelper(Builder builder) {
+ onlyDetectLiveness = builder.onlyDetectLiveness;
+ ftEngine = builder.ftEngine;
+ maskEngine = builder.maskEngine;
+ trackedFaceCount = builder.trackedFaceCount;
+ previewSize = builder.previewSize;
+ frEngine = builder.frEngine;
+ flEngine = builder.flEngine;
+ recognizeCallback = builder.recognizeCallback;
+ recognizeConfiguration = builder.recognizeConfiguration;
+ dualCameraFaceInfoTransformer = builder.dualCameraFaceInfoTransformer;
+ /*
+ * fr 线程队列大小
+ */
+ int frQueueSize = recognizeConfiguration.getMaxDetectFaces();
+ if (builder.frQueueSize > 0) {
+ frQueueSize = builder.frQueueSize;
+ } else {
+ Log.e(TAG, "frThread num must > 0, now using default value:" + frQueueSize);
+ }
+ frThreadQueue = new LinkedBlockingQueue<>(frQueueSize);
+ frExecutor = new ThreadPoolExecutor(1, frQueueSize, 0, TimeUnit.MILLISECONDS, frThreadQueue, r -> {
+ Thread t = new Thread(r);
+ t.setName("frThread-" + t.getId());
+ return t;
+ });
+
+ /*
+ * fl 线程队列大小
+ */
+ int flQueueSize = recognizeConfiguration.getMaxDetectFaces();
+ if (builder.flQueueSize > 0) {
+ flQueueSize = builder.flQueueSize;
+ } else {
+ Log.e(TAG, "flThread num must > 0, now using default value:" + flQueueSize);
+ }
+ flThreadQueue = new LinkedBlockingQueue(flQueueSize);
+ flExecutor = new ThreadPoolExecutor(1, flQueueSize, 0, TimeUnit.MILLISECONDS, flThreadQueue, r -> {
+ Thread t = new Thread(r);
+ t.setName("flThread-" + t.getId());
+ return t;
+ });
+ if (previewSize == null) {
+ throw new RuntimeException("previewSize must be specified!");
+ }
+ if (recognizeConfiguration.isEnableFaceSizeLimit()) {
+ // 由于目前人脸框的宽高接近一致,所以在使用时horizontalSize和verticalSize的值设置成一样
+ faceRecognizeFilterList.add(new FaceSizeFilter(recognizeConfiguration.getFaceSizeLimit(), recognizeConfiguration.getFaceSizeLimit()));
+ }
+ if (recognizeConfiguration.isEnableFaceMoveLimit()) {
+ faceRecognizeFilterList.add(new FaceMoveFilter(recognizeConfiguration.getFaceMoveLimit()));
+ }
+ if (recognizeConfiguration.isEnableFaceAreaLimit()) {
+ faceRecognizeFilterList.add(new FaceRecognizeAreaFilter(recognizeArea));
+ }
+ }
+
+ /**
+ * 请求获取人脸特征数据
+ *
+ * @param nv21 图像数据
+ * @param facePreviewInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ */
+ public void requestFaceFeature(byte[] nv21, FacePreviewInfo facePreviewInfo, int width, int height, int format) {
+ if (frEngine != null && frThreadQueue.remainingCapacity() > 0) {
+ frExecutor.execute(new FaceRecognizeRunnable(nv21, facePreviewInfo, width, height, format));
+ } else {
+ onFaceFeatureInfoGet(null, facePreviewInfo.getTrackId(), ERROR_BUSY);
+ }
+ }
+
+ /**
+ * 请求获取活体检测结果,需要传入活体的参数,以下参数同
+ *
+ * @param nv21 NV21格式的图像数据
+ * @param faceInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ * @param livenessType 活体检测类型
+ * @param waitLock
+ */
+ public void requestFaceLiveness(byte[] nv21, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) {
+ if (flEngine != null && flThreadQueue.remainingCapacity() > 0) {
+ flExecutor.execute(new FaceLivenessDetectRunnable(nv21, faceInfo, width, height, format, livenessType, waitLock));
+ } else {
+ onFaceLivenessInfoGet(null, faceInfo.getTrackId(), ERROR_BUSY);
+ }
+
+ }
+
+ /**
+ * 释放对象
+ */
+ public void release() {
+ if (getFeatureDelayedDisposables != null) {
+ getFeatureDelayedDisposables.clear();
+ }
+ if (!frExecutor.isShutdown()) {
+ frExecutor.shutdownNow();
+ frThreadQueue.clear();
+ }
+ if (!flExecutor.isShutdown()) {
+ flExecutor.shutdownNow();
+ flThreadQueue.clear();
+ }
+ if (faceInfoList != null) {
+ faceInfoList.clear();
+ }
+ if (frThreadQueue != null) {
+ frThreadQueue.clear();
+ frThreadQueue = null;
+ }
+ if (flThreadQueue != null) {
+ flThreadQueue.clear();
+ flThreadQueue = null;
+ }
+ faceInfoList = null;
+ stop();
+ }
+
+ public void stop() {
+ if (faceInfoList != null) {
+ faceInfoList.clear();
+ }
+ if (maskInfoList != null) {
+ maskInfoList.clear();
+ }
+ if (recognizeInfoMap != null) {
+ recognizeInfoMap.clear();
+ }
+ if (facePreviewInfoList != null) {
+ facePreviewInfoList.clear();
+ }
+ }
+
+ /**
+ * 处理帧数据
+ *
+ * @param rgbNv21 可见光相机预览回传的NV21数据
+ * @param irNv21 红外相机预览回传的NV21数据
+ * @param doRecognize 是否进行识别
+ * @return 实时人脸处理结果,封装添加了一个trackId,trackId的获取依赖于faceId,用于记录人脸序号并保存
+ */
+ public List onPreviewFrame(@NonNull byte[] rgbNv21, @Nullable byte[] irNv21, boolean doRecognize) {
+ if (ftEngine != null) {
+ faceInfoList.clear();
+ maskInfoList.clear();
+ facePreviewInfoList.clear();
+ int code = ftEngine.detectFaces(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList);
+ if (code != ErrorInfo.MOK) {
+ onFail(new Exception("detectFaces failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ if (recognizeConfiguration.isKeepMaxFace()) {
+ keepMaxFace(faceInfoList);
+ }
+ refreshTrackId(faceInfoList);
+ if (faceInfoList.isEmpty()) {
+ return facePreviewInfoList;
+ }
+ if (!onlyDetectLiveness) {
+ code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
+ FaceEngine.ASF_MASK_DETECT);
+ if (code == ErrorInfo.MOK) {
+ code = maskEngine.getMask(maskInfoList);
+ if (code != ErrorInfo.MOK) {
+ onFail(new Exception("process getMask failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ } else {
+ onFail(new Exception("process mask failed,code is " + code));
+ return facePreviewInfoList;
+ }
+ }
+
+ for (int i = 0; i < faceInfoList.size(); i++) {
+ FacePreviewInfo facePreviewInfo = new FacePreviewInfo(faceInfoList.get(i), currentTrackIdList.get(i));
+ if (!maskInfoList.isEmpty()) {
+ MaskInfo maskInfo = maskInfoList.get(i);
+ facePreviewInfo.setMask(maskInfo.getMask());
+ }
+ if (rgbFaceRectTransformer != null && recognizeArea != null) {
+ Rect rect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getRect());
+ Rect foreRect = rgbFaceRectTransformer.adjustRect(faceInfoList.get(i).getForeheadRect());
+ facePreviewInfo.setRgbTransformedRect(rect);
+ facePreviewInfo.setForeRect(foreRect);
+ }
+ if (irFaceRectTransformer != null) {
+ FaceInfo faceInfo = faceInfoList.get(i);
+ if (dualCameraFaceInfoTransformer != null) {
+ faceInfo = dualCameraFaceInfoTransformer.transformFaceInfo(faceInfo);
+ }
+ facePreviewInfo.setFaceInfoIr(faceInfo);
+ facePreviewInfo.setIrTransformedRect(irFaceRectTransformer.adjustRect(faceInfo.getRect()));
+ }
+ facePreviewInfoList.add(facePreviewInfo);
+ }
+ clearLeftFace(facePreviewInfoList);
+ if (doRecognize) {
+ doRecognize(rgbNv21, irNv21, facePreviewInfoList);
+ }
+ } else {
+ facePreviewInfoList.clear();
+ }
+ return facePreviewInfoList;
+ }
+
+ public void setRgbFaceRectTransformer(FaceRectTransformer rgbFaceRectTransformer) {
+ this.rgbFaceRectTransformer = rgbFaceRectTransformer;
+ }
+
+ public void setIrFaceRectTransformer(FaceRectTransformer irFaceRectTransformer) {
+ this.irFaceRectTransformer = irFaceRectTransformer;
+ }
+
+ /**
+ * 删除已经离开的人脸
+ *
+ * @param facePreviewInfoList 人脸和trackId列表
+ */
+ private void clearLeftFace(List facePreviewInfoList) {
+ if (facePreviewInfoList == null || facePreviewInfoList.size() == 0) {
+ if (getFeatureDelayedDisposables != null) {
+ getFeatureDelayedDisposables.clear();
+ }
+ }
+ Enumeration keys = recognizeInfoMap.keys();
+ while (keys.hasMoreElements()) {
+ int key = keys.nextElement();
+ boolean contained = false;
+ for (FacePreviewInfo facePreviewInfo : facePreviewInfoList) {
+ if (facePreviewInfo.getTrackId() == key) {
+ contained = true;
+ break;
+ }
+ }
+ if (!contained) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.remove(key);
+ if (recognizeInfo != null) {
+ recognizeCallback.onNoticeChanged("");
+ // 人脸离开时,通知特征提取线程,避免一直等待活体结果
+ synchronized (recognizeInfo.getWaitLock()) {
+ recognizeInfo.getWaitLock().notifyAll();
+ }
+ }
+ }
+ }
+ }
+
+ private void doRecognize(byte[] rgbNv21, byte[] irNv21, List facePreviewInfoList) {
+ if (facePreviewInfoList != null && !facePreviewInfoList.isEmpty() && previewSize != null) {
+ for (FaceRecognizeFilter faceRecognizeFilter : faceRecognizeFilterList) {
+ faceRecognizeFilter.filter(facePreviewInfoList);
+ }
+ for (int i = 0; i < facePreviewInfoList.size(); i++) {
+ FacePreviewInfo facePreviewInfo = facePreviewInfoList.get(i);
+ if (!facePreviewInfo.isQualityPass()) {
+ continue;
+ }
+ //跳过mask值为MaskInfo.UNKNOWN的人脸
+ if (!onlyDetectLiveness && facePreviewInfo.getMask() == MaskInfo.UNKNOWN) {
+ continue;
+ }
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, facePreviewInfo.getTrackId());
+ int status = recognizeInfo.getRecognizeStatus();
+ /*
+ * 在活体检测开启,在人脸识别状态不为成功或人脸活体状态不为处理中(ANALYZING)且不为处理完成(ALIVE、NOT_ALIVE)时重新进行活体检测
+ */
+ if (recognizeConfiguration.isEnableLiveness() && status != RequestFeatureStatus.SUCCEED) {
+ int liveness = recognizeInfo.getLiveness();
+ if (liveness != LivenessInfo.ALIVE && liveness != LivenessInfo.NOT_ALIVE && liveness != RequestLivenessStatus.ANALYZING
+ || status == RequestFeatureStatus.FAILED) {
+ changeLiveness(facePreviewInfo.getTrackId(), RequestLivenessStatus.ANALYZING);
+ requestFaceLiveness(
+ irNv21 == null ? rgbNv21 : irNv21,
+ facePreviewInfo,
+ previewSize.width,
+ previewSize.height,
+ FaceEngine.CP_PAF_NV21,
+ irNv21 == null ? LivenessType.RGB : LivenessType.IR,
+ recognizeInfo.getWaitLock()
+ );
+ }
+ }
+ /*
+ * 对于每个人脸,若状态为空或者为失败,则请求特征提取(可根据需要添加其他判断以限制特征提取次数),
+ * 特征提取回传的人脸特征结果在{@link FaceListener#onFaceFeatureInfoGet(FaceFeature, Integer, Integer)}中回传
+ */
+ if (status == RequestFeatureStatus.TO_RETRY) {
+ changeRecognizeStatus(facePreviewInfo.getTrackId(), RequestFeatureStatus.SEARCHING);
+ requestFaceFeature(
+ rgbNv21, facePreviewInfo,
+ previewSize.width,
+ previewSize.height,
+ FaceEngine.CP_PAF_NV21
+ );
+ }
+ }
+ }
+ }
+
+ @Override
+ public void onFail(Exception e) {
+ Log.e(TAG, "onFail:" + e.getMessage());
+ }
+
+ /**
+ * 获取识别信息,识别信息为空则创建一个新的
+ *
+ * @param recognizeInfoMap 存放识别信息的map
+ * @param trackId 人脸唯一标识
+ * @return 识别信息
+ */
+ public RecognizeInfo getRecognizeInfo(Map recognizeInfoMap, int trackId) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ if (recognizeInfo == null) {
+ recognizeInfo = new RecognizeInfo();
+ recognizeInfoMap.put(trackId, recognizeInfo);
+ }
+ return recognizeInfo;
+ }
+
+ @Override
+ public void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode) {
+ //FR成功
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId);
+ if (faceFeature != null) {
+ // 人脸已离开,不用处理
+ if (recognizeInfo == null) {
+ return;
+ }
+ //不做活体检测的情况,直接搜索
+ if (!recognizeConfiguration.isEnableLiveness()) {
+ searchFace(faceFeature, trackId);
+ }
+ //活体检测通过,搜索特征
+ else if (recognizeInfo.getLiveness() == LivenessInfo.ALIVE) {
+ searchFace(faceFeature, trackId);
+ }
+ //活体检测未出结果,或者非活体,等待
+ else {
+ synchronized (recognizeInfo.getWaitLock()) {
+ try {
+ recognizeInfo.getWaitLock().wait();
+ if (recognizeInfoMap.containsKey(trackId)) {
+ onFaceFeatureInfoGet(faceFeature, trackId, errorCode);
+ }
+ } catch (InterruptedException e) {
+ Log.e(TAG, "onFaceFeatureInfoGet: 等待活体结果时退出界面会执行,正常现象,可注释异常代码块");
+ e.printStackTrace();
+ }
+ }
+ }
+
+ }
+ //特征提取失败时,为了及时提示做个UI反馈,将name修改为"ExtractCode:${errorCode}",再重置状态
+ else {
+ if (recognizeInfo.increaseAndGetExtractErrorRetryCount() > recognizeConfiguration.getExtractRetryCount()) {
+ // 在尝试最大次数后,特征提取仍然失败,则认为识别未通过
+ recognizeInfo.setExtractErrorRetryCount(0);
+ retryRecognizeDelayed(trackId);
+ } else {
+ changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
+ }
+ }
+ }
+
+ /**
+ * 延迟 {@link RecognizeConfiguration#getLivenessFailedRetryInterval()}后,重新进行活体检测
+ *
+ * @param trackId 人脸ID
+ */
+ private void retryLivenessDetectDelayed(final Integer trackId) {
+ Observable.timer(recognizeConfiguration.getLivenessFailedRetryInterval(), TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Observer() {
+ Disposable disposable;
+
+ @Override
+ public void onSubscribe(Disposable d) {
+ disposable = d;
+ delayFaceTaskCompositeDisposable.add(disposable);
+ }
+
+ @Override
+ public void onNext(Long aLong) {
+
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ e.printStackTrace();
+ }
+
+ @Override
+ public void onComplete() {
+ // 将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
+ changeLiveness(trackId, LivenessInfo.UNKNOWN);
+ delayFaceTaskCompositeDisposable.remove(disposable);
+ }
+ });
+ }
+
+ /**
+ * 延迟 {@link RecognizeConfiguration#getRecognizeFailedRetryInterval()}后,重新进行人脸识别
+ *
+ * @param trackId 人脸ID
+ */
+ private void retryRecognizeDelayed(final Integer trackId) {
+ changeRecognizeStatus(trackId, RequestFeatureStatus.FAILED);
+ Observable.timer(recognizeConfiguration.getRecognizeFailedRetryInterval(), TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(new Observer() {
+ Disposable disposable;
+
+ @Override
+ public void onSubscribe(Disposable d) {
+ disposable = d;
+ delayFaceTaskCompositeDisposable.add(disposable);
+ }
+
+ @Override
+ public void onNext(Long aLong) {
+
+ }
+
+ @Override
+ public void onError(Throwable e) {
+ e.printStackTrace();
+ }
+
+ @Override
+ public void onComplete() {
+ // 将该人脸特征提取状态置为FAILED,帧回调处理时会重新进行活体检测
+ changeRecognizeStatus(trackId, RequestFeatureStatus.TO_RETRY);
+ delayFaceTaskCompositeDisposable.remove(disposable);
+ }
+ });
+ }
+
+ @Override
+ public void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode) {
+ if (livenessInfo != null) {
+ int liveness = livenessInfo.getLiveness();
+ Log.i(TAG, "onFaceLivenessInfoGet liveness:" + liveness);
+ changeLiveness(trackId, liveness);
+ // 非活体,重试
+ if (liveness != LivenessInfo.ALIVE) {
+ noticeCurrentStatus("活体检测未通过");
+ // 延迟 FAIL_RETRY_INTERVAL 后,将该人脸状态置为UNKNOWN,帧回调处理时会重新进行活体检测
+ retryLivenessDetectDelayed(trackId);
+ }
+ } else {
+ RecognizeInfo recognizeInfo = getRecognizeInfo(recognizeInfoMap, trackId);
+ // 连续多次活体检测失败(接口调用回传值非0),将活体检测值重置为未知,会在帧回调中重新进行活体检测
+ if (recognizeInfo.increaseAndGetLivenessErrorRetryCount() > recognizeConfiguration.getLivenessRetryCount()) {
+ recognizeInfo.setLivenessErrorRetryCount(0);
+ retryLivenessDetectDelayed(trackId);
+ } else {
+ changeLiveness(trackId, LivenessInfo.UNKNOWN);
+ }
+ }
+ }
+
+ private void noticeCurrentStatus(String notice) {
+ if (recognizeCallback != null) {
+ recognizeCallback.onNoticeChanged(notice);
+ }
+ if (timerDisposable != null && !timerDisposable.isDisposed()) {
+ timerDisposable.dispose();
+ }
+ timerDisposable = Observable.timer(1500, TimeUnit.MILLISECONDS)
+ .observeOn(AndroidSchedulers.mainThread())
+ .subscribe(aLong -> {
+ if (recognizeCallback != null) {
+ recognizeCallback.onNoticeChanged("");
+ }
+ });
+ }
+
+ private void searchFace(final FaceFeature faceFeature, final Integer trackId) {
+ CompareResult compareResult = FaceServer.getInstance().searchFaceFeature(faceFeature);
+ if (compareResult == null || compareResult.getFaceEntity() == null) {
+ retryRecognizeDelayed(trackId);
+ return;
+ }
+ compareResult.setTrackId(trackId);
+ boolean pass = compareResult.getSimilar() > recognizeConfiguration.getSimilarThreshold();
+ recognizeCallback.onRecognized(compareResult, getRecognizeInfo(recognizeInfoMap, trackId).getLiveness(), pass);
+ if (pass) {
+ String msg = "识别通过";
+ Log.i(TAG, "用户名字:" + compareResult.getFaceEntity().getUserName() + ";人脸id:" + compareResult.getFaceEntity().getFaceId() + ";");
+ if (Objects.nonNull(compareResult.getFaceEntity())) {
+ FaceEntity face = compareResult.getFaceEntity();
+ if (Objects.nonNull(face.getUserName()) && Objects.nonNull(face.getFaceId())) {
+ DecimalFormat df = new DecimalFormat("0.00");
+ String formattedNumber = df.format(compareResult.getSimilar() * 100);
+ msg = face.getUserName() + "相似度" + formattedNumber + "%";
+ }
+ }
+ setName(trackId, msg);
+ noticeCurrentStatus(msg);
+ changeRecognizeStatus(trackId, RequestFeatureStatus.SUCCEED);
+ } else {
+ noticeCurrentStatus("未通过:NOT_REGISTERED");
+ retryRecognizeDelayed(trackId);
+ }
+ }
+
+ /**
+ * 人脸特征提取线程
+ */
+ public class FaceRecognizeRunnable implements Runnable {
+ private FaceInfo faceInfo;
+ private int width;
+ private int height;
+ private int format;
+ private Integer trackId;
+ private byte[] nv21Data;
+ private int isMask;
+
+ /**
+ * 异步特征提取任务的构造函数
+ *
+ * @param nv21Data 可见光图像数据
+ * @param facePreviewInfo 人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ */
+ private FaceRecognizeRunnable(byte[] nv21Data, FacePreviewInfo facePreviewInfo, int width, int height, int format) {
+ if (nv21Data == null) {
+ return;
+ }
+ this.nv21Data = nv21Data;
+ this.faceInfo = new FaceInfo(facePreviewInfo.getFaceInfoRgb());
+ this.width = width;
+ this.height = height;
+ this.format = format;
+ this.trackId = facePreviewInfo.getTrackId();
+ this.isMask = facePreviewInfo.getMask();
+ }
+
+ @Override
+ public void run() {
+ if (nv21Data != null) {
+ if (frEngine != null) {
+ if (recognizeConfiguration.isEnableImageQuality()) {
+ /*
+ * 开启人脸质量检测
+ */
+ ImageQualitySimilar qualitySimilar = new ImageQualitySimilar();
+ int iqCode;
+ long iqStartTime = System.currentTimeMillis();
+ synchronized (frEngine) {
+ iqCode = frEngine.imageQualityDetect(nv21Data, width, height, format, faceInfo, isMask, qualitySimilar);
+ }
+ Log.i(TAG, "fr iqTime:" + (System.currentTimeMillis() - iqStartTime) + "ms" + ", mask=" + isMask);
+ if (iqCode == ErrorInfo.MOK) {
+ float quality = qualitySimilar.getScore();
+ float destQuality = isMask == MaskInfo.WORN ? recognizeConfiguration.getImageQualityMaskRecognizeThreshold() :
+ recognizeConfiguration.getImageQualityNoMaskRecognizeThreshold();
+ if (quality >= destQuality) {
+ extractFace();
+ } else {
+ onFaceFail(iqCode, "fr imageQualityDetect score invalid");
+ }
+ } else {
+ onFaceFail(iqCode, "fr imageQuality failed errorCode is " + iqCode);
+ }
+ } else {
+ extractFace();
+ }
+ } else {
+ onFaceFail(ERROR_FR_ENGINE_IS_NULL, "fr failed ,frEngine is null");
+ }
+ }
+ nv21Data = null;
+ }
+
+ /**
+ * 对人脸图像进行特征提取
+ */
+ private void extractFace() {
+ long irStartTime = System.currentTimeMillis();
+ FaceFeature faceFeature = new FaceFeature();
+ int frCode;
+ synchronized (frEngine) {
+ /*
+ * 该场景为识别场景,所以参数“ExtractType”值为ExtractType.RECOGNIZE,且参数“mask”值为实际检测到的值,即isMask
+ */
+ frCode = frEngine.extractFaceFeature(nv21Data, width, height, format, faceInfo, ExtractType.RECOGNIZE, isMask, faceFeature);
+ }
+ Log.i(TAG, "frTime:" + (System.currentTimeMillis() - irStartTime) + "ms");
+ if (frCode == ErrorInfo.MOK) {
+ onFaceFeatureInfoGet(faceFeature, trackId, frCode);
+ } else {
+ onFaceFail(frCode, "fr failed errorCode is " + frCode);
+ }
+ }
+
+ private void onFaceFail(int code, String errorMsg) {
+ onFaceFeatureInfoGet(null, trackId, code);
+ onFail(new Exception(errorMsg));
+ }
+ }
+
+ /**
+ * 活体检测的线程
+ */
+ public class FaceLivenessDetectRunnable implements Runnable {
+ private FaceInfo faceInfo;
+ private int width;
+ private int height;
+ private int format;
+ private Integer trackId;
+ private byte[] nv21Data;
+ private LivenessType livenessType;
+ private Object waitLock;
+
+ /**
+ * 异步活体任务的构造函数
+ *
+ * @param nv21Data 可见光或红外图像数据
+ * @param faceInfo 可见光人脸检测得到的人脸信息
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param format 图像格式
+ * @param livenessType 活体检测类型,可以是可见光活体检测{@link LivenessType#RGB}或红外活体检测{@link LivenessType#IR}
+ * @param waitLock 活体检测通过后,调用该对象的notifyAll函数,通知识别线程活体已通过
+ */
+ private FaceLivenessDetectRunnable(byte[] nv21Data, FacePreviewInfo faceInfo, int width, int height, int format, LivenessType livenessType, Object waitLock) {
+ if (nv21Data == null) {
+ return;
+ }
+ this.nv21Data = nv21Data;
+ this.faceInfo = new FaceInfo(faceInfo.getFaceInfoRgb());
+ this.width = width;
+ this.height = height;
+ this.format = format;
+ this.trackId = faceInfo.getTrackId();
+ this.livenessType = livenessType;
+ this.waitLock = waitLock;
+ }
+
+ @Override
+ public void run() {
+ if (nv21Data != null) {
+ if (flEngine != null) {
+ processLiveness();
+ } else {
+ onProcessFail(ERROR_FL_ENGINE_IS_NULL, "fl failed ,frEngine is null");
+ }
+ }
+ nv21Data = null;
+ }
+
+ /**
+ * 执行活体检测
+ */
+ private void processLiveness() {
+ List livenessInfoList = new ArrayList<>();
+ int flCode = -1;
+ synchronized (flEngine) {
+ long flStartTime = System.currentTimeMillis();
+ if (livenessType == LivenessType.RGB) {
+ // RGB活体检测
+ flCode = flEngine.process(nv21Data, width, height, format, Arrays.asList(faceInfo), FaceEngine.ASF_LIVENESS);
+ } else {
+ // IR活体检测
+ FaceInfo rgbFaceInfo = faceInfo;
+ List irFaceInfoList = new ArrayList<>();
+ int fdCode = flEngine.detectFaces(nv21Data, width, height, format, irFaceInfoList);
+ boolean isFaceOverlap = false;
+ if (fdCode == ErrorInfo.MOK && irFaceInfoList.size() > 0) {
+ keepMaxFace(irFaceInfoList);
+ isFaceOverlap = isFaceSame(rgbFaceInfo, irFaceInfoList.get(0), 0.3);
+ Log.i(TAG, "isFaceOverlap:" + isFaceOverlap);
+ }
+ if (fdCode == ErrorInfo.MOK && isFaceOverlap) {
+ flCode = flEngine.processIr(nv21Data, width, height, format, irFaceInfoList, FaceEngine.ASF_IR_LIVENESS);
+ } else {
+ onFail(new Exception("ir detectFaces failed fdCode:" + fdCode + ", flCode:" + flCode + ",isFaceOverlap:" + isFaceOverlap));
+ }
+ }
+ Log.i(TAG, "flTime:" + (System.currentTimeMillis() - flStartTime) + "ms");
+ }
+ if (flCode == ErrorInfo.MOK) {
+ if (livenessType == LivenessType.RGB) {
+ flCode = flEngine.getLiveness(livenessInfoList);
+ } else {
+ flCode = flEngine.getIrLiveness(livenessInfoList);
+ }
+ }
+
+ if (flCode == ErrorInfo.MOK && !livenessInfoList.isEmpty()) {
+ onFaceLivenessInfoGet(livenessInfoList.get(0), trackId, flCode);
+ if (livenessInfoList.get(0).getLiveness() == LivenessInfo.ALIVE) {
+ synchronized (waitLock) {
+ waitLock.notifyAll();
+ }
+ }
+ } else {
+ onProcessFail(flCode, "fl failed errorCode is " + flCode);
+ }
+ }
+
+ private void onProcessFail(int code, String msg) {
+ onFaceLivenessInfoGet(null, trackId, code);
+ onFail(new Exception(msg));
+ }
+ }
+
+ /**
+ * 判断IR图片中的人脸FaceInfo和RGB图片中的人脸FaceInfo是否是同一个人脸
+ * 此方案原理:判断两个FaceInfo是否有相交区域,如果有相交区域,则说明是同一个人脸
+ *
+ * @param faceInfoList 人脸信息列表
+ * @param faceInfo 人脸信息
+ * @return 人脸信息列表中是否有人脸和传入的人脸信息相交
+ */
+ public static boolean isSameFace(List faceInfoList, FaceInfo faceInfo) {
+ if (faceInfoList == null || faceInfoList.isEmpty() || faceInfo == null) {
+ return false;
+ }
+ for (FaceInfo info : faceInfoList) {
+ if (Rect.intersects(faceInfo.getRect(), info.getRect())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ /**
+ * 计算两个人脸框的重叠面积,以判断它们的重叠程度
+ *
+ * @param rect1 人脸框
+ * @param rect2 人脸框
+ * @return
+ */
+
+ public static Rect getOverlapArea(Rect rect1, Rect rect2) {
+ // 计算两个矩形的重叠区域
+ int left = Math.max(rect1.left, rect2.left);
+ int right = Math.min(rect1.right, rect2.right);
+ int top = Math.max(rect1.top, rect2.top);
+ int bottom = Math.min(rect1.bottom, rect2.bottom);
+
+ // 如果没有重叠区域,则返回空的矩形
+ if (left < right && top < bottom) {
+ return new Rect(left, top, right, bottom);
+ }
+ return null; // 无重叠
+ }
+
+ /**
+ * 设置重叠阈值,当两个矩形的重叠面积超过这个阈值时,认为是同一张脸
+ *
+ * @param rgbFaceInfo rgb人脸信息
+ * @param irFaceInfo ir人脸信息
+ * @param threshold 重叠阈值
+ * @return Boolean
+ */
+
+ public static boolean isFaceSame(FaceInfo rgbFaceInfo, FaceInfo irFaceInfo, double threshold) {
+ Rect rect1 = rgbFaceInfo.getRect();
+ Rect rect2 = irFaceInfo.getRect();
+ Rect overlapRect = getOverlapArea(rect1, rect2);
+ if (overlapRect != null) {
+ int overlapArea = overlapRect.width() * overlapRect.height();
+ int face1Area = rect1.width() * rect1.height();
+ int face2Area = rect2.width() * rect2.height();
+ // 计算重叠面积占两个矩形面积的比例
+ double overlapRatio = (double) overlapArea / Math.min(face1Area, face2Area);
+ Log.d(TAG, "overlapRatio:" + overlapRatio + "," + threshold);
+
+ return overlapRatio > threshold;
+ }
+ return false;
+ }
+
+ /**
+ * 刷新trackId
+ *
+ * @param ftFaceList 传入的人脸列表
+ */
+ private void refreshTrackId(List ftFaceList) {
+ currentTrackIdList.clear();
+ for (FaceInfo faceInfo : ftFaceList) {
+ currentTrackIdList.add(faceInfo.getFaceId() + trackedFaceCount);
+ }
+ if (!ftFaceList.isEmpty()) {
+ currentMaxFaceId = ftFaceList.get(ftFaceList.size() - 1).getFaceId();
+ }
+ }
+
+ /**
+ * 获取当前的最大trackID,可用于退出时保存
+ *
+ * @return 当前trackId
+ */
+ public int getTrackedFaceCount() {
+ // 引擎的人脸下标从0开始,因此需要+1
+ return trackedFaceCount + currentMaxFaceId + 1;
+ }
+
+ /**
+ * 新增搜索成功的人脸
+ *
+ * @param trackId 指定的trackId
+ * @param name trackId对应的人脸
+ */
+ public void setName(int trackId, String name) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ if (recognizeInfo != null) {
+ recognizeInfo.setName(name);
+ }
+ }
+
+
+ /**
+ * 设置转换方式,用于IR活体检测
+ *
+ * @param transformer 转换方式
+ */
+ public void setDualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer transformer) {
+ this.dualCameraFaceInfoTransformer = transformer;
+ }
+
+
+ public String getName(int trackId) {
+ RecognizeInfo recognizeInfo = recognizeInfoMap.get(trackId);
+ return recognizeInfo == null ? null : recognizeInfo.getName();
+ }
+
+
+ /**
+ * 设置可识别区域(相对于View)
+ *
+ * @param recognizeArea 可识别区域
+ */
+ public void setRecognizeArea(Rect recognizeArea) {
+ if (recognizeArea != null) {
+ this.recognizeArea.set(recognizeArea);
+ }
+ }
+
+ @IntDef(value = {
+ RequestFeatureStatus.FAILED,
+ RequestFeatureStatus.SEARCHING,
+ RequestFeatureStatus.SUCCEED,
+ RequestFeatureStatus.TO_RETRY
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RequestFaceFeatureStatus {
+ }
+
+ @IntDef(value = {
+ LivenessInfo.ALIVE,
+ LivenessInfo.NOT_ALIVE,
+ LivenessInfo.UNKNOWN,
+ LivenessInfo.FACE_NUM_MORE_THAN_ONE,
+ LivenessInfo.FACE_TOO_SMALL,
+ LivenessInfo.FACE_ANGLE_TOO_LARGE,
+ LivenessInfo.FACE_BEYOND_BOUNDARY,
+ RequestLivenessStatus.ANALYZING
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ @interface RequestFaceLivenessStatus {
+ }
+
+ /**
+ * 修改人脸识别的状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @param newStatus 新的识别状态,详见{@link RequestFeatureStatus}中的定义
+ */
+ public void changeRecognizeStatus(int trackId, @RequestFaceFeatureStatus int newStatus) {
+ getRecognizeInfo(recognizeInfoMap, trackId).setRecognizeStatus(newStatus);
+ }
+
+ /**
+ * 修改活体活体值或活体检测状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @param newLiveness 新的活体值或活体检测状态
+ */
+ public void changeLiveness(int trackId, @RequestFaceLivenessStatus int newLiveness) {
+ getRecognizeInfo(recognizeInfoMap, trackId).setLiveness(newLiveness);
+ }
+
+ /**
+ * 获取活体值或活体检测状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @return 活体值或活体检测状态
+ */
+ public Integer getLiveness(int trackId) {
+ return getRecognizeInfo(recognizeInfoMap, trackId).getLiveness();
+ }
+
+ /**
+ * 获取人脸识别状态
+ *
+ * @param trackId 根据VIDEO模式人脸检测获取的人脸的唯一标识
+ * @return 人脸识别状态
+ */
+ public Integer getRecognizeStatus(int trackId) {
+ return getRecognizeInfo(recognizeInfoMap, trackId).getRecognizeStatus();
+ }
+
+ /**
+ * 保留ftFaceList中最大的人脸
+ *
+ * @param ftFaceList 人脸追踪时,一帧数据的人脸信息
+ */
+ private static void keepMaxFace(List ftFaceList) {
+ if (ftFaceList == null || ftFaceList.size() <= 1) {
+ return;
+ }
+ FaceInfo maxFaceInfo = ftFaceList.get(0);
+ for (FaceInfo faceInfo : ftFaceList) {
+ if (faceInfo.getRect().width() > maxFaceInfo.getRect().width()) {
+ maxFaceInfo = faceInfo;
+ }
+ }
+ ftFaceList.clear();
+ ftFaceList.add(maxFaceInfo);
+ }
+
+
+ public static final class Builder {
+ private FaceEngine ftEngine;
+ private FaceEngine maskEngine;
+ private FaceEngine frEngine;
+ private FaceEngine flEngine;
+ private Camera.Size previewSize;
+ private boolean onlyDetectLiveness;
+ private RecognizeConfiguration recognizeConfiguration;
+ private RecognizeCallback recognizeCallback;
+ private IDualCameraFaceInfoTransformer dualCameraFaceInfoTransformer;
+ private int frQueueSize;
+ private int flQueueSize;
+ private int trackedFaceCount;
+
+ public Builder() {
+ }
+
+ public Builder recognizeConfiguration(RecognizeConfiguration val) {
+ recognizeConfiguration = val;
+ return this;
+ }
+
+ public Builder dualCameraFaceInfoTransformer(IDualCameraFaceInfoTransformer val) {
+ dualCameraFaceInfoTransformer = val;
+ return this;
+ }
+
+ public Builder recognizeCallback(RecognizeCallback val) {
+ recognizeCallback = val;
+ return this;
+ }
+
+ public Builder ftEngine(FaceEngine val) {
+ ftEngine = val;
+ return this;
+ }
+
+ public Builder maskEngine(FaceEngine val) {
+ maskEngine = val;
+ return this;
+ }
+
+ public Builder frEngine(FaceEngine val) {
+ frEngine = val;
+ return this;
+ }
+
+ public Builder flEngine(FaceEngine val) {
+ flEngine = val;
+ return this;
+ }
+
+ public Builder previewSize(Camera.Size val) {
+ previewSize = val;
+ return this;
+ }
+
+ public Builder frQueueSize(int val) {
+ frQueueSize = val;
+ return this;
+ }
+
+ public Builder flQueueSize(int val) {
+ flQueueSize = val;
+ return this;
+ }
+
+ public Builder trackedFaceCount(int val) {
+ trackedFaceCount = val;
+ return this;
+ }
+
+ public Builder onlyDetectLiveness(boolean val) {
+ onlyDetectLiveness = val;
+ return this;
+ }
+
+ public FaceHelper build() {
+ return new FaceHelper(this);
+ }
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java
new file mode 100644
index 0000000..55bf32d
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/FaceRectTransformer.java
@@ -0,0 +1,222 @@
+package io.flutter.plugins.facediscern.tool.face.help;
+
+import android.graphics.Rect;
+import android.hardware.Camera;
+
+/**
+ * 将检测回传的人脸框(基于NV21数据)转换为View绘制(基于View)所需的人脸框
+ */
+public class FaceRectTransformer {
+ private int previewWidth, previewHeight, canvasWidth, canvasHeight, cameraDisplayOrientation, cameraId;
+ private boolean isMirror;
+ private boolean mirrorHorizontal = false, mirrorVertical = false;
+
+ /**
+ * 创建一个绘制辅助类对象,并且设置绘制相关的参数
+ *
+ * @param previewWidth 预览宽度
+ * @param previewHeight 预览高度
+ * @param canvasWidth 绘制控件的宽度
+ * @param canvasHeight 绘制控件的高度
+ * @param cameraDisplayOrientation 旋转角度
+ * @param cameraId 相机ID
+ * @param isMirror 是否水平镜像显示(若相机是镜像显示的,设为true,用于纠正)
+ * @param mirrorHorizontal 为兼容部分设备使用,水平再次镜像
+ * @param mirrorVertical 为兼容部分设备使用,垂直再次镜像
+ */
+ public FaceRectTransformer(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;
+ int canvasWidth = this.canvasWidth;
+ int canvasHeight = this.canvasHeight;
+ int cameraDisplayOrientation = this.cameraDisplayOrientation;
+ int cameraId = this.cameraId;
+ boolean isMirror = this.isMirror;
+ boolean mirrorHorizontal = this.mirrorHorizontal;
+ boolean mirrorVertical = this.mirrorVertical;
+
+ if (ftRect == null) {
+ return null;
+ }
+
+ Rect rect = new Rect(ftRect);
+ float horizontalRatio;
+ float verticalRatio;
+ 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();
+ 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 void setPreviewWidth(int previewWidth) {
+ this.previewWidth = previewWidth;
+ }
+
+ public void setPreviewHeight(int previewHeight) {
+ this.previewHeight = previewHeight;
+ }
+
+ public void setCanvasWidth(int canvasWidth) {
+ this.canvasWidth = canvasWidth;
+ }
+
+ public void setCanvasHeight(int canvasHeight) {
+ this.canvasHeight = canvasHeight;
+ }
+
+ public void setCameraDisplayOrientation(int cameraDisplayOrientation) {
+ this.cameraDisplayOrientation = cameraDisplayOrientation;
+ }
+
+ public void setCameraId(int cameraId) {
+ this.cameraId = cameraId;
+ }
+
+ public void setMirror(boolean mirror) {
+ isMirror = mirror;
+ }
+
+ public int getPreviewWidth() {
+ return previewWidth;
+ }
+
+ public int getPreviewHeight() {
+ return previewHeight;
+ }
+
+ public int getCanvasWidth() {
+ return canvasWidth;
+ }
+
+ public int getCanvasHeight() {
+ return canvasHeight;
+ }
+
+ public int getCameraDisplayOrientation() {
+ return cameraDisplayOrientation;
+ }
+
+ public int getCameraId() {
+ return cameraId;
+ }
+
+ public boolean isMirror() {
+ return isMirror;
+ }
+
+ public boolean isMirrorHorizontal() {
+ return mirrorHorizontal;
+ }
+
+ public void setMirrorHorizontal(boolean mirrorHorizontal) {
+ this.mirrorHorizontal = mirrorHorizontal;
+ }
+
+ public boolean isMirrorVertical() {
+ return mirrorVertical;
+ }
+
+ public void setMirrorVertical(boolean mirrorVertical) {
+ this.mirrorVertical = mirrorVertical;
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java
new file mode 100644
index 0000000..56d3cd6
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/FaceListener.java
@@ -0,0 +1,37 @@
+package io.flutter.plugins.facediscern.tool.face.help.callback;
+
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.LivenessInfo;
+
+/**
+ * 人脸处理回调
+ */
+public interface FaceListener {
+ /**
+ * 当出现异常时执行
+ *
+ * @param e 异常信息
+ */
+ void onFail(Exception e);
+
+
+ /**
+ * 请求人脸特征后的回调
+ *
+ * @param faceFeature 人脸特征数据
+ * @param trackId 人脸Id(相当于请求码)
+ * @param errorCode 错误码
+ */
+ void onFaceFeatureInfoGet(@Nullable FaceFeature faceFeature, Integer trackId, Integer errorCode);
+
+ /**
+ * 请求活体检测后的回调
+ *
+ * @param livenessInfo 活体检测结果
+ * @param trackId 人脸Id(相当于请求码)
+ * @param errorCode 错误码
+ */
+ void onFaceLivenessInfoGet(@Nullable LivenessInfo livenessInfo, Integer trackId, Integer errorCode);
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java
new file mode 100644
index 0000000..6a98cae
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/IDualCameraFaceInfoTransformer.java
@@ -0,0 +1,16 @@
+package io.flutter.plugins.facediscern.tool.face.help.callback;
+
+import com.arcsoft.face.FaceInfo;
+
+/**
+ * 设置双目识别时,将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式
+ */
+public interface IDualCameraFaceInfoTransformer {
+ /**
+ * 将RGB Camera帧数据检测到的人脸信息用于IR Camera帧数据活体检测时的转换方式
+ *
+ * @param faceInfo RGB Camera帧数据检测到的人脸信息
+ * @return 转换后,用于IR活体检测的FaceInfo
+ */
+ FaceInfo transformFaceInfo(FaceInfo faceInfo);
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java
new file mode 100644
index 0000000..ebda709
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/help/callback/RecognizeCallback.java
@@ -0,0 +1,20 @@
+package io.flutter.plugins.facediscern.tool.face.help.callback;
+
+
+import io.flutter.plugins.facediscern.tool.face.model.CompareResult;
+
+public interface RecognizeCallback {
+ /**
+ * 识别结果回调
+ *
+ * @param compareResult 比对结果
+ * @param liveness 活体值
+ * @param similarPass 是否通过(依据设置的阈值)
+ */
+ void onRecognized(CompareResult compareResult, Integer liveness, boolean similarPass);
+
+ /**
+ * 提示文字变更的回调
+ */
+ void onNoticeChanged(String notice);
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java
new file mode 100644
index 0000000..8ac8a88
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/image/ImageUtil.java
@@ -0,0 +1,229 @@
+package io.flutter.plugins.facediscern.tool.face.image;
+
+import android.content.ContentResolver;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Rect;
+import android.net.Uri;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public class ImageUtil {
+ public static final int DEFAULT_MAX_WIDTH = 1920;
+ public static final int DEFAULT_MAX_HEIGHT = 1080;
+
+
+ private static final int MASK_A = 0xFF000000;
+ private static final int MASK_R = 0x00FF0000;
+ private static final int MASK_G = 0x0000FF00;
+ private static final int MASK_B = 0x000000FF;
+
+ public static int rgbToY(int r, int g, int b) {
+ return (((66 * r + 129 * g + 25 * b + 128) >> 8) + 16);
+ }
+
+ public static int rgbToU(int r, int g, int b) {
+ return (((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128);
+ }
+
+ public static int rgbToV(int r, int g, int b) {
+ return (((112 * r - 94 * g - 18 * b + 128) >> 8) + 128);
+ }
+
+ public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, Rect rect) {
+ if (rect == null || rect.isEmpty()) {
+ return;
+ }
+ drawRectOnNv21(nv21, width, height, color, strokeWidth, rect.left, rect.top, rect.right, rect.bottom);
+ }
+
+ public static void drawRectOnNv21(byte[] nv21, int width, int height, int color, int strokeWidth, int left, int top,
+ int right, int bottom) {
+ if ((strokeWidth & 1) == 1) {
+ strokeWidth += 1;
+ }
+ // 确保边界是4的倍数
+ left &= ~0b11;
+ top &= ~0b11;
+ right &= ~0b11;
+ bottom &= ~0b11;
+ // 对于溢出图像的边,不绘制
+ boolean drawLeft = true, drawTop = true, drawRight = true, drawBottom = true;
+ if (left <= 0) {
+ left = 0;
+ drawLeft = false;
+ }
+ if (top <= 0) {
+ top = 0;
+ drawTop = false;
+ }
+ if (right >= width) {
+ right = width;
+ drawRight = false;
+ }
+ if (bottom >= height) {
+ bottom = height;
+ drawBottom = false;
+ }
+
+ // 取出R G B的值,并转换为Y U V
+ int r = (color & MASK_R) >> 16;
+ int g = (color & MASK_G) >> 8;
+ int b = color & MASK_B;
+ int y = rgbToY(r, g, b);
+ int u = rgbToU(r, g, b);
+ int v = rgbToV(r, g, b);
+
+ // 根据边框的strokeWidth确定内边界
+ int innerTop = top + strokeWidth;
+ int innerBottom = bottom - strokeWidth;
+ int innerRight = right - strokeWidth;
+
+ int horizontalPixels = right - left;
+ int yStartIndex;
+ int uvStartIndex;
+ boolean drawUV;
+ if (drawTop) {
+ yStartIndex = top * width + left;
+ uvStartIndex = width * height + ((top / 2 * width) + left);
+ drawUV = false;
+ for (int i = top; i < innerTop; i++) {
+ for (int j = 0; j < horizontalPixels; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < horizontalPixels; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+
+ if (drawLeft) {
+ //左边
+ yStartIndex = innerTop * width + left;
+ uvStartIndex = width * height + (innerTop / 2 * width + left);
+ drawUV = false;
+ for (int i = innerTop; i < innerBottom; i++) {
+ for (int j = 0; j < strokeWidth; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < strokeWidth; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+ if (drawRight) {
+ //右边
+ yStartIndex = innerTop * width + innerRight;
+ uvStartIndex = width * height + (innerTop / 2 * width + innerRight);
+ drawUV = false;
+ for (int i = innerTop; i < innerBottom; i++) {
+ for (int j = 0; j < strokeWidth; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < strokeWidth; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+
+ if (drawBottom) {
+ //下边
+ yStartIndex = innerBottom * width + left;
+ uvStartIndex = width * height + ((innerBottom / 2 * width) + left);
+ drawUV = false;
+ for (int i = innerBottom; i < bottom; i++) {
+ for (int j = 0; j < horizontalPixels; j++) {
+ nv21[yStartIndex + j] = (byte) y;
+ }
+ yStartIndex += width;
+ if (drawUV = !drawUV) {
+ for (int j = 0; j < horizontalPixels; j += 2) {
+ nv21[uvStartIndex + j] = (byte) v;
+ nv21[uvStartIndex + j + 1] = (byte) u;
+ }
+ uvStartIndex += width;
+ }
+ }
+ }
+ }
+
+ /**
+ * 缩放图像,如果需要缩放,就顺便把宽高对齐给做了
+ *
+ * @param bitmap 原图
+ * @param maxWidth 最大目标宽度
+ * @param maxHeight 最大目标高度
+ * @return 缩放后的图像
+ */
+ public static Bitmap scaleBitmap(Bitmap bitmap, int maxWidth, int maxHeight) {
+ float horizontalScale = ((float) bitmap.getWidth()) / maxWidth;
+ float verticalScale = ((float) bitmap.getHeight()) / maxHeight;
+ if (horizontalScale < 1 || verticalScale < 1) {
+ return bitmap;
+ }
+ float maxScale = Math.max(horizontalScale, verticalScale);
+ // 确保为4的倍数
+ int newWidth = (int) (bitmap.getWidth() / maxScale) & ~0b11;
+ int newHeight = (int) (bitmap.getHeight() / maxScale) & ~0b11;
+
+ return Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true);
+ }
+
+ /**
+ * 将Uri转换为Bitmap,并限制最大宽高
+ */
+ public static Bitmap uriToScaledBitmap(Context context, Uri uri, int maxWidth, int maxHeight) {
+ ContentResolver contentResolver = context.getContentResolver();
+ byte[] data;
+ try {
+ InputStream input = null;
+ input = contentResolver.openInputStream(uri);
+ data = new byte[input.available()];
+ input.read(data);
+ input.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return jpegToScaledBitmap(data, maxWidth, maxHeight);
+ }
+
+ /**
+ * 将jpeg形式的压缩图像转换为Bitmap,并限制最大宽高
+ *
+ * @param jpeg jpeg图像数据
+ * @param maxWidth 限制的最大宽度
+ * @param maxHeight 限制的最大高度
+ * @return 宽高小于限制值的Bitmap对象
+ */
+ public static Bitmap jpegToScaledBitmap(byte[] jpeg, int maxWidth, int maxHeight) {
+ BitmapFactory.Options options = new BitmapFactory.Options();
+ options.inJustDecodeBounds = true;
+ BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
+
+ int inSampleSize = 1;
+ while (options.outWidth / inSampleSize > maxWidth || options.outHeight / inSampleSize > maxHeight) {
+ inSampleSize++;
+ }
+ options.inSampleSize = inSampleSize;
+ options.inJustDecodeBounds = false;
+ return BitmapFactory.decodeByteArray(jpeg, 0, jpeg.length, options);
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java
new file mode 100644
index 0000000..82df7ff
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/CompareResult.java
@@ -0,0 +1,58 @@
+package io.flutter.plugins.facediscern.tool.face.model;
+
+
+
+public class CompareResult {
+ private FaceEntity faceEntity;
+ private float similar;
+ private int trackId;
+ private int compareCode;
+ private long cost;
+
+ public CompareResult(FaceEntity faceEntity, float similar, int compareCode,long cost) {
+ this.faceEntity = faceEntity;
+ this.similar = similar;
+ this.compareCode = compareCode;
+ this.cost = cost;
+ }
+
+ public FaceEntity getFaceEntity() {
+ return faceEntity;
+ }
+
+ public void setFaceEntity(FaceEntity faceEntity) {
+ this.faceEntity = faceEntity;
+ }
+
+ public float getSimilar() {
+ return similar;
+ }
+
+ public void setSimilar(float similar) {
+ this.similar = similar;
+ }
+
+ public int getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(int trackId) {
+ this.trackId = trackId;
+ }
+
+ public int getCompareCode() {
+ return compareCode;
+ }
+
+ public void setCompareCode(int compareCode) {
+ this.compareCode = compareCode;
+ }
+
+ public long getCost() {
+ return cost;
+ }
+
+ public void setCost(long cost) {
+ this.cost = cost;
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java
new file mode 100644
index 0000000..cce18d4
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FaceEntity.java
@@ -0,0 +1,78 @@
+package io.flutter.plugins.facediscern.tool.face.model;
+
+
+public class FaceEntity {
+ // 人脸模型id
+ Integer faceId;
+ // 用户id
+ String userId;
+ // 用户人脸样本id
+ Integer sampleId;
+ // 用户名
+ String userName;
+ // 人脸图片文件路径
+ String faceFilePath;
+ // 人脸模型
+ byte[] faceModel;
+
+ public FaceEntity() {
+
+ }
+
+ public FaceEntity(byte[] faceModel) {
+ this.faceModel = faceModel;
+ }
+
+ public FaceEntity(Integer faceId, byte[] faceModel) {
+ this.faceId = faceId;
+ this.faceModel = faceModel;
+ }
+
+ public Integer getFaceId() {
+ return faceId;
+ }
+
+ public void setFaceId(Integer faceId) {
+ this.faceId = faceId;
+ }
+
+ public byte[] getFaceModel() {
+ return faceModel;
+ }
+
+ public void setFaceModel(byte[] faceModel) {
+ this.faceModel = faceModel;
+ }
+
+ public String getUserId() {
+ return userId;
+ }
+
+ public void setUserId(String userId) {
+ this.userId = userId;
+ }
+
+ public Integer getSampleId() {
+ return sampleId;
+ }
+
+ public void setSampleId(Integer sampleId) {
+ this.sampleId = sampleId;
+ }
+
+ public String getUserName() {
+ return userName;
+ }
+
+ public void setUserName(String userName) {
+ this.userName = userName;
+ }
+
+ public String getFaceFilePath() {
+ return faceFilePath;
+ }
+
+ public void setFaceFilePath(String faceFilePath) {
+ this.faceFilePath = faceFilePath;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java
new file mode 100644
index 0000000..41e85f3
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/FacePreviewInfo.java
@@ -0,0 +1,156 @@
+package io.flutter.plugins.facediscern.tool.face.model;
+
+import android.graphics.Rect;
+
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.LivenessInfo;
+
+/**
+ * 人脸追踪时的信息
+ */
+public class FacePreviewInfo {
+ /**
+ * RGB人脸信息,包括人脸框和人脸角度
+ */
+ private FaceInfo faceInfoRgb;
+ /**
+ * IR人脸信息,包括人脸框和人脸角度
+ */
+ private FaceInfo faceInfoIr;
+ /**
+ * 可见光成像对应的用于FaceRectView绘制的Rect
+ */
+ private Rect rgbTransformedRect;
+ /**
+ * 红外成像对应的用于FaceRectView绘制的Rect
+ */
+ private Rect irTransformedRect;
+ private int rgbLiveness = LivenessInfo.UNKNOWN;
+ private int irLiveness = LivenessInfo.UNKNOWN;
+ private float imageQuality = 0f;
+ /**
+ * 识别区域是否合法
+ */
+ private boolean recognizeAreaValid;
+ /**
+ * 基于{@link FaceInfo#getFaceId()}的一个偏移值,可理解为SDK截至目前检测到的人次,唯一性同faceId
+ */
+ private int trackId;
+ /**
+ * 整体质量是否通过,包括人脸大小、角度、移动速度等
+ */
+ private boolean qualityPass = true;
+
+ /**
+ * 是否戴口罩
+ */
+ private int mask;
+
+ private Rect foreRect;
+
+ public Rect getForeRect() {
+ return foreRect;
+ }
+
+ public void setForeRect(Rect foreRect) {
+ this.foreRect = foreRect;
+ }
+
+
+ public FacePreviewInfo(FaceInfo faceInfoRgb) {
+ this.faceInfoRgb = faceInfoRgb;
+ }
+ public FacePreviewInfo(FaceInfo faceInfoRgb, int trackId) {
+ this.faceInfoRgb = faceInfoRgb;
+ this.trackId = trackId;
+ }
+
+ public FaceInfo getFaceInfoRgb() {
+ return faceInfoRgb;
+ }
+
+ public void setFaceInfoRgb(FaceInfo faceInfoRgb) {
+ this.faceInfoRgb = faceInfoRgb;
+ }
+
+
+ public int getTrackId() {
+ return trackId;
+ }
+
+ public void setTrackId(int trackId) {
+ this.trackId = trackId;
+ }
+
+ public void setRgbTransformedRect(Rect rgbTransformedRect) {
+ this.rgbTransformedRect = rgbTransformedRect;
+ }
+
+ public void setIrTransformedRect(Rect irTransformedRect) {
+ this.irTransformedRect = irTransformedRect;
+ }
+
+ public Rect getRgbTransformedRect() {
+ return rgbTransformedRect;
+ }
+
+ public Rect getIrTransformedRect() {
+ return irTransformedRect;
+ }
+
+ public boolean isRecognizeAreaValid() {
+ return recognizeAreaValid;
+ }
+
+ public void setRecognizeAreaValid(boolean recognizeAreaValid) {
+ this.recognizeAreaValid = recognizeAreaValid;
+ }
+
+ public void setFaceInfoIr(FaceInfo faceInfoIr) {
+ this.faceInfoIr = faceInfoIr;
+ }
+
+ public FaceInfo getFaceInfoIr() {
+ return faceInfoIr;
+ }
+
+ public int getRgbLiveness() {
+ return rgbLiveness;
+ }
+
+ public void setRgbLiveness(int rgbLiveness) {
+ this.rgbLiveness = rgbLiveness;
+ }
+
+ public int getIrLiveness() {
+ return irLiveness;
+ }
+
+ public void setIrLiveness(int irLiveness) {
+ this.irLiveness = irLiveness;
+ }
+
+ public void setImageQuality(float imageQuality) {
+ this.imageQuality = imageQuality;
+ }
+
+ public float getImageQuality() {
+ return imageQuality;
+ }
+
+ public boolean isQualityPass() {
+ return qualityPass;
+ }
+
+ public void setQualityPass(boolean qualityPass) {
+ this.qualityPass = qualityPass;
+ }
+
+ public int getMask() {
+ return mask;
+ }
+
+ public void setMask(int mask) {
+ this.mask = mask;
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java
new file mode 100644
index 0000000..e67f62e
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeConfiguration.java
@@ -0,0 +1,300 @@
+package io.flutter.plugins.facediscern.tool.face.model;
+
+import com.arcsoft.face.LivenessParam;
+
+/**
+ * 识别相关的配置项
+ */
+public class RecognizeConfiguration {
+ /**
+ * 产生特征提取失败示语的特征提取次数(小于该值不提示)
+ */
+ private int extractRetryCount;
+ /**
+ * 产生活体检测失败示语的活体检测次数(小于该值不提示)
+ */
+ private int livenessRetryCount;
+ /**
+ * 最大人脸检测数量
+ */
+ private int maxDetectFaces;
+ /**
+ * 识别阈值
+ */
+ private float similarThreshold;
+ /**
+ * 图像质量检测阈值:适用于不戴口罩且人脸识别场景
+ */
+ private float imageQualityNoMaskRecognizeThreshold;
+ /**
+ * 图像质量检测阈值:适用于戴口罩且人脸识别场景
+ */
+ private float imageQualityMaskRecognizeThreshold;
+ /**
+ * 识别失败重试间隔
+ */
+ private int recognizeFailedRetryInterval;
+ /**
+ * 活体检测未通过重试间隔
+ */
+ private int livenessFailedRetryInterval;
+ /**
+ * 启用活体
+ */
+ private boolean enableLiveness;
+ /**
+ * 启用图像质量检测
+ */
+ private boolean enableImageQuality;
+ /**
+ * 识别区域限制
+ */
+ private boolean enableFaceAreaLimit;
+ /**
+ * 仅识别最大人脸
+ */
+ private boolean keepMaxFace;
+ /**
+ * 活体阈值设置
+ */
+ private LivenessParam livenessParam;
+
+
+ /**
+ * 启用人脸边长限制
+ */
+ private boolean enableFaceSizeLimit = false;
+ /**
+ * 启用人脸移动限制
+ */
+ private boolean enableFaceMoveLimit = false;
+ /**
+ * 人脸边长限制值
+ */
+ private int faceSizeLimit = 0;
+ /**
+ * 人脸上下针移动限制值
+ */
+ private int faceMoveLimit = 0;
+
+
+ public RecognizeConfiguration(Builder builder) {
+ this.extractRetryCount = builder.extractRetryCount;
+ this.livenessRetryCount = builder.livenessRetryCount;
+ this.livenessFailedRetryInterval = builder.livenessFailedRetryInterval;
+ this.maxDetectFaces = builder.maxDetectFaces;
+ this.similarThreshold = builder.similarThreshold;
+ this.imageQualityNoMaskRecognizeThreshold = builder.imageQualityNoMaskRecognizeThreshold;
+ this.imageQualityMaskRecognizeThreshold = builder.imageQualityMaskRecognizeThreshold;
+ this.enableLiveness = builder.enableLiveness;
+ this.enableImageQuality = builder.enableImageQuality;
+ this.enableFaceAreaLimit = builder.enableFaceAreaLimit;
+ this.keepMaxFace = builder.keepMaxFace;
+ this.recognizeFailedRetryInterval = builder.recognizeFailedRetryInterval;
+ this.livenessParam = builder.livenessParam;
+ this.enableFaceSizeLimit = builder.enableFaceSizeLimit;
+ this.enableFaceMoveLimit = builder.enableFaceMoveLimit;
+ this.faceSizeLimit = builder.faceSizeLimit;
+ this.faceMoveLimit = builder.faceMoveLimit;
+ }
+
+ //TODO: demo不实现所有配置,若以下项也需要进行自定义配置,可参考其他配置项实现
+ public static class Builder {
+ private int extractRetryCount = 3;
+ private int livenessRetryCount = 3;
+ private int maxDetectFaces = 3;
+ private int recognizeFailedRetryInterval = 0;
+ private int livenessFailedRetryInterval = 0;
+ private float similarThreshold = 0.8f;
+ private float imageQualityNoMaskRecognizeThreshold = 0.49f;
+ private float imageQualityMaskRecognizeThreshold = 0.29f;
+ private boolean enableLiveness = false;
+ private boolean enableFaceAreaLimit = false;
+ private boolean enableImageQuality = false;
+ private boolean enableFaceSizeLimit = false;
+ private boolean enableFaceMoveLimit = false;
+ private int faceSizeLimit = 0;
+ private int faceMoveLimit = 0;
+ private boolean keepMaxFace = false;
+ private LivenessParam livenessParam;
+
+ public Builder recognizeFailedRetryInterval(int val) {
+ this.recognizeFailedRetryInterval = val;
+ return this;
+ }
+
+ public Builder livenessFailedRetryInterval(int val) {
+ this.livenessFailedRetryInterval = val;
+ return this;
+ }
+
+ public Builder extractRetryCount(int val) {
+ this.extractRetryCount = val;
+ return this;
+ }
+
+ public Builder livenessRetryCount(int val) {
+ this.livenessRetryCount = val;
+ return this;
+ }
+
+ public Builder maxDetectFaces(int val) {
+ this.maxDetectFaces = val;
+ return this;
+ }
+
+ public Builder similarThreshold(float val) {
+ this.similarThreshold = val;
+ return this;
+ }
+
+ public Builder imageQualityNoMaskRecognizeThreshold(float val) {
+ this.imageQualityNoMaskRecognizeThreshold = val;
+ return this;
+ }
+
+ public Builder imageQualityMaskRecognizeThreshold(float val) {
+ this.imageQualityMaskRecognizeThreshold = val;
+ return this;
+ }
+
+ public Builder enableLiveness(boolean val) {
+ this.enableLiveness = val;
+ return this;
+ }
+
+
+ public Builder enableImageQuality(boolean val) {
+ this.enableImageQuality = val;
+ return this;
+ }
+ public Builder enableFaceAreaLimit(boolean val) {
+ this.enableFaceAreaLimit = val;
+ return this;
+ }
+
+ public Builder enableFaceSizeLimit(boolean val) {
+ this.enableFaceSizeLimit = val;
+ return this;
+ }
+
+ public Builder enableFaceMoveLimit(boolean val) {
+ this.enableFaceMoveLimit = val;
+ return this;
+ }
+
+ public Builder faceSizeLimit(int val) {
+ this.faceSizeLimit = val;
+ return this;
+ }
+
+ public Builder faceMoveLimit(int val) {
+ this.faceMoveLimit = val;
+ return this;
+ }
+
+ public Builder keepMaxFace(boolean val) {
+ this.keepMaxFace = val;
+ return this;
+ }
+
+ public Builder livenessParam(LivenessParam val) {
+ this.livenessParam = val;
+ return this;
+ }
+
+
+ public RecognizeConfiguration build() {
+ return new RecognizeConfiguration(this);
+ }
+ }
+
+ public float getImageQualityNoMaskRecognizeThreshold() {
+ return imageQualityNoMaskRecognizeThreshold;
+ }
+
+ public float getImageQualityMaskRecognizeThreshold() {
+ return imageQualityMaskRecognizeThreshold;
+ }
+
+ public boolean isEnableImageQuality() {
+ return enableImageQuality;
+ }
+
+ public boolean isEnableFaceAreaLimit() {
+ return enableFaceAreaLimit;
+ }
+
+ public LivenessParam getLivenessParam() {
+ return livenessParam;
+ }
+
+ public int getExtractRetryCount() {
+ return extractRetryCount;
+ }
+
+ public int getLivenessRetryCount() {
+ return livenessRetryCount;
+ }
+
+ public int getMaxDetectFaces() {
+ return maxDetectFaces;
+ }
+
+ public float getSimilarThreshold() {
+ return similarThreshold;
+ }
+
+ public boolean isEnableLiveness() {
+ return enableLiveness;
+ }
+
+
+ public int getRecognizeFailedRetryInterval() {
+ return recognizeFailedRetryInterval;
+ }
+
+ public int getLivenessFailedRetryInterval() {
+ return livenessFailedRetryInterval;
+ }
+
+ public boolean isKeepMaxFace() {
+ return keepMaxFace;
+ }
+
+ public boolean isEnableFaceSizeLimit() {
+ return enableFaceSizeLimit;
+ }
+
+ public boolean isEnableFaceMoveLimit() {
+ return enableFaceMoveLimit;
+ }
+
+ public int getFaceSizeLimit() {
+ return faceSizeLimit;
+ }
+
+ public int getFaceMoveLimit() {
+ return faceMoveLimit;
+ }
+
+ @Override
+ public String toString() {
+ return
+ "extractRetryCount: " + extractRetryCount + "\r\n" +
+ "similarThreshold: " + similarThreshold + "\r\n" +
+ "recognizeFailedRetryInterval: " + recognizeFailedRetryInterval + "\r\n" +
+
+ "keepMaxFace: " + keepMaxFace + "\r\n" +
+ "maxDetectFaces: " + maxDetectFaces + "\r\n" +
+
+ "enableImageQuality: " + enableImageQuality + "\r\n" +
+ "imageQualityNoMaskRecognizeThreshold: " + imageQualityNoMaskRecognizeThreshold + "\r\n" +
+ "imageQualityMaskRecognizeThreshold: " + imageQualityMaskRecognizeThreshold + "\r\n" +
+
+ "enableLiveness: " + enableLiveness + "\r\n" +
+ "livenessRetryCount: " + livenessRetryCount + "\r\n" +
+ "livenessParams: " + (livenessParam == null ? null : (livenessParam.getRgbThreshold() + "," + livenessParam.getIrThreshold()));
+
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java
new file mode 100644
index 0000000..9350b37
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/model/RecognizeInfo.java
@@ -0,0 +1,86 @@
+package io.flutter.plugins.facediscern.tool.face.model;
+
+import com.arcsoft.face.LivenessInfo;
+import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus;
+
+/**
+ * 单个人脸(faceId)识别过程中的信息
+ */
+public class RecognizeInfo {
+ /**
+ * 用于记录人脸识别相关状态
+ */
+ private int recognizeStatus = RequestFeatureStatus.TO_RETRY;
+ /**
+ * 用于记录人脸特征提取出错重试次数
+ */
+ private int extractErrorRetryCount;
+ /**
+ * 用于存储活体值
+ */
+ private int liveness = LivenessInfo.UNKNOWN;
+ /**
+ * 用于存储活体检测出错重试次数
+ */
+ private int livenessErrorRetryCount;
+ /**
+ * 用户姓名,用于显示
+ */
+ private String name;
+ /**
+ * 特征等活体的lock
+ */
+ private Object waitLock = new Object();
+
+ public int getRecognizeStatus() {
+ return recognizeStatus;
+ }
+
+ public void setRecognizeStatus(int recognizeStatus) {
+ this.recognizeStatus = recognizeStatus;
+ }
+
+ public void setLiveness(int liveness) {
+ this.liveness = liveness;
+ }
+
+ public int increaseAndGetExtractErrorRetryCount() {
+ return ++extractErrorRetryCount;
+ }
+
+ public int getLiveness() {
+ return liveness;
+ }
+
+ public int increaseAndGetLivenessErrorRetryCount() {
+ return ++livenessErrorRetryCount;
+ }
+
+ public void setExtractErrorRetryCount(int extractErrorRetryCount) {
+ this.extractErrorRetryCount = extractErrorRetryCount;
+ }
+
+ public void setLivenessErrorRetryCount(int livenessErrorRetryCount) {
+ this.livenessErrorRetryCount = livenessErrorRetryCount;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Object getWaitLock() {
+ return waitLock;
+ }
+
+ public int getExtractErrorRetryCount() {
+ return extractErrorRetryCount;
+ }
+
+ public int getLivenessErrorRetryCount() {
+ return livenessErrorRetryCount;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java
new file mode 100644
index 0000000..04b092d
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/FaceServer.java
@@ -0,0 +1,359 @@
+package io.flutter.plugins.facediscern.tool.face.servcie;
+
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Rect;
+import android.os.Environment;
+import android.util.Log;
+
+import com.arcsoft.face.ErrorInfo;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceFeature;
+import com.arcsoft.face.FaceFeatureInfo;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.MaskInfo;
+import com.arcsoft.face.SearchResult;
+import com.arcsoft.face.enums.DetectFaceOrientPriority;
+import com.arcsoft.face.enums.DetectMode;
+import com.arcsoft.face.enums.ExtractType;
+import com.arcsoft.imageutil.ArcSoftImageFormat;
+import com.arcsoft.imageutil.ArcSoftImageUtil;
+import com.arcsoft.imageutil.ArcSoftImageUtilError;
+import com.arcsoft.imageutil.ArcSoftRotateDegree;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+
+import io.flutter.plugins.facediscern.tool.face.model.CompareResult;
+import io.flutter.plugins.facediscern.tool.face.model.FaceEntity;
+
+/**
+ * 人脸库操作类,包含注册和搜索
+ */
+public class FaceServer {
+ private static final String TAG = "FaceServer";
+ private static FaceEngine faceEngine = null;
+ private static volatile FaceServer faceServer = null;
+ private List faceRegisterInfoList = new ArrayList<>();
+ private String imageRootPath;
+ /**
+ * 最大注册人脸数
+ */
+ private static final int MAX_REGISTER_FACE_COUNT = 30000;
+
+ public synchronized void init(Context context) {
+ if (faceEngine == null && context != null) {
+ faceEngine = new FaceEngine();
+ int engineCode = faceEngine.init(
+ context,
+ DetectMode.ASF_DETECT_MODE_IMAGE,
+ DetectFaceOrientPriority.ASF_OP_ALL_OUT,
+ 1,
+ FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT
+ );
+ if (engineCode != ErrorInfo.MOK) {
+ Log.e(TAG, "人脸引擎初始化失败code" + engineCode);
+ }
+ }
+ }
+
+ public static FaceServer getInstance() {
+ if (faceServer == null) {
+ synchronized (FaceServer.class) {
+ if (faceServer == null) {
+ faceServer = new FaceServer();
+ }
+ }
+ }
+ return faceServer;
+ }
+
+ /**
+ * 销毁
+ */
+ public synchronized void release() {
+ if (faceRegisterInfoList != null) {
+ faceRegisterInfoList.clear();
+ faceRegisterInfoList = null;
+ }
+ if (faceEngine != null) {
+ synchronized (faceEngine) {
+ faceEngine.unInit();
+ }
+ faceEngine = null;
+ }
+ faceServer = null;
+ }
+
+ public int registerFaceFeatureInfoListFromDb(List faceEntityList) {
+ if (faceEngine == null) return -99;
+ List faceFeatureInfoList = new ArrayList<>();
+ for (FaceEntity faceEntity : faceEntityList) {
+ FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo((int) faceEntity.getFaceId(), faceEntity.getFaceModel());
+ faceFeatureInfoList.add(faceFeatureInfo);
+ }
+ //首先清除FaceEngine中所有人脸数据,再添加新的人脸数据
+ faceEngine.removeFaceFeature(-1);
+ int code = faceEngine.registerFaceFeature(faceFeatureInfoList);
+ if (code == ErrorInfo.MOK) {
+ faceRegisterInfoList.addAll(faceEntityList);
+ }
+ return code;
+ }
+
+ // 注册单个人脸数据
+ public int registerFaceFeatureInfoFromDb(FaceEntity faceEntity) {
+ if (faceEntity != null && faceEngine != null) {
+ FaceFeatureInfo faceFeatureInfo = new FaceFeatureInfo(faceEntity.getFaceId(), faceEntity.getFaceModel());
+ int code = faceEngine.registerFaceFeature(faceFeatureInfo);
+ if (code == ErrorInfo.MOK) {
+ faceEntity.setFaceModel(faceFeatureInfo.getFeatureData());
+ // 查询一下确保入库成功
+ FaceFeatureInfo afterDetail = getFaceFeature(faceEntity.getFaceId());
+ if (Objects.nonNull(afterDetail)) {
+ long count = faceRegisterInfoList.stream().filter(v -> Objects.equals(v.getFaceId(), faceEntity.getFaceId())).count();
+ if (count == 0) {
+ faceRegisterInfoList.add(faceEntity);
+ }
+ return code;
+ }
+ }
+ }
+ return -99;
+ }
+
+ // 从人脸数据库中搜索人脸
+ public FaceFeatureInfo getFaceFeature(int searchId) {
+ FaceFeatureInfo faceFeatureInfo = null;
+ try {
+ faceFeatureInfo = faceEngine.getFaceFeature(searchId);
+ } catch (IllegalArgumentException e) {
+ Log.e(TAG, "从人脸数据库中搜索人脸error:" + e.getMessage(), e);
+ }
+ return faceFeatureInfo;
+ }
+
+ // 获取人脸数量
+ public int getTotalFaceCount() {
+ if (faceEngine == null) return -99;
+ return faceEngine.getFaceCount();
+ }
+
+ // 删除人脸-1删除全部
+ public int removeFace(int searchId) {
+ if (faceEngine == null) return -99;
+ return faceEngine.removeFaceFeature(searchId);
+ }
+
+ // 在特征库中搜索
+ public CompareResult searchFaceFeature(FaceFeature faceFeature) {
+ if (faceFeature == null) return null;
+ if (faceEngine == null) return null;
+ SearchResult searchResult = null;
+ try {
+ long searchStart = System.currentTimeMillis();
+ searchResult = faceEngine.searchFaceFeature(faceFeature);
+ long searchCost = System.currentTimeMillis() - searchStart;
+ if (searchResult != null) {
+ FaceFeatureInfo faceFeatureInfo = searchResult.getFaceFeatureInfo();
+ FaceEntity entity = faceRegisterInfoList.stream().filter(v -> Objects.equals(v.getFaceId(), faceFeatureInfo.getSearchId())).findFirst().get();
+ if (Objects.isNull(entity)) {
+ entity = new FaceEntity(faceFeatureInfo.getSearchId(), faceFeatureInfo.getFeatureData());
+ }
+ Log.i(TAG, "查询人脸总耗时:" + (System.currentTimeMillis() - searchStart) + "ms;username:" + entity.getUserName() + ";faceId:" + entity.getFaceId() + ";");
+ return new CompareResult(entity, searchResult.getMaxSimilar(), ErrorInfo.MOK, searchCost);
+ }
+ } catch (IllegalArgumentException exception) {
+ Log.i(TAG, "exception:" + exception.getMessage());
+ }
+ return null;
+ }
+
+
+ public byte[] registerBgr24(Context context, byte[] bgr24, int width, int height, String name) {
+ if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) {
+ Log.e(TAG, "registerBgr24: invalid params");
+ return null;
+ }
+ //人脸检测
+ List faceInfoList = new ArrayList<>();
+ int code;
+ synchronized (faceEngine) {
+ code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList);
+ }
+ if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) {
+ code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList,
+ FaceEngine.ASF_MASK_DETECT);
+ if (code == ErrorInfo.MOK) {
+ List maskInfoList = new ArrayList<>();
+ faceEngine.getMask(maskInfoList);
+ if (!maskInfoList.isEmpty()) {
+ int isMask = maskInfoList.get(0).getMask();
+ if (isMask == MaskInfo.WORN) {
+ /*
+ * 注册照要求不戴口罩
+ */
+ Log.e(TAG, "registerBgr24: maskInfo is worn");
+ return null;
+ }
+ if (isMask == MaskInfo.UNKNOWN) {
+ Log.e(TAG, "registerBgr24: maskInfo is unknown");
+ return null;
+ }
+ }
+ } else {
+ Log.e(TAG, "registerBgr24 mask process fail res=" + code);
+ return null;
+ }
+ FaceFeature faceFeature = new FaceFeature();
+ /*
+ * 特征提取,注册人脸时参数extractType值为ExtractType.REGISTER,参数mask的值为MaskInfo.NOT_WORN
+ */
+ synchronized (faceEngine) {
+ code = faceEngine.extractFaceFeature(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList.get(0),
+ ExtractType.REGISTER, MaskInfo.NOT_WORN, faceFeature);
+ }
+ String userName = name == null ? String.valueOf(System.currentTimeMillis()) : name;
+ //保存注册结果(注册图、特征数据)
+ if (code == ErrorInfo.MOK) {
+ //为了美观,扩大rect截取注册图
+ Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect());
+ if (cropRect == null) {
+ Log.e(TAG, "registerBgr24: cropRect is null");
+ return null;
+ }
+ cropRect.left &= ~3;
+ cropRect.top &= ~3;
+ cropRect.right &= ~3;
+ cropRect.bottom &= ~3;
+ String imgPath = getImagePath(userName, context);
+ // 创建一个头像的Bitmap,存放旋转结果图
+ Bitmap headBmp = getHeadImage(bgr24, width, height, faceInfoList.get(0).getOrient(), cropRect, ArcSoftImageFormat.BGR24);
+ try {
+ FileOutputStream fos = new FileOutputStream(imgPath);
+ headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
+ fos.close();
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ return faceFeature.getFeatureData();
+ } else {
+ Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code);
+ return null;
+ }
+ } else {
+ Log.e(TAG, "registerBgr24: no face detected, code is " + code);
+ return null;
+ }
+ }
+
+ private String getImagePath(String name, Context context) {
+ if (imageRootPath == null) {
+ imageRootPath = getImageDir(context);
+ File dir = new File(imageRootPath);
+ if (!dir.exists() && !dir.mkdirs()) {
+ return null;
+ }
+ }
+ return imageRootPath + File.separator + name + "_" + System.currentTimeMillis() + ".jpg";
+ }
+
+ private String getImageDir(Context context) {
+ String path = context
+ .getApplicationContext()
+ .getExternalFilesDir(Environment.DIRECTORY_PICTURES)
+ + File.separator + "faceDB" + File.separator + "registerFaces";
+ Log.i(TAG, "存储图片的地址为path:" + path);
+ return path;
+ }
+
+ /**
+ * 将图像中需要截取的Rect向外扩张一倍,若扩张一倍会溢出,则扩张到边界,若Rect已溢出,则收缩到边界
+ *
+ * @param width 图像宽度
+ * @param height 图像高度
+ * @param srcRect 原Rect
+ * @return 调整后的Rect
+ */
+ private static Rect getBestRect(int width, int height, Rect srcRect) {
+ if (srcRect == null) {
+ return null;
+ }
+ Rect rect = new Rect(srcRect);
+
+ // 原rect边界已溢出宽高的情况
+ int maxOverFlow = Math.max(-rect.left, Math.max(-rect.top, Math.max(rect.right - width, rect.bottom - height)));
+ if (maxOverFlow >= 0) {
+ rect.inset(maxOverFlow, maxOverFlow);
+ return rect;
+ }
+
+ // 原rect边界未溢出宽高的情况
+ int padding = rect.height() / 2;
+
+ // 若以此padding扩张rect会溢出,取最大padding为四个边距的最小值
+ if (!(rect.left - padding > 0 && rect.right + padding < width && rect.top - padding > 0 && rect.bottom + padding < height)) {
+ padding = Math.min(Math.min(Math.min(rect.left, width - rect.right), height - rect.bottom), rect.top);
+ }
+ rect.inset(-padding, -padding);
+ return rect;
+ }
+
+ private Bitmap getHeadImage(byte[] originImageData, int width, int height, int orient, Rect cropRect, ArcSoftImageFormat imageFormat) {
+ byte[] headImageData = ArcSoftImageUtil.createImageData(cropRect.width(), cropRect.height(), imageFormat);
+ int cropCode = ArcSoftImageUtil.cropImage(originImageData, headImageData, width, height, cropRect, imageFormat);
+ if (cropCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("crop image failed, code is " + cropCode);
+ }
+
+ //判断人脸旋转角度,若不为0度则旋转注册图
+ byte[] rotateHeadImageData = null;
+ int cropImageWidth;
+ int cropImageHeight;
+ // 90度或270度的情况,需要宽高互换
+ if (orient == FaceEngine.ASF_OC_90 || orient == FaceEngine.ASF_OC_270) {
+ cropImageWidth = cropRect.height();
+ cropImageHeight = cropRect.width();
+ } else {
+ cropImageWidth = cropRect.width();
+ cropImageHeight = cropRect.height();
+ }
+ ArcSoftRotateDegree rotateDegree = null;
+ switch (orient) {
+ case FaceEngine.ASF_OC_90:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_270;
+ break;
+ case FaceEngine.ASF_OC_180:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_180;
+ break;
+ case FaceEngine.ASF_OC_270:
+ rotateDegree = ArcSoftRotateDegree.DEGREE_90;
+ break;
+ case FaceEngine.ASF_OC_0:
+ default:
+ rotateHeadImageData = headImageData;
+ break;
+ }
+ // 非0度的情况,旋转图像
+ if (rotateDegree != null) {
+ rotateHeadImageData = new byte[headImageData.length];
+ int rotateCode = ArcSoftImageUtil.rotateImage(headImageData, rotateHeadImageData, cropRect.width(), cropRect.height(), rotateDegree, imageFormat);
+ if (rotateCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("rotate image failed, code is : " + rotateCode + ", code description is : ");
+ }
+ }
+ // 将创建一个Bitmap,并将图像数据存放到Bitmap中
+ Bitmap headBmp = Bitmap.createBitmap(cropImageWidth, cropImageHeight, Bitmap.Config.RGB_565);
+ int imageDataToBitmapCode = ArcSoftImageUtil.imageDataToBitmap(rotateHeadImageData, headBmp, imageFormat);
+ if (imageDataToBitmapCode != ArcSoftImageUtilError.CODE_SUCCESS) {
+ throw new RuntimeException("failed to transform image data to bitmap, code is : " + imageDataToBitmapCode
+ + ", code description is : ");
+ }
+ return headBmp;
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java
new file mode 100644
index 0000000..7d2d7c8
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/servcie/RegisterFailedException.java
@@ -0,0 +1,7 @@
+package io.flutter.plugins.facediscern.tool.face.servcie;
+
+public class RegisterFailedException extends Exception {
+ public RegisterFailedException(String message) {
+ super(message);
+ }
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java
new file mode 100644
index 0000000..d438d88
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/face/widget/FaceRectView.java
@@ -0,0 +1,295 @@
+package io.flutter.plugins.facediscern.tool.face.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.util.AttributeSet;
+import android.view.View;
+
+import androidx.annotation.Nullable;
+
+import com.arcsoft.face.AgeInfo;
+import com.arcsoft.face.FaceAttributeInfo;
+import com.arcsoft.face.GenderInfo;
+import com.arcsoft.face.LivenessInfo;
+
+import java.util.List;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+/**
+ * 用于显示人脸信息的控件
+ */
+public class FaceRectView extends View {
+ private CopyOnWriteArrayList drawInfoList = new CopyOnWriteArrayList<>();
+
+ // 画笔,复用
+ private Paint paint;
+
+ // 默认人脸框厚度
+ private static final int DEFAULT_FACE_RECT_THICKNESS = 6;
+
+ 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++) {
+ drawFaceRect(canvas, drawInfoList.get(i), DEFAULT_FACE_RECT_THICKNESS, paint);
+ }
+ }
+ }
+
+ public void clearFaceInfo() {
+ drawInfoList.clear();
+ postInvalidate();
+ }
+
+ public void addFaceInfo(DrawInfo faceInfo) {
+ drawInfoList.add(faceInfo);
+ postInvalidate();
+ }
+
+ public void addFaceInfo(List faceInfoList) {
+ drawInfoList.addAll(faceInfoList);
+ postInvalidate();
+ }
+
+ public void drawRealtimeFaceInfo(List drawInfoList) {
+ clearFaceInfo();
+ if (drawInfoList == null || drawInfoList.size() == 0) {
+ return;
+ }
+ addFaceInfo(drawInfoList);
+ }
+
+ public static class DrawInfo {
+ private Rect rect;
+ private int sex;
+ private int age;
+ private int liveness;
+ private int color;
+ private int isWithinBoundary;
+ private String name = null;
+ private boolean drawRectInfo;
+ private Rect foreheadRect;
+ private FaceAttributeInfo faceAttributeInfo;
+ private boolean rgbRect;
+
+ public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name) {
+ this.rect = rect;
+ this.sex = sex;
+ this.age = age;
+ this.liveness = liveness;
+ this.color = color;
+ this.name = name;
+ }
+
+ public DrawInfo(Rect rect, int sex, int age, int liveness, int color, String name, int isWithinBoundary, Rect foreheadRect,
+ FaceAttributeInfo faceAttributeInfo, boolean drawRectInfo, boolean rgbRect) {
+ this.rect = rect;
+ this.sex = sex;
+ this.age = age;
+ this.liveness = liveness;
+ this.color = color;
+ this.name = name;
+ this.isWithinBoundary = isWithinBoundary;
+ this.drawRectInfo = drawRectInfo;
+ this.foreheadRect = foreheadRect;
+ this.faceAttributeInfo = faceAttributeInfo;
+ this.rgbRect = rgbRect;
+ }
+
+ public DrawInfo(DrawInfo drawInfo) {
+ if (drawInfo == null) {
+ return;
+ }
+ this.rect = drawInfo.rect;
+ this.sex = drawInfo.sex;
+ this.age = drawInfo.age;
+ this.liveness = drawInfo.liveness;
+ this.color = drawInfo.color;
+ this.name = drawInfo.name;
+ }
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public Rect getRect() {
+ return rect;
+ }
+
+ public void setRect(Rect rect) {
+ this.rect = rect;
+ }
+
+ public int getSex() {
+ return sex;
+ }
+
+ public void setSex(int sex) {
+ this.sex = sex;
+ }
+
+ public int getAge() {
+ return age;
+ }
+
+ public void setAge(int age) {
+ this.age = age;
+ }
+
+ public int getLiveness() {
+ return liveness;
+ }
+
+ public void setLiveness(int liveness) {
+ this.liveness = liveness;
+ }
+
+ public int getColor() {
+ return color;
+ }
+
+ public void setColor(int color) {
+ this.color = color;
+ }
+
+ public boolean isDrawRectInfo() {
+ return drawRectInfo;
+ }
+
+ public void setDrawRectInfo(boolean drawRectInfo) {
+ this.drawRectInfo = drawRectInfo;
+ }
+
+ public Rect getForeheadRect() {
+ return foreheadRect;
+ }
+
+ public void setForeheadRect(Rect foreheadRect) {
+ this.foreheadRect = foreheadRect;
+ }
+
+ public FaceAttributeInfo getFaceAttributeInfo() {
+ return faceAttributeInfo;
+ }
+
+ public void setFaceAttributeInfo(FaceAttributeInfo faceAttributeInfo) {
+ this.faceAttributeInfo = faceAttributeInfo;
+ }
+
+ public int getIsWithinBoundary() {
+ return isWithinBoundary;
+ }
+
+ public void setIsWithinBoundary(int isWithinBoundary) {
+ this.isWithinBoundary = isWithinBoundary;
+ }
+ }
+
+
+ /**
+ * 绘制数据信息到view上,若 {@link DrawInfo#getName()} 不为null则绘制 {@link DrawInfo#getName()}
+ *
+ * @param canvas 需要被绘制的view的canvas
+ * @param drawInfo 绘制信息
+ * @param faceRectThickness 人脸框厚度
+ * @param paint 画笔
+ */
+ private 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);
+
+ // 绘制文字,用最细的即可,避免在某些低像素设备上文字模糊
+ paint.setStrokeWidth(1);
+
+ if (drawInfo.getName() == null) {
+ paint.setStyle(Paint.Style.FILL_AND_STROKE);
+ paint.setTextSize(rect.width() / 12);
+ String str = (drawInfo.getSex() == GenderInfo.MALE ? "MALE" : (drawInfo.getSex() == GenderInfo.FEMALE ? "FEMALE" : "UNKNOWN"))
+ + ","
+ + (drawInfo.getAge() == AgeInfo.UNKNOWN_AGE ? "UNKNOWN" : 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() / 12);
+ canvas.drawText(drawInfo.getName(), rect.left, rect.top - 10, paint);
+ }
+
+ if (drawInfo.drawRectInfo && drawInfo.rgbRect) {
+ Rect foreRect = drawInfo.foreheadRect;
+ if (foreRect != null) {
+ Path forePath = new Path();
+ forePath.moveTo(foreRect.left, foreRect.top);
+ forePath.lineTo(foreRect.right, foreRect.top);
+ forePath.lineTo(foreRect.right, foreRect.bottom);
+ forePath.lineTo(foreRect.left, foreRect.bottom);
+ forePath.lineTo(foreRect.left, foreRect.top);
+ paint.setStyle(Paint.Style.STROKE);
+ paint.setStrokeWidth(3);
+ canvas.drawPath(forePath, paint);
+ }
+
+ FaceAttributeInfo attributeInfo = drawInfo.getFaceAttributeInfo();
+ if (attributeInfo != null) {
+ paint.setStyle(Paint.Style.FILL_AND_STROKE);
+ int textSize = rect.width() / 8;
+ paint.setStrokeWidth(1);
+ paint.setTextSize(textSize);
+ int defX = rect.left;
+ int defY = rect.bottom + rect.width() / 8;
+
+ String isStrInfo0 = drawInfo.getIsWithinBoundary() == 1 ? "是" : "否";
+ String strInfo0 = "是否在相机范围内: " + isStrInfo0;
+ canvas.drawText(strInfo0, defX, defY, paint);
+
+ String isStrInfo1 = attributeInfo.getWearGlasses() == 1 ? "是" : "否";
+ String strInfo1 = "是否戴眼镜: " + isStrInfo1;
+ canvas.drawText(strInfo1, defX, defY + textSize, paint);
+ }
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java
new file mode 100644
index 0000000..4f94960
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/tool/file/FileUtil.java
@@ -0,0 +1,71 @@
+package io.flutter.plugins.facediscern.tool.file;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+
+public class FileUtil {
+ /**
+ * 读取文件中的数据内容
+ *
+ * @param file 文件
+ * @return 二进制数据内容
+ */
+ public static byte[] fileToData(File file) {
+ FileInputStream fis = null;
+ try {
+ fis = new FileInputStream(file);
+ byte[] data = new byte[fis.available()];
+ fis.read(data);
+ fis.close();
+ return data;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return null;
+ }
+ }
+
+ public static boolean saveDataToFile(byte[] data, File file, boolean append) {
+ if (data == null){
+ return false;
+ }
+ File parentFile = file.getParentFile();
+ if (parentFile == null) {
+ return false;
+ }
+ if (!file.getParentFile().exists() && !file.getParentFile().mkdirs()) {
+ return false;
+ }
+ FileOutputStream fos = null;
+ try {
+ fos = new FileOutputStream(file, append);
+ int bufferSize = 1024;
+ int index = 0;
+ while (index < data.length) {
+ if (data.length - index < bufferSize) {
+ bufferSize = data.length - index;
+ }
+ fos.write(data, index, bufferSize);
+ index += bufferSize;
+ }
+ return true;
+ } catch (IOException e) {
+ e.printStackTrace();
+ return false;
+ } finally {
+ try {
+ if (fos != null) {
+ fos.close();
+ }
+ } catch (IOException e) {
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static boolean saveDataToFile(byte[] data, File file) {
+ return saveDataToFile(data, file, false);
+ }
+
+}
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java
new file mode 100644
index 0000000..0548c62
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/FaceDiscern.java
@@ -0,0 +1,323 @@
+package io.flutter.plugins.facediscern.ui;
+
+import android.Manifest;
+import android.content.Intent;
+import android.graphics.Point;
+import android.hardware.Camera;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+
+import androidx.activity.EdgeToEdge;
+import androidx.core.app.ActivityCompat;
+import androidx.databinding.DataBindingUtil;
+import androidx.lifecycle.ViewModelProvider;
+
+import com.example.pj_supermarket_cloud.R;
+import com.example.pj_supermarket_cloud.databinding.ActivityFaceDiscernBinding;
+
+import io.flutter.Log;
+import io.flutter.plugins.facediscern.flutter.FlutterFaceDiscernPlugin;
+import io.flutter.plugins.facediscern.tool.camera.DualCameraHelper;
+import io.flutter.plugins.facediscern.tool.camera.callback.CameraListener;
+import io.flutter.plugins.facediscern.tool.face.constants.LivenessType;
+import io.flutter.plugins.facediscern.tool.face.help.FaceRectTransformer;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+import io.flutter.plugins.facediscern.tool.face.widget.FaceRectView;
+import io.flutter.plugins.facediscern.ui.base.BaseActivity;
+import io.flutter.plugins.facediscern.ui.viewmodel.FaceDiscernViewModel;
+import io.flutter.plugin.common.MethodChannel;
+
+import java.util.List;
+import java.util.Objects;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+public class FaceDiscern extends BaseActivity implements ViewTreeObserver.OnGlobalLayoutListener {
+
+ private static final String TAG = "人脸识别页面";
+
+ private static final String[] NEEDED_PERMISSIONS = new String[]{
+ Manifest.permission.CAMERA,
+ Manifest.permission.READ_PHONE_STATE
+ };
+
+ private FaceRectTransformer rgbFaceRectTransformer;
+
+ // 彩色摄像头
+ private DualCameraHelper rgbCameraHelper;
+
+ private FaceDiscernViewModel viewModel;
+
+ private ActivityFaceDiscernBinding binding;
+
+ private Boolean previewLock = false;
+
+ private static final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out);
+ EdgeToEdge.enable(this);
+ binding = DataBindingUtil.setContentView(this, R.layout.activity_face_discern);
+ runOnUiThread(() -> {
+ this.initViewModel();
+ initView();
+ MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel;
+ if (Objects.nonNull(channel)) channel.invokeMethod("faceDiscernPageInit", null);
+ });
+ Log.e(TAG, "相机触发onCreate");
+ }
+
+ @Override
+ public void onGlobalLayout() {
+ binding.dualCameraTexturePreviewRgb.getViewTreeObserver().removeOnGlobalLayoutListener(FaceDiscern.this);
+ if (!checkPermissions(NEEDED_PERMISSIONS)) {
+ ActivityCompat.requestPermissions(this, NEEDED_PERMISSIONS, 0x001);
+ return;
+ }
+ initRgbCamera();
+ Log.e(TAG, "相机onGlobalLayout");
+ }
+
+ // 请求权限的回调
+ @Override
+ protected void afterRequestPermission(int requestCode, boolean isAllGranted) {
+ super.afterRequestPermission(requestCode, isAllGranted);
+ if (!checkPermissions(NEEDED_PERMISSIONS)) {
+ return;
+ }
+ initRgbCamera();
+ }
+
+ @Override
+ protected void onNewIntent(Intent intent) {
+ super.onNewIntent(intent);
+ Log.e(TAG, "相机触发onNewIntent");
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (rgbCameraHelper != null) {
+ rgbCameraHelper.release();
+ rgbCameraHelper = null;
+ }
+ if (binding != null) binding = null;
+ viewModel.destroy();
+ Log.e(TAG, "相机触发onDestroy");
+ }
+
+ @Override
+ protected void onPause() {
+ super.onPause();
+ if (rgbCameraHelper != null) {
+ rgbCameraHelper.stop();
+ }
+ if (viewModel != null) viewModel.stop();
+ binding.dualCameraFaceRectView.clearFaceInfo();
+ previewLock = true;
+ Log.i(TAG, "相机onPause");
+ }
+
+ @Override
+ protected void onResume() {
+ super.onResume();
+ MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel;
+ if (Objects.nonNull(channel)) {
+ channel.invokeMethod("faceDiscernPageInit", null);
+ }
+ if (rgbCameraHelper != null) {
+ rgbCameraHelper.resumePreview();
+ }
+ scheduler.schedule(() -> {
+ try {
+ if (viewModel != null) {
+ viewModel.stop();
+ previewLock = false;
+ Log.i(TAG, "相机开始识别");
+ }
+ } catch (Exception e) {
+ Log.e(TAG, "相机开始识别错误", e);
+ }
+ }, 500, TimeUnit.MILLISECONDS);
+ Log.i(TAG, "相机onResume");
+ }
+
+ @Override
+ protected void onStop() {
+ super.onStop();
+ if (rgbCameraHelper != null && !isFinishing()) {
+ rgbCameraHelper.stop();
+ }
+ }
+
+ // 手机返回按钮
+ @Override
+ public void onBackPressed() {
+ Log.e(TAG, "相机onBackPressed");
+ Intent intent = new Intent();
+ intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
+ intent.setClassName(this.getPackageName(), this.getPackageName() + ".MainActivity");
+ this.startActivity(intent);
+ MethodChannel channel = FlutterFaceDiscernPlugin.staticChannel;
+ if (Objects.isNull(channel)) return;
+ channel.invokeMethod("faceDiscernBack", null);
+ }
+
+
+ /* ***************** start 初始化 start ***************** */
+ private void initViewModel() {
+ viewModel = new ViewModelProvider(this).get(FaceDiscernViewModel.class);
+ viewModel.init(FaceDiscern.this);
+
+ }
+
+ private void initView() {
+ binding.dualCameraTexturePreviewRgb.getViewTreeObserver().addOnGlobalLayoutListener(FaceDiscern.this);
+ }
+
+ private void initRgbCamera() {
+ CameraListener cameraListener = new CameraListener() {
+ @Override
+ public void onCameraOpened(Camera camera, int cameraId, int displayOrientation, boolean isMirror) {
+ runOnUiThread(() -> {
+ Camera.Size previewSizeRgb = camera.getParameters().getPreviewSize();
+ ViewGroup.LayoutParams layoutParams = adjustPreviewViewSize(
+ binding.dualCameraTexturePreviewRgb,
+ binding.dualCameraTexturePreviewRgb,
+ binding.dualCameraFaceRectView,
+ previewSizeRgb,
+ displayOrientation,
+ 1.0f
+ );
+ rgbFaceRectTransformer = new FaceRectTransformer(
+ previewSizeRgb.width,
+ previewSizeRgb.height,
+ layoutParams.width,
+ layoutParams.height,
+ displayOrientation,
+ cameraId,
+ isMirror,
+ true,
+ true
+ );
+ viewModel.onRgbCameraOpened(camera);
+ viewModel.setRgbFaceRectTransformer(rgbFaceRectTransformer);
+ Log.e(TAG, "相机打开,宽:" + previewSizeRgb.width + ",高:" + previewSizeRgb.height + ";layout宽:" + layoutParams.width + ",layout高:" + layoutParams.height + ";");
+ });
+ }
+
+ @Override
+ public void onPreview(byte[] data, Camera camera) {
+ if (previewLock) return;
+ binding.dualCameraFaceRectView.clearFaceInfo();
+ List facePreviewInfoList = viewModel.onPreviewFrame(data, true);
+ if (facePreviewInfoList != null && rgbFaceRectTransformer != null) {
+ drawPreviewInfo(facePreviewInfoList);
+ }
+ }
+
+ @Override
+ public void onCameraClosed() {
+ Log.e(TAG, "相机关闭");
+ }
+
+ @Override
+ public void onCameraError(Exception e) {
+ Log.e(TAG, "相机错误error", e);
+ }
+
+ @Override
+ public void onCameraConfigurationChanged(int cameraID, int displayOrientation) {
+
+ }
+ };
+ // 使用前置相机
+ int mCameraId = 0;
+ int numberOfCameras = Camera.getNumberOfCameras();
+ Camera.CameraInfo cameraInfo = new Camera.CameraInfo();
+ for (int i = 0; i < numberOfCameras; i++) {
+ Camera.getCameraInfo(i, cameraInfo);
+ if (cameraInfo.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
+ mCameraId = i;
+ break;
+ }
+ }
+ Point previewSize = new Point(binding.dualCameraTexturePreviewRgb.getMeasuredWidth(), binding.dualCameraTexturePreviewRgb.getMeasuredHeight());
+ rgbCameraHelper = new DualCameraHelper.Builder()
+ .previewViewSize(previewSize)
+ .previewSize(previewSize)
+ .rotation(getWindowManager().getDefaultDisplay().getRotation())
+ .additionalRotation(0)
+ .specificCameraId(mCameraId)
+ .isMirror(false)
+ .previewOn(binding.dualCameraTexturePreviewRgb)
+ .cameraListener(cameraListener)
+ .build();
+ rgbCameraHelper.init();
+ rgbCameraHelper.start();
+ viewModel.setRgbCameraHelper(rgbCameraHelper);
+ }
+ /* ***************** end end end ***************** */
+
+
+ private ViewGroup.LayoutParams adjustPreviewViewSize(View rgbPreview, View previewView, FaceRectView faceRectView, Camera.Size previewSize, int displayOrientation, float scale) {
+ ViewGroup.LayoutParams layoutParams = previewView.getLayoutParams();
+ int measuredWidth = previewView.getMeasuredWidth();
+ int measuredHeight = previewView.getMeasuredHeight();
+ float ratio = ((float) previewSize.height) / (float) previewSize.width;
+ if (ratio > 1) {
+ ratio = 1 / ratio;
+ }
+ if (displayOrientation % 180 == 0) {
+ layoutParams.width = measuredWidth;
+ layoutParams.height = (int) (measuredWidth * ratio);
+ } else {
+ layoutParams.height = measuredHeight;
+ layoutParams.width = (int) (measuredHeight * ratio);
+ }
+ if (scale < 1f) {
+ ViewGroup.LayoutParams rgbParam = rgbPreview.getLayoutParams();
+ layoutParams.width = (int) (rgbParam.width * scale);
+ layoutParams.height = (int) (rgbParam.height * scale);
+ } else {
+ layoutParams.width *= scale;
+ layoutParams.height *= scale;
+ }
+
+ DisplayMetrics metrics = new DisplayMetrics();
+ getWindowManager().getDefaultDisplay().getMetrics(metrics);
+
+ if (layoutParams.width >= metrics.widthPixels) {
+ float viewRatio = layoutParams.width / ((float) metrics.widthPixels);
+ layoutParams.width /= viewRatio;
+ layoutParams.height /= viewRatio;
+ }
+ if (layoutParams.height >= metrics.heightPixels) {
+ float viewRatio = layoutParams.height / ((float) metrics.heightPixels);
+ layoutParams.width /= viewRatio;
+ layoutParams.height /= viewRatio;
+ }
+
+ previewView.setLayoutParams(layoutParams);
+ faceRectView.setLayoutParams(layoutParams);
+ return layoutParams;
+ }
+
+ /**
+ * 绘制RGB、IR画面的实时人脸信息
+ *
+ * @param facePreviewInfoList RGB画面的实时人脸信息
+ */
+ private void drawPreviewInfo(List facePreviewInfoList) {
+ if (rgbFaceRectTransformer != null) {
+ List rgbDrawInfoList = viewModel.getDrawInfo(facePreviewInfoList, LivenessType.RGB, false);
+ binding.dualCameraFaceRectView.drawRealtimeFaceInfo(rgbDrawInfoList);
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java
new file mode 100644
index 0000000..ad124f6
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/base/BaseActivity.java
@@ -0,0 +1,82 @@
+package io.flutter.plugins.facediscern.ui.base;
+
+import android.app.Activity;
+import android.content.pm.PackageManager;
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.core.content.ContextCompat;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
+public class BaseActivity extends AppCompatActivity {
+ private static ExecutorService executor;
+ private static List activityList;
+
+ @Override
+ protected void onCreate(@Nullable Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ if (activityList == null) {
+ activityList = new ArrayList<>();
+ }
+ if (executor == null) {
+ executor = new ThreadPoolExecutor(
+ 1,
+ 1,
+ 0L,
+ TimeUnit.MILLISECONDS,
+ new LinkedBlockingQueue(),
+ r -> {
+ Thread t = new Thread(r);
+ t.setName("activity-sub-thread-" + t.getId());
+ return t;
+ });
+ }
+ activityList.add(this);
+ }
+
+ @Override
+ protected void onDestroy() {
+ activityList.remove(this);
+ if (activityList.isEmpty()) {
+ activityList = null;
+ executor.shutdown();
+ executor = null;
+ }
+ super.onDestroy();
+ }
+
+ @Override
+ public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
+ super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+ boolean isAllGranted = true;
+ for (int grantResult : grantResults) {
+ isAllGranted &= (grantResult == PackageManager.PERMISSION_GRANTED);
+ }
+ afterRequestPermission(requestCode, isAllGranted);
+ }
+
+ // 请求权限的回调
+ protected void afterRequestPermission(int requestCode, boolean isAllGranted) {
+
+ }
+
+ // 检查权限
+ protected boolean checkPermissions(String[] neededPermissions) {
+ if (neededPermissions == null || neededPermissions.length == 0) {
+ return true;
+ }
+ boolean allGranted = true;
+ for (String neededPermission : neededPermissions) {
+ allGranted &= ContextCompat.checkSelfPermission(this, neededPermission) == PackageManager.PERMISSION_GRANTED;
+ }
+ return allGranted;
+ }
+}
\ No newline at end of file
diff --git a/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java
new file mode 100644
index 0000000..dbe491c
--- /dev/null
+++ b/android/app/src/main/java/io/flutter/plugins/facediscern/ui/viewmodel/FaceDiscernViewModel.java
@@ -0,0 +1,309 @@
+package io.flutter.plugins.facediscern.ui.viewmodel;
+
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.hardware.Camera;
+import android.util.Log;
+
+import androidx.lifecycle.MutableLiveData;
+import androidx.lifecycle.ViewModel;
+
+import com.arcsoft.face.AgeInfo;
+import com.arcsoft.face.FaceEngine;
+import com.arcsoft.face.FaceInfo;
+import com.arcsoft.face.GenderInfo;
+import com.arcsoft.face.LivenessInfo;
+import com.arcsoft.face.LivenessParam;
+import com.arcsoft.face.enums.DetectFaceOrientPriority;
+import com.arcsoft.face.enums.DetectMode;
+import com.google.gson.Gson;
+
+import io.flutter.plugins.facediscern.tool.camera.DualCameraHelper;
+import io.flutter.plugins.facediscern.tool.camera.config.PreviewConfig;
+import io.flutter.plugins.facediscern.tool.face.constants.FaceBrodConstant;
+import io.flutter.plugins.facediscern.tool.face.constants.LivenessType;
+import io.flutter.plugins.facediscern.tool.face.constants.RecognizeColor;
+import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus;
+import io.flutter.plugins.facediscern.tool.face.help.FaceHelper;
+import io.flutter.plugins.facediscern.tool.face.help.FaceRectTransformer;
+import io.flutter.plugins.facediscern.tool.face.help.callback.RecognizeCallback;
+import io.flutter.plugins.facediscern.tool.face.model.CompareResult;
+import io.flutter.plugins.facediscern.tool.face.model.FaceEntity;
+import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
+import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfiguration;
+import io.flutter.plugins.facediscern.tool.face.widget.FaceRectView;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Objects;
+
+public class FaceDiscernViewModel extends ViewModel implements RecognizeCallback {
+
+ private final String TAG = "FaceDiscernViewModel";
+
+ // 相机预览的分辨率
+ private Camera.Size previewSize;
+
+ /// ****************** start 引擎 start ******************
+
+ // 用于特征提取的引擎
+ private FaceEngine frEngine;
+ // VIDEO模式人脸检测引擎,用于预览帧人脸追踪及图像质量检测
+ private FaceEngine ftEngine;
+ // VIDEO模式人脸口罩检测引擎,用于预览帧人脸口罩检测
+ private FaceEngine maskEngine;
+ // IMAGE模式活体检测引擎,用于预览帧人脸活体检测
+ private FaceEngine flEngine;
+ // 各个引擎初始化的错误码
+ private MutableLiveData ftInitCode = new MutableLiveData<>();
+ private MutableLiveData frInitCode = new MutableLiveData<>();
+ private MutableLiveData flInitCode = new MutableLiveData<>();
+
+ /// ****************** end 引擎 end ******************
+
+ private Rect recognizeArea = new Rect(Integer.MIN_VALUE, Integer.MIN_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE);
+
+ // 当前活体检测的检测类型
+ private LivenessType livenessType;
+
+ private FaceRectTransformer rgbFaceRectTransformer;
+
+ private PreviewConfig previewConfig;
+
+ // 人脸操作辅助类,推帧即可,内部会进行特征提取、识别
+ private FaceHelper faceHelper;
+
+ // 人脸库数据加载完成
+ private boolean loadFaceList = true;
+
+ // IR活体数据
+ private byte[] irNV21 = null;
+
+ public static final int REGISTER_STATUS_READY = 0;
+ // 注册人脸状态码,注册中
+ public static final int REGISTER_STATUS_PROCESSING = 1;
+ // 册人脸状态码,注册结束(无论成功失败)
+ public static final int REGISTER_STATUS_DONE = 2;
+
+ private int registerStatus = REGISTER_STATUS_DONE;
+
+ private Intent flutterFaceCallbackIntent;
+
+ private Context context;
+
+ private DualCameraHelper rgbCameraHelper;
+
+ private HashMap