Files
arc/example/lib/main.dart
leon 1c4f01904e fix(example): 修复注册后不显示已注册状态的问题
问题:注册按钮只更新了显示列表,没有将特征标识和ID存入映射表
修复:
- 添加 _currentFaceKeys 保存当前帧的人脸特征标识
- 注册时将特征标识和生成的ID存入 _faceIdMap
- 下次检测时可以从映射表中找到已注册的ID

Quick task: 260330-ll6
2026-03-30 15:35:42 +08:00

757 lines
24 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 = [];
/// 当前帧检测到的人脸特征标识列表(用于注册时关联)
List<String> _currentFaceKeys = [];
/// 随机数生成器
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列表
_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 {
// 未检测到人脸时重置状态
if (mounted) {
setState(() {
_faceCount = 0;
_rgbLivenessResult = -1;
_rgbLivenessStatus = '未检测';
_currentFaceIds = [];
_currentFaceKeys = [];
});
}
}
}
} 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] == '未注册') {
final newId = _generateRandomFaceId();
final faceKey = _currentFaceKeys[i];
// 将特征标识和ID存入映射表
_faceIdMap[faceKey] = newId;
_currentFaceIds[i] = newId;
}
}
});
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('人脸ID已生成'),
backgroundColor: Colors.green,
),
);
},
icon: const Icon(Icons.person_add),
label: const Text('注册人脸生成随机ID'),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blue,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
),
),
),
],
],
),
),
],
),
);
}
}