97 lines
2.9 KiB
Dart
97 lines
2.9 KiB
Dart
|
|
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 = <double>[];
|
||
|
|
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;
|
||
|
|
}
|
||
|
|
}
|