feat(example): 添加人脸注册按钮和ID显示功能
- 添加注册按钮,点击生成随机人脸ID(格式:F + 8位数字) - 实时显示检测到的人脸对应的ID - 使用彩色标签区分已注册(绿色)和未注册(橙色)状态 Quick task: 260330-ld1
This commit is contained in:
38
.planning/PROJECT.md
Normal file
38
.planning/PROJECT.md
Normal file
@@ -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*
|
||||||
30
.planning/REQUIREMENTS.md
Normal file
30
.planning/REQUIREMENTS.md
Normal file
@@ -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*
|
||||||
21
.planning/ROADMAP.md
Normal file
21
.planning/ROADMAP.md
Normal file
@@ -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*
|
||||||
27
.planning/STATE.md
Normal file
27
.planning/STATE.md
Normal file
@@ -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*
|
||||||
13
.planning/config.json
Normal file
13
.planning/config.json
Normal file
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
163
.planning/phase-1/PLAN.md
Normal file
163
.planning/phase-1/PLAN.md
Normal file
@@ -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<FaceFeatureInfo> 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<Map<String, Object>> 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<Map<String, dynamic>?> registerFaceFeature({
|
||||||
|
required int searchId,
|
||||||
|
required Uint8List featureData,
|
||||||
|
String? faceTag,
|
||||||
|
})
|
||||||
|
|
||||||
|
/// 批量注册人脸特征到人脸库
|
||||||
|
/// [faceList] 人脸列表,每项包含 searchId, featureData, faceTag(可选)
|
||||||
|
/// 返回包含 success, errorCode, message 的 Map
|
||||||
|
Future<Map<String, dynamic>?> registerFaceFeatureBatch({
|
||||||
|
required List<Map<String, dynamic>> 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`
|
||||||
27
.planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md
Normal file
27
.planning/quick/260330-ld1-example-id-id/260330-ld1-PLAN.md
Normal file
@@ -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<int, String>` 存储人脸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*
|
||||||
@@ -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*
|
||||||
748
example/lib/main.dart
Normal file
748
example/lib/main.dart
Normal file
@@ -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<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
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<void> 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<void> _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<void> _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<CameraDescription> cameras;
|
||||||
|
|
||||||
|
const CameraPreviewScreen({
|
||||||
|
super.key,
|
||||||
|
required this.arcPlugin,
|
||||||
|
required this.cameras,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CameraPreviewScreen> createState() => _CameraPreviewScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||||
|
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<String, String> _faceIdMap = {};
|
||||||
|
/// 当前帧检测到的人脸ID列表
|
||||||
|
List<String> _currentFaceIds = [];
|
||||||
|
/// 随机数生成器
|
||||||
|
final Random _random = Random();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_initCamera();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 初始化摄像头
|
||||||
|
Future<void> _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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user