81 lines
2.2 KiB
Dart
81 lines
2.2 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
import 'dart:math' as math;
|
||
|
|
import 'dart:typed_data';
|
||
|
|
import 'dart:ui';
|
||
|
|
|
||
|
|
import 'package:image/image.dart' as img;
|
||
|
|
|
||
|
|
import 'coin_detector.dart';
|
||
|
|
|
||
|
|
class CropResult {
|
||
|
|
const CropResult({
|
||
|
|
required this.bytes,
|
||
|
|
required this.width,
|
||
|
|
required this.height,
|
||
|
|
required this.usedDetection,
|
||
|
|
});
|
||
|
|
|
||
|
|
final Uint8List bytes;
|
||
|
|
final int width;
|
||
|
|
final int height;
|
||
|
|
final bool usedDetection;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Circular crop of the detected coin (square output with transparent-ish bg).
|
||
|
|
class CoinCropper {
|
||
|
|
static Future<CropResult> cropAndCorrect({
|
||
|
|
required String imagePath,
|
||
|
|
CoinBox? box,
|
||
|
|
double padding = 0.12,
|
||
|
|
}) async {
|
||
|
|
final raw = await File(imagePath).readAsBytes();
|
||
|
|
var decoded = img.decodeImage(raw);
|
||
|
|
if (decoded == null) {
|
||
|
|
throw StateError('Failed to decode image: $imagePath');
|
||
|
|
}
|
||
|
|
decoded = img.bakeOrientation(decoded);
|
||
|
|
|
||
|
|
final usedDetection = box != null;
|
||
|
|
final center = box?.effectiveCenter ??
|
||
|
|
Offset(decoded.width / 2, decoded.height / 2);
|
||
|
|
var radius = box?.effectiveRadius ??
|
||
|
|
math.min(decoded.width, decoded.height) * 0.35;
|
||
|
|
radius *= (1 + padding);
|
||
|
|
|
||
|
|
final size = (radius * 2).round().clamp(32, 2048);
|
||
|
|
final out = img.Image(width: size, height: size, numChannels: 4);
|
||
|
|
img.fill(out, color: img.ColorRgba8(0, 0, 0, 0));
|
||
|
|
|
||
|
|
final r2 = radius * radius;
|
||
|
|
for (var y = 0; y < size; y++) {
|
||
|
|
for (var x = 0; x < size; x++) {
|
||
|
|
final dx = x + 0.5 - size / 2;
|
||
|
|
final dy = y + 0.5 - size / 2;
|
||
|
|
if (dx * dx + dy * dy > r2) continue;
|
||
|
|
final sx = (center.dx + dx).round();
|
||
|
|
final sy = (center.dy + dy).round();
|
||
|
|
if (sx < 0 || sy < 0 || sx >= decoded.width || sy >= decoded.height) {
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
final p = decoded.getPixel(sx, sy);
|
||
|
|
out.setPixelRgba(x, y, p.r.toInt(), p.g.toInt(), p.b.toInt(), 255);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const outSize = 320;
|
||
|
|
final resized = img.copyResize(
|
||
|
|
out,
|
||
|
|
width: outSize,
|
||
|
|
height: outSize,
|
||
|
|
interpolation: img.Interpolation.cubic,
|
||
|
|
);
|
||
|
|
final bytes = Uint8List.fromList(img.encodePng(resized));
|
||
|
|
return CropResult(
|
||
|
|
bytes: bytes,
|
||
|
|
width: resized.width,
|
||
|
|
height: resized.height,
|
||
|
|
usedDetection: usedDetection,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|