import 'dart:math' as math; import 'dart:ui'; import 'package:image/image.dart' as img; class RefinedCircle { const RefinedCircle({required this.center, required this.radius}); final Offset center; final double radius; } /// Refine an AABB into a circular coin contour via radial edge search. class CoinContourRefiner { static RefinedCircle? refine(img.Image image, Rect aabb) { if (aabb.width < 16 || aabb.height < 16) return null; final cx = aabb.center.dx; final cy = aabb.center.dy; final maxR = math.max(aabb.width, aabb.height) * 0.62; final minR = math.min(aabb.width, aabb.height) * 0.28; if (maxR <= minR + 2) { return RefinedCircle( center: Offset(cx, cy), radius: math.min(aabb.width, aabb.height) / 2, ); } // Sample edge strength along rays; vote for radius with strongest ring. const rays = 64; final radiusVotes = []; for (var i = 0; i < rays; i++) { final ang = (i / rays) * math.pi * 2; final dx = math.cos(ang); final dy = math.sin(ang); var bestR = (minR + maxR) / 2; var bestMag = -1.0; for (var r = minR; r <= maxR; r += 1.5) { final x1 = (cx + dx * r).round(); final y1 = (cy + dy * r).round(); final x0 = (cx + dx * (r - 2)).round(); final y0 = (cy + dy * (r - 2)).round(); if (!_in(image, x0, y0) || !_in(image, x1, y1)) continue; final g0 = _gray(image, x0, y0); final g1 = _gray(image, x1, y1); final mag = (g1 - g0).abs(); if (mag > bestMag) { bestMag = mag; bestR = r; } } if (bestMag > 8) radiusVotes.add(bestR); } if (radiusVotes.length < rays * 0.35) { return RefinedCircle( center: Offset(cx, cy), radius: math.min(aabb.width, aabb.height) / 2 * 0.96, ); } radiusVotes.sort(); final radius = radiusVotes[radiusVotes.length ~/ 2] * 1.04; // Slight center nudge from opposite ray midpoints. var nx = 0.0; var ny = 0.0; var n = 0; for (var i = 0; i < rays; i += 2) { final ang = (i / rays) * math.pi * 2; final r = radiusVotes[math.min(i, radiusVotes.length - 1)]; nx += cx + math.cos(ang) * (r - radius); ny += cy + math.sin(ang) * (r - radius); n++; } final center = n == 0 ? Offset(cx, cy) : Offset( ((nx / n) + cx) / 2, ((ny / n) + cy) / 2, ); final clamped = Offset( center.dx.clamp(radius, image.width - radius), center.dy.clamp(radius, image.height - radius), ); return RefinedCircle(center: clamped, radius: radius); } static bool _in(img.Image image, int x, int y) => x >= 0 && y >= 0 && x < image.width && y < image.height; static double _gray(img.Image image, int x, int y) { final p = image.getPixel(x, y); return 0.299 * p.r + 0.587 * p.g + 0.114 * p.b; } }