feat:人脸机器实时增量更新

This commit is contained in:
dzc committed 2026-08-17 13:39:18 +08:00
1 parent 8cfbbd768e
commit 3770173604
29 files changed
+1584 -359

No files matched your search

@@ -186,6 +186,10 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
break;
case "startRecognition":
try {
if (FlutterTool.isRegistrationInProgress()) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库更新中,请稍后重试", null);
break;
}
result.success(sessionManager.startRecognition(stringArgument(call, "sessionId"), stringArgument(call, "scene"), this));
} catch (FaceRecognitionSessionManager.SessionBusyException e) {
result.error("SESSION_BUSY", e.getMessage(), e.getActiveSessionId());
@@ -197,6 +201,10 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
break;
case "retryRecognition":
try {
if (FlutterTool.isRegistrationInProgress()) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库更新中,请稍后重试", null);
break;
}
result.success(sessionManager.retryRecognition(stringArgument(call, "sessionId")));
} catch (Exception e) {
result.error("RECOGNITION_RETRY_FAILED", e.getMessage(), null);
@@ -268,6 +276,12 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
case "rebuildFaceLibrary":
startRegistrationTask(call.argument("userId"), result, false);
break;
case "extractFaceFeature":
startFaceFeatureExtraction(stringArgument(call, "filePath"), result);
break;
case "applyFaceDelta":
startFaceDelta(call, result);
break;
case "getFace":
String sampleId = Objects.requireNonNull(call.argument("sampleId")).toString();
Map<String, Object> face = FlutterTool.getFace(context, Integer.parseInt(sampleId));
@@ -312,6 +326,84 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
});
}
/**
* 在统一人脸注册占用标记和后台执行器中提取临时图片特征。
*
* @param filePath 临时图片绝对路径
* @param result Flutter MethodChannel 异步结果
*/
private void startFaceFeatureExtraction(String filePath, MethodChannel.Result result) {
if (!FlutterTool.tryStartRegistration()) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
FACE_EXECUTOR.execute(() -> {
try {
Map<String, Object> extraction = FlutterTool.extractFaceFeature(context, filePath);
MAIN_HANDLER.post(() -> result.success(extraction));
} catch (Exception exception) {
Log.e(TAG, "提取人脸特征失败:" + exception.getMessage(), exception);
MAIN_HANDLER.post(() -> result.error("EXTRACT_FACE_FEATURE_FAILED", "人脸特征提取失败", null));
} finally {
FlutterTool.finishRegistration();
}
});
}
/**
* 在统一人脸注册占用标记和后台执行器中应用单样本增删。
*
* @param call 包含 operation、sampleId、用户元数据和可选特征字节的调用
* @param result Flutter MethodChannel 异步结果
*/
private void startFaceDelta(MethodCall call, MethodChannel.Result result) {
if (!FlutterTool.tryStartRegistration()) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
FACE_EXECUTOR.execute(() -> {
try {
String operation = stringArgument(call, "operation");
Number sampleIdValue = call.argument("sampleId");
if (sampleIdValue == null) throw new IllegalArgumentException("sampleId不能为空");
byte[] featureData = byteArrayArgument(call.argument("featureData"));
Map<String, Object> delta = FlutterTool.applyFaceDelta(
context,
operation,
sampleIdValue.intValue(),
stringArgument(call, "userId"),
stringArgument(call, "username"),
featureData
);
MAIN_HANDLER.post(() -> result.success(delta));
} catch (Exception exception) {
Log.e(TAG, "应用人脸增量失败:" + exception.getMessage(), exception);
MAIN_HANDLER.post(() -> result.error("APPLY_FACE_DELTA_FAILED", "人脸库增量更新失败", null));
} finally {
FlutterTool.finishRegistration();
}
});
}
/**
* 将 Flutter StandardMessageCodec 的数字列表转换为虹软特征字节。
*
* @param value 可为空的 byte[] 或数字列表
* @return 特征字节;参数为空时返回空数组
*/
private byte[] byteArrayArgument(Object value) {
if (value instanceof byte[]) return (byte[]) value;
if (!(value instanceof List)) return new byte[0];
List<?> values = (List<?>) value;
byte[] result = new byte[values.size()];
for (int index = 0; index < values.size(); index++) {
Object item = values.get(index);
if (!(item instanceof Number)) throw new IllegalArgumentException("featureData包含非数字值");
result[index] = ((Number) item).byteValue();
}
return result;
}
private void emitRegistrationProgress(Map<String, Object> progress) {
MAIN_HANDLER.post(() -> {
EventChannel.EventSink sink = registrationEventSink;
@@ -24,6 +24,7 @@ import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
@@ -107,9 +108,17 @@ public class FlutterTool {
FaceServer faceServer = FaceServer.getInstance();
if (activated) faceServer.init(context);
int databaseCount = getUserFace(context, null).size();
List<FaceEntity> databaseFaces = getUserFace(context, null);
int databaseCount = databaseFaces.size();
int memoryCount = activated && faceServer.isEngineReady() ? Math.max(faceServer.getTotalFaceCount(), 0) : 0;
boolean registrationInProgress = REGISTRATION_IN_PROGRESS.get();
Set<Integer> databaseSampleIds = new HashSet<>();
for (FaceEntity face : databaseFaces) {
if (face.getSampleId() != null) databaseSampleIds.add(face.getSampleId());
}
Set<Integer> memorySampleIds = activated && faceServer.isEngineReady()
? faceServer.getRegisteredFaceIds()
: new HashSet<>();
status.put("activated", activated);
status.put("activationCode", activeCode);
@@ -121,7 +130,7 @@ public class FlutterTool {
status.put("databaseCount", databaseCount);
status.put("memoryCount", memoryCount);
status.put("registrationInProgress", registrationInProgress);
status.put("libraryConsistent", !registrationInProgress && databaseCount == memoryCount);
status.put("libraryConsistent", !registrationInProgress && databaseSampleIds.equals(memorySampleIds));
status.put("registrationProgress", new HashMap<>(latestRegistrationProgress));
status.put("lastRegistrationResult", latestRegistrationResult == null ? null : new HashMap<>(latestRegistrationResult));
return status;
@@ -284,6 +293,68 @@ public class FlutterTool {
return i == 0;
}
/**
* 从临时图片提取虹软注册特征,不修改 SQLite 或虹软内存人脸库。
*
* @param context Android 应用上下文
* @param filePath 同步服务写入的临时图片绝对路径
* @return 包含成功标记、安全摘要和特征字节的 MethodChannel 结果
*/
static public Map<String, Object> extractFaceFeature(Context context, String filePath) {
Map<String, Object> result = new HashMap<>();
try {
byte[] featureData = extractFaceModelFromFile(context, filePath);
boolean success = featureData != null && featureData.length > 0;
result.put("success", success);
result.put("message", success ? "人脸特征提取成功" : "图片中未提取到可注册人脸");
result.put("featureData", success ? featureData : new byte[0]);
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]);
return result;
}
}
/**
* 在虹软内存库中应用单样本增删;替换失败由 FaceServer 恢复旧特征。
*
* @param context Android 应用上下文
* @param operation upsert 或 delete
* @param sampleId 虹软 32 位搜索标识
* @param userId 用户主键,仅 upsert 使用
* @param username 用户姓名,仅作为内存识别元数据
* @param featureData 已通过提取校验的特征字节,仅 upsert 使用
* @return 包含成功标记、SDK 返回码和安全摘要的结果
*/
static public Map<String, Object> applyFaceDelta(Context context, String operation, int sampleId,
String userId, String username, byte[] featureData) {
Map<String, Object> result = new HashMap<>();
FaceServer faceServer = FaceServer.getInstance();
faceServer.init(context);
int code;
if ("delete".equals(operation)) {
code = faceServer.removeFace(sampleId);
} else if ("upsert".equals(operation) && featureData != null && featureData.length > 0) {
FaceEntity entity = new FaceEntity();
entity.setFaceId(sampleId);
entity.setSampleId(sampleId);
entity.setUserId(userId);
entity.setUserName(username);
entity.setFaceModel(featureData);
code = faceServer.replaceFaceFeature(entity);
} else {
code = ErrorInfo.MERR_INVALID_PARAM;
}
boolean success = code == ErrorInfo.MOK;
result.put("success", success);
result.put("code", code);
result.put("message", success ? "人脸库增量更新成功" : "人脸库增量更新失败");
return result;
}
// 旧通道保留同步包装;新页面通过 rebuildFaceLibrary 获取完整结果和进度。
static public List<Integer> registerFaceByDatabase(Context context, String userId) {
@@ -341,7 +412,13 @@ public class FlutterTool {
for (int start = 0; start < validFaces.size(); start += REGISTER_BATCH_SIZE) {
int end = Math.min(start + REGISTER_BATCH_SIZE, validFaces.size());
List<FaceEntity> batch = new ArrayList<>(validFaces.subList(start, end));
int code = faceServer.registerFaceFeatureInfoBatchFromDb(batch);
int code;
try {
code = faceServer.registerFaceFeatureInfoBatchFromDb(batch);
} catch (Exception exception) {
code = -99;
Log.e(TAG, "批量注册抛出异常,改为逐条处理error:" + exception.getMessage(), exception);
}
if (code == ErrorInfo.MOK) {
success += batch.size();
processed += batch.size();
@@ -352,7 +429,13 @@ public class FlutterTool {
Log.w(TAG, "批量注册失败code:" + code + ";改为逐条定位失败样本");
for (FaceEntity face : batch) {
int singleCode = faceServer.registerFaceFeatureInfoFromDb(face);
int singleCode;
try {
singleCode = faceServer.registerFaceFeatureInfoFromDb(face);
} catch (Exception exception) {
singleCode = -99;
Log.e(TAG, "单条注册异常sampleId:" + maskSampleId(face.getSampleId()) + ";error:" + exception.getMessage(), exception);
}
if (singleCode == ErrorInfo.MOK) {
success++;
} else if (face.getSampleId() != null) {
@@ -377,17 +460,7 @@ public class FlutterTool {
private static boolean extractAndPersistFaceModel(Context context, FaceEntity face) {
if (face == null || face.getFaceFilePath() == null) return false;
try {
byte[] bytes = FileUtil.fileToData(new File(face.getFaceFilePath()));
if (bytes == null || bytes.length == 0) return false;
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) return false;
byte[] faceBytes = faceServer.registerBgr24(context, bgr24Data, alignedBitmap.getWidth(), alignedBitmap.getHeight(), String.valueOf(System.currentTimeMillis()));
byte[] faceBytes = extractFaceModelFromFile(context, face.getFaceFilePath());
if (faceBytes == null || faceBytes.length == 0) return false;
face.setFaceModel(faceBytes);
return updateFaceModel(context, face);
@@ -397,6 +470,31 @@ public class FlutterTool {
}
}
/**
* 解码图片并在统一 FaceServer 引擎锁内提取注册特征,不写入业务数据库。
*
* @param context Android 应用上下文
* @param filePath 待校验图片绝对路径
* @return 虹软特征字节,图片无效或未检测到合格人脸时返回空
*/
private static byte[] extractFaceModelFromFile(Context context, String filePath) {
if (filePath == null || filePath.isEmpty()) return null;
byte[] bytes = FileUtil.fileToData(new File(filePath));
if (bytes == null || bytes.length == 0) return null;
Bitmap bitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
if (bitmap == null) return null;
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()));
}
private static boolean hasFaceModel(FaceEntity face) {
return face != null && face.getFaceModel() != null && face.getFaceModel().length > 0;
}
@@ -26,9 +26,12 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.HashSet;
import io.flutter.plugins.facediscern.tool.face.model.CompareResult;
import io.flutter.plugins.facediscern.tool.face.model.FaceEntity;
@@ -129,25 +132,96 @@ public class FaceServer {
return code;
}
/**
* 在同一虹软引擎互斥边界内原子替换单个搜索标识的人脸特征。
*
* <p>替换前复制旧特征和业务元数据;新特征注册失败或 SDK 抛异常时恢复旧特征,
* 避免坏图导致原本可识别的人脸永久丢失。</p>
*
* @param faceEntity 包含搜索标识、用户元数据和新特征的目标人脸
* @return 虹软返回码,插件异常返回内部错误码 {@code -99}
*/
public synchronized int replaceFaceFeature(FaceEntity faceEntity) {
if (faceEntity == null || faceEngine == null || faceEntity.getFaceId() == null || faceEntity.getFaceModel() == null) {
return -99;
}
int searchId = faceEntity.getFaceId();
FaceEntity oldEntity = copyFaceEntity(faceRegisterInfoMap.get(searchId));
try {
FaceFeatureInfo oldFeature = getFaceFeature(searchId);
if (oldEntity == null && oldFeature != null) {
oldEntity = new FaceEntity(searchId, oldFeature.getFeatureData());
}
faceEngine.removeFaceFeature(searchId);
faceRegisterInfoMap.remove(searchId);
FaceFeatureInfo newFeature = new FaceFeatureInfo(searchId, faceEntity.getFaceModel());
int code = faceEngine.registerFaceFeature(Collections.singletonList(newFeature));
if (code == ErrorInfo.MOK && getFaceFeature(searchId) != null) {
faceRegisterInfoMap.put(searchId, faceEntity);
return code;
}
restoreFaceFeature(searchId, oldEntity);
return code == ErrorInfo.MOK ? -99 : code;
} catch (Exception exception) {
Log.e(TAG, "替换人脸特征异常searchId:" + searchId + ";error:" + exception.getMessage(), exception);
restoreFaceFeature(searchId, oldEntity);
return -99;
}
}
/**
* 返回当前虹软内存库已登记的搜索标识快照,用于集合一致性判断。
*
* @return 与内部映射解耦的搜索标识集合
*/
public synchronized Set<Integer> getRegisteredFaceIds() {
return new HashSet<>(faceRegisterInfoMap.keySet());
}
/**
* 恢复单样本替换前的人脸特征;没有旧特征时保持目标搜索标识为空。
*
* @param searchId 需要恢复的虹软搜索标识
* @param oldEntity 替换前的特征和元数据,新增场景为空
*/
private void restoreFaceFeature(int searchId, FaceEntity oldEntity) {
try {
faceEngine.removeFaceFeature(searchId);
faceRegisterInfoMap.remove(searchId);
if (oldEntity == null || oldEntity.getFaceModel() == null) return;
FaceFeatureInfo oldFeature = new FaceFeatureInfo(searchId, oldEntity.getFaceModel());
if (faceEngine.registerFaceFeature(Collections.singletonList(oldFeature)) == ErrorInfo.MOK) {
faceRegisterInfoMap.put(searchId, oldEntity);
} else {
Log.e(TAG, "恢复旧人脸特征失败searchId:" + searchId);
}
} catch (Exception exception) {
Log.e(TAG, "恢复旧人脸特征异常searchId:" + searchId + ";error:" + exception.getMessage(), exception);
}
}
/**
* 深拷贝内存库中的人脸业务元数据和特征,避免回滚快照被后续修改。
*
* @param source 待复制的人脸记录
* @return 独立副本,源记录为空时返回空
*/
private FaceEntity copyFaceEntity(FaceEntity source) {
if (source == null) return null;
FaceEntity copy = new FaceEntity();
copy.setFaceId(source.getFaceId());
copy.setSampleId(source.getSampleId());
copy.setUserId(source.getUserId());
copy.setUserName(source.getUserName());
copy.setFaceFilePath(source.getFaceFilePath());
copy.setFaceModel(source.getFaceModel() == null ? null : source.getFaceModel().clone());
return copy;
}
// 注册单个人脸数据
public synchronized int registerFaceFeatureInfoFromDb(FaceEntity faceEntity) {
if (faceEntity != null && faceEngine != null) {
// ArcSoft 不允许同一个 searchId 重复注册,增量更新前只移除当前用户的旧记录。
faceEngine.removeFaceFeature(faceEntity.getFaceId());
faceRegisterInfoMap.remove(faceEntity.getFaceId());
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)) {
faceRegisterInfoMap.put(faceEntity.getFaceId(), faceEntity);
return code;
}
}
}
return -99;
// 历史单条入口也复用带旧特征回滚的替换逻辑,避免 SDK 异常后丢失原可用样本。
return replaceFaceFeature(faceEntity);
}
// 从人脸数据库中搜索人脸
@@ -170,6 +244,11 @@ public class FaceServer {
// 删除人脸-1删除全部
public synchronized int removeFace(int searchId) {
if (faceEngine == null) return -99;
// MQ 和每日快照都可能重复删除同一样本;内存库已不存在时直接按幂等成功处理。
if (searchId != -1 && getFaceFeature(searchId) == null) {
faceRegisterInfoMap.remove(searchId);
return ErrorInfo.MOK;
}
int code = faceEngine.removeFaceFeature(searchId);
if (code == ErrorInfo.MOK) {
if (searchId == -1) {
@@ -199,7 +278,7 @@ public class FaceServer {
}
// 在特征库中搜索
public CompareResult searchFaceFeature(FaceFeature faceFeature) {
public synchronized CompareResult searchFaceFeature(FaceFeature faceFeature) {
if (faceFeature == null) return null;
if (faceEngine == null) return null;
SearchResult searchResult = null;
@@ -223,7 +302,7 @@ public class FaceServer {
}
public byte[] registerBgr24(Context context, byte[] bgr24, int width, int height, String name) {
public synchronized 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;