import 'dart:typed_data'; import 'package:image/image.dart' as img; import 'package:tflite_flutter/tflite_flutter.dart'; /// Blur / sharpness result from CoinSnap `blur.tflite`. class BlurResult { const BlurResult({ required this.blurScore, required this.clearScore, required this.isBlurry, }); /// Softmax-ish class 0 = blur, class 1 = clear (from model). final double blurScore; final double clearScore; final bool isBlurry; double get confidence => isBlurry ? blurScore : clearScore; } /// CoinSnap blur classifier (`assets/ml/blur.tflite`). /// /// Input: `[1, 3, 224, 224]` NCHW float32, letterbox on white, pixels / 255. /// Output: `[1, 2]` → `[blur, clear]`. class BlurDetector { BlurDetector({this.clearThreshold = 0.55}); static const int inputSize = 224; static const String assetPath = 'assets/ml/blur.tflite'; /// Clear score must be ≥ this to pass (mirrors app-side threshold). final double clearThreshold; Interpreter? _interpreter; bool _loading = false; Future ensureLoaded() async { if (_interpreter != null || _loading) return; _loading = true; try { _interpreter = await Interpreter.fromAsset(assetPath); } finally { _loading = false; } } Future evaluate(img.Image image) async { await ensureLoaded(); final interp = _interpreter; if (interp == null) { return const BlurResult(blurScore: 0, clearScore: 1, isBlurry: false); } final canvas = _letterboxWhite(image, inputSize); final flat = _toNchw(canvas); // Nested list matches [1, 3, 224, 224] NCHW. final inputNd = List.generate( 1, (_) => List.generate( 3, (c) => List.generate( inputSize, (y) => List.generate(inputSize, (x) { final i = c * inputSize * inputSize + y * inputSize + x; return flat[i]; }), ), ), ); final output = List.generate(1, (_) => List.filled(2, 0.0)); interp.run(inputNd, output); final blur = output[0][0]; final clear = output[0][1]; final isBlurry = clear < clearThreshold; return BlurResult(blurScore: blur, clearScore: clear, isBlurry: isBlurry); } Future dispose() async { _interpreter?.close(); _interpreter = null; } static img.Image _letterboxWhite(img.Image src, int size) { final scale = size / (src.width > src.height ? src.width : src.height); final nw = (src.width * scale).round().clamp(1, size); final nh = (src.height * scale).round().clamp(1, size); final resized = img.copyResize( src, width: nw, height: nh, interpolation: img.Interpolation.linear, ); final canvas = img.Image(width: size, height: size); img.fill(canvas, color: img.ColorRgb8(255, 255, 255)); final padX = ((size - nw) / 2).floor(); final padY = ((size - nh) / 2).floor(); img.compositeImage(canvas, resized, dstX: padX, dstY: padY); return canvas; } static Float32List _toNchw(img.Image canvas) { final out = Float32List(1 * 3 * inputSize * inputSize); const plane = inputSize * inputSize; for (var y = 0; y < inputSize; y++) { for (var x = 0; x < inputSize; x++) { final p = canvas.getPixel(x, y); final i = y * inputSize + x; out[i] = p.r / 255.0; out[plane + i] = p.g / 255.0; out[plane * 2 + i] = p.b / 255.0; } } return out; } }