187 lines
5.4 KiB
Dart
187 lines
5.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
|
|
import 'package:pj_supermarket_cloud/common/models/plugs/face_recognition_session_models.dart';
|
|
import 'package:pj_supermarket_cloud/common/style/xfj_design_tokens.dart';
|
|
import 'package:pj_supermarket_cloud/common/widgets/face/face_recognition_preview_layer.dart';
|
|
|
|
/// 验证仅识别最大脸时的面积排序、可见文案和语义颜色。
|
|
void main() {
|
|
const smallFace = FaceRecognitionFaceBox(
|
|
left: 0.05,
|
|
top: 0.10,
|
|
right: 0.25,
|
|
bottom: 0.30,
|
|
recognizeStatus: 'waiting',
|
|
livenessStatus: 'unknown',
|
|
);
|
|
const largestFace = FaceRecognitionFaceBox(
|
|
left: 0.30,
|
|
top: 0.20,
|
|
right: 0.70,
|
|
bottom: 0.70,
|
|
recognizeStatus: 'comparing',
|
|
livenessStatus: 'alive',
|
|
similarity: 0.86,
|
|
);
|
|
const mediumFace = FaceRecognitionFaceBox(
|
|
left: 0.72,
|
|
top: 0.15,
|
|
right: 0.97,
|
|
bottom: 0.55,
|
|
recognizeStatus: 'failed',
|
|
livenessStatus: 'notAlive',
|
|
similarity: 0.42,
|
|
);
|
|
|
|
test('会话事件解析原生最大脸配置并保留全部人脸框', () {
|
|
final event = FaceRecognitionSessionEvent.fromMap({
|
|
'sessionId': 'face-session-1',
|
|
'scene': 'cashier',
|
|
'state': 'detecting',
|
|
'revision': 3,
|
|
'keepMaxFace': true,
|
|
'faceBoxes': [
|
|
{
|
|
'left': 0.05,
|
|
'top': 0.10,
|
|
'right': 0.25,
|
|
'bottom': 0.30,
|
|
'recognizeStatus': 'waiting',
|
|
'livenessStatus': 'unknown',
|
|
},
|
|
{
|
|
'left': 0.30,
|
|
'top': 0.20,
|
|
'right': 0.70,
|
|
'bottom': 0.70,
|
|
'recognizeStatus': 'comparing',
|
|
'livenessStatus': 'alive',
|
|
},
|
|
],
|
|
});
|
|
|
|
expect(event.keepMaxFace, isTrue);
|
|
expect(event.eventSequence, 0);
|
|
expect(event.faceBoxes, hasLength(2));
|
|
});
|
|
|
|
test('原生事件可解析进程级单调序号', () {
|
|
final event = FaceRecognitionSessionEvent.fromMap({
|
|
'sessionId': 'face-session-2',
|
|
'scene': 'reservation',
|
|
'state': 'detecting',
|
|
'revision': 3,
|
|
'eventSequence': 17,
|
|
'faceBoxes': const [],
|
|
});
|
|
|
|
expect(event.eventSequence, 17);
|
|
});
|
|
|
|
test('归一化面积计算异常坐标时不会产生负占比', () {
|
|
const invalidFace = FaceRecognitionFaceBox(
|
|
left: 0.8,
|
|
top: 0.7,
|
|
right: 0.2,
|
|
bottom: 0.1,
|
|
recognizeStatus: 'waiting',
|
|
livenessStatus: 'unknown',
|
|
);
|
|
|
|
expect(smallFace.normalizedArea, closeTo(0.04, 0.000001));
|
|
expect(invalidFace.normalizedArea, 0);
|
|
});
|
|
|
|
test('最大脸模式按面积选中绿色目标,其余使用红色文字标识', () {
|
|
final painter = _painter(
|
|
boxes: const [smallFace, largestFace, mediumFace],
|
|
keepMaxFace: true,
|
|
);
|
|
|
|
expect(painter.primaryFaceIndex, 1);
|
|
expect(painter.colorFor(1), XfjDesignTokens.success);
|
|
expect(painter.colorFor(0), XfjDesignTokens.danger);
|
|
expect(painter.colorFor(2), XfjDesignTokens.danger);
|
|
expect(painter.labelFor(1), contains('识别目标 · 占画面 20.0%'));
|
|
expect(painter.labelFor(1), contains('相似度 86.0%'));
|
|
expect(painter.labelFor(0), contains('非识别目标 · 占画面 4.0%'));
|
|
expect(painter.labelFor(0), contains('不参与识别'));
|
|
});
|
|
|
|
test('旋转和镜像不改变最大脸面积判断', () {
|
|
final boxes = const [smallFace, largestFace, mediumFace];
|
|
final rotatedPainter = _painter(
|
|
boxes: boxes,
|
|
keepMaxFace: true,
|
|
rotation: 90,
|
|
mirror: true,
|
|
);
|
|
final normalPainter = _painter(
|
|
boxes: boxes,
|
|
keepMaxFace: true,
|
|
);
|
|
|
|
expect(rotatedPainter.primaryFaceIndex, normalPainter.primaryFaceIndex);
|
|
});
|
|
|
|
test('关闭最大脸模式后继续沿用识别结果颜色且不显示面积标签', () {
|
|
final painter = _painter(
|
|
boxes: const [smallFace, largestFace, mediumFace],
|
|
keepMaxFace: false,
|
|
);
|
|
|
|
expect(painter.colorFor(0), XfjDesignTokens.primary);
|
|
expect(painter.colorFor(1), XfjDesignTokens.success);
|
|
expect(painter.colorFor(2), XfjDesignTokens.danger);
|
|
expect(painter.labelFor(1), isNot(contains('占画面')));
|
|
expect(painter.labelFor(1), contains('相似度 86.0%'));
|
|
});
|
|
|
|
test('低相似度和尚未比对的人脸仍全部进入绘制器', () {
|
|
const lowSimilarityFace = FaceRecognitionFaceBox(
|
|
left: 0.08,
|
|
top: 0.12,
|
|
right: 0.34,
|
|
bottom: 0.48,
|
|
recognizeStatus: 'failed',
|
|
livenessStatus: 'unknown',
|
|
similarity: 0.12,
|
|
);
|
|
const waitingFace = FaceRecognitionFaceBox(
|
|
left: 0.55,
|
|
top: 0.10,
|
|
right: 0.88,
|
|
bottom: 0.52,
|
|
recognizeStatus: 'waiting',
|
|
livenessStatus: 'unknown',
|
|
);
|
|
final painter = _painter(
|
|
boxes: const [lowSimilarityFace, waitingFace],
|
|
keepMaxFace: true,
|
|
);
|
|
|
|
expect(painter.boxes, hasLength(2));
|
|
expect(painter.labelFor(0), contains('非识别目标'));
|
|
expect(painter.labelFor(0), contains('相似度 12.0%'));
|
|
expect(painter.labelFor(1), contains('识别目标'));
|
|
expect(painter.labelFor(1), contains('检测中'));
|
|
});
|
|
}
|
|
|
|
/// 创建只用于判断面积、颜色和标签的绘制器,不实际申请摄像头 Texture。
|
|
FaceRecognitionBoxPainter _painter({
|
|
required List<FaceRecognitionFaceBox> boxes,
|
|
required bool keepMaxFace,
|
|
int rotation = 0,
|
|
bool mirror = false,
|
|
}) {
|
|
return FaceRecognitionBoxPainter(
|
|
boxes: boxes,
|
|
rotation: rotation,
|
|
mirror: mirror,
|
|
livenessEnabled: true,
|
|
keepMaxFace: keepMaxFace,
|
|
similarityThreshold: 0.8,
|
|
);
|
|
}
|