123 lines
4.8 KiB
Dart
123 lines
4.8 KiB
Dart
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;
|
||
}
|
||
|
||
/// 读取单像素 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<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);
|
||
}
|
||
}
|