94 lines
3.4 KiB
Dart
94 lines
3.4 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
||
import 'package:observer/camera/camera_view_model.dart';
|
||
import 'package:observer/detection/detection_result.dart';
|
||
import 'package:observer/reminder/reminder.dart';
|
||
|
||
DetectionResult box(
|
||
String label,
|
||
double score,
|
||
double l,
|
||
double t,
|
||
double r,
|
||
double b, {
|
||
int modelId = -1,
|
||
int classId = 0,
|
||
}) =>
|
||
DetectionResult(
|
||
label: label,
|
||
score: score,
|
||
left: l,
|
||
top: t,
|
||
right: r,
|
||
bottom: b,
|
||
modelId: modelId,
|
||
classId: classId,
|
||
);
|
||
|
||
/// 真实 Reminder 会触碰音频/震动插件通道,测试用记录桩(onDetected 同步记录)。
|
||
/// AudioPlayer 构造里的 _create 平台调用在测试环境抛 MissingPluginException,
|
||
/// 已被插件内部 catch,不影响用例。
|
||
class _RecordingReminder extends Reminder {
|
||
final List<String> alerts = [];
|
||
@override
|
||
void onDetected(String label) => alerts.add(label);
|
||
}
|
||
|
||
void feed(CameraViewModel vm, List<DetectionResult> results) {
|
||
vm.onFramesAnalyzed(results, 90, 1280, 720, const [], const [],
|
||
lastProcessMs: 8);
|
||
}
|
||
|
||
void main() {
|
||
TestWidgetsFlutterBinding.ensureInitialized();
|
||
|
||
group('dedupeVisibleOverlaps', () {
|
||
test('同类别同位置重复框:只保留最高分', () {
|
||
final visible = [
|
||
box('雉鸡', 0.6, 0.3, 0.3, 0.5, 0.5, modelId: 1),
|
||
box('斑鸠', 0.5, 0.3, 0.3, 0.5, 0.5, modelId: 2),
|
||
];
|
||
final out = dedupeVisibleOverlaps(visible);
|
||
expect(out.length, 1);
|
||
expect(out.single.label, '雉鸡');
|
||
expect(out.single.score, 0.6);
|
||
});
|
||
|
||
test('不同类别同位置(目标×疑似):不同语义各自保留', () {
|
||
final visible = [
|
||
box('雉鸡', 0.6, 0.3, 0.3, 0.5, 0.5, classId: 0),
|
||
box('生境', 0.8, 0.2, 0.2, 0.6, 0.6, classId: 1),
|
||
];
|
||
final out = dedupeVisibleOverlaps(visible);
|
||
expect(out.length, 2);
|
||
});
|
||
|
||
test('中心不在对方框内的偏移重叠:两个独立目标不误并', () {
|
||
// 覆盖 ~40% 但中心互不在对方框内(并肩两个目标)
|
||
final visible = [
|
||
box('雉鸡', 0.7, 0.10, 0.10, 0.40, 0.40),
|
||
box('雉鸡', 0.5, 0.28, 0.10, 0.58, 0.40),
|
||
];
|
||
expect(dedupeVisibleOverlaps(visible).length, 2);
|
||
});
|
||
});
|
||
|
||
test('跨标签同目标补挂:交替检出同一目标只一条轨迹/一次提醒', () async {
|
||
final reminder = _RecordingReminder();
|
||
final vm = CameraViewModel(reminder: reminder);
|
||
feed(vm, [box('雉鸡', 0.6, 0.2, 0.2, 0.5, 0.5, modelId: 1)]);
|
||
// 越过 500ms 显示窗(确认轨迹开始显示、提醒窗口开启)
|
||
await Future<void>.delayed(const Duration(milliseconds: 600));
|
||
// 模型 2 检出同一目标:细长小框贴大框边角(中心距 ~0.2 > 跨标签收紧半径
|
||
// 0.072、覆盖 ~0.31 也不到 0.45),但小框中心在大框内 → 补挂进雉鸡
|
||
// 轨迹而不是新建第二条
|
||
feed(vm, [box('斑鸠', 0.55, 0.38, 0.445, 0.62, 0.535, modelId: 2)]);
|
||
await Future<void>.delayed(const Duration(milliseconds: 10));
|
||
|
||
expect(reminder.alerts, ['雉鸡'],
|
||
reason: '补挂进原轨迹:提醒按轨迹原标签只触发一次;若新建第二条轨迹'
|
||
'(斑鸠未达 500ms 显示窗)本帧不会触发提醒');
|
||
expect(vm.state.results.length, 1, reason: '同位置只有一条轨迹可见');
|
||
expect(vm.state.results.single.label, '斑鸠', reason: '框内容跟随后到的检测');
|
||
});
|
||
}
|