This commit is contained in:
dzc committed 2026-08-31 15:09:38 +08:00
1 parent f6b37348c4
commit 917f03bc58
61 files changed
+4233 -367

No files matched your search

@@ -16,7 +16,7 @@ public class DatabaseHelper extends SQLiteOpenHelper {
private static final String DATABASE_PATH = "pj_cashier/database";
private static final String DATABASE_NAME = "cash_register.db";
/** 与 Flutter Floor 的 @Database 版本保持一致,避免原生层提前推进 user_version。 */
private static final int DATABASE_VERSION = 11;
private static final int DATABASE_VERSION = 12;
public DatabaseHelper(Context context) {
super(context, getCustomDatabasePath(context), null, DATABASE_VERSION);
@@ -41,6 +41,45 @@ public class DatabaseHelper extends SQLiteOpenHelper {
@Override
public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
onCreate(db);
if (oldVersion < 12) {
migrateFaceQuality(db);
}
}
/**
* 从版本11无损补齐本地人脸质量字段;重复执行安全,避免原生打开数据库时覆盖 Floor 迁移。
*/
private void migrateFaceQuality(SQLiteDatabase db) {
if (!hasColumn(db, "face", "qualityScore")) {
db.execSQL("ALTER TABLE face ADD COLUMN qualityScore REAL");
}
if (!hasColumn(db, "face", "qualityStatus")) {
db.execSQL("ALTER TABLE face ADD COLUMN qualityStatus TEXT NOT NULL DEFAULT 'pending'");
}
if (!hasColumn(db, "face", "qualityCheckedAt")) {
db.execSQL("ALTER TABLE face ADD COLUMN qualityCheckedAt TEXT");
}
if (!hasColumn(db, "face", "qualityErrorCode")) {
db.execSQL("ALTER TABLE face ADD COLUMN qualityErrorCode INTEGER");
}
db.execSQL("UPDATE face SET qualityStatus = 'pending' WHERE qualityStatus IS NULL OR qualityStatus NOT IN ('pending', 'detected', 'failed')");
// Floor 的可空时间转换器使用空字符串承载业务空值,原生兜底升级必须保持同一存储语义。
db.execSQL("UPDATE face SET qualityCheckedAt = '' WHERE qualityCheckedAt IS NULL");
db.execSQL("CREATE INDEX IF NOT EXISTS index_face_qualityStatus_qualityScore ON face(qualityStatus, qualityScore)");
}
/**
* 检查表中是否已经存在目标字段。
*/
private boolean hasColumn(SQLiteDatabase db, String table, String column) {
try (Cursor cursor = db.rawQuery("PRAGMA table_info(" + table + ")", null)) {
int nameIndex = cursor.getColumnIndex("name");
while (cursor.moveToNext()) {
if (nameIndex >= 0 && column.equals(cursor.getString(nameIndex))) {
return true;
}
}
}
return false;
}
}
@@ -40,6 +40,8 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
private static final String EVENT_TAG = "local/face_discern_event";
private static final String SESSION_EVENT_TAG = "local/face_discern_session_event";
private static final String REGISTRATION_EVENT_TAG = "local/face_discern_registration_event";
/** 在库照片质量检测进度事件通道。 */
private static final String QUALITY_AUDIT_EVENT_TAG = "local/face_discern_quality_audit_event";
private static final ExecutorService FACE_EXECUTOR = Executors.newSingleThreadExecutor();
private static final Handler MAIN_HANDLER = new Handler(Looper.getMainLooper());
@@ -51,9 +53,13 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
private EventChannel eventChannel;
private EventChannel sessionEventChannel;
private EventChannel registrationEventChannel;
/** 当前Flutter Engine对应的质量检测进度通道。 */
private EventChannel qualityAuditEventChannel;
private volatile EventChannel.EventSink faceEventSink;
private volatile EventChannel.EventSink sessionEventSink;
private volatile EventChannel.EventSink registrationEventSink;
/** 当前质量检测进度订阅;页面离开时置空但不终止原生任务。 */
private volatile EventChannel.EventSink qualityAuditEventSink;
private MethodChannel.Result pendingResult;
@@ -115,6 +121,21 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
registrationEventSink = null;
}
});
qualityAuditEventChannel = new EventChannel(binding.getBinaryMessenger(), QUALITY_AUDIT_EVENT_TAG);
qualityAuditEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
/** 订阅后立即返回最新进度,支持页面重建。 */
@Override
public void onListen(Object arguments, EventChannel.EventSink events) {
qualityAuditEventSink = events;
events.success(FlutterTool.getLatestQualityAuditProgress());
}
/** 取消页面订阅,不取消仍在后台执行的批量检测。 */
@Override
public void onCancel(Object arguments) {
qualityAuditEventSink = null;
}
});
}
@Override
@@ -125,9 +146,11 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
eventChannel.setStreamHandler(null);
sessionEventChannel.setStreamHandler(null);
registrationEventChannel.setStreamHandler(null);
if (qualityAuditEventChannel != null) qualityAuditEventChannel.setStreamHandler(null);
faceEventSink = null;
sessionEventSink = null;
registrationEventSink = null;
qualityAuditEventSink = null;
unregisterLegacyReceiver();
if (sessionManager != null) {
for (Long outputId : previewOutputs.values().toArray(new Long[0])) {
@@ -295,6 +318,9 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
case "extractFaceFeature":
startFaceFeatureExtraction(stringArgument(call, "filePath"), result);
break;
case "auditFaceQuality":
startFaceQualityAudit(call.argument("items"), result);
break;
case "applyFaceDelta":
startFaceDelta(call, result);
break;
@@ -366,6 +392,35 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
});
}
/**
* 在统一人脸操作互斥锁内批量检测存量图片,并通过独立事件通道报告进度。
*
* @param rawItems Flutter提交的样本编号与本地路径列表
* @param result 批量检测完成结果
*/
private void startFaceQualityAudit(Object rawItems, MethodChannel.Result result) {
if (!(rawItems instanceof List)) {
result.error("INVALID_QUALITY_AUDIT_ITEMS", "质量检测参数无效", null);
return;
}
if (!FlutterTool.tryStartRegistration()) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
FACE_EXECUTOR.execute(() -> {
try {
Map<String, Object> audit = FlutterTool.auditFaceQuality(
context, (List<?>) rawItems, this::emitQualityAuditProgress);
MAIN_HANDLER.post(() -> result.success(audit));
} catch (Exception exception) {
Log.e(TAG, "在库人脸质量检测失败:" + exception.getMessage(), exception);
MAIN_HANDLER.post(() -> result.error("FACE_QUALITY_AUDIT_FAILED", "在库人脸质量检测失败", null));
} finally {
FlutterTool.finishRegistration();
}
});
}
/**
* 在统一人脸注册占用标记和后台执行器中应用单样本增删。
*
@@ -427,6 +482,14 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
});
}
/** 将质量检测进度安全切回主线程发送给Flutter。 */
private void emitQualityAuditProgress(Map<String, Object> progress) {
MAIN_HANDLER.post(() -> {
EventChannel.EventSink sink = qualityAuditEventSink;
if (sink != null) sink.success(progress);
});
}
private String stringArgument(MethodCall call, String key) {
Object value = call.argument(key);
return value == null ? "" : value.toString();
@@ -47,11 +47,18 @@ public class FlutterTool {
private static final AtomicBoolean REGISTRATION_IN_PROGRESS = new AtomicBoolean(false);
private static volatile Map<String, Object> latestRegistrationProgress = idleRegistrationProgress();
private static volatile Map<String, Object> latestRegistrationResult;
private static volatile Map<String, Object> latestQualityAuditProgress = idleQualityAuditProgress();
public interface RegistrationProgressListener {
void onProgress(Map<String, Object> progress);
}
/** 接收原生在库质量检测进度并转发给Flutter事件通道。 */
public interface QualityAuditProgressListener {
/** 发布一份不包含图片路径和人员信息的进度快照。 */
void onProgress(Map<String, Object> progress);
}
static void updateConfig(String appId, String activeKey, String sdkKey) {
Constants.APP_ID = appId == null ? "" : appId.trim();
Constants.ACTIVE_KEY = activeKey == null ? "" : activeKey.trim();
@@ -152,6 +159,11 @@ public class FlutterTool {
return new HashMap<>(latestRegistrationProgress);
}
/** 返回最新在库图片质量检测进度,供新订阅者立即恢复页面状态。 */
static Map<String, Object> getLatestQualityAuditProgress() {
return new HashMap<>(latestQualityAuditProgress);
}
static Integer parseId(String id) {
try {
@@ -239,29 +251,38 @@ public class FlutterTool {
static public Boolean registerFace(Context context, byte[] bytes, FaceEntity entity) {
FaceServer faceServer = FaceServer.getInstance();
faceServer.init(context);
// 类型转换
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap == null) return false;
Bitmap alignedBitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true);
byte[] bgr24Data = ArcSoftImageUtil.createImageData(alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24);
int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24);
if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) {
Log.e(TAG, "注册人脸,bitmapToImageData错误code:" + transformCode);
if (alignedBitmap == null) {
bitmap.recycle();
return false;
}
byte[] faceBytes = faceServer.registerBgr24(
context,
bgr24Data,
alignedBitmap.getWidth(),
alignedBitmap.getHeight(),
String.valueOf(System.currentTimeMillis())
);
if (faceBytes == null) {
Log.e(TAG, "注册人脸,registerBgr24错误");
return false;
try {
byte[] bgr24Data = ArcSoftImageUtil.createImageData(alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24);
int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24);
if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) {
Log.e(TAG, "注册人脸,bitmapToImageData错误code:" + transformCode);
return false;
}
byte[] faceBytes = faceServer.registerBgr24(
context,
bgr24Data,
alignedBitmap.getWidth(),
alignedBitmap.getHeight(),
String.valueOf(System.currentTimeMillis())
);
if (faceBytes == null) {
Log.e(TAG, "注册人脸,registerBgr24错误");
return false;
}
entity.setFaceModel(faceBytes);
int code = faceServer.registerFaceFeatureInfoFromDb(entity);
return code == ErrorInfo.MOK;
} finally {
if (alignedBitmap != bitmap && !alignedBitmap.isRecycled()) alignedBitmap.recycle();
if (!bitmap.isRecycled()) bitmap.recycle();
}
entity.setFaceModel(faceBytes);
int code = faceServer.registerFaceFeatureInfoFromDb(entity);
return code == ErrorInfo.MOK;
}
@@ -303,21 +324,72 @@ public class FlutterTool {
static public Map<String, Object> extractFaceFeature(Context context, String filePath) {
Map<String, Object> result = new HashMap<>();
try {
byte[] featureData = extractFaceModelFromFile(context, filePath);
FaceServer.QualityFeatureResult nativeResult = extractFaceResultFromFile(context, filePath);
byte[] featureData = nativeResult.featureData;
boolean success = featureData != null && featureData.length > 0;
result.put("success", success);
result.put("message", success ? "人脸特征提取成功" : "图片中未提取到可注册人脸");
result.put("featureData", success ? featureData : new byte[0]);
result.put("qualityStatus", nativeResult.qualityStatus);
result.put("qualityScore", nativeResult.qualityScore);
result.put("qualityErrorCode", nativeResult.qualityErrorCode);
return result;
} catch (Exception exception) {
Log.e(TAG, "提取人脸特征异常error:" + exception.getMessage(), exception);
result.put("success", false);
result.put("message", "人脸特征提取失败");
result.put("featureData", new byte[0]);
result.put("qualityStatus", "failed");
result.put("qualityErrorCode", -99);
return result;
}
}
/**
* 批量检测Flutter提交的本地人脸图片质量,逐图释放Bitmap并返回一一对应的结果。
*/
static public Map<String, Object> auditFaceQuality(Context context, List<?> items, QualityAuditProgressListener listener) {
List<Map<String, Object>> results = new ArrayList<>();
int total = items == null ? 0 : items.size();
int success = 0;
int failed = 0;
publishQualityAuditProgress(listener, 0, total, success, failed, null, true, "准备检测在库人脸");
if (items != null) {
for (Object rawItem : items) {
Map<String, Object> itemResult = new HashMap<>();
String sampleId = "";
try {
if (!(rawItem instanceof Map)) throw new IllegalArgumentException("检测项格式错误");
Map<?, ?> item = (Map<?, ?>) rawItem;
sampleId = item.get("sampleId") == null ? "" : item.get("sampleId").toString();
String filePath = item.get("filePath") == null ? "" : item.get("filePath").toString();
FaceServer.QualityFeatureResult quality = detectFaceQualityFromFile(context, filePath);
itemResult.put("sampleId", sampleId);
itemResult.put("status", quality.qualityStatus);
itemResult.put("score", quality.qualityScore);
itemResult.put("errorCode", quality.qualityErrorCode);
boolean detected = "detected".equals(quality.qualityStatus) && quality.qualityScore != null;
itemResult.put("message", detected ? "图片质量检测成功" : "图片质量检测失败");
if (detected) success++; else failed++;
} catch (Exception exception) {
Log.w(TAG, "在库图片质量检测失败sampleId:" + maskSampleIdText(sampleId) + ";error:" + exception.getClass().getSimpleName());
itemResult.put("sampleId", sampleId);
itemResult.put("status", "failed");
itemResult.put("errorCode", -99);
itemResult.put("message", "图片质量检测失败");
failed++;
}
results.add(itemResult);
publishQualityAuditProgress(listener, results.size(), total, success, failed, sampleId, true, "正在检测在库人脸");
}
}
publishQualityAuditProgress(listener, total, total, success, failed, null, false, "在库人脸质量检测完成");
Map<String, Object> auditResult = new HashMap<>();
auditResult.put("results", results);
auditResult.put("message", "在库人脸质量检测完成");
return auditResult;
}
/**
* 在虹软内存库中应用单样本增删;替换失败由 FaceServer 恢复旧特征。
*
@@ -478,21 +550,107 @@ public class FlutterTool {
* @return 虹软特征字节,图片无效或未检测到合格人脸时返回空
*/
private static byte[] extractFaceModelFromFile(Context context, String filePath) {
if (filePath == null || filePath.isEmpty()) return null;
return extractFaceResultFromFile(context, filePath).featureData;
}
/** 将本地文件一次转换为BGR24并同时返回特征与质量结果。 */
private static FaceServer.QualityFeatureResult extractFaceResultFromFile(Context context, String filePath) {
FaceServer.QualityFeatureResult failed = new FaceServer.QualityFeatureResult();
failed.qualityStatus = "failed";
failed.qualityErrorCode = ErrorInfo.MERR_INVALID_PARAM;
if (filePath == null || filePath.isEmpty()) return failed;
byte[] bytes = FileUtil.fileToData(new File(filePath));
if (bytes == null || bytes.length == 0) return null;
if (bytes == null || bytes.length == 0) return failed;
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap == null) return null;
if (bitmap == null) return failed;
Bitmap alignedBitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true);
if (alignedBitmap == null) return null;
byte[] bgr24Data = ArcSoftImageUtil.createImageData(
alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24);
int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24);
if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) return null;
FaceServer faceServer = FaceServer.getInstance();
faceServer.init(context);
return faceServer.registerBgr24(context, bgr24Data, alignedBitmap.getWidth(),
alignedBitmap.getHeight(), String.valueOf(System.currentTimeMillis()));
if (alignedBitmap == null) {
bitmap.recycle();
return failed;
}
try {
byte[] bgr24Data = ArcSoftImageUtil.createImageData(
alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24);
int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24);
if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) {
failed.qualityErrorCode = transformCode;
return failed;
}
FaceServer faceServer = FaceServer.getInstance();
faceServer.init(context);
return faceServer.registerBgr24WithQuality(context, bgr24Data, alignedBitmap.getWidth(),
alignedBitmap.getHeight(), String.valueOf(System.currentTimeMillis()));
} finally {
if (alignedBitmap != bitmap && !alignedBitmap.isRecycled()) alignedBitmap.recycle();
if (!bitmap.isRecycled()) bitmap.recycle();
}
}
/** 将本地文件转换为BGR24后仅执行质量检测。 */
private static FaceServer.QualityFeatureResult detectFaceQualityFromFile(Context context, String filePath) {
FaceServer.QualityFeatureResult failed = new FaceServer.QualityFeatureResult();
failed.qualityStatus = "failed";
failed.qualityErrorCode = ErrorInfo.MERR_INVALID_PARAM;
if (filePath == null || filePath.isEmpty()) return failed;
byte[] bytes = FileUtil.fileToData(new File(filePath));
if (bytes == null || bytes.length == 0) return failed;
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap == null) return failed;
Bitmap alignedBitmap = ArcSoftImageUtil.getAlignedBitmap(bitmap, true);
if (alignedBitmap == null) {
bitmap.recycle();
return failed;
}
try {
byte[] bgr24Data = ArcSoftImageUtil.createImageData(alignedBitmap.getWidth(), alignedBitmap.getHeight(), ArcSoftImageFormat.BGR24);
int transformCode = ArcSoftImageUtil.bitmapToImageData(alignedBitmap, bgr24Data, ArcSoftImageFormat.BGR24);
if (transformCode != ArcSoftImageUtilError.CODE_SUCCESS) {
failed.qualityErrorCode = transformCode;
return failed;
}
FaceServer faceServer = FaceServer.getInstance();
faceServer.init(context);
return faceServer.detectBgr24Quality(bgr24Data, alignedBitmap.getWidth(), alignedBitmap.getHeight());
} finally {
if (alignedBitmap != bitmap && !alignedBitmap.isRecycled()) alignedBitmap.recycle();
if (!bitmap.isRecycled()) bitmap.recycle();
}
}
/** 发布并缓存质量检测进度,供页面中途进入时恢复。 */
private static void publishQualityAuditProgress(QualityAuditProgressListener listener, int processed, int total,
int success, int failed, String currentSampleId,
boolean inProgress, String message) {
Map<String, Object> progress = new HashMap<>();
progress.put("processed", processed);
progress.put("total", total);
progress.put("successCount", success);
progress.put("failedCount", failed);
progress.put("currentSampleId", currentSampleId);
progress.put("inProgress", inProgress);
progress.put("message", message);
latestQualityAuditProgress = progress;
if (listener != null) listener.onProgress(new HashMap<>(progress));
}
/** 创建尚未执行质量检测时的空进度快照。 */
private static Map<String, Object> idleQualityAuditProgress() {
Map<String, Object> progress = new HashMap<>();
progress.put("processed", 0);
progress.put("total", 0);
progress.put("successCount", 0);
progress.put("failedCount", 0);
progress.put("currentSampleId", null);
progress.put("inProgress", false);
progress.put("message", "尚未执行在库人脸质量检测");
return progress;
}
/** 对质量检测日志中的样本编号做最小脱敏。 */
private static String maskSampleIdText(String sampleId) {
if (sampleId == null || sampleId.isEmpty()) return "-";
if (sampleId.length() <= 4) return "***" + sampleId;
return sampleId.substring(0, 1) + "***" + sampleId.substring(sampleId.length() - 3);
}
private static boolean hasFaceModel(FaceEntity face) {
@@ -14,6 +14,8 @@ public class RecognizeConfiguration {
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_LIBRARY_FACE_QUALITY_THRESHOLD = 0.60F;
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;
@@ -43,6 +45,10 @@ public class RecognizeConfiguration {
* 图像质量检测阈值:适用于戴口罩且人脸识别场景
*/
private float imageQualityMaskRecognizeThreshold;
/**
* 在库照片质量诊断阈值,不参与实时识别和特征注册。
*/
private float libraryFaceQualityThreshold;
/**
* 识别失败重试间隔
*/
@@ -99,6 +105,7 @@ public class RecognizeConfiguration {
this.similarThreshold = builder.similarThreshold;
this.imageQualityNoMaskRecognizeThreshold = builder.imageQualityNoMaskRecognizeThreshold;
this.imageQualityMaskRecognizeThreshold = builder.imageQualityMaskRecognizeThreshold;
this.libraryFaceQualityThreshold = builder.libraryFaceQualityThreshold;
this.enableLiveness = builder.enableLiveness;
this.enableImageQuality = builder.enableImageQuality;
this.enableFaceAreaLimit = builder.enableFaceAreaLimit;
@@ -121,6 +128,8 @@ public class RecognizeConfiguration {
private float similarThreshold = DEFAULT_SIMILAR_THRESHOLD;
private float imageQualityNoMaskRecognizeThreshold = DEFAULT_IMAGE_QUALITY_NO_MASK_THRESHOLD;
private float imageQualityMaskRecognizeThreshold = DEFAULT_IMAGE_QUALITY_MASK_THRESHOLD;
/** 在库照片质量诊断阈值,允许范围为0到1。 */
private float libraryFaceQualityThreshold = DEFAULT_LIBRARY_FACE_QUALITY_THRESHOLD;
private boolean enableLiveness = DEFAULT_ENABLE_LIVENESS;
private boolean enableFaceAreaLimit = false;
private boolean enableImageQuality = DEFAULT_ENABLE_IMAGE_QUALITY;
@@ -171,6 +180,17 @@ public class RecognizeConfiguration {
return this;
}
/**
* 设置在库照片质量诊断阈值。
*
* @param val 0到1之间的质量分界值
* @return 当前构建器
*/
public Builder libraryFaceQualityThreshold(float val) {
this.libraryFaceQualityThreshold = val;
return this;
}
public Builder enableLiveness(boolean val) {
this.enableLiveness = val;
return this;
@@ -230,6 +250,15 @@ public class RecognizeConfiguration {
return imageQualityMaskRecognizeThreshold;
}
/**
* 读取在库照片质量诊断阈值。
*
* @return 0到1之间的质量分界值
*/
public float getLibraryFaceQualityThreshold() {
return libraryFaceQualityThreshold;
}
public boolean isEnableImageQuality() {
return enableImageQuality;
}
@@ -304,6 +333,7 @@ public class RecognizeConfiguration {
"enableImageQuality: " + enableImageQuality + "\r\n" +
"imageQualityNoMaskRecognizeThreshold: " + imageQualityNoMaskRecognizeThreshold + "\r\n" +
"imageQualityMaskRecognizeThreshold: " + imageQualityMaskRecognizeThreshold + "\r\n" +
"libraryFaceQualityThreshold: " + libraryFaceQualityThreshold + "\r\n" +
"enableLiveness: " + enableLiveness + "\r\n" +
"livenessRetryCount: " + livenessRetryCount + "\r\n" +
@@ -28,6 +28,7 @@ public final class RecognizeConfigurationStore {
.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))
.libraryFaceQualityThreshold(preferences.getFloat("libraryFaceQualityThreshold", RecognizeConfiguration.DEFAULT_LIBRARY_FACE_QUALITY_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))
@@ -48,6 +49,7 @@ public final class RecognizeConfigurationStore {
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);
float libraryFaceQualityThreshold = floatValue(values, "libraryFaceQualityThreshold", current.getLibraryFaceQualityThreshold(), 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);
@@ -62,6 +64,7 @@ public final class RecognizeConfigurationStore {
.putBoolean("enableImageQuality", enableImageQuality)
.putFloat("imageQualityNoMaskRecognizeThreshold", noMaskThreshold)
.putFloat("imageQualityMaskRecognizeThreshold", maskThreshold)
.putFloat("libraryFaceQualityThreshold", libraryFaceQualityThreshold)
.putBoolean("keepMaxFace", keepMaxFace)
.putInt("maxDetectFaces", maxDetectFaces)
.putInt("extractRetryCount", extractRetryCount)
@@ -88,6 +91,7 @@ public final class RecognizeConfigurationStore {
result.put("enableImageQuality", configuration.isEnableImageQuality());
result.put("imageQualityNoMaskRecognizeThreshold", configuration.getImageQualityNoMaskRecognizeThreshold());
result.put("imageQualityMaskRecognizeThreshold", configuration.getImageQualityMaskRecognizeThreshold());
result.put("libraryFaceQualityThreshold", configuration.getLibraryFaceQualityThreshold());
result.put("keepMaxFace", configuration.isKeepMaxFace());
result.put("maxDetectFaces", configuration.getMaxDetectFaces());
result.put("extractRetryCount", configuration.getExtractRetryCount());
@@ -11,6 +11,7 @@ import com.arcsoft.face.FaceEngine;
import com.arcsoft.face.FaceFeature;
import com.arcsoft.face.FaceFeatureInfo;
import com.arcsoft.face.FaceInfo;
import com.arcsoft.face.ImageQualitySimilar;
import com.arcsoft.face.MaskInfo;
import com.arcsoft.face.SearchResult;
import com.arcsoft.face.enums.DetectFaceOrientPriority;
@@ -45,6 +46,8 @@ public class FaceServer {
private static volatile FaceServer faceServer = null;
private final Map<Integer, FaceEntity> faceRegisterInfoMap = new HashMap<>();
private int engineInitCode = -99;
private int imageQualityInitCode = -99;
private boolean imageQualityReady;
private String imageRootPath;
/**
* 最大注册人脸数
@@ -54,17 +57,35 @@ public class FaceServer {
public synchronized void init(Context context) {
if (faceEngine == null && context != null) {
faceEngine = new FaceEngine();
engineInitCode = faceEngine.init(
int requestedCode = 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
FaceEngine.ASF_FACE_RECOGNITION | FaceEngine.ASF_FACE_DETECT | FaceEngine.ASF_MASK_DETECT | FaceEngine.ASF_IMAGEQUALITY
);
if (engineInitCode != ErrorInfo.MOK) {
Log.e(TAG, "人脸引擎初始化失败code" + engineInitCode);
imageQualityInitCode = requestedCode;
if (requestedCode == ErrorInfo.MOK) {
engineInitCode = requestedCode;
imageQualityReady = true;
} else {
Log.w(TAG, "图像质量模块不可用,回退基础人脸引擎code" + requestedCode);
faceEngine.unInit();
faceEngine = null;
faceEngine = new FaceEngine();
engineInitCode = 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
);
imageQualityReady = false;
if (engineInitCode != ErrorInfo.MOK) {
Log.e(TAG, "人脸引擎初始化失败code" + engineInitCode);
faceEngine.unInit();
faceEngine = null;
}
}
}
}
@@ -77,6 +98,16 @@ public class FaceServer {
return faceEngine != null && engineInitCode == ErrorInfo.MOK;
}
/** 返回当前授权是否包含虹软图像质量模块。 */
public synchronized boolean isImageQualityReady() {
return imageQualityReady;
}
/** 返回图像质量模块初始化返回码,用于终端诊断授权状态。 */
public synchronized int getImageQualityInitCode() {
return imageQualityInitCode;
}
public static FaceServer getInstance() {
if (faceServer == null) {
synchronized (FaceServer.class) {
@@ -100,6 +131,8 @@ public class FaceServer {
faceEngine = null;
}
engineInitCode = -99;
imageQualityInitCode = -99;
imageQualityReady = false;
faceServer = null;
}
@@ -302,10 +335,23 @@ public class FaceServer {
}
/** 保持旧调用方返回特征字节的兼容入口。 */
public synchronized byte[] registerBgr24(Context context, byte[] bgr24, int width, int height, String name) {
return registerBgr24WithQuality(context, bgr24, width, height, name).featureData;
}
/**
* 在统一解码结果上同时执行图像质量检测和虹软特征提取。
*
* <p>质量模块不可用或检测失败时仅记录failed状态,仍继续原有特征提取,兼容免费版授权。</p>
*/
public synchronized QualityFeatureResult registerBgr24WithQuality(Context context, byte[] bgr24, int width, int height, String name) {
QualityFeatureResult qualityResult = new QualityFeatureResult();
if (faceEngine == null || context == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) {
Log.e(TAG, "registerBgr24: invalid params");
return null;
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = ErrorInfo.MERR_INVALID_PARAM;
return qualityResult;
}
//人脸检测
List<FaceInfo> faceInfoList = new ArrayList<>();
@@ -314,6 +360,25 @@ public class FaceServer {
code = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList);
}
if (code == ErrorInfo.MOK && !faceInfoList.isEmpty()) {
if (imageQualityReady) {
ImageQualitySimilar qualitySimilar = new ImageQualitySimilar();
int qualityCode;
synchronized (faceEngine) {
qualityCode = faceEngine.imageQualityDetect(
bgr24, width, height, FaceEngine.CP_PAF_BGR24,
faceInfoList.get(0), MaskInfo.NOT_WORN, qualitySimilar);
}
if (qualityCode == ErrorInfo.MOK) {
qualityResult.qualityStatus = "detected";
qualityResult.qualityScore = (double) qualitySimilar.getScore();
} else {
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = qualityCode;
}
} else {
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = imageQualityInitCode;
}
code = faceEngine.process(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList,
FaceEngine.ASF_MASK_DETECT);
if (code == ErrorInfo.MOK) {
@@ -326,16 +391,18 @@ public class FaceServer {
* 注册照要求不戴口罩
*/
Log.e(TAG, "registerBgr24: maskInfo is worn");
return null;
return qualityResult;
}
if (isMask == MaskInfo.UNKNOWN) {
Log.e(TAG, "registerBgr24: maskInfo is unknown");
return null;
return qualityResult;
}
}
} else {
Log.e(TAG, "registerBgr24 mask process fail res=" + code);
return null;
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = code;
return qualityResult;
}
FaceFeature faceFeature = new FaceFeature();
/*
@@ -352,7 +419,9 @@ public class FaceServer {
Rect cropRect = getBestRect(width, height, faceInfoList.get(0).getRect());
if (cropRect == null) {
Log.e(TAG, "registerBgr24: cropRect is null");
return null;
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = ErrorInfo.MERR_INVALID_PARAM;
return qualityResult;
}
cropRect.left &= ~3;
cropRect.top &= ~3;
@@ -361,25 +430,83 @@ public class FaceServer {
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);
try (FileOutputStream fos = new FileOutputStream(imgPath)) {
headBmp.compress(Bitmap.CompressFormat.JPEG, 100, fos);
fos.close();
} catch (IOException e) {
e.printStackTrace();
return null;
Log.e(TAG, "registerBgr24: save head image failed", e);
return qualityResult;
} finally {
if (!headBmp.isRecycled()) headBmp.recycle();
}
return faceFeature.getFeatureData();
qualityResult.featureData = faceFeature.getFeatureData();
return qualityResult;
} else {
Log.e(TAG, "registerBgr24: extract face feature failed, code is " + code);
return null;
return qualityResult;
}
} else {
Log.e(TAG, "registerBgr24: no face detected, code is " + code);
return null;
qualityResult.qualityStatus = "failed";
qualityResult.qualityErrorCode = code == ErrorInfo.MOK
? ErrorInfo.MERR_FSDK_FACEFEATURE_NO_FACE
: code;
return qualityResult;
}
}
/**
* 仅检测BGR24在库照片的质量,不提取或写入人脸特征。
*
* @param bgr24 BGR24图片数据
* @param width 图片宽度
* @param height 图片高度
* @return detected或failed质量结果
*/
public synchronized QualityFeatureResult detectBgr24Quality(byte[] bgr24, int width, int height) {
QualityFeatureResult result = new QualityFeatureResult();
if (faceEngine == null || bgr24 == null || width % 4 != 0 || bgr24.length != width * height * 3) {
result.qualityStatus = "failed";
result.qualityErrorCode = ErrorInfo.MERR_INVALID_PARAM;
return result;
}
List<FaceInfo> faceInfoList = new ArrayList<>();
int detectCode = faceEngine.detectFaces(bgr24, width, height, FaceEngine.CP_PAF_BGR24, faceInfoList);
if (detectCode != ErrorInfo.MOK || faceInfoList.isEmpty()) {
result.qualityStatus = "failed";
result.qualityErrorCode = detectCode == ErrorInfo.MOK ? ErrorInfo.MERR_FSDK_FACEFEATURE_NO_FACE : detectCode;
return result;
}
if (!imageQualityReady) {
result.qualityStatus = "failed";
result.qualityErrorCode = imageQualityInitCode;
return result;
}
ImageQualitySimilar qualitySimilar = new ImageQualitySimilar();
int qualityCode = faceEngine.imageQualityDetect(
bgr24, width, height, FaceEngine.CP_PAF_BGR24,
faceInfoList.get(0), MaskInfo.NOT_WORN, qualitySimilar);
if (qualityCode == ErrorInfo.MOK) {
result.qualityStatus = "detected";
result.qualityScore = (double) qualitySimilar.getScore();
} else {
result.qualityStatus = "failed";
result.qualityErrorCode = qualityCode;
}
return result;
}
/** 原生图片特征与质量诊断的组合结果。 */
public static class QualityFeatureResult {
/** 注册特征字节,提取失败时为空。 */
public byte[] featureData;
/** 质量状态,稳定值为detected或failed。 */
public String qualityStatus = "pending";
/** 检测成功时的质量分。 */
public Double qualityScore;
/** 检测失败时的错误码。 */
public Integer qualityErrorCode;
}
private String getImagePath(String name, Context context) {
if (imageRootPath == null) {
imageRootPath = getImageDir(context);