feat:摄像头选择,副屏页面修改

This commit is contained in:
dzc committed 2026-09-10 15:25:41 +08:00
1 parent 2e929c199b
commit a81a79b69f
9 files changed
+613 -10

No files matched your search

@@ -250,6 +250,32 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
case "hasCamera":
result.success(sessionManager.hasCamera());
break;
case "getCameraChoices":
// 探测会短暂打开每个摄像头,必须串行进入人脸执行器且不占用主线程。
FACE_EXECUTOR.execute(() -> {
try {
Map<String, Object> choices = FlutterTool.getCameraChoices(context);
MAIN_HANDLER.post(() -> result.success(choices));
} catch (Exception e) {
Log.e(TAG, "读取摄像头列表失败:" + e.getMessage(), e);
MAIN_HANDLER.post(() -> result.error("CAMERA_CHOICES_FAILED", "读取摄像头列表失败", null));
}
});
break;
case "setCameraSelection":
Number selectedCameraId = call.argument("cameraId");
if (selectedCameraId == null) {
result.error("INVALID_CAMERA_SELECTION", "缺少摄像头编号", null);
break;
}
FlutterTool.setCameraSelection(context, selectedCameraId.intValue());
// 空闲时立即释放热相机让下次识别按新编号打开;识别中延迟到会话结束。
sessionManager.onCameraSelectionChanged();
result.success(true);
break;
case "getCameraSelection":
result.success(FlutterTool.getCameraSelection(context));
break;
case "cleanupOrderEvidenceFiles":
List<?> rawReferencedPaths = call.argument("referencedPaths");
Number olderThan = call.argument("olderThan");
@@ -354,7 +380,10 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
String activeKey = stringArgument(call, "activeKey");
String sdkKey = stringArgument(call, "sdkKey");
if (FlutterTool.isRegistrationInProgress()) {
result.success(FlutterTool.getFaceStatusWhileRegistering(context, appId, activeKey, sdkKey));
Log.i(TAG, "人脸状态查询遇到互斥占用owner:" + FlutterTool.getRegistrationOwner() + ";");
Map<String, Object> busyStatus = FlutterTool.getFaceStatusWhileRegistering(context, appId, activeKey, sdkKey);
busyStatus.put("registrationOwner", FlutterTool.getRegistrationOwner());
result.success(busyStatus);
return;
}
FACE_EXECUTOR.execute(() -> {
@@ -369,7 +398,7 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
}
private void startRegistrationTask(String userId, MethodChannel.Result result, boolean legacyResult) {
if (!FlutterTool.tryStartRegistration()) {
if (!FlutterTool.tryStartRegistration("rebuildFaceLibrary")) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在注册,请稍候", null);
return;
}
@@ -399,7 +428,7 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
* @param result Flutter MethodChannel 异步结果
*/
private void startFaceFeatureExtraction(String filePath, MethodChannel.Result result) {
if (!FlutterTool.tryStartRegistration()) {
if (!FlutterTool.tryStartRegistration("extractFaceFeature")) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
@@ -427,7 +456,7 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
result.error("INVALID_QUALITY_AUDIT_ITEMS", "质量检测参数无效", null);
return;
}
if (!FlutterTool.tryStartRegistration()) {
if (!FlutterTool.tryStartRegistration("auditFaceQuality")) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
@@ -452,7 +481,7 @@ public class FlutterFaceDiscernPlugin implements FlutterPlugin, MethodChannel.Me
* @param result Flutter MethodChannel 异步结果
*/
private void startFaceDelta(MethodCall call, MethodChannel.Result result) {
if (!FlutterTool.tryStartRegistration()) {
if (!FlutterTool.tryStartRegistration("applyFaceDelta")) {
result.error("REGISTRATION_IN_PROGRESS", "人脸库正在更新,请稍候", null);
return;
}
@@ -34,6 +34,8 @@ import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import io.flutter.plugins.facediscern.flutter.config.Constants;
import io.flutter.plugins.facediscern.tool.camera.CameraColorProbe;
import io.flutter.plugins.facediscern.tool.camera.config.CameraSelectionStore;
import io.flutter.plugins.facediscern.tool.face.model.FaceEntity;
import io.flutter.plugins.facediscern.tool.face.servcie.FaceServer;
import io.flutter.plugins.facediscern.tool.file.FileUtil;
@@ -150,6 +152,50 @@ public class FlutterTool {
return REGISTRATION_IN_PROGRESS.get();
}
// 枚举本机摄像头并逐一探测彩色/黑白输出,供设置页人工选择识别摄像头。
// 只能在 FACE_EXECUTOR 上调用:探测会短暂打开摄像头,不能与人脸会话或主线程并发。
static Map<String, Object> getCameraChoices(Context context) {
Map<String, Object> result = new HashMap<>();
List<Map<String, Object>> choices = new ArrayList<>();
int selectedId = CameraSelectionStore.loadRgbCameraId(context);
result.put("selectedCameraId", selectedId);
try {
int cameraCount = android.hardware.Camera.getNumberOfCameras();
android.hardware.Camera.CameraInfo info = new android.hardware.Camera.CameraInfo();
for (int id = 0; id < cameraCount; id++) {
android.hardware.Camera.getCameraInfo(id, info);
Map<String, Object> choice = new HashMap<>();
choice.put("cameraId", id);
choice.put("facing", info.facing == android.hardware.Camera.CameraInfo.CAMERA_FACING_FRONT
? "front" : "back");
choice.put("colorKind", CameraColorProbe.probeColorKind(id));
choices.add(choice);
}
} catch (Exception e) {
Log.e(TAG, "枚举摄像头失败error:" + e.getMessage(), e);
}
result.put("choices", choices);
return result;
}
// 保存人工选择的识别摄像头;负数归一化为自动。持久化由调用方通知会话管理器失效热相机。
static void setCameraSelection(Context context, int cameraId) {
CameraSelectionStore.saveRgbCameraId(context, cameraId);
Log.i(TAG, "人工选择识别摄像头cameraId:" + cameraId + ";");
}
// 只读当前人工选择,不探测摄像头;供设置页轻量回显。
static int getCameraSelection(Context context) {
return CameraSelectionStore.loadRgbCameraId(context);
}
// 最近一次占用互斥锁的操作名,仅用于诊断日志;锁空闲时可能残留上一次的值。
private static volatile String REGISTRATION_OWNER = "";
static String getRegistrationOwner() {
return REGISTRATION_OWNER;
}
// 注册/特征提取/质量审计占线期间的状态查询:只读授权文件与最新进度缓存,
// 不触碰 FaceServer 锁也不查数据库,供主线程即时返回,避免在单线程执行器里
// 排队等待整库任务完成后才能应答。
@@ -182,8 +228,10 @@ public class FlutterTool {
return status;
}
static boolean tryStartRegistration() {
return REGISTRATION_IN_PROGRESS.compareAndSet(false, true);
static boolean tryStartRegistration(String owner) {
boolean acquired = REGISTRATION_IN_PROGRESS.compareAndSet(false, true);
if (acquired) REGISTRATION_OWNER = owner == null ? "" : owner;
return acquired;
}
static void finishRegistration() {
@@ -35,6 +35,7 @@ import io.flutter.plugins.facediscern.tool.face.model.FacePreviewInfo;
import io.flutter.plugins.facediscern.tool.face.model.RecognitionFrameSnapshot;
import io.flutter.plugins.facediscern.tool.face.constants.RequestFeatureStatus;
import io.flutter.plugins.facediscern.tool.face.constants.RequestLivenessStatus;
import io.flutter.plugins.facediscern.tool.camera.config.CameraSelectionStore;
import io.flutter.plugins.facediscern.tool.camera.config.PreviewConfig;
import io.flutter.plugins.facediscern.ui.viewmodel.FaceDiscernViewModel;
import io.flutter.view.TextureRegistry;
@@ -375,6 +376,17 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
if (releaseNow) releaseResources(true, true);
}
/**
* 人工切换识别摄像头后让下次识别按新编号开相机;活动识别延迟到当前顾客结束。
*/
public void onCameraSelectionChanged() {
boolean releaseNow;
synchronized (sessionLock) {
releaseNow = activeSessionId == null;
}
if (releaseNow) releaseResources(true, true);
}
public void releaseForLowMemory() {
synchronized (sessionLock) {
if (activeSessionId != null) {
@@ -1064,8 +1076,15 @@ public final class FaceRecognitionSessionManager implements Camera.PreviewCallba
@SuppressWarnings("deprecation")
private int findRgbCameraId() {
int fallback = -1;
int cameraCount = Camera.getNumberOfCameras();
// 双目终端存在厂家把彩色/红外排线装反的情况,facing 枚举角色不可信;
// 设置页人工指定的摄像头编号永远优先。
int manualId = CameraSelectionStore.loadRgbCameraId(context);
if (manualId >= 0 && manualId < cameraCount) {
Log.i(TAG, "使用人工选择的识别摄像头 cameraId=" + manualId + ";cameraCount=" + cameraCount);
return manualId;
}
int fallback = -1;
Camera.CameraInfo info = new Camera.CameraInfo();
for (int i = 0; i < cameraCount; i++) {
Camera.getCameraInfo(i, info);
@@ -0,0 +1,138 @@
package io.flutter.plugins.facediscern.tool.camera;
import android.graphics.ImageFormat;
import android.graphics.SurfaceTexture;
import android.hardware.Camera;
import android.util.Log;
import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
/**
* 通过预览帧色度判断单目摄像头输出彩色还是黑白(红外)画面。
*
* 元数据(facing/朝向)无法识别厂家排线装反的双目设备,唯一可靠的区分信号在
* 像素层:红外/黑白传感器的 NV21 色度平面(U/V 交错字节)恒为中性值 128,
* 彩色传感器对任何真实场景都会出现明显色度偏移。探测结果只作为设置页的
* 人工选择参考标签,不作为自动切换依据。
*/
public final class CameraColorProbe {
private static final String TAG = "CameraHelper";
/** 判定为彩色的每字节平均色度偏移阈值;真实场景彩色画面通常远高于该值。 */
private static final double COLOR_CHROMA_THRESHOLD = 2.0;
/** 单摄像头最多采集的帧数,取各帧中的最大色度偏移作为判定值。 */
private static final int MAX_FRAMES = 6;
/** 整个探测(含开相机)的硬超时,超时按未知处理,不阻塞调用方。 */
private static final long PROBE_TIMEOUT_MS = 2500;
private CameraColorProbe() {
}
public static final String KIND_COLOR = "color";
public static final String KIND_MONO = "mono";
public static final String KIND_UNKNOWN = "unknown";
/**
* 打开指定摄像头采集若干 NV21 帧并统计色度偏移。
*
* @return color=彩色;mono=黑白/红外;unknown=相机被占用或超时无法判定
*/
@SuppressWarnings("deprecation")
public static String probeColorKind(int cameraId) {
Camera camera = null;
try {
camera = Camera.open(cameraId);
Camera.Parameters parameters = camera.getParameters();
parameters.setPreviewFormat(ImageFormat.NV21);
Camera.Size size = chooseSmallestSize(parameters.getSupportedPreviewSizes());
if (size != null) {
parameters.setPreviewSize(size.width, size.height);
}
camera.setParameters(parameters);
// 无渲染目标时使用离屏纹理承载预览,仅消费回调帧。
camera.setPreviewTexture(new SurfaceTexture(0));
final CountDownLatch latch = new CountDownLatch(MAX_FRAMES);
final AtomicReference<Double> maxMetric = new AtomicReference<>(0.0);
final AtomicInteger receivedFrames = new AtomicInteger(0);
camera.setPreviewCallback((nv21, callbackCamera) -> {
if (nv21 == null || nv21.length == 0) {
latch.countDown();
return;
}
double metric = chromaDeviation(nv21);
double previous = maxMetric.get();
if (metric > previous) maxMetric.set(metric);
receivedFrames.incrementAndGet();
latch.countDown();
});
camera.startPreview();
boolean completed = latch.await(PROBE_TIMEOUT_MS, TimeUnit.MILLISECONDS);
double metric = maxMetric.get();
int frames = receivedFrames.get();
String kind = frames == 0
? KIND_UNKNOWN
: (metric >= COLOR_CHROMA_THRESHOLD ? KIND_COLOR : KIND_MONO);
Log.i(TAG, "摄像头色度探测cameraId=" + cameraId + ";kind=" + kind
+ ";chroma=" + String.format("%.2f", metric)
+ ";frames=" + frames + ";timeout=" + !completed);
return kind;
} catch (Throwable error) {
Log.w(TAG, "摄像头色度探测失败cameraId=" + cameraId + ";error=" + error.getMessage());
return KIND_UNKNOWN;
} finally {
if (camera != null) {
try {
camera.setPreviewCallback(null);
camera.stopPreview();
} catch (Exception ignore) {
}
try {
camera.release();
} catch (Exception ignore) {
}
}
}
}
/** NV21 色度平面采样:每个采样字节相对中性值 128 的平均偏移,单色输出恒接近 0。 */
static double chromaDeviation(byte[] nv21) {
int lumaEnd = estimateLumaEnd(nv21.length);
int chromaLength = nv21.length - lumaEnd;
if (chromaLength <= 0) return 0;
int step = Math.max(1, chromaLength / 4096);
long total = 0;
int samples = 0;
for (int index = lumaEnd; index < nv21.length; index += step) {
total += Math.abs((nv21[index] & 0xFF) - 128);
samples++;
}
return samples == 0 ? 0 : total / (double) samples;
}
/**
* 常见 Camera1 NV21 布局为 width*height 的 Y 平面后跟 VU 交错的色度平面,
* 总长为亮度区的 3/2;无法拿到精确分辨率时按该比例估算亮度区结束位置。
*/
static int estimateLumaEnd(int bufferLength) {
return bufferLength * 2 / 3;
}
@SuppressWarnings("deprecation")
private static Camera.Size chooseSmallestSize(List<Camera.Size> sizes) {
if (sizes == null || sizes.isEmpty()) return null;
Camera.Size smallest = sizes.get(0);
for (Camera.Size size : sizes) {
if (size.width * size.height < smallest.width * smallest.height) {
smallest = size;
}
}
return smallest;
}
}
@@ -0,0 +1,41 @@
package io.flutter.plugins.facediscern.tool.camera.config;
import android.content.Context;
import android.content.SharedPreferences;
/**
* 保存终端本机的彩色识别摄像头人工选择。
*
* 双目人脸设备的彩色/红外摄像头只靠 facing 枚举角色区分,厂家排线装反时
* 枚举为 BACK 的摄像头实际输出红外黑白画面,元数据层面无法判定,因此提供
* 人工指定入口。-1 表示自动(维持按 facing 枚举选择的既有行为)。
*/
public final class CameraSelectionStore {
private static final String PREFERENCES_NAME = "face_camera_selection";
private static final String KEY_RGB_CAMERA_ID = "rgb_camera_id";
public static final int AUTO_CAMERA_ID = -1;
private CameraSelectionStore() {
}
/** 返回人工选择的彩色摄像头编号;-1 表示未指定,按设备枚举自动选择。 */
public static int loadRgbCameraId(Context context) {
try {
return preferences(context).getInt(KEY_RGB_CAMERA_ID, AUTO_CAMERA_ID);
} catch (Exception error) {
return AUTO_CAMERA_ID;
}
}
/** 写入人工选择;负数一律归一化为自动。 */
public static void saveRgbCameraId(Context context, int cameraId) {
preferences(context)
.edit()
.putInt(KEY_RGB_CAMERA_ID, cameraId < 0 ? AUTO_CAMERA_ID : cameraId)
.apply();
}
private static SharedPreferences preferences(Context context) {
return context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE);
}
}