Files
observer/flutter_app/lib/camera/detection_overlay.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

152 lines
4.7 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 'package:flutter/material.dart';
import '../detection/coordinate_mapper.dart';
import '../detection/detection_result.dart';
/// 检测框绘制分级:
/// - 野鸡 confirmed:红色实线 3px(强证据)
/// - 野鸡 candidate:红色虚线 2px 半透明(待确认,弱提示)
/// - 疑似(生境预警):黄色虚线 2px 半透明(常驻静态预警,弱化渲染)
/// 标签附带距离估计(针孔模型 焦距px×参考体型/框高px)。
class DetectionOverlay extends StatelessWidget {
final List<DetectionResult> results;
final int rotation;
final int imageWidthPx;
final int imageHeightPx;
const DetectionOverlay({
super.key,
required this.results,
required this.rotation,
required this.imageWidthPx,
required this.imageHeightPx,
});
@override
Widget build(BuildContext context) {
return IgnorePointer(
child: CustomPaint(
painter: _OverlayPainter(results, rotation, imageWidthPx, imageHeightPx),
child: const SizedBox.expand(),
),
);
}
}
class _OverlayPainter extends CustomPainter {
final List<DetectionResult> results;
final int rotation;
final int imageWidthPx;
final int imageHeightPx;
_OverlayPainter(this.results, this.rotation, this.imageWidthPx,
this.imageHeightPx);
static const _colors = {
'pheasant': Color(0xFFE53935),
'suspect': Color(0xFFFDD835),
};
static const _labels = {'pheasant': '野鸡', 'suspect': '疑似'};
/// 参考体型(米):野鸡身高 / 植被高度(参照旧 DistanceEstimator
static const _refSizeM = {'pheasant': 0.45, 'suspect': 0.50};
/// iPhone 13 主摄在 1280 高预览下的估算焦距 px5.1mm / 5.30mm 传感器),
/// 单目误差 ±30%,仅作参考
static const double focalPx = 1230;
static const double maxDistanceM = 120;
@override
void paint(Canvas canvas, Size size) {
for (final r in results) {
final rect = CoordinateMapper.mapToView(
r.left,
r.top,
r.right,
r.bottom,
rotation,
imageWidthPx,
imageHeightPx,
size.width,
size.height,
);
final color = _colors[r.label] ?? Colors.white;
final isSuspect = r.label == 'suspect';
final confirmed = r.confirmed && !isSuspect;
final paint = Paint()
..color = color.withValues(alpha: confirmed ? 1.0 : 0.55)
..style = PaintingStyle.stroke
..strokeWidth = confirmed ? 3 : 2
..isAntiAlias = true;
final box = Rect.fromLTRB(rect.left, rect.top, rect.right, rect.bottom);
if (confirmed) {
canvas.drawRect(box, paint);
} else {
_drawDashedRect(canvas, box, paint);
}
// 标签:框上方,含距离;多模型时标注来源模型名(内置资产不标)
final dist = _distanceLabel(r);
final modelTag = r.modelName.isNotEmpty && r.modelName != '内置'
? '[${r.modelName}]'
: '';
final text =
'${_labels[r.label] ?? r.label}$modelTag ${(r.score * 100).toInt()}%$dist';
final textPainter = TextPainter(
text: TextSpan(
text: text,
style: TextStyle(
color: color.withValues(alpha: confirmed ? 1.0 : 0.8),
fontSize: 14,
fontWeight: FontWeight.w600,
shadows: const [Shadow(color: Colors.black, blurRadius: 3)],
),
),
textDirection: TextDirection.ltr,
)..layout();
final top = math.max(0.0, rect.top - 22);
final left = math.max(0.0, rect.left);
textPainter.paint(canvas, Offset(left + 4, top));
}
}
String _distanceLabel(DetectionResult r) {
final refH = _refSizeM[r.label];
if (refH == null) return '';
final hPx = r.height * imageHeightPx;
if (hPx < 8) return '';
final m = focalPx * refH / hPx;
if (m > maxDistanceM) return '';
return ' ≈${m.round()}m';
}
void _drawDashedRect(Canvas canvas, Rect r, Paint paint,
{double dash = 10, double gap = 6}) {
void dashLine(Offset a, Offset b) {
final total = (b - a).distance;
if (total <= 0) return;
final dir = (b - a) / total;
var d = 0.0;
while (d < total) {
final e = math.min(d + dash, total);
canvas.drawLine(a + dir * d, a + dir * e, paint);
d += dash + gap;
}
}
dashLine(r.topLeft, r.topRight);
dashLine(r.topRight, r.bottomRight);
dashLine(r.bottomRight, r.bottomLeft);
dashLine(r.bottomLeft, r.topLeft);
}
@override
bool shouldRepaint(_OverlayPainter oldDelegate) =>
oldDelegate.results != results ||
oldDelegate.rotation != rotation ||
oldDelegate.imageWidthPx != imageWidthPx ||
oldDelegate.imageHeightPx != imageHeightPx;
}