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 '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) {
|
||||||
|
_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) {
|
Future<void> _verifyFace() async {
|
||||||
// 使用人脸矩形区域的位置和大小作为特征标识
|
if (_currentFaceInfo == null || _currentNv21Data == null) {
|
||||||
final rectLeft = face['rectLeft'] as int? ?? 0;
|
_showMessage('未检测到人脸');
|
||||||
final rectTop = face['rectTop'] as int? ?? 0;
|
return;
|
||||||
final rectRight = face['rectRight'] as int? ?? 0;
|
}
|
||||||
final rectBottom = face['rectBottom'] as int? ?? 0;
|
|
||||||
// 使用位置和大小计算一个简单的特征值(允许一定容差)
|
if (_rgbLivenessResult != 1) {
|
||||||
final centerX = (rectLeft + rectRight) ~/ 2;
|
_showMessage('请确保是真人');
|
||||||
final centerY = (rectTop + rectBottom) ~/ 2;
|
return;
|
||||||
// 按照区块划分(每50像素为一个区块)
|
}
|
||||||
final blockX = centerX ~/ 50;
|
|
||||||
final blockY = centerY ~/ 50;
|
// 读取存储的特征
|
||||||
return '$blockX-$blockY';
|
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(
|
child: CameraPreview(_cameraController!),
|
||||||
quarterTurns: -1,
|
|
||||||
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),
|
|
||||||
Text(
|
|
||||||
'最后检测时间: $_lastDetectionTime',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white70,
|
|
||||||
fontSize: 14,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
// RGB 活体检测状态
|
|
||||||
|
// 人脸和活体状态
|
||||||
Row(
|
Row(
|
||||||
children: [
|
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(
|
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),
|
||||||
Text(
|
Row(
|
||||||
'人脸ID:',
|
children: [
|
||||||
style: const TextStyle(
|
Icon(
|
||||||
color: Colors.white,
|
_verified ? Icons.verified_user : Icons.warning,
|
||||||
fontSize: 14,
|
color: _verified ? Colors.green : Colors.red,
|
||||||
fontWeight: FontWeight.bold,
|
size: 24,
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 8),
|
||||||
const SizedBox(height: 4),
|
Text(
|
||||||
Wrap(
|
'相似度: ${(_similarity * 100).toStringAsFixed(1)}%',
|
||||||
spacing: 8,
|
style: TextStyle(
|
||||||
runSpacing: 4,
|
color: _verified ? Colors.green : Colors.red,
|
||||||
children: _currentFaceIds.asMap().entries.map((entry) {
|
fontSize: 18,
|
||||||
final index = entry.key;
|
fontWeight: FontWeight.bold,
|
||||||
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),
|
const SizedBox(width: 8),
|
||||||
borderRadius: BorderRadius.circular(4),
|
Text(
|
||||||
border: Border.all(
|
_verified ? '验证通过' : '非本人',
|
||||||
color: isRegistered ? Colors.green : Colors.orange,
|
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),
|
||||||
const SizedBox(height: 12),
|
|
||||||
SizedBox(
|
// 操作按钮
|
||||||
width: double.infinity,
|
Row(
|
||||||
child: ElevatedButton.icon(
|
children: [
|
||||||
onPressed: () {
|
Expanded(
|
||||||
// 为未注册的人脸生成随机ID,并存入映射表
|
child: ElevatedButton.icon(
|
||||||
setState(() {
|
onPressed: _mode == 0 ? _registerFace : _verifyFace,
|
||||||
for (int i = 0; i < _faceCount; i++) {
|
icon: Icon(_mode == 0 ? Icons.person_add : Icons.verified_user),
|
||||||
if (_currentFaceIds[i] == '未注册') {
|
label: Text(_mode == 0 ? '注册人脸' : '验证身份'),
|
||||||
final newId = _generateRandomFaceId();
|
style: ElevatedButton.styleFrom(
|
||||||
final faceKey = _currentFaceKeys[i];
|
backgroundColor: _mode == 0 ? Colors.blue : Colors.green,
|
||||||
// 将特征标识和ID存入映射表
|
foregroundColor: Colors.white,
|
||||||
_faceIdMap[faceKey] = newId;
|
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||||
_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: 8),
|
||||||
|
Text(
|
||||||
|
_mode == 0
|
||||||
|
? '提示: 注册时会提取人脸特征并保存到本地'
|
||||||
|
: '提示: 验证时会与已注册的特征进行比对,阈值 0.8',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white54,
|
||||||
|
fontSize: 11,
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
91
example/pubspec.yaml
Normal file
91
example/pubspec.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user