1
This commit is contained in:
@@ -4,6 +4,7 @@ import 'package:flutter/foundation.dart';
|
||||
|
||||
import '../detection/detection_result.dart';
|
||||
import '../detection/motion_aggregator.dart';
|
||||
import '../detection/nms.dart';
|
||||
import '../reminder/reminder.dart';
|
||||
|
||||
@immutable
|
||||
@@ -21,6 +22,10 @@ class CameraUiState {
|
||||
final int debugLastMs;
|
||||
final String debugYuv;
|
||||
|
||||
/// 近 [CameraViewModel.latencyWindow] 帧推理耗时滚动平均(毫秒;无样本为 null)。
|
||||
/// 给 C 端用户看的设备识别延迟。
|
||||
final double? latencyAvgMs;
|
||||
|
||||
const CameraUiState({
|
||||
this.modelReady = false,
|
||||
this.results = const [],
|
||||
@@ -34,6 +39,7 @@ class CameraUiState {
|
||||
this.framesReceived = 0,
|
||||
this.debugLastMs = 0,
|
||||
this.debugYuv = '',
|
||||
this.latencyAvgMs,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -43,6 +49,9 @@ class CameraUiState {
|
||||
/// 高于 0.35 视为强证据。
|
||||
/// - 低于 0.35 的框:需要多帧稳定([confirmFrames] 帧)或 活动证据
|
||||
/// (运动区域/背景新出现区域重叠)才确认显示。
|
||||
/// - 同位置去重(2026-09-03):多模型对同一目标交替检出时各轨迹都会在
|
||||
/// 忘记窗内持续显示 → 轨迹层跨标签同目标补挂 + 显示层
|
||||
/// [dedupeVisibleOverlaps] 同类别强重叠只保留高置信度框。
|
||||
class CameraViewModel extends ChangeNotifier {
|
||||
static const int maxTracks = 30;
|
||||
static const double motionBoost = 0.15;
|
||||
@@ -52,6 +61,10 @@ class CameraViewModel extends ChangeNotifier {
|
||||
static const int displayAgeMs = 500;
|
||||
static const int forgetMs = 2000;
|
||||
|
||||
/// 延迟滚动平均窗口(帧数):单帧抖动大,取近期均值给用户展示
|
||||
static const int latencyWindow = 30;
|
||||
final List<int> _procWindow = [];
|
||||
|
||||
/// 推理后台 isolate 是否就绪(由相机页创建 worker 后设置)
|
||||
bool modelReady = false;
|
||||
|
||||
@@ -68,6 +81,8 @@ class CameraViewModel extends ChangeNotifier {
|
||||
void setModelReady(bool ready) {
|
||||
if (modelReady == ready) return;
|
||||
modelReady = ready;
|
||||
// 换 worker / 停识别:旧窗口样本作废,从零起算
|
||||
_procWindow.clear();
|
||||
_state = CameraUiState(modelReady: ready);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -122,9 +137,18 @@ class CameraViewModel extends ChangeNotifier {
|
||||
for (final r in results) {
|
||||
if (r.score > highest) highest = r.score;
|
||||
}
|
||||
if (lastProcessMs > 0) {
|
||||
_procWindow.add(lastProcessMs);
|
||||
if (_procWindow.length > latencyWindow) {
|
||||
_procWindow.removeAt(0);
|
||||
}
|
||||
}
|
||||
final latencyAvg = _procWindow.isEmpty
|
||||
? null
|
||||
: _procWindow.reduce((a, b) => a + b) / _procWindow.length;
|
||||
_state = CameraUiState(
|
||||
modelReady: modelReady,
|
||||
results: visible,
|
||||
results: dedupeVisibleOverlaps(visible),
|
||||
rotation: rotation,
|
||||
imageWidthPx: imageWidthPx,
|
||||
imageHeightPx: imageHeightPx,
|
||||
@@ -135,6 +159,7 @@ class CameraViewModel extends ChangeNotifier {
|
||||
framesReceived: framesReceived,
|
||||
debugLastMs: lastProcessMs,
|
||||
debugYuv: yuvDiag.isNotEmpty ? yuvDiag : _state.debugYuv,
|
||||
latencyAvgMs: latencyAvg,
|
||||
);
|
||||
notifyListeners();
|
||||
}
|
||||
@@ -161,6 +186,24 @@ class CameraViewModel extends ChangeNotifier {
|
||||
best = t;
|
||||
}
|
||||
}
|
||||
// 跨标签同目标补挂(2026-09-03 用户实测修订:重叠位置只保留高置信度框):
|
||||
// 中心距超过跨标签收紧半径、但几何强重叠指向同一位置(不同模型对同一
|
||||
// 目标的框偏移/紧致度不同)的检测并入既有轨迹,防止同目标两条轨迹并存
|
||||
// → 同位置双名常驻/重复提醒。疑似↔目标(不同类别预警)不并入。
|
||||
if (best == null) {
|
||||
_Track? adopt;
|
||||
var adoptD = double.infinity;
|
||||
for (final t in _tracks.values) {
|
||||
if (matched.contains(t.id)) continue;
|
||||
if (t.isSuspect != r.isSuspect || !sameTarget(t.result, r)) continue;
|
||||
final d = _centerDist(t.result, r);
|
||||
if (d < adoptD) {
|
||||
adoptD = d;
|
||||
adopt = t;
|
||||
}
|
||||
}
|
||||
best = adopt;
|
||||
}
|
||||
if (best != null) {
|
||||
matched.add(best.id);
|
||||
best.update(r, now);
|
||||
@@ -234,8 +277,30 @@ class _Track {
|
||||
: lastSeenMs = firstSeenMs,
|
||||
label = result.label;
|
||||
|
||||
/// 当前框是否疑似类别(随关联的最新检测更新——轨迹框在跨标签补挂后
|
||||
/// 会换成其他标签的框;轨迹 label 仅创建时记账)
|
||||
bool get isSuspect => result.isSuspect;
|
||||
|
||||
void update(DetectionResult r, int now) {
|
||||
lastSeenMs = now;
|
||||
result = r;
|
||||
}
|
||||
}
|
||||
|
||||
/// 同屏抑制(2026-09-03 用户实测修订:同一位置/重叠位置出现同/异模型检出的
|
||||
/// 物种只保留高置信度者)。根因:多个模型对同一目标**交替**检出时,每帧合并
|
||||
/// 层只压掉当帧低分框,但各自轨迹都落在 2s 忘记窗内持续显示 → 同位置双名
|
||||
/// 常驻。显示层兜底:可见框内同类别(都目标/都疑似)、且 [sameTarget] 强
|
||||
/// 重叠指向同一位置的框每帧只保留最高分者。异类别(目标×疑似生境预警)是
|
||||
/// 两种语义不同的框,同位置也各自保留。
|
||||
List<DetectionResult> dedupeVisibleOverlaps(List<DetectionResult> visible) {
|
||||
if (visible.length <= 1) return visible;
|
||||
final sorted = [...visible]..sort((a, b) => b.score.compareTo(a.score));
|
||||
final kept = <DetectionResult>[];
|
||||
for (final r in sorted) {
|
||||
if (!kept.any((k) => k.isSuspect == r.isSuspect && sameTarget(k, r))) {
|
||||
kept.add(r);
|
||||
}
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user