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 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 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; } }