card-recog-demo/lib/services/card_scan_gate.dart
2026-07-14 22:51:07 -07:00

88 lines
2.6 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 'card_detector.dart';
/// Quick post-capture gate: guide-crop + ML Kit. No box → ask user to retake.
class CardScanGate {
/// Returns true when a card-like border is found inside the shooting guide.
static Future<bool> hasDetectableBorder({
required String imagePath,
Size? captureViewport,
CardDetector? detector,
}) async {
final owned = detector == null;
final det = detector ?? CardDetector();
try {
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 false;
}
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);
final raster = img.copyCrop(
fullRaster,
x: gx,
y: gy,
width: gw,
height: gh,
);
final dir = await getTemporaryDirectory();
final guidePath =
'${dir.path}/card_gate_${DateTime.now().millisecondsSinceEpoch}.jpg';
await File(guidePath).writeAsBytes(
img.encodeJpg(raster, quality: 95),
flush: true,
);
final boxes = await det.detectFile(guidePath);
final guideSize = Size(
raster.width.toDouble(),
raster.height.toDouble(),
);
final best = det.pickBest(boxes, imageSize: guideSize);
return best != null && boxes.isNotEmpty;
} finally {
if (owned) {
await det.dispose();
}
}
}
}