feat:添加识别策略
This commit is contained in:
1 parent
0d0924c314
commit
1c348fc18e
20 files changed
+1500
-74
No files matched your search
+25
@@ -25,6 +25,8 @@ import io.flutter.plugin.common.MethodCall;
|
||||
import io.flutter.plugin.common.MethodChannel;
|
||||
import io.flutter.plugins.facediscern.session.FaceRecognitionSessionManager;
|
||||
import io.flutter.plugins.facediscern.tool.face.constants.FaceBrodConstant;
|
||||
import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfiguration;
|
||||
import io.flutter.plugins.facediscern.tool.face.model.RecognizeConfigurationStore;
|
||||
import io.flutter.plugins.facediscern.ui.FaceDiscern;
|
||||
import io.flutter.view.TextureRegistry;
|
||||
|
||||
@@ -199,6 +201,29 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
|
||||
case "getRecognitionState":
|
||||
result.success(sessionManager.getState());
|
||||
break;
|
||||
case "getRecognizeConfiguration":
|
||||
result.success(RecognizeConfigurationStore.toMap(RecognizeConfigurationStore.load(context)));
|
||||
break;
|
||||
case "setRecognizeConfiguration":
|
||||
try {
|
||||
RecognizeConfiguration configuration = RecognizeConfigurationStore.save(context, call.arguments instanceof Map ? (Map<?, ?>) call.arguments : null);
|
||||
sessionManager.onRecognitionConfigurationChanged();
|
||||
result.success(RecognizeConfigurationStore.toMap(configuration));
|
||||
} catch (IllegalArgumentException e) {
|
||||
result.error("INVALID_RECOGNIZE_CONFIGURATION", e.getMessage(), null);
|
||||
} catch (Exception e) {
|
||||
result.error("SAVE_RECOGNIZE_CONFIGURATION_FAILED", e.getMessage(), null);
|
||||
}
|
||||
break;
|
||||
case "resetRecognizeConfiguration":
|
||||
try {
|
||||
RecognizeConfiguration configuration = RecognizeConfigurationStore.reset(context);
|
||||
sessionManager.onRecognitionConfigurationChanged();
|
||||
result.success(RecognizeConfigurationStore.toMap(configuration));
|
||||
} catch (Exception e) {
|
||||
result.error("RESET_RECOGNIZE_CONFIGURATION_FAILED", e.getMessage(), null);
|
||||
}
|
||||
break;
|
||||
case "active":
|
||||
String appId = stringArgument(call, "appId");
|
||||
String activeKey = stringArgument(call, "activeKey");
|
||||
|
||||
+57
-21
@@ -10,8 +10,6 @@ import android.hardware.Camera;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
|
||||
import androidx.core.content.ContextCompat;
|
||||
|
||||
@@ -103,6 +101,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
private long revision;
|
||||
private long sessionGeneration;
|
||||
private long lastFaceBoxEmitAt;
|
||||
private boolean recognitionConfigurationDirty;
|
||||
private Handler warmReleaseHandler = new Handler(Looper.getMainLooper());
|
||||
private final Runnable warmReleaseTask = this::releaseWarmResources;
|
||||
|
||||
@@ -152,6 +151,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
|
||||
public Map<String, Object> startRecognition(String sessionId, String scene, Listener owner) throws Exception {
|
||||
if (sessionId == null || sessionId.trim().isEmpty()) throw new IllegalArgumentException("缺少人脸会话编号");
|
||||
boolean reloadConfiguration;
|
||||
synchronized (sessionLock) {
|
||||
if (activeSessionId != null && !Objects.equals(activeSessionId, sessionId)) {
|
||||
throw new SessionBusyException(activeSessionId);
|
||||
@@ -163,9 +163,12 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
activeOwner = owner;
|
||||
revision = 0;
|
||||
state = State.INITIALIZING;
|
||||
reloadConfiguration = recognitionConfigurationDirty;
|
||||
emitState("正在启动摄像头", null, null);
|
||||
}
|
||||
try {
|
||||
// 保存配置时不打断上一位顾客;下一次 Session 启动前再重建引擎并读取新值。
|
||||
if (reloadConfiguration) releaseResources(false, false);
|
||||
ensureWarmResources();
|
||||
resetProcessorForSession(sessionId);
|
||||
} catch (Exception error) {
|
||||
@@ -175,7 +178,8 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
emitState(error.getMessage() == null ? "人脸识别初始化失败" : error.getMessage(), null, "INITIALIZATION_FAILED");
|
||||
}
|
||||
}
|
||||
releaseResources(true, true);
|
||||
// 初始化失败仍保留业务 Session,用户点击重试时可以在同一 Session 内重新冷启动。
|
||||
releaseResources(false, false);
|
||||
throw error;
|
||||
}
|
||||
synchronized (sessionLock) {
|
||||
@@ -225,6 +229,11 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
processorToStop = detachActiveSession();
|
||||
}
|
||||
stopProcessor(processorToStop);
|
||||
if (isRecognitionConfigurationDirty()) {
|
||||
releaseResources(true, true);
|
||||
} else {
|
||||
emitResourceState("摄像头热保持中");
|
||||
}
|
||||
synchronized (sessionLock) {
|
||||
return snapshot();
|
||||
}
|
||||
@@ -238,6 +247,11 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
processorToStop = detachActiveSession();
|
||||
}
|
||||
stopProcessor(processorToStop);
|
||||
if (isRecognitionConfigurationDirty()) {
|
||||
releaseResources(true, true);
|
||||
} else {
|
||||
emitResourceState("摄像头热保持中");
|
||||
}
|
||||
synchronized (sessionLock) {
|
||||
return snapshot();
|
||||
}
|
||||
@@ -262,6 +276,19 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 空闲时立即释放热资源,让下次识别读取新配置;活动识别则延迟到当前顾客结束,避免中断支付。
|
||||
*/
|
||||
public void onRecognitionConfigurationChanged() {
|
||||
boolean releaseNow;
|
||||
synchronized (sessionLock) {
|
||||
recognitionConfigurationDirty = true;
|
||||
releaseNow = activeSessionId == null;
|
||||
}
|
||||
// 设置页没有预览纹理占用,空闲时立即释放可避免下一次创建纹理后再重建 Renderer。
|
||||
if (releaseNow) releaseResources(true, true);
|
||||
}
|
||||
|
||||
public void releaseForLowMemory() {
|
||||
synchronized (sessionLock) {
|
||||
if (activeSessionId != null) {
|
||||
@@ -314,7 +341,8 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
Camera.CameraInfo info = new Camera.CameraInfo();
|
||||
Camera.getCameraInfo(cameraId, info);
|
||||
mirror = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT;
|
||||
displayOrientation = calculateDisplayOrientation(info, currentDisplayRotation());
|
||||
// OES SurfaceTexture 默认不会应用 Camera.setDisplayOrientation,Flutter 端只需补偿传感器方向。
|
||||
displayOrientation = info.orientation;
|
||||
camera = Camera.open(cameraId);
|
||||
Camera.Parameters parameters = camera.getParameters();
|
||||
previewSize = choosePreviewSize(parameters.getSupportedPreviewSizes());
|
||||
@@ -329,7 +357,9 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
camera.setParameters(parameters);
|
||||
camera.setDisplayOrientation(displayOrientation);
|
||||
// OES 输出保持传感器原始方向,主、副两个 Flutter Texture 统一按 rotation 字段旋转;
|
||||
// 若此处同时 setDisplayOrientation,会在部分设备上和 Flutter RotatedBox 叠加成双重旋转。
|
||||
camera.setDisplayOrientation(0);
|
||||
Camera openedCamera = camera;
|
||||
camera.setErrorCallback((error, callbackCamera) -> mainHandler.post(() -> {
|
||||
synchronized (sessionLock) {
|
||||
@@ -547,6 +577,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
previewRenderer = null;
|
||||
previewSize = null;
|
||||
warmResourcesReady = false;
|
||||
recognitionConfigurationDirty = false;
|
||||
if (clearActiveSession) {
|
||||
activeSessionId = null;
|
||||
activeScene = null;
|
||||
@@ -573,6 +604,8 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
if (rendererToRelease != null) rendererToRelease.release();
|
||||
// Camera 真正释放后再广播资源快照,避免顶栏在 20 分钟超时后仍显示为开启。
|
||||
emitResourceState(state == State.COLD ? "摄像头已关闭" : "摄像头资源已释放");
|
||||
}
|
||||
|
||||
private void ensureOwner(String sessionId) {
|
||||
@@ -581,6 +614,12 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isRecognitionConfigurationDirty() {
|
||||
synchronized (sessionLock) {
|
||||
return recognitionConfigurationDirty;
|
||||
}
|
||||
}
|
||||
|
||||
private void emitState(String message, List<Map<String, Double>> boxes, String errorCode) {
|
||||
Map<String, Object> event = snapshot();
|
||||
event.put("message", message);
|
||||
@@ -590,6 +629,19 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
for (Listener listener : listeners) listener.onSessionEvent(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源状态不增加业务 revision,活动 Session 会忽略它,常驻设备状态仍能获知 Camera 开关变化。
|
||||
*/
|
||||
private void emitResourceState(String message) {
|
||||
Map<String, Object> event;
|
||||
synchronized (sessionLock) {
|
||||
event = snapshot();
|
||||
event.put("message", message);
|
||||
event.put("faceBoxes", new ArrayList<>());
|
||||
}
|
||||
for (Listener listener : listeners) listener.onSessionEvent(event);
|
||||
}
|
||||
|
||||
private Map<String, Object> snapshot() {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("sessionId", activeSessionId == null ? "" : activeSessionId);
|
||||
@@ -635,22 +687,6 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
return best;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private int calculateDisplayOrientation(Camera.CameraInfo info, int rotation) {
|
||||
int degrees = rotation * 90;
|
||||
if (info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT) {
|
||||
int result = (info.orientation + degrees) % 360;
|
||||
return (360 - result) % 360;
|
||||
}
|
||||
return (info.orientation - degrees + 360) % 360;
|
||||
}
|
||||
|
||||
@SuppressWarnings("deprecation")
|
||||
private int currentDisplayRotation() {
|
||||
WindowManager windowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
|
||||
return windowManager == null ? Surface.ROTATION_0 : windowManager.getDefaultDisplay().getRotation();
|
||||
}
|
||||
|
||||
public static final class SessionBusyException extends Exception {
|
||||
private final String activeSessionId;
|
||||
|
||||
|
||||
+25
-12
@@ -6,6 +6,19 @@ import com.arcsoft.face.LivenessParam;
|
||||
* 识别相关的配置项
|
||||
*/
|
||||
public class RecognizeConfiguration {
|
||||
public static final int DEFAULT_EXTRACT_RETRY_COUNT = 3;
|
||||
public static final int DEFAULT_LIVENESS_RETRY_COUNT = 3;
|
||||
public static final int DEFAULT_MAX_DETECT_FACES = 1;
|
||||
public static final int DEFAULT_RECOGNIZE_FAILED_RETRY_INTERVAL = 0;
|
||||
public static final int DEFAULT_LIVENESS_FAILED_RETRY_INTERVAL = 0;
|
||||
public static final float DEFAULT_SIMILAR_THRESHOLD = 0.8F;
|
||||
public static final float DEFAULT_IMAGE_QUALITY_NO_MASK_THRESHOLD = 0.49F;
|
||||
public static final float DEFAULT_IMAGE_QUALITY_MASK_THRESHOLD = 0.29F;
|
||||
public static final float DEFAULT_RGB_LIVENESS_THRESHOLD = 0.5F;
|
||||
public static final boolean DEFAULT_ENABLE_LIVENESS = false;
|
||||
public static final boolean DEFAULT_ENABLE_IMAGE_QUALITY = true;
|
||||
public static final boolean DEFAULT_KEEP_MAX_FACE = true;
|
||||
|
||||
/**
|
||||
* 产生特征提取失败示语的特征提取次数(小于该值不提示)
|
||||
*/
|
||||
@@ -100,23 +113,23 @@ public class RecognizeConfiguration {
|
||||
|
||||
//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 int extractRetryCount = DEFAULT_EXTRACT_RETRY_COUNT;
|
||||
private int livenessRetryCount = DEFAULT_LIVENESS_RETRY_COUNT;
|
||||
private int maxDetectFaces = DEFAULT_MAX_DETECT_FACES;
|
||||
private int recognizeFailedRetryInterval = DEFAULT_RECOGNIZE_FAILED_RETRY_INTERVAL;
|
||||
private int livenessFailedRetryInterval = DEFAULT_LIVENESS_FAILED_RETRY_INTERVAL;
|
||||
private float similarThreshold = DEFAULT_SIMILAR_THRESHOLD;
|
||||
private float imageQualityNoMaskRecognizeThreshold = DEFAULT_IMAGE_QUALITY_NO_MASK_THRESHOLD;
|
||||
private float imageQualityMaskRecognizeThreshold = DEFAULT_IMAGE_QUALITY_MASK_THRESHOLD;
|
||||
private boolean enableLiveness = DEFAULT_ENABLE_LIVENESS;
|
||||
private boolean enableFaceAreaLimit = false;
|
||||
private boolean enableImageQuality = false;
|
||||
private boolean enableImageQuality = DEFAULT_ENABLE_IMAGE_QUALITY;
|
||||
private boolean enableFaceSizeLimit = false;
|
||||
private boolean enableFaceMoveLimit = false;
|
||||
private int faceSizeLimit = 0;
|
||||
private int faceMoveLimit = 0;
|
||||
private boolean keepMaxFace = false;
|
||||
private LivenessParam livenessParam;
|
||||
private boolean keepMaxFace = DEFAULT_KEEP_MAX_FACE;
|
||||
private LivenessParam livenessParam = new LivenessParam(DEFAULT_RGB_LIVENESS_THRESHOLD, DEFAULT_RGB_LIVENESS_THRESHOLD);
|
||||
|
||||
public Builder recognizeFailedRetryInterval(int val) {
|
||||
this.recognizeFailedRetryInterval = val;
|
||||
|
||||
+138
@@ -0,0 +1,138 @@
|
||||
package io.flutter.plugins.facediscern.tool.face.model;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import com.arcsoft.face.LivenessParam;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 保存终端本机的人脸识别策略,不写入业务数据库,避免配置升级影响 Floor 与人脸共享库。
|
||||
*/
|
||||
public final class RecognizeConfigurationStore {
|
||||
private static final String PREFERENCES_NAME = "face_recognize_configuration";
|
||||
|
||||
private RecognizeConfigurationStore() {
|
||||
}
|
||||
|
||||
public static RecognizeConfiguration load(Context context) {
|
||||
SharedPreferences preferences = preferences(context);
|
||||
return new RecognizeConfiguration.Builder()
|
||||
.enableLiveness(preferences.getBoolean("enableLiveness", RecognizeConfiguration.DEFAULT_ENABLE_LIVENESS))
|
||||
.similarThreshold(preferences.getFloat("similarThreshold", RecognizeConfiguration.DEFAULT_SIMILAR_THRESHOLD))
|
||||
.livenessParam(new LivenessParam(
|
||||
preferences.getFloat("rgbLivenessThreshold", RecognizeConfiguration.DEFAULT_RGB_LIVENESS_THRESHOLD),
|
||||
RecognizeConfiguration.DEFAULT_RGB_LIVENESS_THRESHOLD))
|
||||
.enableImageQuality(preferences.getBoolean("enableImageQuality", RecognizeConfiguration.DEFAULT_ENABLE_IMAGE_QUALITY))
|
||||
.imageQualityNoMaskRecognizeThreshold(preferences.getFloat("imageQualityNoMaskRecognizeThreshold", RecognizeConfiguration.DEFAULT_IMAGE_QUALITY_NO_MASK_THRESHOLD))
|
||||
.imageQualityMaskRecognizeThreshold(preferences.getFloat("imageQualityMaskRecognizeThreshold", RecognizeConfiguration.DEFAULT_IMAGE_QUALITY_MASK_THRESHOLD))
|
||||
.keepMaxFace(preferences.getBoolean("keepMaxFace", RecognizeConfiguration.DEFAULT_KEEP_MAX_FACE))
|
||||
.maxDetectFaces(preferences.getInt("maxDetectFaces", RecognizeConfiguration.DEFAULT_MAX_DETECT_FACES))
|
||||
.extractRetryCount(preferences.getInt("extractRetryCount", RecognizeConfiguration.DEFAULT_EXTRACT_RETRY_COUNT))
|
||||
.livenessRetryCount(preferences.getInt("livenessRetryCount", RecognizeConfiguration.DEFAULT_LIVENESS_RETRY_COUNT))
|
||||
.recognizeFailedRetryInterval(preferences.getInt("recognizeFailedRetryInterval", RecognizeConfiguration.DEFAULT_RECOGNIZE_FAILED_RETRY_INTERVAL))
|
||||
.livenessFailedRetryInterval(preferences.getInt("livenessFailedRetryInterval", RecognizeConfiguration.DEFAULT_LIVENESS_FAILED_RETRY_INTERVAL))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Flutter 只传允许现场调整的白名单字段,原生再次校验范围,防止异常参数拖垮识别线程。
|
||||
*/
|
||||
public static RecognizeConfiguration save(Context context, Map<?, ?> values) {
|
||||
RecognizeConfiguration current = load(context);
|
||||
boolean enableLiveness = booleanValue(values, "enableLiveness", current.isEnableLiveness());
|
||||
float similarThreshold = floatValue(values, "similarThreshold", current.getSimilarThreshold(), 0.5F, 0.99F);
|
||||
float rgbLivenessThreshold = floatValue(values, "rgbLivenessThreshold", rgbLivenessThreshold(current), 0.3F, 0.9F);
|
||||
boolean enableImageQuality = booleanValue(values, "enableImageQuality", current.isEnableImageQuality());
|
||||
float noMaskThreshold = floatValue(values, "imageQualityNoMaskRecognizeThreshold", current.getImageQualityNoMaskRecognizeThreshold(), 0F, 1F);
|
||||
float maskThreshold = floatValue(values, "imageQualityMaskRecognizeThreshold", current.getImageQualityMaskRecognizeThreshold(), 0F, 1F);
|
||||
boolean keepMaxFace = booleanValue(values, "keepMaxFace", current.isKeepMaxFace());
|
||||
int maxDetectFaces = intValue(values, "maxDetectFaces", current.getMaxDetectFaces(), 1, 3);
|
||||
int extractRetryCount = intValue(values, "extractRetryCount", current.getExtractRetryCount(), 0, 10);
|
||||
int livenessRetryCount = intValue(values, "livenessRetryCount", current.getLivenessRetryCount(), 0, 10);
|
||||
int recognizeRetryInterval = intValue(values, "recognizeFailedRetryInterval", current.getRecognizeFailedRetryInterval(), 0, 5000);
|
||||
int livenessRetryInterval = intValue(values, "livenessFailedRetryInterval", current.getLivenessFailedRetryInterval(), 0, 5000);
|
||||
|
||||
boolean saved = preferences(context).edit()
|
||||
.putBoolean("enableLiveness", enableLiveness)
|
||||
.putFloat("similarThreshold", similarThreshold)
|
||||
.putFloat("rgbLivenessThreshold", rgbLivenessThreshold)
|
||||
.putBoolean("enableImageQuality", enableImageQuality)
|
||||
.putFloat("imageQualityNoMaskRecognizeThreshold", noMaskThreshold)
|
||||
.putFloat("imageQualityMaskRecognizeThreshold", maskThreshold)
|
||||
.putBoolean("keepMaxFace", keepMaxFace)
|
||||
.putInt("maxDetectFaces", maxDetectFaces)
|
||||
.putInt("extractRetryCount", extractRetryCount)
|
||||
.putInt("livenessRetryCount", livenessRetryCount)
|
||||
.putInt("recognizeFailedRetryInterval", recognizeRetryInterval)
|
||||
.putInt("livenessFailedRetryInterval", livenessRetryInterval)
|
||||
.commit();
|
||||
if (!saved) throw new IllegalStateException("人脸识别配置保存失败");
|
||||
return load(context);
|
||||
}
|
||||
|
||||
public static RecognizeConfiguration reset(Context context) {
|
||||
if (!preferences(context).edit().clear().commit()) {
|
||||
throw new IllegalStateException("人脸识别配置恢复失败");
|
||||
}
|
||||
return load(context);
|
||||
}
|
||||
|
||||
public static Map<String, Object> toMap(RecognizeConfiguration configuration) {
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("enableLiveness", configuration.isEnableLiveness());
|
||||
result.put("similarThreshold", configuration.getSimilarThreshold());
|
||||
result.put("rgbLivenessThreshold", rgbLivenessThreshold(configuration));
|
||||
result.put("enableImageQuality", configuration.isEnableImageQuality());
|
||||
result.put("imageQualityNoMaskRecognizeThreshold", configuration.getImageQualityNoMaskRecognizeThreshold());
|
||||
result.put("imageQualityMaskRecognizeThreshold", configuration.getImageQualityMaskRecognizeThreshold());
|
||||
result.put("keepMaxFace", configuration.isKeepMaxFace());
|
||||
result.put("maxDetectFaces", configuration.getMaxDetectFaces());
|
||||
result.put("extractRetryCount", configuration.getExtractRetryCount());
|
||||
result.put("livenessRetryCount", configuration.getLivenessRetryCount());
|
||||
result.put("recognizeFailedRetryInterval", configuration.getRecognizeFailedRetryInterval());
|
||||
result.put("livenessFailedRetryInterval", configuration.getLivenessFailedRetryInterval());
|
||||
result.put("livenessType", "RGB");
|
||||
return result;
|
||||
}
|
||||
|
||||
private static SharedPreferences preferences(Context context) {
|
||||
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
private static float rgbLivenessThreshold(RecognizeConfiguration configuration) {
|
||||
LivenessParam param = configuration.getLivenessParam();
|
||||
return param == null ? RecognizeConfiguration.DEFAULT_RGB_LIVENESS_THRESHOLD : param.getRgbThreshold();
|
||||
}
|
||||
|
||||
private static boolean booleanValue(Map<?, ?> values, String key, boolean fallback) {
|
||||
Object value = values == null ? null : values.get(key);
|
||||
if (value == null) return fallback;
|
||||
if (!(value instanceof Boolean)) throw new IllegalArgumentException(key + " 必须为布尔值");
|
||||
return (Boolean) value;
|
||||
}
|
||||
|
||||
private static float floatValue(Map<?, ?> values, String key, float fallback, float min, float max) {
|
||||
Object value = values == null ? null : values.get(key);
|
||||
if (value == null) return fallback;
|
||||
if (!(value instanceof Number)) throw new IllegalArgumentException(key + " 必须为数字");
|
||||
float result = ((Number) value).floatValue();
|
||||
if (Float.isNaN(result) || Float.isInfinite(result) || result < min || result > max) {
|
||||
throw new IllegalArgumentException(key + " 必须在 " + min + " 到 " + max + " 之间");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private static int intValue(Map<?, ?> values, String key, int fallback, int min, int max) {
|
||||
Object value = values == null ? null : values.get(key);
|
||||
if (value == null) return fallback;
|
||||
if (!(value instanceof Number)) throw new IllegalArgumentException(key + " 必须为整数");
|
||||
int result = ((Number) value).intValue();
|
||||
if (result < min || result > max) {
|
||||
throw new IllegalArgumentException(key + " 必须在 " + min + " 到 " + max + " 之间");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
+17
-20
@@ -36,6 +36,7 @@ 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.RecognizeConfigurationStore;
|
||||
import io.flutter.plugins.facediscern.tool.face.widget.FaceRectView;
|
||||
import io.flutter.plugins.facediscern.ui.FaceDiscern;
|
||||
|
||||
@@ -141,37 +142,30 @@ public class FaceDiscernViewModel extends ViewModel implements RecognizeCallback
|
||||
setContext(context);
|
||||
boolean switchCamera = false;
|
||||
previewConfig = new PreviewConfig(switchCamera ? Camera.CameraInfo.CAMERA_FACING_FRONT : Camera.CameraInfo.CAMERA_FACING_BACK, switchCamera ? Camera.CameraInfo.CAMERA_FACING_BACK : Camera.CameraInfo.CAMERA_FACING_FRONT, 0, 0);
|
||||
// 填入在设置界面设置好的配置信息
|
||||
boolean enableLive = false;
|
||||
boolean enableFaceQualityDetect = true;
|
||||
boolean enableFaceMoveLimit = false;
|
||||
boolean enableFaceSizeLimit = false;
|
||||
int faceSizeLimit = 160;
|
||||
int faceMoveLimit = 20;
|
||||
int maxDetectFaces = 1;
|
||||
boolean keepMaxFace = true;
|
||||
float similarThreshold = 0.8F;
|
||||
float imageQualityNoMaskRecognizeThreshold = 0.49F;
|
||||
float imageQualityMaskRecognizeThreshold = 0.29F;
|
||||
RecognizeConfiguration configuration = new RecognizeConfiguration.Builder().enableFaceMoveLimit(enableFaceMoveLimit).enableFaceSizeLimit(enableFaceSizeLimit).faceSizeLimit(faceSizeLimit).faceMoveLimit(faceMoveLimit).enableLiveness(enableLive).enableImageQuality(enableFaceQualityDetect).maxDetectFaces(maxDetectFaces).keepMaxFace(keepMaxFace).similarThreshold(similarThreshold).imageQualityNoMaskRecognizeThreshold(imageQualityNoMaskRecognizeThreshold).imageQualityMaskRecognizeThreshold(imageQualityMaskRecognizeThreshold).livenessParam(new LivenessParam(0.5F, 0.5F)).build();
|
||||
// 单摄像头终端仅支持 RGB 活体,配置由设置页持久化并在每次冷启动识别引擎时读取。
|
||||
livenessType = LivenessType.RGB;
|
||||
RecognizeConfiguration configuration = RecognizeConfigurationStore.load(context);
|
||||
boolean enableLive = configuration.isEnableLiveness();
|
||||
boolean enableFaceQualityDetect = configuration.isEnableImageQuality();
|
||||
int maxDetectFaces = configuration.getMaxDetectFaces();
|
||||
|
||||
ftEngine = new FaceEngine();
|
||||
ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 1, FaceEngine.ASF_FACE_DETECT));
|
||||
ftInitCode.postValue(ftEngine.init(context, DetectMode.ASF_DETECT_MODE_VIDEO, DetectFaceOrientPriority.ASF_OP_ALL_OUT, maxDetectFaces, FaceEngine.ASF_FACE_DETECT));
|
||||
maskEngine = new FaceEngine();
|
||||
ftInitCode.postValue(maskEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 1, FaceEngine.ASF_MASK_DETECT));
|
||||
ftInitCode.postValue(maskEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, maxDetectFaces, FaceEngine.ASF_MASK_DETECT));
|
||||
|
||||
frEngine = new FaceEngine();
|
||||
int frEngineMask = FaceEngine.ASF_FACE_RECOGNITION;
|
||||
if (enableFaceQualityDetect) {
|
||||
frEngineMask |= FaceEngine.ASF_IMAGEQUALITY;
|
||||
}
|
||||
frInitCode.postValue(frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, 10, frEngineMask));
|
||||
frInitCode.postValue(frEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_0_ONLY, maxDetectFaces, frEngineMask));
|
||||
//启用活体检测时,才初始化活体引擎
|
||||
if (enableLive) {
|
||||
flEngine = new FaceEngine();
|
||||
int flEngineMask = (livenessType == LivenessType.RGB ? FaceEngine.ASF_LIVENESS : (FaceEngine.ASF_IR_LIVENESS | FaceEngine.ASF_FACE_DETECT));
|
||||
flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, 10, flEngineMask));
|
||||
LivenessParam livenessParam = new LivenessParam(0.5F, 0.5F);
|
||||
flInitCode.postValue(flEngine.init(context, DetectMode.ASF_DETECT_MODE_IMAGE, DetectFaceOrientPriority.ASF_OP_ALL_OUT, maxDetectFaces, flEngineMask));
|
||||
LivenessParam livenessParam = configuration.getLivenessParam();
|
||||
flEngine.setLivenessParam(livenessParam);
|
||||
}
|
||||
recognizeConfiguration.setValue(configuration);
|
||||
@@ -179,8 +173,11 @@ public class FaceDiscernViewModel extends ViewModel implements RecognizeCallback
|
||||
}
|
||||
|
||||
public boolean isEngineReady() {
|
||||
RecognizeConfiguration configuration = recognizeConfiguration.getValue();
|
||||
return Objects.equals(ftInitCode.getValue(), ErrorInfo.MOK)
|
||||
&& Objects.equals(frInitCode.getValue(), ErrorInfo.MOK);
|
||||
&& Objects.equals(frInitCode.getValue(), ErrorInfo.MOK)
|
||||
&& (configuration == null || !configuration.isEnableLiveness()
|
||||
|| Objects.equals(flInitCode.getValue(), ErrorInfo.MOK));
|
||||
}
|
||||
|
||||
private void initFaceHelper(Camera.Size lastPreviewSize) {
|
||||
@@ -193,7 +190,7 @@ public class FaceDiscernViewModel extends ViewModel implements RecognizeCallback
|
||||
}
|
||||
int horizontalOffset = 0;
|
||||
int verticalOffset = 0;
|
||||
int maxDetectFaceNum = 1;
|
||||
int maxDetectFaceNum = recognizeConfiguration.getValue().getMaxDetectFaces();
|
||||
faceHelper = new FaceHelper.Builder().ftEngine(ftEngine).maskEngine(maskEngine).frEngine(frEngine).flEngine(flEngine).frQueueSize(maxDetectFaceNum).flQueueSize(maxDetectFaceNum).previewSize(previewSize).recognizeCallback(faceHelperCallback).recognizeConfiguration(recognizeConfiguration.getValue()).trackedFaceCount(trackedFaceCount == null ? 0 : trackedFaceCount).dualCameraFaceInfoTransformer(faceInfo -> {
|
||||
FaceInfo irFaceInfo = new FaceInfo(faceInfo);
|
||||
irFaceInfo.getRect().offset(horizontalOffset, verticalOffset);
|
||||
|
||||
@@ -14,6 +14,7 @@ import io.flutter.plugins.facediscern.flutter.FlutterFaceDiscernPlugin
|
||||
import io.flutter.plugins.facediscern.flutter.FlutterFaceSubScreenPlugin
|
||||
import io.flutter.plugins.facediscern.session.FaceRecognitionSessionManager
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.rex.flutter_subscreen_plugin.FlutterSubScreenProvider
|
||||
import com.rex.flutter_subscreen_plugin.FlutterSubscreenPlugin
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
@@ -69,6 +70,25 @@ class MainActivity : FlutterActivity() {
|
||||
KioskModeManager.enterIfAvailable(this)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val subScreenProvider = FlutterSubScreenProvider.instance
|
||||
try {
|
||||
// Android 12 以下副屏使用 Overlay Presentation,任务被划掉时必须在
|
||||
// Activity 销毁前同步移除窗口并销毁独立 Engine,不能只依赖 Dart 生命周期回调。
|
||||
try {
|
||||
subScreenProvider.closeSubDisplay()
|
||||
} finally {
|
||||
try {
|
||||
subScreenProvider.flutterEngine?.destroy()
|
||||
} finally {
|
||||
subScreenProvider.onDispose()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
super.onDestroy()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
if (level >= TRIM_MEMORY_RUNNING_LOW) {
|
||||
|
||||
Reference in new issue
Block a user