Files
arc/example/lib/main.dart
leon 86e268ae1c fix: 修复特征提取失败(错误码81929)
问题: extractFaceFeature 返回错误码 81929 (MERR_FSDK_FACEFEATURE_FACEDATA)

根本原因: detectFaces 返回的人脸信息缺少 faceData 字段,
而虹软 SDK 的 extractFaceFeature 必须要有这个字段才能提取特征

修复:
- FaceEngineManager.convertFaceInfoToList: 添加返回 faceData
- ArcPlugin.handleExtractFaceFeature: 接收并传递 faceData 参数
- Dart API: extractFaceFeature 添加 faceData 参数
- example: 传递 faceData 到特征提取调用

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-30 16:58:12 +08:00

895 lines
27 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: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;
}
// 检查 faceData 是否存在
final faceData = _currentFaceInfo!['faceData'] as Uint8List?;
if (faceData == null) {
_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!['faceOrientation'] as int? ?? 0,
faceId: _currentFaceInfo!['faceId'] as int? ?? -1,
faceData: faceData, // 关键:传递 faceData这是虹软 SDK 特征提取必需的数据
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 errorCode = result?['errorCode'] ?? -1;
final errorMsg = result?['message'] ?? '未知错误';
debugPrint('特征提取失败: errorCode=$errorCode, message=$errorMsg');
setState(() {
_statusMessage = '特征提取失败: [$errorCode] $errorMsg';
});
}
} catch (e) {
setState(() {
_statusMessage = '注册异常: $e';
});
}
}
/// 验证人脸
Future<void> _verifyFace() async {
if (_currentFaceInfo == null || _currentNv21Data == null) {
_showMessage('未检测到人脸');
return;
}
if (_rgbLivenessResult != 1) {
_showMessage('请确保是真人');
return;
}
// 检查 faceData 是否存在
final faceData = _currentFaceInfo!['faceData'] as Uint8List?;
if (faceData == null) {
_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!['faceOrientation'] as int? ?? 0,
faceId: _currentFaceInfo!['faceId'] as int? ?? -1,
faceData: faceData, // 关键:传递 faceData这是虹软 SDK 特征提取必需的数据
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,
),
),
],
),
),
],
),
);
}
}