feat:摄像头选择,副屏页面修改
This commit is contained in:
1 parent
2e929c199b
commit
a81a79b69f
9 files changed
+613
-10
No files matched your search
+34
-5
@@ -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() {
|
||||
|
||||
+20
-1
@@ -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);
|
||||
|
||||
+138
@@ -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;
|
||||
}
|
||||
}
|
||||
+41
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user