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 alerts = []; @override void onDetected(String label) => alerts.add(label); } void feed(CameraViewModel vm, List 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.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.delayed(const Duration(milliseconds: 10)); expect(reminder.alerts, ['雉鸡'], reason: '补挂进原轨迹:提醒按轨迹原标签只触发一次;若新建第二条轨迹' '(斑鸠未达 500ms 显示窗)本帧不会触发提醒'); expect(vm.state.results.length, 1, reason: '同位置只有一条轨迹可见'); expect(vm.state.results.single.label, '斑鸠', reason: '框内容跟随后到的检测'); }); }