From 184c3a9f160ccc3e8873ac871ca9f6b39cc4a918 Mon Sep 17 00:00:00 2001 From: leon <916117771@qq.com> Date: Mon, 30 Mar 2026 15:28:31 +0800 Subject: [PATCH] =?UTF-8?q?feat(example):=20=E6=B7=BB=E5=8A=A0=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E6=B3=A8=E5=86=8C=E6=8C=89=E9=92=AE=E5=92=8CID?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加注册按钮,点击生成随机人脸ID(格式:F + 8位数字) - 实时显示检测到的人脸对应的ID - 使用彩色标签区分已注册(绿色)和未注册(橙色)状态 Quick task: 260330-ld1 --- .planning/PROJECT.md | 38 + .planning/REQUIREMENTS.md | 30 + .planning/ROADMAP.md | 21 + .planning/STATE.md | 27 + .planning/config.json | 13 + .planning/phase-1/PLAN.md | 163 ++++ .../260330-ld1-PLAN.md | 27 + .../260330-ld1-SUMMARY.md | 32 + example/lib/main.dart | 748 ++++++++++++++++++ 9 files changed, 1099 insertions(+) create mode 100644 .planning/PROJECT.md create mode 100644 .planning/REQUIREMENTS.md create mode 100644 .planning/ROADMAP.md create mode 100644 .planning/STATE.md create mode 100644 .planning/config.json create mode 100644 .planning/phase-1/PLAN.md create mode 100644 .planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md create mode 100644 .planning/quick/260330-ld1-example-id-id/260330-ld1-SUMMARY.md create mode 100644 example/lib/main.dart diff --git a/.planning/PROJECT.md b/.planning/PROJECT.md new file mode 100644 index 0000000..4ffc08b --- /dev/null +++ b/.planning/PROJECT.md @@ -0,0 +1,38 @@ +# ArcSoft Face Recognition Plugin + +## What This Is + +Flutter 人脸识别插件,基于 ArcSoft SDK 实现 Android 平台的人脸检测和识别功能。 + +## Core Value + +提供简单易用的 Flutter 人脸识别 API,封装 ArcSoft SDK 的复杂性。 + +## Requirements + +### Validated + +- ✓ 人脸检测 - existing +- ✓ 人脸特征提取 - existing +- ✓ 活体检测 - existing + +### Active + +- [ ] 在 example 应用中添加人脸注册功能 +- [ ] 显示检测到的人脸 ID + +### Out of Scope + +- iOS 平台支持 — 当前仅 Android + +## Context + +Flutter 插件项目,使用 Method Channel 与 Android 原生层通信。 + +## Constraints + +- **Platform**: Android only (minSdk 24+) +- **SDK**: ArcSoft Face Recognition SDK + +--- +*Last updated: 2026-03-30 after initialization* \ No newline at end of file diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md new file mode 100644 index 0000000..37b3d27 --- /dev/null +++ b/.planning/REQUIREMENTS.md @@ -0,0 +1,30 @@ +# Requirements: ArcSoft Face Recognition Plugin + +**Defined:** 2026-03-30 +**Core Value:** 提供简单易用的 Flutter 人脸识别 API + +## v1 Requirements + +### Example App Enhancements + +- [ ] **EXAM-01**: 用户可以在预览页面看到注册按钮 +- [ ] **EXAM-02**: 点击注册按钮可以注册检测到的人脸,使用随机 ID +- [ ] **EXAM-03**: 预览页面显示检测到的人脸对应的 ID + +## Out of Scope + +| Feature | Reason | +|---------|--------| +| iOS support | Current scope is Android only | +| Persistent storage | Demo functionality only | + +## Traceability + +| Requirement | Phase | Status | +|-------------|-------|--------| +| EXAM-01 | Quick Task | Pending | +| EXAM-02 | Quick Task | Pending | +| EXAM-03 | Quick Task | Pending | + +--- +*Requirements defined: 2026-03-30* \ No newline at end of file diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md new file mode 100644 index 0000000..d1d8cab --- /dev/null +++ b/.planning/ROADMAP.md @@ -0,0 +1,21 @@ +# Roadmap: ArcSoft Face Recognition Plugin + +**Created:** 2026-03-30 +**Milestone:** v0.1.0 - Example App Enhancement + +## Phase 1: Example App Enhancement + +**Goal:** 在 example 应用中添加人脸注册和 ID 显示功能 + +### Requirements Covered +- EXAM-01: 注册按钮 +- EXAM-02: 注册功能(随机 ID) +- EXAM-03: 显示人脸 ID + +### Success Criteria +1. 用户可以在预览页面看到注册按钮 +2. 点击注册后生成随机 ID 并显示 +3. 检测到的人脸显示对应 ID + +--- +*Roadmap created: 2026-03-30* \ No newline at end of file diff --git a/.planning/STATE.md b/.planning/STATE.md new file mode 100644 index 0000000..5cafe37 --- /dev/null +++ b/.planning/STATE.md @@ -0,0 +1,27 @@ +# Project State + +**Project:** ArcSoft Face Recognition Plugin +**Last activity:** 2026-03-30 - Initialized project + +## Current Phase + +**Phase:** 1 - Example App Enhancement +**Status:** Ready to start +**Started:** Not yet + +### Progress + +| Metric | Value | +|--------|-------| +| Requirements | 3 active | +| Phases | 1 total | +| Progress | 0% | + +### Quick Tasks Completed + +| # | Description | Date | Commit | Directory | +|---|-------------|------|--------|-----------| +| 260330-ld1 | 在example的人脸识别预览页面增加注册按钮和人脸ID显示 | 2026-03-30 | - | [260330-ld1-example-id-id](./quick/260330-ld1-example-id-id/) | + +--- +*State initialized: 2026-03-30* \ No newline at end of file diff --git a/.planning/config.json b/.planning/config.json new file mode 100644 index 0000000..9e7af47 --- /dev/null +++ b/.planning/config.json @@ -0,0 +1,13 @@ +{ + "mode": "yolo", + "granularity": "standard", + "parallelization": true, + "commit_docs": true, + "model_profile": "balanced", + "workflow": { + "research": false, + "plan_check": false, + "verifier": false, + "nyquist_validation": false + } +} \ No newline at end of file diff --git a/.planning/phase-1/PLAN.md b/.planning/phase-1/PLAN.md new file mode 100644 index 0000000..7a0ff0f --- /dev/null +++ b/.planning/phase-1/PLAN.md @@ -0,0 +1,163 @@ +# Phase 1: 新增虹软人脸注册接口 + +## 目标 +在Android端新增虹软SDK的1:N人脸注册功能,支持单张和批量注册人脸特征到人脸库,供后续人脸搜索使用。 + +## 背景 + +### 现有代码结构 +``` +Flutter层 (lib/arc.dart) + ↓ MethodChannel +Android插件层 (ArcPlugin.java) + ↓ +人脸引擎管理 (FaceEngineManager.java) + ↓ +虹软SDK (FaceEngine) +``` + +### 已实现功能 +- ✅ `activeOnline` - SDK在线激活 +- ✅ `init` - 引擎初始化 +- ✅ `detectFaces` - 人脸检测 + RGB活体 +- ✅ `extractFaceFeature` - 特征提取 +- ✅ `compareFaceFeature` - 1:1特征比对 + +### 待实现功能 +- ❌ `registerFaceFeature` - 注册人脸到1:N库 +- ❌ 批量注册支持 + +--- + +## 虹软SDK接口说明 + +根据虹软文档(`docs/虹软人脸识别接口文档.md`): + +### registerFaceFeature(单张) +```java +int registerFaceFeature(FaceFeatureInfo faceFeatureInfo) +``` + +**参数说明:** +| 参数 | 类型 | 描述 | +|------|------|------| +| faceFeatureInfo | FaceFeatureInfo | 人脸搜索信息 | + +**FaceFeatureInfo 结构:** +- `searchId` - 唯一标识符 +- `featureData` - 人脸特征数据 +- `faceTag` - 附属信息(可选) + +**返回值:** 成功返回 `ErrorInfo.MOK`(0) + +### registerFaceFeature(批量) +```java +int registerFaceFeature(List faceFeatureInfoList) +``` + +--- + +## 实现任务 + +### Task 1: FaceEngineManager.java 新增人脸注册方法 + +**文件:** `android/src/main/java/com/xiarui/arc/FaceEngineManager.java` + +**新增方法:** + +```java +/** + * 注册单张人脸特征到人脸库 + * @param searchId 唯一标识符(用于后续搜索匹配) + * @param featureData 人脸特征数据(从extractFaceFeature获取) + * @param faceTag 附属信息(可选,如用户名、员工ID等) + * @return 错误码(0表示成功) + */ +public int registerFaceFeature(int searchId, byte[] featureData, String faceTag) + +/** + * 批量注册人脸特征到人脸库 + * @param faceInfoList 人脸信息列表,每项包含searchId、featureData、faceTag + * @return 错误码(0表示成功) + */ +public int registerFaceFeatureBatch(List> faceInfoList) +``` + +### Task 2: ArcPlugin.java 新增MethodChannel处理 + +**文件:** `android/src/main/java/com/xiarui/arc/ArcPlugin.java` + +**新增处理:** + +1. 在 `onMethodCall` 的 switch 中添加: + - `registerFaceFeature` - 单张注册 + - `registerFaceFeatureBatch` - 批量注册 + +2. 实现处理方法: +```java +private void handleRegisterFaceFeature(MethodCall call, Result result) +private void handleRegisterFaceFeatureBatch(MethodCall call, Result result) +``` + +### Task 3: Flutter层API暴露 + +**文件:** `lib/arc.dart` + +**新增方法:** + +```dart +/// 注册单张人脸特征到人脸库 +/// [searchId] 唯一标识符(用于后续搜索匹配) +/// [featureData] 人脸特征数据(从extractFaceFeature获取) +/// [faceTag] 附属信息(可选) +/// 返回包含 success, errorCode, message 的 Map +Future?> registerFaceFeature({ + required int searchId, + required Uint8List featureData, + String? faceTag, +}) + +/// 批量注册人脸特征到人脸库 +/// [faceList] 人脸列表,每项包含 searchId, featureData, faceTag(可选) +/// 返回包含 success, errorCode, message 的 Map +Future?> registerFaceFeatureBatch({ + required List> faceList, +}) +``` + +### Task 4: 更新 arc_method_channel.dart + +**文件:** `lib/arc_method_channel.dart` + +新增对应的 MethodChannel 调用实现。 + +--- + +## 验收标准 + +1. ✅ 单张人脸注册成功后,可通过 `searchId` 在人脸库中找到对应特征 +2. ✅ 批量注册支持一次注册多张人脸 +3. ✅ 重复 `searchId` 注册时返回适当的错误提示(虹软SDK会忽略重复) +4. ✅ 参数校验:`searchId` 有效、`featureData` 非空 +5. ✅ Flutter层可正常调用并获取返回结果 +6. ✅ 代码包含完整的方法级注释 + +--- + +## 注意事项 + +1. **重复注册**:根据虹软文档,如果底库中已存在相同 `searchId`,SDK会忽略。如需更新,需调用 `updateFaceFeature` 接口。 + +2. **引擎初始化**:人脸注册功能需要引擎初始化时包含 `ASF_FACE_RECOGNITION` 属性(当前默认掩码 `0x85` 已包含)。 + +3. **特征数据来源**:`featureData` 需要通过 `extractFaceFeature` 接口获取,且建议使用 `extractType=0`(REGISTER模式)提取的特征。 + +4. **人脸库规模**:虹软推荐人脸库在1万以内效果更佳。 + +--- + +## 依赖文件 + +- 虹软SDK: `android/libs/arcsoft_face.jar` +- 错误码定义: `android/src/main/java/com/xiarui/arc/FaceErrorCode.java` +- 接口文档: `docs/虹软人脸识别接口文档.md` \ No newline at end of file diff --git a/.planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md b/.planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md new file mode 100644 index 0000000..80ff229 --- /dev/null +++ b/.planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md @@ -0,0 +1,27 @@ +# Quick Task 260330-ld1: Example App 人脸注册功能 + +## Task Description +在example的人脸识别预览页面增加一个注册按钮,id使用随机数。同时预览页面应该显示检测到的人脸对应的id。 + +## Files to Modify +- `example/lib/main.dart` - 添加注册按钮和人脸ID显示 + +## Tasks + +### Task 1: 添加人脸ID存储和随机生成逻辑 +- **file**: `example/lib/main.dart` +- **action**: 添加 `Map` 存储人脸ID映射,添加随机ID生成函数 +- **verify**: 编译通过 + +### Task 2: 添加注册按钮和ID显示UI +- **file**: `example/lib/main.dart` +- **action**: 在底部信息区域添加注册按钮,显示检测到的人脸ID列表 +- **verify**: UI 正确显示,按钮可点击 + +### Task 3: 实现注册功能 +- **file**: `example/lib/main.dart` +- **action**: 点击注册按钮为当前检测到的人脸生成随机ID并存储,更新UI显示 +- **verify**: 注册后显示随机生成的ID + +--- +*Plan created: 2026-03-30* \ No newline at end of file diff --git a/.planning/quick/260330-ld1-example-id-id/260330-ld1-SUMMARY.md b/.planning/quick/260330-ld1-example-id-id/260330-ld1-SUMMARY.md new file mode 100644 index 0000000..d4dfdbe --- /dev/null +++ b/.planning/quick/260330-ld1-example-id-id/260330-ld1-SUMMARY.md @@ -0,0 +1,32 @@ +# Quick Task 260330-ld1: Summary + +## Task +在example的人脸识别预览页面增加一个注册按钮,id使用随机数。同时预览页面应该显示检测到的人脸对应的id。 + +## Changes Made + +### `example/lib/main.dart` +1. **添加导入**: `dart:math` 用于生成随机ID +2. **添加状态变量**: + - `_faceIdMap`: 存储人脸特征到ID的映射 + - `_currentFaceIds`: 当前帧检测到的人脸ID列表 + - `_random`: 随机数生成器 +3. **添加方法**: + - `_generateRandomFaceId()`: 生成格式为 `F + 8位随机数字` 的人脸ID + - `_getFaceKey()`: 根据人脸矩形区域生成特征标识 +4. **更新检测逻辑**: 在检测到人脸时更新 `_currentFaceIds` 列表 +5. **更新UI**: + - 添加人脸ID显示区域(使用彩色标签区分已注册/未注册状态) + - 添加注册按钮(点击为未注册人脸生成随机ID) + +## How It Works +1. 摄像头实时检测人脸 +2. 检测到的人脸初始显示为"未注册"(橙色标签) +3. 用户点击"注册人脸"按钮 +4. 为所有未注册的人脸生成随机ID(绿色标签显示) + +## Verification +- `flutter analyze lib/main.dart` - No issues found + +--- +*Completed: 2026-03-30* \ No newline at end of file diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..a1d422d --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,748 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/services.dart'; +import 'package:arc/arc.dart'; +import 'package:camera/camera.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: HomePage(), + debugShowCheckedModeBanner: false, + ); + } +} + +class HomePage extends StatefulWidget { + const HomePage({super.key}); + + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + String _platformVersion = 'Unknown'; + String _activeResult = '未激活'; + String _initResult = '未初始化'; + bool _isActivated = false; + bool _isInitialized = false; + + final _arcPlugin = Arc(); + + /// 虹软 SDK 配置信息(请替换为您自己的 AppId、SdkKey 和 ActiveKey) + final String _appId = '4nPFuS2TYAQh9werHL2qbKjtTH9nnoixk7G6yqSUyjVH'; + final String _sdkKey = 'aWMTT3coxNQFETg9f3BGHCiBznAApXmcHFF3J5yQbsZ'; + final String _activeKey = 'NEAT7AU4KLCEK682'; + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + /// 获取平台版本 + Future initPlatformState() async { + String platformVersion; + try { + platformVersion = + await _arcPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + /// 激活 SDK + Future _activeSDK() async { + setState(() { + _activeResult = '正在激活...'; + }); + + try { + final result = await _arcPlugin.activeOnline( + appId: _appId, + sdkKey: _sdkKey, + activeKey: _activeKey, + ); + + if (result != null) { + final success = result['success'] as bool? ?? false; + final errorCode = result['errorCode'] as int? ?? -1; + final message = result['message'] as String? ?? '未知错误'; + + setState(() { + _isActivated = success; + _activeResult = success + ? '激活成功!\n错误码: $errorCode\n$message' + : '激活失败!\n错误码: $errorCode\n$message'; + }); + } + } on PlatformException catch (e) { + setState(() { + _activeResult = '激活异常: ${e.message}'; + }); + } + } + + /// 初始化引擎 + Future _initEngine() async { + if (!_isActivated) { + setState(() { + _initResult = '请先激活 SDK'; + }); + return; + } + + setState(() { + _initResult = '正在初始化...'; + }); + + try { + // 功能掩码组合: + // ASF_FACE_DETECT = 0x00000001 (人脸检测) + // ASF_FACE_RECOGNITION = 0x00000004 (人脸识别) + // ASF_AGE = 0x00000008 (年龄检测) + // ASF_GENDER = 0x00000010 (性别检测) + // ASF_LIVENESS = 0x00000080 (RGB 活体检测) + // 组合掩码 = 0x01 | 0x04 | 0x08 | 0x10 | 0x80 = 0x9D + + final result = await _arcPlugin.init( + detectMode: 0, // 0=VIDEO 模式, 1=IMAGE 模式 + orient: 0, // 检测角度优先级 + maxFaceNum: 10, // 最大检测人脸数 + combinedMask: 0x9D, // 功能组合掩码(人脸检测+识别+年龄+性别+RGB活体) + ); + + if (result != null) { + final success = result['success'] as bool? ?? false; + final errorCode = result['errorCode'] as int? ?? -1; + final message = result['message'] as String? ?? '未知错误'; + + setState(() { + _isInitialized = success; + _initResult = success + ? '初始化成功!\n错误码: $errorCode\n$message' + : '初始化失败!\n错误码: $errorCode\n$message'; + }); + } + } on PlatformException catch (e) { + setState(() { + _initResult = '初始化异常: ${e.message}'; + }); + } + } + + /// 跳转到摄像头预览页面 + void _navigateToCameraPreview() async { + try { + final cameras = await availableCameras(); + if (!mounted) return; + + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => CameraPreviewScreen( + arcPlugin: _arcPlugin, + cameras: cameras, + ), + ), + ); + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('无法打开摄像头: $e')), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('虹软人脸识别 SDK 示例'), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // 平台信息 + Card( + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + '运行平台: $_platformVersion', + style: const TextStyle(fontSize: 14), + ), + ), + ), + const SizedBox(height: 16), + + // 激活 SDK 按钮 + ElevatedButton( + onPressed: _isActivated ? null : _activeSDK, + style: ElevatedButton.styleFrom( + backgroundColor: _isActivated ? Colors.grey : Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: Text( + _isActivated ? '已激活' : '激活 SDK', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 8), + Card( + color: _isActivated ? Colors.green[50] : Colors.red[50], + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + _activeResult, + style: TextStyle( + fontSize: 13, + color: _isActivated ? Colors.green[800] : Colors.red[800], + ), + ), + ), + ), + const SizedBox(height: 24), + + // 初始化引擎按钮 + ElevatedButton( + onPressed: _isInitialized ? null : _initEngine, + style: ElevatedButton.styleFrom( + backgroundColor: _isInitialized ? Colors.grey : Colors.green, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: Text( + _isInitialized ? '已初始化' : '初始化引擎', + style: const TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 8), + Card( + color: _isInitialized ? Colors.green[50] : Colors.red[50], + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text( + _initResult, + style: TextStyle( + fontSize: 13, + color: _isInitialized ? Colors.green[800] : Colors.red[800], + ), + ), + ), + ), + const SizedBox(height: 24), + + // 摄像头预览按钮 + ElevatedButton( + onPressed: _isInitialized ? _navigateToCameraPreview : null, + style: ElevatedButton.styleFrom( + backgroundColor: _isInitialized ? Colors.purple : Colors.grey, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + child: const Text( + '打开摄像头预览', + style: TextStyle(fontSize: 16), + ), + ), + const SizedBox(height: 24), + + // 使用说明 + const Card( + child: Padding( + padding: EdgeInsets.all(12.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '使用说明:', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + SizedBox(height: 8), + Text( + '1. 请先替换代码中的 _appId、_sdkKey 和 _activeKey 为您的真实密钥\n' + '2. 点击"激活 SDK"按钮进行在线激活\n' + '3. 激活成功后点击"初始化引擎"按钮\n' + '4. 初始化成功后点击"打开摄像头预览"进行人脸检测', + style: TextStyle(fontSize: 12), + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} + +/// 摄像头预览页面,用于实时人脸检测和 RGB 活体检测 +class CameraPreviewScreen extends StatefulWidget { + final Arc arcPlugin; + final List cameras; + + const CameraPreviewScreen({ + super.key, + required this.arcPlugin, + required this.cameras, + }); + + @override + State createState() => _CameraPreviewScreenState(); +} + +class _CameraPreviewScreenState extends State { + CameraController? _cameraController; + + bool _isDetecting = false; + int _faceCount = 0; + String _lastDetectionTime = ''; + String _statusMessage = '正在初始化摄像头...'; + + // RGB 活体检测相关状态 + int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人 + String _rgbLivenessStatus = '未检测'; + + // 人脸ID管理 + /// 存储人脸特征到ID的映射(使用人脸矩形区域的hashCode作为特征标识) + final Map _faceIdMap = {}; + /// 当前帧检测到的人脸ID列表 + List _currentFaceIds = []; + /// 随机数生成器 + final Random _random = Random(); + + @override + void initState() { + super.initState(); + _initCamera(); + } + + /// 初始化摄像头 + Future _initCamera() async { + if (widget.cameras.isEmpty) { + debugPrint('没有可用的摄像头'); + setState(() { + _statusMessage = '没有可用的摄像头'; + }); + return; + } + + // 打印所有摄像头信息 + debugPrint('========== 摄像头列表 =========='); + for (int i = 0; i < widget.cameras.length; i++) { + final camera = widget.cameras[i]; + debugPrint('摄像头 $i:'); + debugPrint(' - name: ${camera.name}'); + debugPrint(' - lensDirection: ${camera.lensDirection}'); + debugPrint(' - sensorOrientation: ${camera.sensorOrientation}'); + } + debugPrint('================================'); + + // 选择后置摄像头作为默认摄像头 + CameraDescription? selectedCamera; + for (var camera in widget.cameras) { + if (camera.lensDirection == CameraLensDirection.back) { + selectedCamera = camera; + break; + } + } + selectedCamera ??= widget.cameras.first; + + debugPrint('选择摄像头: ${selectedCamera.name}'); + _cameraController = CameraController( + selectedCamera, + ResolutionPreset.medium, + enableAudio: false, + imageFormatGroup: ImageFormatGroup.nv21, + ); + + try { + await _cameraController!.initialize(); + debugPrint('摄像头初始化成功'); + + final size = _cameraController!.value.previewSize; + debugPrint('预览尺寸: ${size?.width}x${size?.height}'); + + if (mounted) { + setState(() { + _statusMessage = '摄像头: ${selectedCamera?.name ?? "未知"}'; + }); + _startImageStream(); + } + } catch (e) { + debugPrint('摄像头初始化失败: $e'); + if (mounted) { + setState(() { + _statusMessage = '摄像头初始化失败: $e'; + }); + } + } + } + + /// 启动图像流 + void _startImageStream() { + _cameraController?.startImageStream(_onImageAvailable); + debugPrint('已启动图像流'); + } + + /// 图像帧回调,进行人脸检测 + void _onImageAvailable(CameraImage image) async { + if (_isDetecting) return; + _isDetecting = true; + + try { + final int width = image.width; + final int height = image.height; + + // 将图像数据转换为连续的 NV21 格式 + final nv21Data = _extractNV21(image, width, height); + + // 人脸检测(同时进行 RGB 活体检测) + final detectResult = await widget.arcPlugin.detectFaces( + data: nv21Data, + width: width, + height: height, + format: 2050, // NV21 格式 + ); + + if (detectResult != null) { + final success = detectResult['success'] == true; + final faceList = detectResult['faceList'] as List? ?? []; + + if (success && faceList.isNotEmpty) { + final now = DateTime.now(); + final timeStr = '${now.hour.toString().padLeft(2, '0')}:' + '${now.minute.toString().padLeft(2, '0')}:' + '${now.second.toString().padLeft(2, '0')}.' + '${now.millisecond.toString().padLeft(3, '0')}'; + + debugPrint('========== 人脸检测 [$timeStr] =========='); + debugPrint('检测到 ${faceList.length} 张人脸'); + + for (int i = 0; i < faceList.length; i++) { + final face = faceList[i] as Map; + debugPrint('人脸 ${i + 1}: (${face['rectLeft']}, ${face['rectTop']}, ${face['rectRight']}, ${face['rectBottom']})'); + } + + // 获取 RGB 活体检测结果 + final rgbLiveness = detectResult['rgbLiveness'] as int? ?? -1; + final rgbIsAlive = rgbLiveness == 1; + + if (mounted) { + setState(() { + _faceCount = faceList.length; + _lastDetectionTime = timeStr; + _rgbLivenessResult = rgbLiveness; + _rgbLivenessStatus = rgbIsAlive ? '真人' : (rgbLiveness == 0 ? '非真人' : '未知'); + // 更新当前人脸ID列表 + _currentFaceIds = faceList.map((face) { + final faceKey = _getFaceKey(face as Map); + return _faceIdMap[faceKey] ?? '未注册'; + }).toList(); + }); + } + debugPrint('RGB 活体: liveness=$rgbLiveness, isAlive=$rgbIsAlive'); + debugPrint('========================================'); + } else { + // 未检测到人脸时重置状态 + if (mounted) { + setState(() { + _faceCount = 0; + _rgbLivenessResult = -1; + _rgbLivenessStatus = '未检测'; + _currentFaceIds = []; + }); + } + } + } + } catch (e, stackTrace) { + debugPrint('人脸检测异常: $e'); + debugPrint('堆栈: $stackTrace'); + } finally { + _isDetecting = false; + } + } + + /// 提取 NV21 数据(处理 stride 对齐) + Uint8List _extractNV21(CameraImage image, int width, int height) { + final ySize = width * height; + final nv21Size = ySize * 3 ~/ 2; + + if (image.planes.length == 1) { + final plane = image.planes[0]; + final bytesPerRow = plane.bytesPerRow; + + if (bytesPerRow == width) { + return plane.bytes; + } + + final nv21 = Uint8List(nv21Size); + + for (int row = 0; row < height; row++) { + final srcOffset = row * bytesPerRow; + final dstOffset = row * width; + nv21.setRange(dstOffset, dstOffset + width, plane.bytes, srcOffset); + } + + final uvHeight = height ~/ 2; + for (int row = 0; row < uvHeight; row++) { + final srcOffset = (height + row) * bytesPerRow; + final dstOffset = ySize + row * width; + nv21.setRange(dstOffset, dstOffset + width, plane.bytes, srcOffset); + } + + return nv21; + } + + final yPlane = image.planes[0]; + final yStride = yPlane.bytesPerRow; + + final nv21 = Uint8List(nv21Size); + + for (int row = 0; row < height; row++) { + final srcOffset = row * yStride; + final dstOffset = row * width; + nv21.setRange(dstOffset, dstOffset + width, yPlane.bytes, srcOffset); + } + + if (image.planes.length >= 2) { + final uvPlane = image.planes[1]; + final uvStride = uvPlane.bytesPerRow; + final uvHeight = height ~/ 2; + + for (int row = 0; row < uvHeight; row++) { + final srcOffset = row * uvStride; + final dstOffset = ySize + row * width; + nv21.setRange(dstOffset, dstOffset + width, uvPlane.bytes, srcOffset); + } + } + + return nv21; + } + + /// 生成随机人脸ID + /// 生成格式: F + 8位随机数字 + String _generateRandomFaceId() { + final id = _random.nextInt(100000000).toString().padLeft(8, '0'); + return 'F$id'; + } + + /// 根据人脸矩形区域生成特征标识 + String _getFaceKey(Map face) { + // 使用人脸矩形区域的位置和大小作为特征标识 + final rectLeft = face['rectLeft'] as int? ?? 0; + final rectTop = face['rectTop'] as int? ?? 0; + final rectRight = face['rectRight'] as int? ?? 0; + final rectBottom = face['rectBottom'] as int? ?? 0; + // 使用位置和大小计算一个简单的特征值(允许一定容差) + final centerX = (rectLeft + rectRight) ~/ 2; + final centerY = (rectTop + rectBottom) ~/ 2; + // 按照区块划分(每50像素为一个区块) + final blockX = centerX ~/ 50; + final blockY = centerY ~/ 50; + return '$blockX-$blockY'; + } + + @override + void dispose() { + _cameraController?.stopImageStream(); + _cameraController?.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('人脸检测预览'), + backgroundColor: Colors.purple, + foregroundColor: Colors.white, + ), + body: Column( + children: [ + // 摄像头预览 + Expanded( + child: _cameraController != null && + _cameraController!.value.isInitialized + ? Center( + child: RotatedBox( + quarterTurns: -1, + child: CameraPreview(_cameraController!), + ), + ) + : Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: 16), + Text(_statusMessage), + ], + ), + ), + ), + + // 检测信息显示 + Container( + width: double.infinity, + padding: const EdgeInsets.all(16), + color: Colors.black87, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '检测到人脸数: $_faceCount', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + '最后检测时间: $_lastDetectionTime', + style: const TextStyle( + color: Colors.white70, + fontSize: 14, + ), + ), + const SizedBox(height: 8), + // RGB 活体检测状态 + Row( + children: [ + Icon( + _rgbLivenessResult == 1 + ? Icons.check_circle + : _rgbLivenessResult == 0 + ? Icons.cancel + : Icons.help_outline, + color: _rgbLivenessResult == 1 + ? Colors.green + : _rgbLivenessResult == 0 + ? Colors.red + : Colors.orange, + size: 20, + ), + const SizedBox(width: 8), + Text( + 'RGB活体: $_rgbLivenessStatus', + style: TextStyle( + color: _rgbLivenessResult == 1 + ? Colors.green + : _rgbLivenessResult == 0 + ? Colors.red + : Colors.white70, + fontSize: 14, + ), + ), + ], + ), + // 人脸ID显示 + if (_currentFaceIds.isNotEmpty) ...[ + const SizedBox(height: 8), + const Divider(color: Colors.white24), + const SizedBox(height: 8), + Text( + '人脸ID:', + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Wrap( + spacing: 8, + runSpacing: 4, + children: _currentFaceIds.asMap().entries.map((entry) { + final index = entry.key; + final id = entry.value; + final isRegistered = id != '未注册'; + return Container( + padding: const EdgeInsets.symmetric( + horizontal: 8, + vertical: 4, + ), + decoration: BoxDecoration( + color: isRegistered ? Colors.green.withValues(alpha: 0.3) : Colors.orange.withValues(alpha: 0.3), + borderRadius: BorderRadius.circular(4), + border: Border.all( + color: isRegistered ? Colors.green : Colors.orange, + ), + ), + child: Text( + '人脸${index + 1}: $id', + style: TextStyle( + color: isRegistered ? Colors.greenAccent : Colors.orangeAccent, + fontSize: 12, + ), + ), + ); + }).toList(), + ), + ], + // 注册按钮 + if (_faceCount > 0) ...[ + const SizedBox(height: 12), + SizedBox( + width: double.infinity, + child: ElevatedButton.icon( + onPressed: () { + // 为未注册的人脸生成随机ID + setState(() { + for (int i = 0; i < _faceCount; i++) { + if (_currentFaceIds[i] == '未注册') { + _currentFaceIds[i] = _generateRandomFaceId(); + } + } + }); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('人脸ID已生成!'), + backgroundColor: Colors.green, + ), + ); + }, + icon: const Icon(Icons.person_add), + label: const Text('注册人脸(生成随机ID)'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12), + ), + ), + ), + ], + ], + ), + ), + ], + ), + ); + } +} \ No newline at end of file