迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)

This commit is contained in:
2026-08-24 12:35:24 +08:00
parent d056f01965
commit 961523d94c
218 changed files with 13391 additions and 3232 deletions
+113
View File
@@ -0,0 +1,113 @@
import 'dart:math' as math;
import 'dart:typed_data';
import 'detection_result.dart';
/// 运行时视觉先验:对低置信度野鸡框做多线索过滤,降低户外误报。
///
/// 仅对 score < [maxScore]0.35)的 pheasant 框生效;高分框与
/// suspect(生境预警)不参与过滤,避免误杀。
///
/// 线索:
/// - 颜色:绿色主导(草/叶)、蓝色主导(天空/水)、平坦低饱和(键盘/石头/文字)
/// - 位置:中心在画面上部 15%(天空区)——野鸡是地栖动物,不会出现在天空
///
/// 采样在原始 planes 上进行(后台 isolate 内,不依赖 UI 线程)。
class VisualPrior {
static const double maxScore = 0.35;
static const double skyTopRatio = 0.15;
// 颜色判定阈值(与 tflite_detector 的 YUV 有限范围展开一致)
static const double greenDiff = 20;
static const double blueDiff = 10;
static const double flatRange = 10;
// 采样点中满足条件的比例超过即拒绝
static const double greenRatio = 0.5;
static const double blueRatio = 0.4;
static const double flatRatio = 0.6;
static List<DetectionResult> filter(
List<DetectionResult> results, {
required List<Uint8List> planes,
required List<int> strides,
required int width,
required int height,
required bool isBgra,
}) {
if (results.isEmpty || width <= 0 || height <= 0) return results;
final kept = <DetectionResult>[];
for (final r in results) {
final lowConfPheasant = r.label == 'pheasant' && r.score < maxScore;
if (lowConfPheasant && _reject(r, planes, strides, width, height, isBgra)) {
continue;
}
kept.add(r);
}
return kept;
}
static bool _reject(DetectionResult r, List<Uint8List> planes,
List<int> strides, int width, int height, bool isBgra) {
// 位置线索:detectRaw 输出为图像坐标系,centerY 直接可判天空区
if (r.centerY < skyTopRatio) return true;
// 颜色线索:框中心 ±20% 区域 5×5 采样(小框采样点重合也没关系)
final cx = (r.centerX * width).round().clamp(0, width - 1).toInt();
final cy = (r.centerY * height).round().clamp(0, height - 1).toInt();
final halfW = math.max(1.0, r.width * width * 0.2);
final halfH = math.max(1.0, r.height * height * 0.2);
var green = 0, blue = 0, flat = 0, total = 0;
for (var gy = -2; gy <= 2; gy++) {
for (var gx = -2; gx <= 2; gx++) {
final px = (cx + gx * halfW / 2).round().clamp(0, width - 1).toInt();
final py = (cy + gy * halfH / 2).round().clamp(0, height - 1).toInt();
final (r_, g_, b_) = _pixel(planes, strides, px, py, width, height, isBgra);
total++;
final mn = math.min(r_, math.min(g_, b_));
final mx = math.max(r_, math.max(g_, b_));
if (g_ - r_ > greenDiff && g_ - b_ > greenDiff) green++;
if (b_ > r_ + blueDiff) blue++;
if (mx - mn < flatRange) flat++;
}
}
if (total == 0) return false;
if (green / total > greenRatio) return true;
if (blue / total > blueRatio) return true;
if (flat / total > flatRatio) return true;
return false;
}
/// 读取单像素 RGB0~255)。
/// BGRA 单平面:每像素 4 字节 [b,g,r,a]
/// YUVy 平面 + 4:2:0 半分辨率 U/VNV12 交错或 I420 分离)。
static (double, double, double) _pixel(List<Uint8List> planes,
List<int> strides, int x, int y, int width, int height, bool isBgra) {
if (isBgra) {
final src = planes[0];
final i = y * strides[0] + x * 4;
return (src[i + 2].toDouble(), src[i + 1].toDouble(), src[i].toDouble());
}
final yy =
(planes[0][y * strides[0] + x] - 16.0) * (255.0 / 219.0);
final nv12 = planes.length == 2;
final ux = (x ~/ 2).clamp(0, width ~/ 2 - 1).toInt();
final uy = (y ~/ 2).clamp(0, height ~/ 2 - 1).toInt();
final uvStride = strides[1];
final un = ((nv12
? planes[1][uy * uvStride + ux * 2].toDouble()
: planes[1][uy * uvStride + ux].toDouble()) -
128.0) *
(255.0 / 224.0);
final vn = ((nv12
? planes[1][uy * uvStride + ux * 2 + 1].toDouble()
: planes[2][uy * uvStride + ux].toDouble()) -
128.0) *
(255.0 / 224.0);
final r = yy + 1.402 * vn;
final g = yy - 0.344136 * un - 0.714136 * vn;
final b = yy + 1.772 * un;
return (r, g, b);
}
}