Files
observer/flutter_app/lib/detection/visual_prior.dart
T
admin a0b115d954 训练体系整合与标注单阶段化
- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮
- 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite)
- 数据目录/权重不进 git;.gitignore 迁移至仓库根
2026-08-26 18:22:56 +08:00

123 lines
4.8 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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,
required bool rgbaOrder,
}) {
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, rgbaOrder)) {
continue;
}
kept.add(r);
}
return kept;
}
static bool _reject(DetectionResult r, List<Uint8List> planes,
List<int> strides, int width, int height, bool isBgra, bool rgbaOrder) {
// 位置线索: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, rgbaOrder);
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)。
/// 8888 单平面按实际字节序取通道:BGRA=[b,g,r,a]iOS 插件)、
/// RGBA=[r,g,b,a]Android 自写原生通道)——字节序写死会让 Android
/// 低分框采样到 R/B 互换的颜色(橙色野鸡身被误判成"蓝色")整批误杀;
/// 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,
bool rgbaOrder) {
if (isBgra) {
final src = planes[0];
final i = y * strides[0] + x * 4;
final rOff = rgbaOrder ? 0 : 2;
final bOff = rgbaOrder ? 2 : 0;
return (src[i + rOff].toDouble(), src[i + 1].toDouble(),
src[i + bOff].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);
}
}