迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user