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 filter( List results, { required List planes, required List strides, required int width, required int height, required bool isBgra, required bool rgbaOrder, }) { if (results.isEmpty || width <= 0 || height <= 0) return results; final kept = []; 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 planes, List 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; } /// 读取单像素 RGB(0~255)。 /// 8888 单平面按实际字节序取通道:BGRA=[b,g,r,a](iOS 插件)、 /// RGBA=[r,g,b,a](Android 自写原生通道)——字节序写死会让 Android /// 低分框采样到 R/B 互换的颜色(橙色环颈雉鸡身被误判成"蓝色")整批误杀; /// YUV:y 平面 + 4:2:0 半分辨率 U/V(NV12 交错或 I420 分离)。 static (double, double, double) _pixel(List planes, List 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); } }