- 标注:AI 预标注直写 labels_json(去候选确认两阶段);重叠去重(minIoU);全量标注按钮 - 训练:脚本迁移入 server/training/(Go 化 prepare_yolo/analyze_rfdetr,保留 train_server.py);tflite 产物自检并入训练流程(check_tflite) - 数据目录/权重不进 git;.gitignore 迁移至仓库根
152 lines
4.7 KiB
Dart
152 lines
4.7 KiB
Dart
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 高预览下的估算焦距 px(5.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;
|
||
}
|