123 lines
3.5 KiB
Dart
123 lines
3.5 KiB
Dart
|
|
import 'dart:io';
|
||
|
|
import 'dart:ui' as ui;
|
||
|
|
|
||
|
|
import 'package:flutter/material.dart';
|
||
|
|
import 'package:image/image.dart' as img;
|
||
|
|
import 'package:path_provider/path_provider.dart';
|
||
|
|
|
||
|
|
import '../widgets/guide_frame.dart';
|
||
|
|
import 'blur_detector.dart';
|
||
|
|
import 'coin_detector.dart';
|
||
|
|
|
||
|
|
enum CoinGateFail { none, blurry, noCoin }
|
||
|
|
|
||
|
|
class CoinGateResult {
|
||
|
|
const CoinGateResult({
|
||
|
|
required this.ok,
|
||
|
|
this.fail = CoinGateFail.none,
|
||
|
|
this.blur,
|
||
|
|
this.box,
|
||
|
|
});
|
||
|
|
|
||
|
|
final bool ok;
|
||
|
|
final CoinGateFail fail;
|
||
|
|
final BlurResult? blur;
|
||
|
|
final CoinBox? box;
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Post-capture gate: blur.tflite → detect.tflite (CoinSnap order).
|
||
|
|
class CoinScanGate {
|
||
|
|
static Future<CoinGateResult> evaluate({
|
||
|
|
required String imagePath,
|
||
|
|
Size? captureViewport,
|
||
|
|
CoinDetector? detector,
|
||
|
|
BlurDetector? blurDetector,
|
||
|
|
}) async {
|
||
|
|
final ownedDet = detector == null;
|
||
|
|
final ownedBlur = blurDetector == null;
|
||
|
|
final det = detector ?? CoinDetector();
|
||
|
|
final blur = blurDetector ?? BlurDetector();
|
||
|
|
|
||
|
|
try {
|
||
|
|
final raster = await _guideCrop(imagePath, captureViewport);
|
||
|
|
if (raster == null) {
|
||
|
|
return const CoinGateResult(ok: false, fail: CoinGateFail.noCoin);
|
||
|
|
}
|
||
|
|
|
||
|
|
final blurResult = await blur.evaluate(raster);
|
||
|
|
if (blurResult.isBlurry) {
|
||
|
|
return CoinGateResult(
|
||
|
|
ok: false,
|
||
|
|
fail: CoinGateFail.blurry,
|
||
|
|
blur: blurResult,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
final boxes = await det.detectImage(raster);
|
||
|
|
final best = det.pickBest(
|
||
|
|
boxes,
|
||
|
|
imageSize: Size(raster.width.toDouble(), raster.height.toDouble()),
|
||
|
|
);
|
||
|
|
if (best == null) {
|
||
|
|
return CoinGateResult(
|
||
|
|
ok: false,
|
||
|
|
fail: CoinGateFail.noCoin,
|
||
|
|
blur: blurResult,
|
||
|
|
);
|
||
|
|
}
|
||
|
|
return CoinGateResult(ok: true, blur: blurResult, box: best);
|
||
|
|
} finally {
|
||
|
|
if (ownedDet) await det.dispose();
|
||
|
|
if (ownedBlur) await blur.dispose();
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
static Future<img.Image?> _guideCrop(
|
||
|
|
String imagePath,
|
||
|
|
Size? captureViewport,
|
||
|
|
) async {
|
||
|
|
final raw = await File(imagePath).readAsBytes();
|
||
|
|
final codec = await ui.instantiateImageCodec(raw);
|
||
|
|
final frame = await codec.getNextFrame();
|
||
|
|
final fullImage = frame.image;
|
||
|
|
final rgba = await fullImage.toByteData(format: ui.ImageByteFormat.rawRgba);
|
||
|
|
if (rgba == null) {
|
||
|
|
fullImage.dispose();
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
final fullRaster = img.Image.fromBytes(
|
||
|
|
width: fullImage.width,
|
||
|
|
height: fullImage.height,
|
||
|
|
bytes: rgba.buffer,
|
||
|
|
bytesOffset: rgba.offsetInBytes,
|
||
|
|
numChannels: 4,
|
||
|
|
order: img.ChannelOrder.rgba,
|
||
|
|
);
|
||
|
|
fullImage.dispose();
|
||
|
|
|
||
|
|
final imageSize = Size(
|
||
|
|
fullRaster.width.toDouble(),
|
||
|
|
fullRaster.height.toDouble(),
|
||
|
|
);
|
||
|
|
final guide = captureViewport != null
|
||
|
|
? GuideFrame.inCoverImage(viewport: captureViewport, imageSize: imageSize)
|
||
|
|
: GuideFrame.inImage(imageSize);
|
||
|
|
|
||
|
|
final gx = guide.left.round().clamp(0, fullRaster.width - 1);
|
||
|
|
final gy = guide.top.round().clamp(0, fullRaster.height - 1);
|
||
|
|
final gw = guide.width.round().clamp(1, fullRaster.width - gx);
|
||
|
|
final gh = guide.height.round().clamp(1, fullRaster.height - gy);
|
||
|
|
return img.copyCrop(fullRaster, x: gx, y: gy, width: gw, height: gh);
|
||
|
|
}
|
||
|
|
|
||
|
|
/// Persist guide crop for preview reuse.
|
||
|
|
static Future<String> writeGuideJpeg(img.Image raster) async {
|
||
|
|
final dir = await getTemporaryDirectory();
|
||
|
|
final path =
|
||
|
|
'${dir.path}/coin_guide_${DateTime.now().millisecondsSinceEpoch}.jpg';
|
||
|
|
await File(path).writeAsBytes(img.encodeJpg(raster, quality: 95), flush: true);
|
||
|
|
return path;
|
||
|
|
}
|
||
|
|
}
|