Files
observer/flutter_app/lib/detection/background_model.dart
T
2026-09-03 10:01:42 +08:00

95 lines
3.0 KiB
Dart

import 'dart:math' as math;
import 'dart:typed_data';
import 'detection_result.dart';
import 'motion_aggregator.dart';
/// 静态场景背景建模:运行均值 + 方差,帧差高于自适应阈值的像素记为"新出现",
/// 分块聚合为新颖区域(novelty)。
///
/// 固定机位下,常驻物体(键盘/石头/文字)永远属于背景、不产生新颖区域;
/// 走进画面的目标(动物移动/新出现)才会触发。比相邻帧差分更强的证据:
/// 风吹草动是持续的背景更新,不会长期标记为新颖。
class BackgroundModel {
final int maxWidth;
final int maxHeight;
static const double learnRate = 0.05;
static const double kSigma = 2.5;
static const int minDiff = 15;
static const int minPixels = 12;
Float32List? _mean;
Float32List? _var;
int _tw = 0;
BackgroundModel({this.maxWidth = 128, this.maxHeight = 128});
/// 后台 isolate 用原始数据接口(与 MotionDetector 同源:直接取 planes[0])。
List<MotionRegion> updateRaw(
Uint8List yPlane, int yStride, int width, int height) {
final scale =
maxWidth / width < maxHeight / height ? maxWidth / width : maxHeight / height;
final tw = (width * scale).toInt().clamp(1, maxWidth);
final th = (height * scale).toInt().clamp(1, maxHeight);
if (tw == 0 || th == 0) return const [];
final gray = Float32List(tw * th);
for (var oy = 0; oy < th; oy++) {
final sy = (oy / scale).toInt().clamp(0, height - 1);
final idx = oy * tw;
for (var ox = 0; ox < tw; ox++) {
final sx = (ox / scale).toInt().clamp(0, width - 1);
gray[idx + ox] = yPlane[sy * yStride + sx].toDouble();
}
}
return update(gray, tw, th);
}
List<MotionRegion> update(Float32List gray, int tw, int th) {
final n = gray.length;
final mean = _mean;
final variance = _var;
if (mean == null || variance == null || mean.length != n || _tw != tw) {
_mean = Float32List.fromList(gray);
_var = Float32List(n);
_tw = tw;
return const [];
}
final diff = Uint8List(n);
var fgCount = 0;
for (var i = 0; i < n; i++) {
final g = gray[i];
final m = mean[i];
final d = (g - m).abs();
if (d > kSigma * math.sqrt(variance[i]) + minDiff) {
diff[i] = 1;
fgCount++;
// 前景像素不更新背景,避免把移动目标吸收进背景
} else {
// 静态像素缓慢吸收进背景,适应光照漂移
final nm = m + learnRate * (g - m);
mean[i] = nm;
variance[i] =
variance[i] + learnRate * ((g - nm) * (g - nm) - variance[i]);
}
}
// 全屏大变化 → 相机移动/场景切换,重建背景
if (fgCount > n ~/ 2) {
_mean = null;
_var = null;
return const [];
}
if (fgCount < minPixels) return const [];
return MotionAggregator.aggregate(diff, tw, th);
}
/// 相机切换后重置,避免旧场景背景
void reset() {
_mean = null;
_var = null;
}
}