迁移 Flutter 端与训练脚本,模型/训练产物移出 git(遵循纯代码约定)
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
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 text =
|
||||
'${_labels[r.label] ?? r.label} ${(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;
|
||||
}
|
||||
Reference in New Issue
Block a user