Files
observer/flutter_app/lib/camera/detection_overlay.dart
T
2026-08-26 18:15:54 +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;
}