bug修改
This commit is contained in:
1 parent
adbd915b45
commit
2ee78a1561
38 files changed
+2069
-240
No files matched your search
+20
@@ -20,6 +20,7 @@ import java.nio.ByteOrder;
|
||||
import java.nio.FloatBuffer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
import io.flutter.view.TextureRegistry;
|
||||
@@ -65,6 +66,13 @@ final class FacePreviewRenderer implements SurfaceTexture.OnFrameAvailableListen
|
||||
private Handler renderHandler;
|
||||
private final Object lock = new Object();
|
||||
private final Map<Long, OutputTarget> outputs = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 当前主、副 Flutter Engine 实际持有的预览输出数量。
|
||||
* Camera 回调线程只读取该原子值,不跨线程直接访问渲染线程拥有的 outputs。
|
||||
*/
|
||||
private final AtomicInteger activeOutputCount = new AtomicInteger();
|
||||
|
||||
private final float[] textureMatrix = new float[16];
|
||||
|
||||
private EGLDisplay eglDisplay = EGL14.EGL_NO_DISPLAY;
|
||||
@@ -127,6 +135,7 @@ final class FacePreviewRenderer implements SurfaceTexture.OnFrameAvailableListen
|
||||
throw new IllegalStateException("无法创建 Flutter 人脸预览 EGLSurface");
|
||||
}
|
||||
outputs.put(outputId, new OutputTarget(entry, surface, eglSurface));
|
||||
activeOutputCount.set(outputs.size());
|
||||
Log.i(TAG, "预览输出已添加 outputId=" + outputId + ";flutterTextureId=" + entry.id() + ";outputCount=" + outputs.size());
|
||||
});
|
||||
return outputId;
|
||||
@@ -138,6 +147,15 @@ final class FacePreviewRenderer implements SurfaceTexture.OnFrameAvailableListen
|
||||
handler.post(() -> releaseOutput(outputId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否仍有界面需要实时人脸框。
|
||||
*
|
||||
* @return 主屏或副屏至少持有一个有效 Texture 输出时返回 true
|
||||
*/
|
||||
boolean hasActiveOutputs() {
|
||||
return activeOutputCount.get() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功后继续保留 Camera 热资源,只暂停向主副屏 Texture 提交新帧。
|
||||
* 这样界面会停在通过阈值的最后一帧,下一位顾客开始时再恢复,不需要重新开 Camera。
|
||||
@@ -311,6 +329,7 @@ final class FacePreviewRenderer implements SurfaceTexture.OnFrameAvailableListen
|
||||
private void releaseOutput(long outputId) {
|
||||
OutputTarget output = outputs.remove(outputId);
|
||||
if (output == null) return;
|
||||
activeOutputCount.set(outputs.size());
|
||||
if (eglDisplay != EGL14.EGL_NO_DISPLAY && output.eglSurface != EGL14.EGL_NO_SURFACE) {
|
||||
EGL14.eglDestroySurface(eglDisplay, output.eglSurface);
|
||||
}
|
||||
@@ -331,6 +350,7 @@ final class FacePreviewRenderer implements SurfaceTexture.OnFrameAvailableListen
|
||||
if (released) return;
|
||||
released = true;
|
||||
for (Long textureId : outputs.keySet().toArray(new Long[0])) releaseOutput(textureId);
|
||||
activeOutputCount.set(0);
|
||||
if (cameraTexture != null) {
|
||||
cameraTexture.setOnFrameAvailableListener(null);
|
||||
cameraTexture.release();
|
||||
|
||||
+298
-68
@@ -9,6 +9,7 @@ import android.graphics.SurfaceTexture;
|
||||
import android.hardware.Camera;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import android.util.Log;
|
||||
import android.view.Surface;
|
||||
import android.view.WindowManager;
|
||||
@@ -45,8 +46,19 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
private static final long WARM_TIMEOUT_MS = 20L * 60L * 1000L;
|
||||
private static final int TARGET_WIDTH = 1280;
|
||||
private static final int TARGET_HEIGHT = 720;
|
||||
|
||||
/** 人脸框事件的最小发送间隔,避免主副屏因高频 Map 消息持续重建。 */
|
||||
private static final long FACE_BOX_THROTTLE_MS = 100L;
|
||||
|
||||
/**
|
||||
* 虹软在运动模糊或角度变化时允许的短暂漏检时间。
|
||||
* 每张脸分别保留最近坐标,连续漏检超时后才确认该人员已经离开。
|
||||
*/
|
||||
private static final long FACE_BOX_MISS_GRACE_MS = 600L;
|
||||
|
||||
/** 结果展示期仅做人脸检测的最小帧间隔,限制热预览的 CPU 消耗。 */
|
||||
private static final long PREVIEW_ONLY_DETECTION_INTERVAL_MS = 100L;
|
||||
|
||||
public enum State {
|
||||
COLD("cold"),
|
||||
INITIALIZING("initializing"),
|
||||
@@ -87,6 +99,8 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
thread.setPriority(Thread.NORM_PRIORITY + 1);
|
||||
return thread;
|
||||
});
|
||||
|
||||
/** Camera 回调是否已有唯一帧任务在执行或排队,由成功取得 CAS 的任务负责释放。 */
|
||||
private final AtomicBoolean frameInFlight = new AtomicBoolean(false);
|
||||
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();
|
||||
private final Object sessionLock = new Object();
|
||||
@@ -105,8 +119,20 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
private String activeScene;
|
||||
private Listener activeOwner;
|
||||
private long revision;
|
||||
|
||||
/** 进程生命周期内单调递增的事件序号,用于主副屏丢弃跨业务会话迟到的状态。 */
|
||||
private long eventSequence;
|
||||
|
||||
private long sessionGeneration;
|
||||
private long lastFaceBoxEmitAt;
|
||||
|
||||
/** 结果展示期最近一次提交“仅检测”任务的时间戳,单位为毫秒。 */
|
||||
private long lastPreviewOnlyDetectionAt;
|
||||
|
||||
/** 每个虹软跟踪编号最近一次真实检测到的时间戳,使用单调时钟且单位为毫秒。 */
|
||||
private Map<Integer, Long> lastFaceBoxSeenAt = new HashMap<>();
|
||||
|
||||
/** 当前向 Flutter 展示的人脸框,包含真实检测框和仍在漏检宽限内的单人缓存。 */
|
||||
private List<Map<String, Object>> lastFaceBoxes = new ArrayList<>();
|
||||
private boolean recognitionConfigurationDirty;
|
||||
private Handler warmReleaseHandler = new Handler(Looper.getMainLooper());
|
||||
@@ -189,7 +215,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
revision = 0;
|
||||
state = State.INITIALIZING;
|
||||
reloadConfiguration = recognitionConfigurationDirty;
|
||||
emitState("正在启动摄像头", null, null);
|
||||
emitState("正在启动摄像头", recentFaceBoxesLocked(SystemClock.elapsedRealtime()), null);
|
||||
}
|
||||
try {
|
||||
// 保存配置时不打断上一位顾客;下一次 Session 启动前再重建引擎并读取新值。
|
||||
@@ -213,7 +239,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
throw new IllegalStateException("当前人脸会话已结束");
|
||||
}
|
||||
state = State.DETECTING;
|
||||
emitState("请正对摄像头", null, null);
|
||||
emitState("请正对摄像头", recentFaceBoxesLocked(SystemClock.elapsedRealtime()), null);
|
||||
Log.i(TAG, "人脸会话进入识别状态 sessionId=" + sessionId);
|
||||
return snapshot();
|
||||
}
|
||||
@@ -223,7 +249,10 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
synchronized (sessionLock) {
|
||||
ensureOwner(sessionId);
|
||||
state = State.INITIALIZING;
|
||||
emitState(warmResourcesReady ? "正在重置识别状态" : "正在重新启动摄像头", null, null);
|
||||
emitState(
|
||||
warmResourcesReady ? "正在重置识别状态" : "正在重新启动摄像头",
|
||||
recentFaceBoxesLocked(SystemClock.elapsedRealtime()),
|
||||
null);
|
||||
}
|
||||
try {
|
||||
ensureWarmResources();
|
||||
@@ -241,7 +270,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
synchronized (sessionLock) {
|
||||
ensureOwner(sessionId);
|
||||
state = State.DETECTING;
|
||||
emitState("请调整位置后重新识别", null, null);
|
||||
emitState("请调整位置后重新识别", recentFaceBoxesLocked(SystemClock.elapsedRealtime()), null);
|
||||
return snapshot();
|
||||
}
|
||||
}
|
||||
@@ -252,6 +281,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
if (activeSessionId == null) return snapshot();
|
||||
if (sessionId != null && !sessionId.isEmpty() && !Objects.equals(activeSessionId, sessionId)) return snapshot();
|
||||
state = State.CANCELLED;
|
||||
clearFaceBoxCacheLocked();
|
||||
emitState(message == null ? "已取消人脸识别" : message, null, null);
|
||||
processorToStop = detachActiveSession();
|
||||
}
|
||||
@@ -266,14 +296,23 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前业务识别但保留热 Camera,并恢复 Texture 的实时帧提交。
|
||||
*
|
||||
* <p>成功回调会冻结最后一帧供独立弹框确认;调用方结束 Session 后必须解冻,
|
||||
* 持续查询页面才能在十秒菜单展示期继续显示实时画面,同时又不处理下一位人脸。</p>
|
||||
*/
|
||||
public Map<String, Object> finishRecognition(String sessionId) {
|
||||
FaceDiscernViewModel processorToStop;
|
||||
FacePreviewRenderer renderer;
|
||||
synchronized (sessionLock) {
|
||||
if (activeSessionId == null) return snapshot();
|
||||
if (sessionId != null && !sessionId.isEmpty() && !Objects.equals(activeSessionId, sessionId)) return snapshot();
|
||||
processorToStop = detachActiveSession();
|
||||
renderer = previewRenderer;
|
||||
}
|
||||
stopProcessor(processorToStop);
|
||||
if (renderer != null) renderer.setFrozen(false);
|
||||
if (isRecognitionConfigurationDirty()) {
|
||||
releaseResources(true, true);
|
||||
} else {
|
||||
@@ -297,9 +336,16 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回当前原生资源和业务会话快照;检测、成功及热预览状态会附带可继续展示的人脸框。
|
||||
*
|
||||
* @return 可供 MethodChannel 或新 EventChannel 订阅者立即渲染的完整状态
|
||||
*/
|
||||
public Map<String, Object> getState() {
|
||||
synchronized (sessionLock) {
|
||||
return snapshot();
|
||||
Map<String, Object> result = snapshot();
|
||||
result.put("faceBoxes", currentStateFaceBoxesLocked(SystemClock.elapsedRealtime()));
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,15 +475,43 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
displayOrientation, info.facing, mirror, true, true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 接收 Camera1 的 NV21 回调,并按当前会话状态提交识别或仅检测任务。
|
||||
* 未被选中的帧会立即归还 Camera 缓冲区,避免高频回调积压。
|
||||
*
|
||||
* @param data Camera1 复用的 NV21 帧缓冲区
|
||||
* @param callbackCamera 产生该帧的 Camera 实例
|
||||
*/
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void onPreviewFrame(byte[] data, Camera callbackCamera) {
|
||||
if (data == null || callbackCamera == null) return;
|
||||
boolean recognize;
|
||||
boolean recognize = false;
|
||||
boolean previewOnly = false;
|
||||
FaceDiscernViewModel frameProcessor = null;
|
||||
Camera.Size framePreviewSize = null;
|
||||
long frameGeneration = 0L;
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
synchronized (sessionLock) {
|
||||
recognize = state == State.DETECTING && activeSessionId != null && processor != null;
|
||||
// Camera、处理器和业务代次必须在入队时固定,禁止上一轮已经回调的 NV21 帧跨轮次进入新会话。
|
||||
if (callbackCamera == camera && processor != null && previewSize != null) {
|
||||
recognize = state == State.DETECTING && activeSessionId != null;
|
||||
previewOnly = !recognize
|
||||
&& state == State.WARM_IDLE
|
||||
&& activeSessionId == null
|
||||
&& warmResourcesReady
|
||||
&& previewRenderer != null
|
||||
&& previewRenderer.hasActiveOutputs()
|
||||
&& now - lastPreviewOnlyDetectionAt >= PREVIEW_ONLY_DETECTION_INTERVAL_MS;
|
||||
if (recognize || previewOnly) {
|
||||
frameProcessor = processor;
|
||||
framePreviewSize = previewSize;
|
||||
frameGeneration = sessionGeneration;
|
||||
}
|
||||
if (previewOnly) lastPreviewOnlyDetectionAt = now;
|
||||
}
|
||||
}
|
||||
if (!recognize || !frameInFlight.compareAndSet(false, true)) {
|
||||
if ((!recognize && !previewOnly) || !frameInFlight.compareAndSet(false, true)) {
|
||||
try {
|
||||
callbackCamera.addCallbackBuffer(data);
|
||||
} catch (RuntimeException ignored) {
|
||||
@@ -445,10 +519,22 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
return;
|
||||
}
|
||||
try {
|
||||
final boolean doRecognize = recognize;
|
||||
final FaceDiscernViewModel expectedProcessor = frameProcessor;
|
||||
final Camera.Size expectedPreviewSize = framePreviewSize;
|
||||
final long expectedGeneration = frameGeneration;
|
||||
recognitionExecutor.execute(() -> {
|
||||
try {
|
||||
processFrame(data);
|
||||
processFrame(
|
||||
data,
|
||||
doRecognize,
|
||||
callbackCamera,
|
||||
expectedProcessor,
|
||||
expectedPreviewSize,
|
||||
expectedGeneration);
|
||||
} finally {
|
||||
// 只有成功取得 CAS 的任务可以释放标记,状态切换不得提前清零并让第二帧并发入队。
|
||||
frameInFlight.set(false);
|
||||
try {
|
||||
callbackCamera.addCallbackBuffer(data);
|
||||
} catch (RuntimeException ignored) {
|
||||
@@ -464,77 +550,213 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
}
|
||||
}
|
||||
|
||||
private void processFrame(byte[] data) {
|
||||
FaceDiscernViewModel currentProcessor;
|
||||
long frameGeneration;
|
||||
synchronized (sessionLock) {
|
||||
if (state != State.DETECTING || activeSessionId == null || processor == null) {
|
||||
frameInFlight.set(false);
|
||||
return;
|
||||
}
|
||||
currentProcessor = processor;
|
||||
frameGeneration = sessionGeneration;
|
||||
}
|
||||
/**
|
||||
* 处理识别帧或热预览检测帧;两种模式复用同一执行器和处理锁,禁止并发调用虹软引擎。
|
||||
*
|
||||
* @param data Camera1 回传的 NV21 原始帧
|
||||
* @param doRecognize true 时允许特征比对;false 时只获取人脸坐标用于画框
|
||||
* @param expectedCamera 帧入队时所属的 Camera,冷重启后必须与当前实例一致
|
||||
* @param expectedProcessor 帧入队时所属的虹软处理器
|
||||
* @param expectedPreviewSize 帧入队时使用的 NV21 尺寸
|
||||
* @param expectedGeneration 帧入队时所属的业务会话代次
|
||||
*/
|
||||
private void processFrame(
|
||||
byte[] data,
|
||||
boolean doRecognize,
|
||||
Camera expectedCamera,
|
||||
FaceDiscernViewModel expectedProcessor,
|
||||
Camera.Size expectedPreviewSize,
|
||||
long expectedGeneration) {
|
||||
try {
|
||||
List<FacePreviewInfo> faces;
|
||||
List<Map<String, Object>> boxes;
|
||||
synchronized (processorLock) {
|
||||
if (!isCurrentDetectingProcessor(currentProcessor, frameGeneration)) return;
|
||||
faces = currentProcessor.onPreviewFrame(data, true);
|
||||
if (!isCurrentFrameProcessor(
|
||||
expectedCamera,
|
||||
expectedProcessor,
|
||||
expectedGeneration,
|
||||
doRecognize)) return;
|
||||
List<FacePreviewInfo> faces = expectedProcessor.onPreviewFrame(data, doRecognize);
|
||||
// FaceHelper 返回内部可变列表;必须在处理锁内复制坐标和状态,避免 finish/start 清理列表时产生空框或并发遍历。
|
||||
boxes = buildFaceBoxes(faces, expectedPreviewSize, expectedProcessor);
|
||||
}
|
||||
emitFaceBoxes(faces, frameGeneration);
|
||||
emitFaceBoxes(boxes, expectedGeneration, doRecognize);
|
||||
} catch (Exception error) {
|
||||
Log.e(TAG, "处理人脸预览帧失败", error);
|
||||
handleFatalFailure("人脸识别引擎异常,请重试", "RECOGNITION_ENGINE_ERROR", frameGeneration);
|
||||
} finally {
|
||||
frameInFlight.set(false);
|
||||
handleFatalFailure("人脸识别引擎异常,请重试", "RECOGNITION_ENGINE_ERROR", expectedGeneration);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isCurrentDetectingProcessor(FaceDiscernViewModel expectedProcessor, long expectedGeneration) {
|
||||
/**
|
||||
* 判断当前状态是否允许处理指定模式的 Camera 帧;调用方必须持有 sessionLock。
|
||||
*
|
||||
* @param doRecognize true 表示业务识别帧,false 表示结果期热预览检测帧
|
||||
* @return 当前处理器、会话与预览输出均仍有效时返回 true
|
||||
*/
|
||||
private boolean canProcessFrameLocked(boolean doRecognize) {
|
||||
if (doRecognize) {
|
||||
return state == State.DETECTING && activeSessionId != null;
|
||||
}
|
||||
return state == State.WARM_IDLE
|
||||
&& activeSessionId == null
|
||||
&& warmResourcesReady
|
||||
&& previewRenderer != null
|
||||
&& previewRenderer.hasActiveOutputs();
|
||||
}
|
||||
|
||||
/**
|
||||
* 在耗时检测前二次校验处理器与会话代次,防止 finish/start 过渡帧串入新状态。
|
||||
*
|
||||
* @param expectedCamera 任务创建时持有的 Camera
|
||||
* @param expectedProcessor 任务创建时持有的虹软处理器
|
||||
* @param expectedGeneration 任务创建时的业务会话代次
|
||||
* @param doRecognize true 表示业务识别,false 表示仅检测
|
||||
* @return 任务仍属于当前状态时返回 true
|
||||
*/
|
||||
private boolean isCurrentFrameProcessor(
|
||||
Camera expectedCamera,
|
||||
FaceDiscernViewModel expectedProcessor,
|
||||
long expectedGeneration,
|
||||
boolean doRecognize) {
|
||||
synchronized (sessionLock) {
|
||||
return processor == expectedProcessor
|
||||
return camera == expectedCamera
|
||||
&& processor == expectedProcessor
|
||||
&& sessionGeneration == expectedGeneration
|
||||
&& state == State.DETECTING
|
||||
&& activeSessionId != null;
|
||||
&& canProcessFrameLocked(doRecognize);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitFaceBoxes(List<FacePreviewInfo> faces, long frameGeneration) {
|
||||
long now = System.currentTimeMillis();
|
||||
if (now - lastFaceBoxEmitAt < FACE_BOX_THROTTLE_MS) return;
|
||||
lastFaceBoxEmitAt = now;
|
||||
/**
|
||||
* 将虹软当前帧结果复制为 Flutter 通道使用的归一化框。
|
||||
* 相似度和活体状态只决定颜色与标签,不参与框的过滤。
|
||||
*
|
||||
* @param faces 虹软当前帧检测到的全部人脸
|
||||
* @param currentPreviewSize NV21 原始预览尺寸
|
||||
* @param currentProcessor 当前虹软处理器,用于读取最近识别状态
|
||||
* @return 与内部 FaceHelper 列表生命周期无关的人脸框快照
|
||||
*/
|
||||
private List<Map<String, Object>> buildFaceBoxes(
|
||||
List<FacePreviewInfo> faces,
|
||||
Camera.Size currentPreviewSize,
|
||||
FaceDiscernViewModel currentProcessor) {
|
||||
List<Map<String, Object>> boxes = new ArrayList<>();
|
||||
Camera.Size currentPreviewSize;
|
||||
FaceDiscernViewModel currentProcessor;
|
||||
synchronized (sessionLock) {
|
||||
currentPreviewSize = previewSize;
|
||||
currentProcessor = processor;
|
||||
if (faces == null) return boxes;
|
||||
for (FacePreviewInfo face : faces) {
|
||||
// 传原始 NV21 坐标,Flutter 再与 Texture 使用同一套旋转/镜像变换,避免框和画面各自补偿后错位。
|
||||
Rect rect = face.getFaceInfoRgb() == null ? null : face.getFaceInfoRgb().getRect();
|
||||
if (rect == null) continue;
|
||||
Map<String, Object> box = new HashMap<>();
|
||||
box.put("trackId", face.getTrackId());
|
||||
box.put("left", clamp(rect.left / (double) currentPreviewSize.width));
|
||||
box.put("top", clamp(rect.top / (double) currentPreviewSize.height));
|
||||
box.put("right", clamp(rect.right / (double) currentPreviewSize.width));
|
||||
box.put("bottom", clamp(rect.bottom / (double) currentPreviewSize.height));
|
||||
Float similarity = currentProcessor.getFaceSimilarity(face.getTrackId());
|
||||
if (similarity != null) box.put("similarity", similarity.doubleValue());
|
||||
box.put("recognizeStatus", recognizeStatusName(currentProcessor.getFaceRecognizeStatus(face.getTrackId())));
|
||||
box.put("livenessStatus", livenessStatusName(currentProcessor.getFaceLiveness(face.getTrackId())));
|
||||
boxes.add(box);
|
||||
}
|
||||
if (faces != null && currentPreviewSize != null) {
|
||||
for (FacePreviewInfo face : faces) {
|
||||
// 传原始 NV21 坐标,Flutter 再与 Texture 使用同一套旋转/镜像变换,避免框和画面各自补偿后错位。
|
||||
Rect rect = face.getFaceInfoRgb() == null ? null : face.getFaceInfoRgb().getRect();
|
||||
if (rect == null) continue;
|
||||
Map<String, Object> box = new HashMap<>();
|
||||
box.put("left", clamp(rect.left / (double) currentPreviewSize.width));
|
||||
box.put("top", clamp(rect.top / (double) currentPreviewSize.height));
|
||||
box.put("right", clamp(rect.right / (double) currentPreviewSize.width));
|
||||
box.put("bottom", clamp(rect.bottom / (double) currentPreviewSize.height));
|
||||
if (currentProcessor != null) {
|
||||
Float similarity = currentProcessor.getFaceSimilarity(face.getTrackId());
|
||||
if (similarity != null) box.put("similarity", similarity.doubleValue());
|
||||
box.put("recognizeStatus", recognizeStatusName(currentProcessor.getFaceRecognizeStatus(face.getTrackId())));
|
||||
box.put("livenessStatus", livenessStatusName(currentProcessor.getFaceLiveness(face.getTrackId())));
|
||||
}
|
||||
boxes.add(box);
|
||||
return boxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送稳定后的人脸框。短暂空帧或人数下降会继续沿用上一组框,持续超过宽限才清除。
|
||||
*
|
||||
* @param detectedBoxes 当前检测帧复制出的全部人脸框
|
||||
* @param frameGeneration 当前处理任务的会话代次
|
||||
* @param doRecognize true 表示活动识别会话,false 表示 WARM_IDLE 仅检测预览
|
||||
*/
|
||||
private void emitFaceBoxes(
|
||||
List<Map<String, Object>> detectedBoxes,
|
||||
long frameGeneration,
|
||||
boolean doRecognize) {
|
||||
long now = SystemClock.elapsedRealtime();
|
||||
synchronized (sessionLock) {
|
||||
if (sessionGeneration != frameGeneration || !canProcessFrameLocked(doRecognize)) return;
|
||||
if (now - lastFaceBoxEmitAt < FACE_BOX_THROTTLE_MS) return;
|
||||
lastFaceBoxEmitAt = now;
|
||||
List<Map<String, Object>> visibleBoxes = stabilizeFaceBoxesLocked(detectedBoxes, now);
|
||||
emitState(doRecognize ? "请正对摄像头" : "摄像头预览中", visibleBoxes, null);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 按虹软跟踪编号分别合并当前检测框与短时缓存,避免一个人漏检时冻结其他人的新坐标。
|
||||
* 调用方必须持有 sessionLock。
|
||||
*
|
||||
* @param detectedBoxes 当前帧检测框,不包含任何相似度过滤
|
||||
* @param now 当前单调业务时间戳,单位为毫秒
|
||||
* @return 本帧应向主副屏展示的稳定框列表
|
||||
*/
|
||||
private List<Map<String, Object>> stabilizeFaceBoxesLocked(
|
||||
List<Map<String, Object>> detectedBoxes,
|
||||
long now) {
|
||||
List<Map<String, Object>> visibleBoxes = new ArrayList<>(detectedBoxes);
|
||||
Map<Integer, Map<String, Object>> detectedByTrackId = new HashMap<>();
|
||||
Map<Integer, Long> nextSeenAt = new HashMap<>();
|
||||
for (Map<String, Object> box : detectedBoxes) {
|
||||
Integer trackId = faceTrackId(box);
|
||||
if (trackId == null) continue;
|
||||
detectedByTrackId.put(trackId, box);
|
||||
nextSeenAt.put(trackId, now);
|
||||
}
|
||||
for (Map<String, Object> previousBox : lastFaceBoxes) {
|
||||
Integer trackId = faceTrackId(previousBox);
|
||||
if (trackId == null || detectedByTrackId.containsKey(trackId)) continue;
|
||||
Long lastSeenAt = lastFaceBoxSeenAt.get(trackId);
|
||||
if (lastSeenAt != null && now - lastSeenAt < FACE_BOX_MISS_GRACE_MS) {
|
||||
visibleBoxes.add(previousBox);
|
||||
nextSeenAt.put(trackId, lastSeenAt);
|
||||
}
|
||||
}
|
||||
synchronized (sessionLock) {
|
||||
if (state == State.DETECTING && activeSessionId != null && sessionGeneration == frameGeneration) {
|
||||
lastFaceBoxes = boxes;
|
||||
emitState("请正对摄像头", boxes, null);
|
||||
}
|
||||
lastFaceBoxes = visibleBoxes;
|
||||
lastFaceBoxSeenAt = nextSeenAt;
|
||||
return lastFaceBoxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取原生框快照中的虹软跟踪编号,仅用于进程内稳定框,不属于 Flutter 必填协议字段。
|
||||
*
|
||||
* @param box 单张人脸框快照
|
||||
* @return 可用于逐人缓存的跟踪编号;历史或异常数据缺失时返回 null
|
||||
*/
|
||||
private Integer faceTrackId(Map<String, Object> box) {
|
||||
Object value = box.get("trackId");
|
||||
return value instanceof Number ? ((Number) value).intValue() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回仍在漏检宽限内的最近人脸框,超时则同步清理缓存。
|
||||
* 调用方必须持有 sessionLock。
|
||||
*
|
||||
* @param now 当前时间戳,单位为毫秒
|
||||
* @return 可安全用于初始化或资源状态事件的人脸框
|
||||
*/
|
||||
private List<Map<String, Object>> recentFaceBoxesLocked(long now) {
|
||||
return stabilizeFaceBoxesLocked(new ArrayList<>(), now);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回适合新订阅者的当前框快照;成功态保留冻结结果,实时态只保留仍在漏检宽限内的框。
|
||||
* 调用方必须持有 sessionLock。
|
||||
*
|
||||
* @param now 当前单调业务时间戳,单位为毫秒
|
||||
* @return 与内部列表解耦、可直接放入通道 Map 的人脸框列表
|
||||
*/
|
||||
private List<Map<String, Object>> currentStateFaceBoxesLocked(long now) {
|
||||
if (state == State.SUCCESS) {
|
||||
return new ArrayList<>(lastFaceBoxes);
|
||||
}
|
||||
if (state == State.INITIALIZING || state == State.DETECTING || state == State.WARM_IDLE) {
|
||||
return new ArrayList<>(recentFaceBoxesLocked(now));
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/** 清空人脸框和漏检计时;取消、冷释放及显式失败不得继续展示旧人员。 */
|
||||
private void clearFaceBoxCacheLocked() {
|
||||
lastFaceBoxes = new ArrayList<>();
|
||||
lastFaceBoxSeenAt = new HashMap<>();
|
||||
}
|
||||
|
||||
private double clamp(double value) {
|
||||
@@ -567,6 +789,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
successBox.put("recognizeStatus", "success");
|
||||
successBoxes.add(successBox);
|
||||
}
|
||||
lastFaceBoxes = successBoxes;
|
||||
// 只冻结预览输出,不释放 Camera 热资源;成功框和相似度保留至少 3 秒供顾客确认。
|
||||
if (previewRenderer != null) previewRenderer.setFrozen(true);
|
||||
emitState("识别成功,正在读取信息", successBoxes, null);
|
||||
@@ -583,6 +806,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
preserveSession = activeSessionId != null;
|
||||
if (preserveSession) {
|
||||
state = State.FAILED;
|
||||
clearFaceBoxCacheLocked();
|
||||
emitState(message, null, errorCode);
|
||||
}
|
||||
}
|
||||
@@ -605,11 +829,10 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
FacePreviewRenderer renderer;
|
||||
synchronized (sessionLock) {
|
||||
renderer = previewRenderer;
|
||||
lastFaceBoxes = new ArrayList<>();
|
||||
lastFaceBoxEmitAt = 0;
|
||||
lastPreviewOnlyDetectionAt = 0;
|
||||
}
|
||||
if (renderer != null) renderer.setFrozen(false);
|
||||
frameInFlight.set(false);
|
||||
lastFaceBoxEmitAt = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -621,7 +844,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
activeSessionId = null;
|
||||
activeScene = null;
|
||||
activeOwner = null;
|
||||
frameInFlight.set(false);
|
||||
// 若旧帧仍在执行,必须由该任务的 finally 自行释放标记;提前清零会让 Camera 重复排队并占满回调缓冲。
|
||||
state = warmResourcesReady ? State.WARM_IDLE : State.COLD;
|
||||
scheduleWarmRelease();
|
||||
return processorToStop;
|
||||
@@ -656,7 +879,6 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
synchronized (sessionLock) {
|
||||
cancelWarmRelease();
|
||||
sessionGeneration++;
|
||||
frameInFlight.set(false);
|
||||
cameraToRelease = camera;
|
||||
processorToRelease = processor;
|
||||
rendererToRelease = previewRenderer;
|
||||
@@ -666,6 +888,9 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
previewSize = null;
|
||||
warmResourcesReady = false;
|
||||
recognitionConfigurationDirty = false;
|
||||
clearFaceBoxCacheLocked();
|
||||
lastFaceBoxEmitAt = 0L;
|
||||
lastPreviewOnlyDetectionAt = 0L;
|
||||
if (clearActiveSession) {
|
||||
activeSessionId = null;
|
||||
activeScene = null;
|
||||
@@ -712,6 +937,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
Map<String, Object> event = snapshot();
|
||||
event.put("message", message);
|
||||
event.put("revision", ++revision);
|
||||
event.put("eventSequence", ++eventSequence);
|
||||
event.put("faceBoxes", boxes == null ? new ArrayList<>() : boxes);
|
||||
if (errorCode != null) event.put("errorCode", errorCode);
|
||||
for (Listener listener : listeners) listener.onSessionEvent(event);
|
||||
@@ -724,8 +950,11 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
Map<String, Object> event;
|
||||
synchronized (sessionLock) {
|
||||
event = snapshot();
|
||||
event.put("eventSequence", ++eventSequence);
|
||||
event.put("message", message);
|
||||
event.put("faceBoxes", new ArrayList<>());
|
||||
event.put("faceBoxes", state == State.WARM_IDLE
|
||||
? recentFaceBoxesLocked(SystemClock.elapsedRealtime())
|
||||
: new ArrayList<>());
|
||||
}
|
||||
for (Listener listener : listeners) listener.onSessionEvent(event);
|
||||
}
|
||||
@@ -742,6 +971,7 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
|
||||
result.put("scene", activeScene == null ? "" : activeScene);
|
||||
result.put("state", state.code);
|
||||
result.put("revision", revision);
|
||||
result.put("eventSequence", eventSequence);
|
||||
result.put("width", previewSize == null ? TARGET_WIDTH : previewSize.width);
|
||||
result.put("height", previewSize == null ? TARGET_HEIGHT : previewSize.height);
|
||||
result.put("rotation", displayOrientation);
|
||||
|
||||
+6
-4
@@ -334,7 +334,7 @@ public class FaceHelper implements FaceListener {
|
||||
*
|
||||
* @param rgbNv21 可见光相机预览回传的NV21数据
|
||||
* @param irNv21 红外相机预览回传的NV21数据
|
||||
* @param doRecognize 是否进行识别
|
||||
* @param doRecognize 是否进入口罩、活体、特征提取和比对链;false 时只做人脸检测与跟踪
|
||||
* @return 实时人脸处理结果,封装添加了一个trackId,trackId的获取依赖于faceId,用于记录人脸序号并保存
|
||||
*/
|
||||
public List<FacePreviewInfo> onPreviewFrame(@NonNull byte[] rgbNv21, @Nullable byte[] irNv21, boolean doRecognize) {
|
||||
@@ -353,7 +353,9 @@ public class FaceHelper implements FaceListener {
|
||||
clearLeftFace(facePreviewInfoList);
|
||||
return facePreviewInfoList;
|
||||
}
|
||||
if (!onlyDetectLiveness) {
|
||||
// 结果展示期只需要实时框,跳过口罩与后续识别链可以降低常驻 Camera 的 CPU 占用,
|
||||
// 也避免口罩引擎的单帧异常把已经成功检测到的人脸误报为空列表。
|
||||
if (doRecognize && !onlyDetectLiveness) {
|
||||
code = maskEngine.process(rgbNv21, previewSize.width, previewSize.height, FaceEngine.CP_PAF_NV21, faceInfoList,
|
||||
FaceEngine.ASF_MASK_DETECT);
|
||||
if (code == ErrorInfo.MOK) {
|
||||
@@ -917,7 +919,7 @@ public class FaceHelper implements FaceListener {
|
||||
/**
|
||||
* 异步活体任务的构造函数
|
||||
*
|
||||
* @param nv21Data 可见光或红外图像数据
|
||||
* @param nv21Data 可见光或红外图像数据;构造时复制,避免 Camera 复用缓冲区覆盖异步任务
|
||||
* @param faceInfo 可见光人脸检测得到的人脸信息
|
||||
* @param width 图像宽度
|
||||
* @param height 图像高度
|
||||
@@ -930,7 +932,7 @@ public class FaceHelper implements FaceListener {
|
||||
if (nv21Data == null) {
|
||||
return;
|
||||
}
|
||||
this.nv21Data = nv21Data;
|
||||
this.nv21Data = Arrays.copyOf(nv21Data, nv21Data.length);
|
||||
this.faceInfo = new FaceInfo(faceInfo.getFaceInfoRgb());
|
||||
this.width = width;
|
||||
this.height = height;
|
||||
|
||||
Reference in new issue
Block a user