feat(example): 实现完整的人脸识别示例

- 实现注册流程: 人脸检测 -> 活体检测 -> 特征提取(REGISTER) -> 存储
- 实现验证流程: 人脸检测 -> 活体检测 -> 特征提取(RECOGNIZE) -> 比对
- 使用 extractFaceFeature() 和 compareFaceFeature() API
- 使用 SharedPreferences 存储人脸特征数据
- 支持注册/验证模式切换
- 显示相似度和验证结果

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-30 16:08:33 +08:00
parent fff221bee2
commit 3c7e992489
2 changed files with 418 additions and 207 deletions

View File

@@ -1,10 +1,12 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'dart:async'; import 'dart:async';
import 'dart:math'; import 'dart:typed_data';
import 'dart:convert';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:arc/arc.dart'; import 'package:arc/arc.dart';
import 'package:camera/camera.dart'; import 'package:camera/camera.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() { void main() {
runApp(const MyApp()); runApp(const MyApp());
@@ -123,8 +125,8 @@ class _HomePageState extends State<HomePage> {
final result = await _arcPlugin.init( final result = await _arcPlugin.init(
detectMode: 0, // 0=VIDEO 模式, 1=IMAGE 模式 detectMode: 0, // 0=VIDEO 模式, 1=IMAGE 模式
orient: 0, // 检测角度优先级 orient: 0, // 检测角度优先级
maxFaceNum: 10, // 最大检测人脸数 maxFaceNum: 1, // 单人脸识别场景
combinedMask: 0x9D, // 功能组合掩码(人脸检测+识别+年龄+性别+RGB活体 combinedMask: 0x9D, // 功能组合掩码
); );
if (result != null) { if (result != null) {
@@ -146,8 +148,8 @@ class _HomePageState extends State<HomePage> {
} }
} }
/// 跳转到摄像头预览页面 /// 跳转到人脸识别页面
void _navigateToCameraPreview() async { void _navigateToFaceRecognition() async {
try { try {
final cameras = await availableCameras(); final cameras = await availableCameras();
if (!mounted) return; if (!mounted) return;
@@ -155,7 +157,7 @@ class _HomePageState extends State<HomePage> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
builder: (context) => CameraPreviewScreen( builder: (context) => FaceRecognitionScreen(
arcPlugin: _arcPlugin, arcPlugin: _arcPlugin,
cameras: cameras, cameras: cameras,
), ),
@@ -253,16 +255,16 @@ class _HomePageState extends State<HomePage> {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 摄像头预览按钮 // 人脸识别按钮
ElevatedButton( ElevatedButton(
onPressed: _isInitialized ? _navigateToCameraPreview : null, onPressed: _isInitialized ? _navigateToFaceRecognition : null,
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: _isInitialized ? Colors.purple : Colors.grey, backgroundColor: _isInitialized ? Colors.purple : Colors.grey,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
), ),
child: const Text( child: const Text(
'打开摄像头预览', '开始人脸识别',
style: TextStyle(fontSize: 16), style: TextStyle(fontSize: 16),
), ),
), ),
@@ -284,10 +286,11 @@ class _HomePageState extends State<HomePage> {
), ),
SizedBox(height: 8), SizedBox(height: 8),
Text( Text(
'1. 请先替换代码中的 _appId、_sdkKey 和 _activeKey 为您的真实密钥\n' '1. 点击"激活 SDK"按钮进行在线激活\n'
'2. 点击"激活 SDK"按钮进行在线激活\n' '2. 激活成功后点击"初始化引擎"按钮\n'
'3. 激活成功后点击"初始化引擎"按钮\n' '3. 点击"开始人脸识别"进行注册/验证\n\n'
'4. 初始化成功后点击"打开摄像头预览"进行人脸检测', '注册流程: 人脸检测 → 活体检测 → 特征提取 → 存储\n'
'验证流程: 人脸检测 → 活体检测 → 特征提取 → 比对',
style: TextStyle(fontSize: 12), 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 Arc arcPlugin;
final List<CameraDescription> cameras; final List<CameraDescription> cameras;
const CameraPreviewScreen({ const FaceRecognitionScreen({
super.key, super.key,
required this.arcPlugin, required this.arcPlugin,
required this.cameras, required this.cameras,
}); });
@override @override
State<CameraPreviewScreen> createState() => _CameraPreviewScreenState(); State<FaceRecognitionScreen> createState() => _FaceRecognitionScreenState();
} }
class _CameraPreviewScreenState extends State<CameraPreviewScreen> { class _FaceRecognitionScreenState extends State<FaceRecognitionScreen> {
CameraController? _cameraController; CameraController? _cameraController;
bool _isProcessing = false;
bool _isDetecting = false;
int _faceCount = 0;
String _lastDetectionTime = '';
String _statusMessage = '正在初始化摄像头...'; String _statusMessage = '正在初始化摄像头...';
// RGB 活体检测相关状态 // 检测结果
int _faceCount = 0;
int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人 int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人
String _rgbLivenessStatus = '未检测';
// 人脸ID管理 // 注册的人脸特征数据存储 key
/// 存储人脸特征到ID的映射使用人脸矩形区域的hashCode作为特征标识 static const String _storedFeatureKey = 'stored_face_feature';
final Map<String, String> _faceIdMap = {};
/// 当前帧检测到的人脸ID列表 // 当前帧的人脸信息
List<String> _currentFaceIds = []; Map<String, dynamic>? _currentFaceInfo;
/// 当前帧检测到的人脸特征标识列表(用于注册时关联) Uint8List? _currentNv21Data;
List<String> _currentFaceKeys = []; int _currentWidth = 0;
/// 随机数生成器 int _currentHeight = 0;
final Random _random = Random();
// 模式: 0=注册, 1=验证
int _mode = 0;
String _modeLabel = '注册模式';
// 验证结果
double _similarity = 0.0;
bool _verified = false;
@override @override
void initState() { void initState() {
@@ -347,35 +354,22 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
/// 初始化摄像头 /// 初始化摄像头
Future<void> _initCamera() async { Future<void> _initCamera() async {
if (widget.cameras.isEmpty) { if (widget.cameras.isEmpty) {
debugPrint('没有可用的摄像头');
setState(() { setState(() {
_statusMessage = '没有可用的摄像头'; _statusMessage = '没有可用的摄像头';
}); });
return; 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; CameraDescription? selectedCamera;
for (var camera in widget.cameras) { for (var camera in widget.cameras) {
if (camera.lensDirection == CameraLensDirection.back) { if (camera.lensDirection == CameraLensDirection.front) {
selectedCamera = camera; selectedCamera = camera;
break; break;
} }
} }
selectedCamera ??= widget.cameras.first; selectedCamera ??= widget.cameras.first;
debugPrint('选择摄像头: ${selectedCamera.name}');
_cameraController = CameraController( _cameraController = CameraController(
selectedCamera, selectedCamera,
ResolutionPreset.medium, ResolutionPreset.medium,
@@ -385,19 +379,13 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
try { try {
await _cameraController!.initialize(); await _cameraController!.initialize();
debugPrint('摄像头初始化成功');
final size = _cameraController!.value.previewSize;
debugPrint('预览尺寸: ${size?.width}x${size?.height}');
if (mounted) { if (mounted) {
setState(() { setState(() {
_statusMessage = '摄像头: ${selectedCamera?.name ?? "未知"}'; _statusMessage = '摄像头就绪';
}); });
_startImageStream(); _startImageStream();
} }
} catch (e) { } catch (e) {
debugPrint('摄像头初始化失败: $e');
if (mounted) { if (mounted) {
setState(() { setState(() {
_statusMessage = '摄像头初始化失败: $e'; _statusMessage = '摄像头初始化失败: $e';
@@ -409,91 +397,66 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
/// 启动图像流 /// 启动图像流
void _startImageStream() { void _startImageStream() {
_cameraController?.startImageStream(_onImageAvailable); _cameraController?.startImageStream(_onImageAvailable);
debugPrint('已启动图像流');
} }
/// 图像帧回调,进行人脸检测 /// 图像帧回调 - 人脸检测
void _onImageAvailable(CameraImage image) async { void _onImageAvailable(CameraImage image) async {
if (_isDetecting) return; if (_isProcessing) return;
_isDetecting = true; _isProcessing = true;
try { try {
final int width = image.width; final int width = image.width;
final int height = image.height; final int height = image.height;
// 将图像数据转换为连续的 NV21 格式
final nv21Data = _extractNV21(image, width, height); final nv21Data = _extractNV21(image, width, height);
// 人脸检测(同时进行 RGB 活体检测) // 保存当前帧数据用于后续特征提取
_currentNv21Data = nv21Data;
_currentWidth = width;
_currentHeight = height;
// 人脸检测 + RGB 活体检测
final detectResult = await widget.arcPlugin.detectFaces( final detectResult = await widget.arcPlugin.detectFaces(
data: nv21Data, data: nv21Data,
width: width, width: width,
height: height, height: height,
format: 2050, // NV21 格式 format: 2050,
); );
if (detectResult != null) { if (detectResult != null) {
final success = detectResult['success'] == true; final success = detectResult['success'] == true;
final faceList = detectResult['faceList'] as List? ?? []; final faceList = detectResult['faceList'] as List? ?? [];
final rgbLiveness = detectResult['rgbLiveness'] as int? ?? -1;
if (success && faceList.isNotEmpty) { if (success && faceList.isNotEmpty) {
final now = DateTime.now(); // 保存第一张人脸信息
final timeStr = '${now.hour.toString().padLeft(2, '0')}:' _currentFaceInfo = Map<String, dynamic>.from(faceList[0] as Map);
'${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) { if (mounted) {
setState(() { setState(() {
_faceCount = faceList.length; _faceCount = faceList.length;
_lastDetectionTime = timeStr;
_rgbLivenessResult = rgbLiveness; _rgbLivenessResult = rgbLiveness;
_rgbLivenessStatus = rgbIsAlive ? '真人' : (rgbLiveness == 0 ? '真人' : '未知'); _statusMessage = _rgbLivenessResult == 1 ? '检测到真人' : '检测中...';
// 更新当前人脸特征标识和ID列表
_currentFaceKeys = faceList.map((face) {
return _getFaceKey(face as Map);
}).toList();
_currentFaceIds = _currentFaceKeys.map((key) {
return _faceIdMap[key] ?? '未注册';
}).toList();
}); });
} }
debugPrint('RGB 活体: liveness=$rgbLiveness, isAlive=$rgbIsAlive');
debugPrint('========================================');
} else { } else {
// 未检测到人脸时重置状态 _currentFaceInfo = null;
if (mounted) { if (mounted) {
setState(() { setState(() {
_faceCount = 0; _faceCount = 0;
_rgbLivenessResult = -1; _rgbLivenessResult = -1;
_rgbLivenessStatus = '未检测'; _statusMessage = '未检测到人脸';
_currentFaceIds = [];
_currentFaceKeys = [];
}); });
} }
} }
} }
} catch (e, stackTrace) { } catch (e) {
debugPrint('人脸检测异常: $e'); debugPrint('人脸检测异常: $e');
debugPrint('堆栈: $stackTrace');
} finally { } finally {
_isDetecting = false; _isProcessing = false;
} }
} }
/// 提取 NV21 数据(处理 stride 对齐) /// 提取 NV21 数据
Uint8List _extractNV21(CameraImage image, int width, int height) { Uint8List _extractNV21(CameraImage image, int width, int height) {
final ySize = width * height; final ySize = width * height;
final nv21Size = ySize * 3 ~/ 2; final nv21Size = ySize * 3 ~/ 2;
@@ -550,27 +513,182 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
return nv21; return nv21;
} }
/// 生成随机人脸ID /// 注册人脸
/// 生成格式: F + 8位随机数字 Future<void> _registerFace() async {
String _generateRandomFaceId() { if (_currentFaceInfo == null || _currentNv21Data == null) {
final id = _random.nextInt(100000000).toString().padLeft(8, '0'); _showMessage('未检测到人脸');
return 'F$id'; return;
} }
/// 根据人脸矩形区域生成特征标识 if (_rgbLivenessResult != 1) {
String _getFaceKey(Map face) { _showMessage('请确保是真人');
// 使用人脸矩形区域的位置和大小作为特征标识 return;
final rectLeft = face['rectLeft'] as int? ?? 0; }
final rectTop = face['rectTop'] as int? ?? 0;
final rectRight = face['rectRight'] as int? ?? 0; setState(() {
final rectBottom = face['rectBottom'] as int? ?? 0; _statusMessage = '正在提取特征...';
// 使用位置和大小计算一个简单的特征值(允许一定容差) });
final centerX = (rectLeft + rectRight) ~/ 2;
final centerY = (rectTop + rectBottom) ~/ 2; try {
// 按照区块划分每50像素为一个区块 // 提取特征 (REGISTER 模式)
final blockX = centerX ~/ 50; final result = await widget.arcPlugin.extractFaceFeature(
final blockY = centerY ~/ 50; data: _currentNv21Data!,
return '$blockX-$blockY'; 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';
});
}
}
/// 验证人脸
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 @override
@@ -584,9 +702,21 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('人脸检测预览'), title: Text(_modeLabel),
backgroundColor: Colors.purple, backgroundColor: Colors.purple,
foregroundColor: Colors.white, 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( body: Column(
children: [ children: [
@@ -595,10 +725,7 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
child: _cameraController != null && child: _cameraController != null &&
_cameraController!.value.isInitialized _cameraController!.value.isInitialized
? Center( ? Center(
child: RotatedBox(
quarterTurns: -1,
child: CameraPreview(_cameraController!), child: CameraPreview(_cameraController!),
),
) )
: Center( : Center(
child: Column( child: Column(
@@ -612,7 +739,7 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
), ),
), ),
// 检测信息显示 // 检测信息和操作面板
Container( Container(
width: double.infinity, width: double.infinity,
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -620,26 +747,34 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
// 状态信息
Text( Text(
'检测到人脸数: $_faceCount', _statusMessage,
style: const TextStyle( style: const TextStyle(
color: Colors.white, color: Colors.white,
fontSize: 18, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 8),
// 人脸和活体状态
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( Text(
'最后检测时间: $_lastDetectionTime', '人脸: ${_faceCount > 0 ? "已检测" : "未检测"}',
style: const TextStyle( style: TextStyle(
color: Colors.white70, color: _faceCount > 0 ? Colors.green : Colors.grey,
fontSize: 14, fontSize: 14,
), ),
), ),
const SizedBox(height: 8), const SizedBox(width: 16),
// RGB 活体检测状态
Row(
children: [
Icon( Icon(
_rgbLivenessResult == 1 _rgbLivenessResult == 1
? Icons.check_circle ? Icons.check_circle
@@ -655,98 +790,83 @@ class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
Text( Text(
'RGB活体: $_rgbLivenessStatus', '活体: ${_rgbLivenessResult == 1 ? "真人" : _rgbLivenessResult == 0 ? "非真人" : "未知"}',
style: TextStyle( style: TextStyle(
color: _rgbLivenessResult == 1 color: _rgbLivenessResult == 1
? Colors.green ? Colors.green
: _rgbLivenessResult == 0 : _rgbLivenessResult == 0
? Colors.red ? Colors.red
: Colors.white70, : Colors.orange,
fontSize: 14, fontSize: 14,
), ),
), ),
], ],
), ),
// 人脸ID显示
if (_currentFaceIds.isNotEmpty) ...[ // 验证结果显示
if (_mode == 1 && _similarity > 0) ...[
const SizedBox(height: 8), const SizedBox(height: 8),
const Divider(color: Colors.white24), const Divider(color: Colors.white24),
const SizedBox(height: 8), const SizedBox(height: 8),
Row(
children: [
Icon(
_verified ? Icons.verified_user : Icons.warning,
color: _verified ? Colors.green : Colors.red,
size: 24,
),
const SizedBox(width: 8),
Text( Text(
'人脸ID:', '相似度: ${(_similarity * 100).toStringAsFixed(1)}%',
style: const TextStyle( style: TextStyle(
color: Colors.white, color: _verified ? Colors.green : Colors.red,
fontSize: 14, fontSize: 18,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
), ),
const SizedBox(height: 4), const SizedBox(width: 8),
Wrap( Text(
spacing: 8, _verified ? '验证通过' : '非本人',
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( style: TextStyle(
color: isRegistered ? Colors.greenAccent : Colors.orangeAccent, color: _verified ? Colors.green : Colors.red,
fontSize: 12, fontSize: 14,
), ),
), ),
);
}).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), const SizedBox(height: 12),
label: const Text('注册人脸生成随机ID'),
// 操作按钮
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( style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue, backgroundColor: _mode == 0 ? Colors.blue : Colors.green,
foregroundColor: Colors.white, foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),
), ),
), ),
), ),
], ],
),
// 提示
const SizedBox(height: 8),
Text(
_mode == 0
? '提示: 注册时会提取人脸特征并保存到本地'
: '提示: 验证时会与已注册的特征进行比对,阈值 0.8',
style: const TextStyle(
color: Colors.white54,
fontSize: 11,
),
),
], ],
), ),
), ),

91
example/pubspec.yaml Normal file
View File

@@ -0,0 +1,91 @@
name: arc_example
description: "Demonstrates how to use the arc plugin."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
environment:
sdk: ^3.9.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
arc:
# When depending on this package from a real application you should use:
# arc: ^x.y.z
# See https://dart.dev/tools/pub/dependencies#version-constraints
# The example app is bundled with the plugin so we use a path dependency on
# the parent directory to use the current plugin's version.
path: ../
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8
# 摄像头插件
camera: ^0.11.1
# 本地存储
shared_preferences: ^2.2.2
dev_dependencies:
integration_test:
sdk: flutter
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package