feat(example): 添加人脸注册按钮和ID显示功能

- 添加注册按钮,点击生成随机人脸ID(格式:F + 8位数字)
- 实时显示检测到的人脸对应的ID
- 使用彩色标签区分已注册(绿色)和未注册(橙色)状态

Quick task: 260330-ld1
This commit is contained in:
2026-03-30 15:28:31 +08:00
parent 3c2bb02e33
commit 184c3a9f16
9 changed files with 1099 additions and 0 deletions

748
example/lib/main.dart Normal file
View File

@@ -0,0 +1,748 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:arc/arc.dart';
import 'package:camera/camera.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: HomePage(),
debugShowCheckedModeBanner: false,
);
}
}
class HomePage extends StatefulWidget {
const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
String _platformVersion = 'Unknown';
String _activeResult = '未激活';
String _initResult = '未初始化';
bool _isActivated = false;
bool _isInitialized = false;
final _arcPlugin = Arc();
/// 虹软 SDK 配置信息(请替换为您自己的 AppId、SdkKey 和 ActiveKey
final String _appId = '4nPFuS2TYAQh9werHL2qbKjtTH9nnoixk7G6yqSUyjVH';
final String _sdkKey = 'aWMTT3coxNQFETg9f3BGHCiBznAApXmcHFF3J5yQbsZ';
final String _activeKey = 'NEAT7AU4KLCEK682';
@override
void initState() {
super.initState();
initPlatformState();
}
/// 获取平台版本
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion =
await _arcPlugin.getPlatformVersion() ?? 'Unknown platform version';
} on PlatformException {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) return;
setState(() {
_platformVersion = platformVersion;
});
}
/// 激活 SDK
Future<void> _activeSDK() async {
setState(() {
_activeResult = '正在激活...';
});
try {
final result = await _arcPlugin.activeOnline(
appId: _appId,
sdkKey: _sdkKey,
activeKey: _activeKey,
);
if (result != null) {
final success = result['success'] as bool? ?? false;
final errorCode = result['errorCode'] as int? ?? -1;
final message = result['message'] as String? ?? '未知错误';
setState(() {
_isActivated = success;
_activeResult = success
? '激活成功!\n错误码: $errorCode\n$message'
: '激活失败!\n错误码: $errorCode\n$message';
});
}
} on PlatformException catch (e) {
setState(() {
_activeResult = '激活异常: ${e.message}';
});
}
}
/// 初始化引擎
Future<void> _initEngine() async {
if (!_isActivated) {
setState(() {
_initResult = '请先激活 SDK';
});
return;
}
setState(() {
_initResult = '正在初始化...';
});
try {
// 功能掩码组合:
// ASF_FACE_DETECT = 0x00000001 (人脸检测)
// ASF_FACE_RECOGNITION = 0x00000004 (人脸识别)
// ASF_AGE = 0x00000008 (年龄检测)
// ASF_GENDER = 0x00000010 (性别检测)
// ASF_LIVENESS = 0x00000080 (RGB 活体检测)
// 组合掩码 = 0x01 | 0x04 | 0x08 | 0x10 | 0x80 = 0x9D
final result = await _arcPlugin.init(
detectMode: 0, // 0=VIDEO 模式, 1=IMAGE 模式
orient: 0, // 检测角度优先级
maxFaceNum: 10, // 最大检测人脸数
combinedMask: 0x9D, // 功能组合掩码(人脸检测+识别+年龄+性别+RGB活体
);
if (result != null) {
final success = result['success'] as bool? ?? false;
final errorCode = result['errorCode'] as int? ?? -1;
final message = result['message'] as String? ?? '未知错误';
setState(() {
_isInitialized = success;
_initResult = success
? '初始化成功!\n错误码: $errorCode\n$message'
: '初始化失败!\n错误码: $errorCode\n$message';
});
}
} on PlatformException catch (e) {
setState(() {
_initResult = '初始化异常: ${e.message}';
});
}
}
/// 跳转到摄像头预览页面
void _navigateToCameraPreview() async {
try {
final cameras = await availableCameras();
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => CameraPreviewScreen(
arcPlugin: _arcPlugin,
cameras: cameras,
),
),
);
} catch (e) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('无法打开摄像头: $e')),
);
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('虹软人脸识别 SDK 示例'),
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// 平台信息
Card(
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
'运行平台: $_platformVersion',
style: const TextStyle(fontSize: 14),
),
),
),
const SizedBox(height: 16),
// 激活 SDK 按钮
ElevatedButton(
onPressed: _isActivated ? null : _activeSDK,
style: ElevatedButton.styleFrom(
backgroundColor: _isActivated ? Colors.grey : Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text(
_isActivated ? '已激活' : '激活 SDK',
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 8),
Card(
color: _isActivated ? Colors.green[50] : Colors.red[50],
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_activeResult,
style: TextStyle(
fontSize: 13,
color: _isActivated ? Colors.green[800] : Colors.red[800],
),
),
),
),
const SizedBox(height: 24),
// 初始化引擎按钮
ElevatedButton(
onPressed: _isInitialized ? null : _initEngine,
style: ElevatedButton.styleFrom(
backgroundColor: _isInitialized ? Colors.grey : Colors.green,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: Text(
_isInitialized ? '已初始化' : '初始化引擎',
style: const TextStyle(fontSize: 16),
),
),
const SizedBox(height: 8),
Card(
color: _isInitialized ? Colors.green[50] : Colors.red[50],
child: Padding(
padding: const EdgeInsets.all(12.0),
child: Text(
_initResult,
style: TextStyle(
fontSize: 13,
color: _isInitialized ? Colors.green[800] : Colors.red[800],
),
),
),
),
const SizedBox(height: 24),
// 摄像头预览按钮
ElevatedButton(
onPressed: _isInitialized ? _navigateToCameraPreview : null,
style: ElevatedButton.styleFrom(
backgroundColor: _isInitialized ? Colors.purple : Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text(
'打开摄像头预览',
style: TextStyle(fontSize: 16),
),
),
const SizedBox(height: 24),
// 使用说明
const Card(
child: Padding(
padding: EdgeInsets.all(12.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'使用说明:',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
SizedBox(height: 8),
Text(
'1. 请先替换代码中的 _appId、_sdkKey 和 _activeKey 为您的真实密钥\n'
'2. 点击"激活 SDK"按钮进行在线激活\n'
'3. 激活成功后点击"初始化引擎"按钮\n'
'4. 初始化成功后点击"打开摄像头预览"进行人脸检测',
style: TextStyle(fontSize: 12),
),
],
),
),
),
],
),
),
);
}
}
/// 摄像头预览页面,用于实时人脸检测和 RGB 活体检测
class CameraPreviewScreen extends StatefulWidget {
final Arc arcPlugin;
final List<CameraDescription> cameras;
const CameraPreviewScreen({
super.key,
required this.arcPlugin,
required this.cameras,
});
@override
State<CameraPreviewScreen> createState() => _CameraPreviewScreenState();
}
class _CameraPreviewScreenState extends State<CameraPreviewScreen> {
CameraController? _cameraController;
bool _isDetecting = false;
int _faceCount = 0;
String _lastDetectionTime = '';
String _statusMessage = '正在初始化摄像头...';
// RGB 活体检测相关状态
int _rgbLivenessResult = -1; // -1=未检测, 0=非真人, 1=真人
String _rgbLivenessStatus = '未检测';
// 人脸ID管理
/// 存储人脸特征到ID的映射使用人脸矩形区域的hashCode作为特征标识
final Map<String, String> _faceIdMap = {};
/// 当前帧检测到的人脸ID列表
List<String> _currentFaceIds = [];
/// 随机数生成器
final Random _random = Random();
@override
void initState() {
super.initState();
_initCamera();
}
/// 初始化摄像头
Future<void> _initCamera() async {
if (widget.cameras.isEmpty) {
debugPrint('没有可用的摄像头');
setState(() {
_statusMessage = '没有可用的摄像头';
});
return;
}
// 打印所有摄像头信息
debugPrint('========== 摄像头列表 ==========');
for (int i = 0; i < widget.cameras.length; i++) {
final camera = widget.cameras[i];
debugPrint('摄像头 $i:');
debugPrint(' - name: ${camera.name}');
debugPrint(' - lensDirection: ${camera.lensDirection}');
debugPrint(' - sensorOrientation: ${camera.sensorOrientation}');
}
debugPrint('================================');
// 选择后置摄像头作为默认摄像头
CameraDescription? selectedCamera;
for (var camera in widget.cameras) {
if (camera.lensDirection == CameraLensDirection.back) {
selectedCamera = camera;
break;
}
}
selectedCamera ??= widget.cameras.first;
debugPrint('选择摄像头: ${selectedCamera.name}');
_cameraController = CameraController(
selectedCamera,
ResolutionPreset.medium,
enableAudio: false,
imageFormatGroup: ImageFormatGroup.nv21,
);
try {
await _cameraController!.initialize();
debugPrint('摄像头初始化成功');
final size = _cameraController!.value.previewSize;
debugPrint('预览尺寸: ${size?.width}x${size?.height}');
if (mounted) {
setState(() {
_statusMessage = '摄像头: ${selectedCamera?.name ?? "未知"}';
});
_startImageStream();
}
} catch (e) {
debugPrint('摄像头初始化失败: $e');
if (mounted) {
setState(() {
_statusMessage = '摄像头初始化失败: $e';
});
}
}
}
/// 启动图像流
void _startImageStream() {
_cameraController?.startImageStream(_onImageAvailable);
debugPrint('已启动图像流');
}
/// 图像帧回调,进行人脸检测
void _onImageAvailable(CameraImage image) async {
if (_isDetecting) return;
_isDetecting = true;
try {
final int width = image.width;
final int height = image.height;
// 将图像数据转换为连续的 NV21 格式
final nv21Data = _extractNV21(image, width, height);
// 人脸检测(同时进行 RGB 活体检测)
final detectResult = await widget.arcPlugin.detectFaces(
data: nv21Data,
width: width,
height: height,
format: 2050, // NV21 格式
);
if (detectResult != null) {
final success = detectResult['success'] == true;
final faceList = detectResult['faceList'] as List? ?? [];
if (success && faceList.isNotEmpty) {
final now = DateTime.now();
final timeStr = '${now.hour.toString().padLeft(2, '0')}:'
'${now.minute.toString().padLeft(2, '0')}:'
'${now.second.toString().padLeft(2, '0')}.'
'${now.millisecond.toString().padLeft(3, '0')}';
debugPrint('========== 人脸检测 [$timeStr] ==========');
debugPrint('检测到 ${faceList.length} 张人脸');
for (int i = 0; i < faceList.length; i++) {
final face = faceList[i] as Map;
debugPrint('人脸 ${i + 1}: (${face['rectLeft']}, ${face['rectTop']}, ${face['rectRight']}, ${face['rectBottom']})');
}
// 获取 RGB 活体检测结果
final rgbLiveness = detectResult['rgbLiveness'] as int? ?? -1;
final rgbIsAlive = rgbLiveness == 1;
if (mounted) {
setState(() {
_faceCount = faceList.length;
_lastDetectionTime = timeStr;
_rgbLivenessResult = rgbLiveness;
_rgbLivenessStatus = rgbIsAlive ? '真人' : (rgbLiveness == 0 ? '非真人' : '未知');
// 更新当前人脸ID列表
_currentFaceIds = faceList.map((face) {
final faceKey = _getFaceKey(face as Map);
return _faceIdMap[faceKey] ?? '未注册';
}).toList();
});
}
debugPrint('RGB 活体: liveness=$rgbLiveness, isAlive=$rgbIsAlive');
debugPrint('========================================');
} else {
// 未检测到人脸时重置状态
if (mounted) {
setState(() {
_faceCount = 0;
_rgbLivenessResult = -1;
_rgbLivenessStatus = '未检测';
_currentFaceIds = [];
});
}
}
}
} catch (e, stackTrace) {
debugPrint('人脸检测异常: $e');
debugPrint('堆栈: $stackTrace');
} finally {
_isDetecting = false;
}
}
/// 提取 NV21 数据(处理 stride 对齐)
Uint8List _extractNV21(CameraImage image, int width, int height) {
final ySize = width * height;
final nv21Size = ySize * 3 ~/ 2;
if (image.planes.length == 1) {
final plane = image.planes[0];
final bytesPerRow = plane.bytesPerRow;
if (bytesPerRow == width) {
return plane.bytes;
}
final nv21 = Uint8List(nv21Size);
for (int row = 0; row < height; row++) {
final srcOffset = row * bytesPerRow;
final dstOffset = row * width;
nv21.setRange(dstOffset, dstOffset + width, plane.bytes, srcOffset);
}
final uvHeight = height ~/ 2;
for (int row = 0; row < uvHeight; row++) {
final srcOffset = (height + row) * bytesPerRow;
final dstOffset = ySize + row * width;
nv21.setRange(dstOffset, dstOffset + width, plane.bytes, srcOffset);
}
return nv21;
}
final yPlane = image.planes[0];
final yStride = yPlane.bytesPerRow;
final nv21 = Uint8List(nv21Size);
for (int row = 0; row < height; row++) {
final srcOffset = row * yStride;
final dstOffset = row * width;
nv21.setRange(dstOffset, dstOffset + width, yPlane.bytes, srcOffset);
}
if (image.planes.length >= 2) {
final uvPlane = image.planes[1];
final uvStride = uvPlane.bytesPerRow;
final uvHeight = height ~/ 2;
for (int row = 0; row < uvHeight; row++) {
final srcOffset = row * uvStride;
final dstOffset = ySize + row * width;
nv21.setRange(dstOffset, dstOffset + width, uvPlane.bytes, srcOffset);
}
}
return nv21;
}
/// 生成随机人脸ID
/// 生成格式: F + 8位随机数字
String _generateRandomFaceId() {
final id = _random.nextInt(100000000).toString().padLeft(8, '0');
return 'F$id';
}
/// 根据人脸矩形区域生成特征标识
String _getFaceKey(Map face) {
// 使用人脸矩形区域的位置和大小作为特征标识
final rectLeft = face['rectLeft'] as int? ?? 0;
final rectTop = face['rectTop'] as int? ?? 0;
final rectRight = face['rectRight'] as int? ?? 0;
final rectBottom = face['rectBottom'] as int? ?? 0;
// 使用位置和大小计算一个简单的特征值(允许一定容差)
final centerX = (rectLeft + rectRight) ~/ 2;
final centerY = (rectTop + rectBottom) ~/ 2;
// 按照区块划分每50像素为一个区块
final blockX = centerX ~/ 50;
final blockY = centerY ~/ 50;
return '$blockX-$blockY';
}
@override
void dispose() {
_cameraController?.stopImageStream();
_cameraController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('人脸检测预览'),
backgroundColor: Colors.purple,
foregroundColor: Colors.white,
),
body: Column(
children: [
// 摄像头预览
Expanded(
child: _cameraController != null &&
_cameraController!.value.isInitialized
? Center(
child: RotatedBox(
quarterTurns: -1,
child: CameraPreview(_cameraController!),
),
)
: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const CircularProgressIndicator(),
const SizedBox(height: 16),
Text(_statusMessage),
],
),
),
),
// 检测信息显示
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
color: Colors.black87,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'检测到人脸数: $_faceCount',
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Text(
'最后检测时间: $_lastDetectionTime',
style: const TextStyle(
color: Colors.white70,
fontSize: 14,
),
),
const SizedBox(height: 8),
// RGB 活体检测状态
Row(
children: [
Icon(
_rgbLivenessResult == 1
? Icons.check_circle
: _rgbLivenessResult == 0
? Icons.cancel
: Icons.help_outline,
color: _rgbLivenessResult == 1
? Colors.green
: _rgbLivenessResult == 0
? Colors.red
: Colors.orange,
size: 20,
),
const SizedBox(width: 8),
Text(
'RGB活体: $_rgbLivenessStatus',
style: TextStyle(
color: _rgbLivenessResult == 1
? Colors.green
: _rgbLivenessResult == 0
? Colors.red
: Colors.white70,
fontSize: 14,
),
),
],
),
// 人脸ID显示
if (_currentFaceIds.isNotEmpty) ...[
const SizedBox(height: 8),
const Divider(color: Colors.white24),
const SizedBox(height: 8),
Text(
'人脸ID:',
style: const TextStyle(
color: Colors.white,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 4),
Wrap(
spacing: 8,
runSpacing: 4,
children: _currentFaceIds.asMap().entries.map((entry) {
final index = entry.key;
final id = entry.value;
final isRegistered = id != '未注册';
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: isRegistered ? Colors.green.withValues(alpha: 0.3) : Colors.orange.withValues(alpha: 0.3),
borderRadius: BorderRadius.circular(4),
border: Border.all(
color: isRegistered ? Colors.green : Colors.orange,
),
),
child: Text(
'人脸${index + 1}: $id',
style: TextStyle(
color: isRegistered ? Colors.greenAccent : Colors.orangeAccent,
fontSize: 12,
),
),
);
}).toList(),
),
],
// 注册按钮
if (_faceCount > 0) ...[
const SizedBox(height: 12),
SizedBox(
width: double.infinity,
child: ElevatedButton.icon(
onPressed: () {
// 为未注册的人脸生成随机ID
setState(() {
for (int i = 0; i < _faceCount; i++) {
if (_currentFaceIds[i] == '未注册') {
_currentFaceIds[i] = _generateRandomFaceId();
}
}
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('人脸ID已生成'),
backgroundColor: Colors.green,
),
);
},
icon: const Icon(Icons.person_add),
label: const Text('注册人脸生成随机ID'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
],
),
),
],
),
);
}
}