- 实现注册流程: 人脸检测 -> 活体检测 -> 特征提取(REGISTER) -> 存储 - 实现验证流程: 人脸检测 -> 活体检测 -> 特征提取(RECOGNIZE) -> 比对 - 使用 extractFaceFeature() 和 compareFaceFeature() API - 使用 SharedPreferences 存储人脸特征数据 - 支持注册/验证模式切换 - 显示相似度和验证结果 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
877 lines
26 KiB
Dart
877 lines
26 KiB
Dart
import 'package:flutter/material.dart';
|
||
import 'dart:async';
|
||
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());
|
||
}
|
||
|
||
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: 1, // 单人脸识别场景
|
||
combinedMask: 0x9D, // 功能组合掩码
|
||
);
|
||
|
||
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 _navigateToFaceRecognition() async {
|
||
try {
|
||
final cameras = await availableCameras();
|
||
if (!mounted) return;
|
||
|
||
Navigator.push(
|
||
context,
|
||
MaterialPageRoute(
|
||
builder: (context) => FaceRecognitionScreen(
|
||
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 ? _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),
|
||
),
|
||
),
|
||
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. 点击"激活 SDK"按钮进行在线激活\n'
|
||
'2. 激活成功后点击"初始化引擎"按钮\n'
|
||
'3. 点击"开始人脸识别"进行注册/验证\n\n'
|
||
'注册流程: 人脸检测 → 活体检测 → 特征提取 → 存储\n'
|
||
'验证流程: 人脸检测 → 活体检测 → 特征提取 → 比对',
|
||
style: TextStyle(fontSize: 12),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// 人脸识别页面 - 实现完整的注册和验证流程
|
||
class FaceRecognitionScreen extends StatefulWidget {
|
||
final Arc arcPlugin;
|
||
final List<CameraDescription> cameras;
|
||
|
||
const FaceRecognitionScreen({
|
||
super.key,
|
||
required this.arcPlugin,
|
||
required this.cameras,
|
||
});
|
||
|
||
@override
|
||
State<FaceRecognitionScreen> createState() => _FaceRecognitionScreenState();
|
||
}
|
||
|
||
class _FaceRecognitionScreenState extends State<FaceRecognitionScreen> {
|
||
CameraController? _cameraController;
|
||
bool _isProcessing = false;
|
||
String _statusMessage = '正在初始化摄像头...';
|
||
|
||
// 检测结果
|
||
int _faceCount = 0;
|
||
int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人
|
||
|
||
// 注册的人脸特征数据存储 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() {
|
||
super.initState();
|
||
_initCamera();
|
||
}
|
||
|
||
/// 初始化摄像头
|
||
Future<void> _initCamera() async {
|
||
if (widget.cameras.isEmpty) {
|
||
setState(() {
|
||
_statusMessage = '没有可用的摄像头';
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 选择前置摄像头
|
||
CameraDescription? selectedCamera;
|
||
for (var camera in widget.cameras) {
|
||
if (camera.lensDirection == CameraLensDirection.front) {
|
||
selectedCamera = camera;
|
||
break;
|
||
}
|
||
}
|
||
selectedCamera ??= widget.cameras.first;
|
||
|
||
_cameraController = CameraController(
|
||
selectedCamera,
|
||
ResolutionPreset.medium,
|
||
enableAudio: false,
|
||
imageFormatGroup: ImageFormatGroup.nv21,
|
||
);
|
||
|
||
try {
|
||
await _cameraController!.initialize();
|
||
if (mounted) {
|
||
setState(() {
|
||
_statusMessage = '摄像头就绪';
|
||
});
|
||
_startImageStream();
|
||
}
|
||
} catch (e) {
|
||
if (mounted) {
|
||
setState(() {
|
||
_statusMessage = '摄像头初始化失败: $e';
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
/// 启动图像流
|
||
void _startImageStream() {
|
||
_cameraController?.startImageStream(_onImageAvailable);
|
||
}
|
||
|
||
/// 图像帧回调 - 人脸检测
|
||
void _onImageAvailable(CameraImage image) async {
|
||
if (_isProcessing) return;
|
||
_isProcessing = true;
|
||
|
||
try {
|
||
final int width = image.width;
|
||
final int height = image.height;
|
||
final nv21Data = _extractNV21(image, width, height);
|
||
|
||
// 保存当前帧数据用于后续特征提取
|
||
_currentNv21Data = nv21Data;
|
||
_currentWidth = width;
|
||
_currentHeight = height;
|
||
|
||
// 人脸检测 + RGB 活体检测
|
||
final detectResult = await widget.arcPlugin.detectFaces(
|
||
data: nv21Data,
|
||
width: width,
|
||
height: height,
|
||
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) {
|
||
// 保存第一张人脸信息
|
||
_currentFaceInfo = Map<String, dynamic>.from(faceList[0] as Map);
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_faceCount = faceList.length;
|
||
_rgbLivenessResult = rgbLiveness;
|
||
_statusMessage = _rgbLivenessResult == 1 ? '检测到真人' : '检测中...';
|
||
});
|
||
}
|
||
} else {
|
||
_currentFaceInfo = null;
|
||
if (mounted) {
|
||
setState(() {
|
||
_faceCount = 0;
|
||
_rgbLivenessResult = -1;
|
||
_statusMessage = '未检测到人脸';
|
||
});
|
||
}
|
||
}
|
||
}
|
||
} catch (e) {
|
||
debugPrint('人脸检测异常: $e');
|
||
} finally {
|
||
_isProcessing = false;
|
||
}
|
||
}
|
||
|
||
/// 提取 NV21 数据
|
||
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;
|
||
}
|
||
|
||
/// 注册人脸
|
||
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';
|
||
});
|
||
}
|
||
}
|
||
|
||
/// 验证人脸
|
||
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
|
||
void dispose() {
|
||
_cameraController?.stopImageStream();
|
||
_cameraController?.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(
|
||
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: [
|
||
// 摄像头预览
|
||
Expanded(
|
||
child: _cameraController != null &&
|
||
_cameraController!.value.isInitialized
|
||
? Center(
|
||
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(
|
||
_statusMessage,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 16,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
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(
|
||
'人脸: ${_faceCount > 0 ? "已检测" : "未检测"}',
|
||
style: TextStyle(
|
||
color: _faceCount > 0 ? Colors.green : Colors.grey,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(width: 16),
|
||
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(
|
||
'活体: ${_rgbLivenessResult == 1 ? "真人" : _rgbLivenessResult == 0 ? "非真人" : "未知"}',
|
||
style: TextStyle(
|
||
color: _rgbLivenessResult == 1
|
||
? Colors.green
|
||
: _rgbLivenessResult == 0
|
||
? Colors.red
|
||
: Colors.orange,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
|
||
// 验证结果显示
|
||
if (_mode == 1 && _similarity > 0) ...[
|
||
const SizedBox(height: 8),
|
||
const Divider(color: Colors.white24),
|
||
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(
|
||
'相似度: ${(_similarity * 100).toStringAsFixed(1)}%',
|
||
style: TextStyle(
|
||
color: _verified ? Colors.green : Colors.red,
|
||
fontSize: 18,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
_verified ? '验证通过' : '非本人',
|
||
style: TextStyle(
|
||
color: _verified ? Colors.green : Colors.red,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
|
||
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,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
} |