feat(example): 实现完整的人脸识别示例
- 实现注册流程: 人脸检测 -> 活体检测 -> 特征提取(REGISTER) -> 存储 - 实现验证流程: 人脸检测 -> 活体检测 -> 特征提取(RECOGNIZE) -> 比对 - 使用 extractFaceFeature() 和 compareFaceFeature() API - 使用 SharedPreferences 存储人脸特征数据 - 支持注册/验证模式切换 - 显示相似度和验证结果 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'dart:math';
|
||||
import 'dart:typed_data';
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:arc/arc.dart';
|
||||
import 'package:camera/camera.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
void main() {
|
||||
runApp(const MyApp());
|
||||
@@ -123,8 +125,8 @@ class _HomePageState extends State<HomePage> {
|
||||
final result = await _arcPlugin.init(
|
||||
detectMode: 0, // 0=VIDEO 模式, 1=IMAGE 模式
|
||||
orient: 0, // 检测角度优先级
|
||||
maxFaceNum: 10, // 最大检测人脸数
|
||||
combinedMask: 0x9D, // 功能组合掩码(人脸检测+识别+年龄+性别+RGB活体)
|
||||
maxFaceNum: 1, // 单人脸识别场景
|
||||
combinedMask: 0x9D, // 功能组合掩码
|
||||
);
|
||||
|
||||
if (result != null) {
|
||||
@@ -146,8 +148,8 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 跳转到摄像头预览页面
|
||||
void _navigateToCameraPreview() async {
|
||||
/// 跳转到人脸识别页面
|
||||
void _navigateToFaceRecognition() async {
|
||||
try {
|
||||
final cameras = await availableCameras();
|
||||
if (!mounted) return;
|
||||
@@ -155,7 +157,7 @@ class _HomePageState extends State<HomePage> {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CameraPreviewScreen(
|
||||
builder: (context) => FaceRecognitionScreen(
|
||||
arcPlugin: _arcPlugin,
|
||||
cameras: cameras,
|
||||
),
|
||||
@@ -253,16 +255,16 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// 摄像头预览按钮
|
||||
// 人脸识别按钮
|
||||
ElevatedButton(
|
||||
onPressed: _isInitialized ? _navigateToCameraPreview : null,
|
||||
onPressed: _isInitialized ? _navigateToFaceRecognition : 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),
|
||||
),
|
||||
),
|
||||
@@ -284,10 +286,11 @@ class _HomePageState extends State<HomePage> {
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'1. 请先替换代码中的 _appId、_sdkKey 和 _activeKey 为您的真实密钥\n'
|
||||
'2. 点击"激活 SDK"按钮进行在线激活\n'
|
||||
'3. 激活成功后点击"初始化引擎"按钮\n'
|
||||
'4. 初始化成功后点击"打开摄像头预览"进行人脸检测',
|
||||
'1. 点击"激活 SDK"按钮进行在线激活\n'
|
||||
'2. 激活成功后点击"初始化引擎"按钮\n'
|
||||
'3. 点击"开始人脸识别"进行注册/验证\n\n'
|
||||
'注册流程: 人脸检测 → 活体检测 → 特征提取 → 存储\n'
|
||||
'验证流程: 人脸检测 → 活体检测 → 特征提取 → 比对',
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
],
|
||||
@@ -301,42 +304,46 @@ class _HomePageState extends State<HomePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 摄像头预览页面,用于实时人脸检测和 RGB 活体检测
|
||||
class CameraPreviewScreen extends StatefulWidget {
|
||||
/// 人脸识别页面 - 实现完整的注册和验证流程
|
||||
class FaceRecognitionScreen extends StatefulWidget {
|
||||
final Arc arcPlugin;
|
||||
final List<CameraDescription> cameras;
|
||||
|
||||
const CameraPreviewScreen({
|
||||
const FaceRecognitionScreen({
|
||||
super.key,
|
||||
required this.arcPlugin,
|
||||
required this.cameras,
|
||||
});
|
||||
|
||||
@override
|
||||
State<CameraPreviewScreen> createState() => _CameraPreviewScreenState();
|
||||
State<FaceRecognitionScreen> createState() => _FaceRecognitionScreenState();
|
||||
}
|
||||
|
||||
class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
class _FaceRecognitionScreenState extends State<FaceRecognitionScreen> {
|
||||
CameraController? _cameraController;
|
||||
|
||||
bool _isDetecting = false;
|
||||
int _faceCount = 0;
|
||||
String _lastDetectionTime = '';
|
||||
bool _isProcessing = false;
|
||||
String _statusMessage = '正在初始化摄像头...';
|
||||
|
||||
// RGB 活体检测相关状态
|
||||
// 检测结果
|
||||
int _faceCount = 0;
|
||||
int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人
|
||||
String _rgbLivenessStatus = '未检测';
|
||||
|
||||
// 人脸ID管理
|
||||
/// 存储人脸特征到ID的映射(使用人脸矩形区域的hashCode作为特征标识)
|
||||
final Map<String, String> _faceIdMap = {};
|
||||
/// 当前帧检测到的人脸ID列表
|
||||
List<String> _currentFaceIds = [];
|
||||
/// 当前帧检测到的人脸特征标识列表(用于注册时关联)
|
||||
List<String> _currentFaceKeys = [];
|
||||
/// 随机数生成器
|
||||
final Random _random = Random();
|
||||
// 注册的人脸特征数据存储 key
|
||||
static const String _storedFeatureKey = 'stored_face_feature';
|
||||
|
||||
// 当前帧的人脸信息
|
||||
Map<String, dynamic>? _currentFaceInfo;
|
||||
Uint8List? _currentNv21Data;
|
||||
int _currentWidth = 0;
|
||||
int _currentHeight = 0;
|
||||
|
||||
// 模式: 0=注册, 1=验证
|
||||
int _mode = 0;
|
||||
String _modeLabel = '注册模式';
|
||||
|
||||
// 验证结果
|
||||
double _similarity = 0.0;
|
||||
bool _verified = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -347,35 +354,22 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
/// 初始化摄像头
|
||||
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) {
|
||||
if (camera.lensDirection == CameraLensDirection.front) {
|
||||
selectedCamera = camera;
|
||||
break;
|
||||
}
|
||||
}
|
||||
selectedCamera ??= widget.cameras.first;
|
||||
|
||||
debugPrint('选择摄像头: ${selectedCamera.name}');
|
||||
_cameraController = CameraController(
|
||||
selectedCamera,
|
||||
ResolutionPreset.medium,
|
||||
@@ -385,19 +379,13 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
|
||||
try {
|
||||
await _cameraController!.initialize();
|
||||
debugPrint('摄像头初始化成功');
|
||||
|
||||
final size = _cameraController!.value.previewSize;
|
||||
debugPrint('预览尺寸: ${size?.width}x${size?.height}');
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = '摄像头: ${selectedCamera?.name ?? "未知"}';
|
||||
_statusMessage = '摄像头就绪';
|
||||
});
|
||||
_startImageStream();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('摄像头初始化失败: $e');
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_statusMessage = '摄像头初始化失败: $e';
|
||||
@@ -409,91 +397,66 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
/// 启动图像流
|
||||
void _startImageStream() {
|
||||
_cameraController?.startImageStream(_onImageAvailable);
|
||||
debugPrint('已启动图像流');
|
||||
}
|
||||
|
||||
/// 图像帧回调,进行人脸检测
|
||||
/// 图像帧回调 - 人脸检测
|
||||
void _onImageAvailable(CameraImage image) async {
|
||||
if (_isDetecting) return;
|
||||
_isDetecting = true;
|
||||
if (_isProcessing) return;
|
||||
_isProcessing = true;
|
||||
|
||||
try {
|
||||
final int width = image.width;
|
||||
final int height = image.height;
|
||||
|
||||
// 将图像数据转换为连续的 NV21 格式
|
||||
final nv21Data = _extractNV21(image, width, height);
|
||||
|
||||
// 人脸检测(同时进行 RGB 活体检测)
|
||||
// 保存当前帧数据用于后续特征提取
|
||||
_currentNv21Data = nv21Data;
|
||||
_currentWidth = width;
|
||||
_currentHeight = height;
|
||||
|
||||
// 人脸检测 + RGB 活体检测
|
||||
final detectResult = await widget.arcPlugin.detectFaces(
|
||||
data: nv21Data,
|
||||
width: width,
|
||||
height: height,
|
||||
format: 2050, // NV21 格式
|
||||
format: 2050,
|
||||
);
|
||||
|
||||
if (detectResult != null) {
|
||||
final success = detectResult['success'] == true;
|
||||
final faceList = detectResult['faceList'] as List? ?? [];
|
||||
final rgbLiveness = detectResult['rgbLiveness'] as int? ?? -1;
|
||||
|
||||
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;
|
||||
// 保存第一张人脸信息
|
||||
_currentFaceInfo = Map<String, dynamic>.from(faceList[0] as Map);
|
||||
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_faceCount = faceList.length;
|
||||
_lastDetectionTime = timeStr;
|
||||
_rgbLivenessResult = rgbLiveness;
|
||||
_rgbLivenessStatus = rgbIsAlive ? '真人' : (rgbLiveness == 0 ? '非真人' : '未知');
|
||||
// 更新当前人脸特征标识和ID列表
|
||||
_currentFaceKeys = faceList.map((face) {
|
||||
return _getFaceKey(face as Map);
|
||||
}).toList();
|
||||
_currentFaceIds = _currentFaceKeys.map((key) {
|
||||
return _faceIdMap[key] ?? '未注册';
|
||||
}).toList();
|
||||
_statusMessage = _rgbLivenessResult == 1 ? '检测到真人' : '检测中...';
|
||||
});
|
||||
}
|
||||
debugPrint('RGB 活体: liveness=$rgbLiveness, isAlive=$rgbIsAlive');
|
||||
debugPrint('========================================');
|
||||
} else {
|
||||
// 未检测到人脸时重置状态
|
||||
_currentFaceInfo = null;
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
_faceCount = 0;
|
||||
_rgbLivenessResult = -1;
|
||||
_rgbLivenessStatus = '未检测';
|
||||
_currentFaceIds = [];
|
||||
_currentFaceKeys = [];
|
||||
_statusMessage = '未检测到人脸';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e, stackTrace) {
|
||||
} catch (e) {
|
||||
debugPrint('人脸检测异常: $e');
|
||||
debugPrint('堆栈: $stackTrace');
|
||||
} finally {
|
||||
_isDetecting = false;
|
||||
_isProcessing = false;
|
||||
}
|
||||
}
|
||||
|
||||
/// 提取 NV21 数据(处理 stride 对齐)
|
||||
/// 提取 NV21 数据
|
||||
Uint8List _extractNV21(CameraImage image, int width, int height) {
|
||||
final ySize = width * height;
|
||||
final nv21Size = ySize * 3 ~/ 2;
|
||||
@@ -550,27 +513,182 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
return nv21;
|
||||
}
|
||||
|
||||
/// 生成随机人脸ID
|
||||
/// 生成格式: F + 8位随机数字
|
||||
String _generateRandomFaceId() {
|
||||
final id = _random.nextInt(100000000).toString().padLeft(8, '0');
|
||||
return 'F$id';
|
||||
/// 注册人脸
|
||||
Future<void> _registerFace() async {
|
||||
if (_currentFaceInfo == null || _currentNv21Data == null) {
|
||||
_showMessage('未检测到人脸');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rgbLivenessResult != 1) {
|
||||
_showMessage('请确保是真人');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在提取特征...';
|
||||
});
|
||||
|
||||
try {
|
||||
// 提取特征 (REGISTER 模式)
|
||||
final result = await widget.arcPlugin.extractFaceFeature(
|
||||
data: _currentNv21Data!,
|
||||
width: _currentWidth,
|
||||
height: _currentHeight,
|
||||
rectLeft: _currentFaceInfo!['rectLeft'] as int,
|
||||
rectTop: _currentFaceInfo!['rectTop'] as int,
|
||||
rectRight: _currentFaceInfo!['rectRight'] as int,
|
||||
rectBottom: _currentFaceInfo!['rectBottom'] as int,
|
||||
faceOrientation: _currentFaceInfo!['orient'] as int? ?? 0,
|
||||
faceId: _currentFaceInfo!['faceId'] as int? ?? -1,
|
||||
extractType: 0, // REGISTER 模式
|
||||
mask: 0,
|
||||
);
|
||||
|
||||
if (result != null && result['success'] == true) {
|
||||
final featureData = result['featureData'] as Uint8List;
|
||||
|
||||
// 存储特征数据到本地
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.setString(_storedFeatureKey, _bytesToBase64(featureData));
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '注册成功!特征已保存';
|
||||
});
|
||||
_showMessage('人脸注册成功!');
|
||||
} else {
|
||||
final errorMsg = result?['message'] ?? '未知错误';
|
||||
setState(() {
|
||||
_statusMessage = '特征提取失败: $errorMsg';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_statusMessage = '注册异常: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 根据人脸矩形区域生成特征标识
|
||||
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';
|
||||
/// 验证人脸
|
||||
Future<void> _verifyFace() async {
|
||||
if (_currentFaceInfo == null || _currentNv21Data == null) {
|
||||
_showMessage('未检测到人脸');
|
||||
return;
|
||||
}
|
||||
|
||||
if (_rgbLivenessResult != 1) {
|
||||
_showMessage('请确保是真人');
|
||||
return;
|
||||
}
|
||||
|
||||
// 读取存储的特征
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final storedFeatureBase64 = prefs.getString(_storedFeatureKey);
|
||||
|
||||
if (storedFeatureBase64 == null) {
|
||||
_showMessage('请先注册人脸');
|
||||
return;
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_statusMessage = '正在提取特征...';
|
||||
});
|
||||
|
||||
try {
|
||||
// 提取当前帧特征 (RECOGNIZE 模式)
|
||||
final result = await widget.arcPlugin.extractFaceFeature(
|
||||
data: _currentNv21Data!,
|
||||
width: _currentWidth,
|
||||
height: _currentHeight,
|
||||
rectLeft: _currentFaceInfo!['rectLeft'] as int,
|
||||
rectTop: _currentFaceInfo!['rectTop'] as int,
|
||||
rectRight: _currentFaceInfo!['rectRight'] as int,
|
||||
rectBottom: _currentFaceInfo!['rectBottom'] as int,
|
||||
faceOrientation: _currentFaceInfo!['orient'] as int? ?? 0,
|
||||
faceId: _currentFaceInfo!['faceId'] as int? ?? -1,
|
||||
extractType: 1, // RECOGNIZE 模式
|
||||
mask: 0,
|
||||
);
|
||||
|
||||
if (result != null && result['success'] == true) {
|
||||
final currentFeature = result['featureData'] as Uint8List;
|
||||
final storedFeature = _base64ToBytes(storedFeatureBase64);
|
||||
|
||||
// 比对特征
|
||||
final compareResult = await widget.arcPlugin.compareFaceFeature(
|
||||
featureData1: currentFeature,
|
||||
featureData2: storedFeature,
|
||||
compareModel: 0, // LIFE_PHOTO 模式
|
||||
);
|
||||
|
||||
if (compareResult != null && compareResult['success'] == true) {
|
||||
final similarity = compareResult['similarity'] as double? ?? 0.0;
|
||||
final threshold = 0.8; // 推荐阈值
|
||||
final isMatch = similarity >= threshold;
|
||||
|
||||
setState(() {
|
||||
_similarity = similarity;
|
||||
_verified = isMatch;
|
||||
_statusMessage = isMatch
|
||||
? '验证通过!相似度: ${(similarity * 100).toStringAsFixed(1)}%'
|
||||
: '验证失败!相似度: ${(similarity * 100).toStringAsFixed(1)}%';
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
_statusMessage = '比对失败';
|
||||
});
|
||||
}
|
||||
} else {
|
||||
setState(() {
|
||||
_statusMessage = '特征提取失败';
|
||||
});
|
||||
}
|
||||
} catch (e) {
|
||||
setState(() {
|
||||
_statusMessage = '验证异常: $e';
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// 切换模式
|
||||
void _toggleMode() {
|
||||
setState(() {
|
||||
_mode = _mode == 0 ? 1 : 0;
|
||||
_modeLabel = _mode == 0 ? '注册模式' : '验证模式';
|
||||
_similarity = 0.0;
|
||||
_verified = false;
|
||||
});
|
||||
}
|
||||
|
||||
/// 清除注册数据
|
||||
Future<void> _clearRegistration() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_storedFeatureKey);
|
||||
setState(() {
|
||||
_similarity = 0.0;
|
||||
_verified = false;
|
||||
_statusMessage = '已清除注册数据';
|
||||
});
|
||||
_showMessage('注册数据已清除');
|
||||
}
|
||||
|
||||
/// 字节数组转 Base64
|
||||
String _bytesToBase64(Uint8List bytes) {
|
||||
return base64Encode(bytes);
|
||||
}
|
||||
|
||||
/// Base64 转字节数组
|
||||
Uint8List _base64ToBytes(String base64) {
|
||||
return base64Decode(base64);
|
||||
}
|
||||
|
||||
/// 显示消息
|
||||
void _showMessage(String message) {
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(message)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -584,9 +702,21 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('人脸检测预览'),
|
||||
title: Text(_modeLabel),
|
||||
backgroundColor: Colors.purple,
|
||||
foregroundColor: Colors.white,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.swap_horiz),
|
||||
onPressed: _toggleMode,
|
||||
tooltip: '切换模式',
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.delete_outline),
|
||||
onPressed: _clearRegistration,
|
||||
tooltip: '清除注册数据',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
@@ -595,10 +725,7 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
child: _cameraController != null &&
|
||||
_cameraController!.value.isInitialized
|
||||
? Center(
|
||||
child: RotatedBox(
|
||||
quarterTurns: -1,
|
||||
child: CameraPreview(_cameraController!),
|
||||
),
|
||||
child: CameraPreview(_cameraController!),
|
||||
)
|
||||
: Center(
|
||||
child: Column(
|
||||
@@ -612,7 +739,7 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
),
|
||||
),
|
||||
|
||||
// 检测信息显示
|
||||
// 检测信息和操作面板
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
@@ -620,26 +747,34 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// 状态信息
|
||||
Text(
|
||||
'检测到人脸数: $_faceCount',
|
||||
_statusMessage,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontSize: 18,
|
||||
fontSize: 16,
|
||||
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(
|
||||
_faceCount > 0 ? Icons.face : Icons.face_retouching_off,
|
||||
color: _faceCount > 0 ? Colors.green : Colors.grey,
|
||||
size: 20,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'人脸: ${_faceCount > 0 ? "已检测" : "未检测"}',
|
||||
style: TextStyle(
|
||||
color: _faceCount > 0 ? Colors.green : Colors.grey,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Icon(
|
||||
_rgbLivenessResult == 1
|
||||
? Icons.check_circle
|
||||
@@ -655,98 +790,83 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'RGB活体: $_rgbLivenessStatus',
|
||||
'活体: ${_rgbLivenessResult == 1 ? "真人" : _rgbLivenessResult == 0 ? "非真人" : "未知"}',
|
||||
style: TextStyle(
|
||||
color: _rgbLivenessResult == 1
|
||||
? Colors.green
|
||||
: _rgbLivenessResult == 0
|
||||
? Colors.red
|
||||
: Colors.white70,
|
||||
: Colors.orange,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// 人脸ID显示
|
||||
if (_currentFaceIds.isNotEmpty) ...[
|
||||
|
||||
// 验证结果显示
|
||||
if (_mode == 1 && _similarity > 0) ...[
|
||||
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,
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
_verified ? Icons.verified_user : Icons.warning,
|
||||
color: _verified ? Colors.green : Colors.red,
|
||||
size: 24,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'相似度: ${(_similarity * 100).toStringAsFixed(1)}%',
|
||||
style: TextStyle(
|
||||
color: _verified ? Colors.green : Colors.red,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
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,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
_verified ? '验证通过' : '非本人',
|
||||
style: TextStyle(
|
||||
color: _verified ? Colors.green : Colors.red,
|
||||
fontSize: 14,
|
||||
),
|
||||
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] == '未注册') {
|
||||
final newId = _generateRandomFaceId();
|
||||
final faceKey = _currentFaceKeys[i];
|
||||
// 将特征标识和ID存入映射表
|
||||
_faceIdMap[faceKey] = newId;
|
||||
_currentFaceIds[i] = newId;
|
||||
}
|
||||
}
|
||||
});
|
||||
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),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// 操作按钮
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _mode == 0 ? _registerFace : _verifyFace,
|
||||
icon: Icon(_mode == 0 ? Icons.person_add : Icons.verified_user),
|
||||
label: Text(_mode == 0 ? '注册人脸' : '验证身份'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: _mode == 0 ? Colors.blue : Colors.green,
|
||||
foregroundColor: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// 提示
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
_mode == 0
|
||||
? '提示: 注册时会提取人脸特征并保存到本地'
|
||||
: '提示: 验证时会与已注册的特征进行比对,阈值 0.8',
|
||||
style: const TextStyle(
|
||||
color: Colors.white54,
|
||||
fontSize: 11,
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user