Files
observer/flutter_app/lib/camera/detection_overlay.dart
T
2026-09-01 15:18:05 +08:00

152 lines
4.9 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 _labels = {'pheasant': '环颈雉鸡', 'suspect': '疑似'};
/// 参考体型(米):目标物种(class 0,如环颈雉鸡)身高 / suspect 植被高度
static const double _refSizeSpeciesM = 0.45;
static const double _refSizeSuspectM = 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,
);
// 颜色按类别索引而非 label 文本:模型类别名可能为中文(环颈雉)或
// 随数据集变化,class 0 恒为目标物种(红),其余类恒为 suspect(黄)
final isSuspect = r.classId > 0 || r.label == 'suspect';
final color = isSuspect ? Color(0xFFFDD835) : Color(0xFFE53935);
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}]'
: '';
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 = (r.classId > 0 || r.label == 'suspect')
? _refSizeSuspectM
: _refSizeSpeciesM;
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;
}