118 lines
3.4 KiB
Dart
118 lines
3.4 KiB
Dart
|
|
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<void> ensureLoaded() async {
|
||
|
|
if (_interpreter != null || _loading) return;
|
||
|
|
_loading = true;
|
||
|
|
try {
|
||
|
|
_interpreter = await Interpreter.fromAsset(assetPath);
|
||
|
|
} finally {
|
||
|
|
_loading = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
Future<BlurResult> 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<double>.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<void> 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;
|
||
|
|
}
|
||
|
|
}
|