import 'dart:io'; import 'package:flutter/foundation.dart'; import 'package:image/image.dart' as img; class BlurResult { final double score; // Scaled score (0 - 100) final bool isBlur; final double rawScore; BlurResult({required this.score, required this.isBlur, required this.rawScore}); } class BlurDetector { static BlurResult? mockResult; /// We scale the raw variance of Laplacian score (0 - 500.0) to a scaled score (0 - 100.0). /// Center cropping and Gaussian blur isolate text sharpness. static const double maxRawScore = 500.0; static const double rawThreshold = 400.0; // Below this raw score is blurry (80% of 500.0) static const double scaledThreshold = 80.0; // Corresponding scaled threshold (400 / 500 * 100) static Future checkBlur(String filePath) async { if (mockResult != null) { return mockResult!; } return await compute(_checkBlurIsolate, filePath); } static BlurResult _checkBlurIsolate(String filePath) { try { final bytes = File(filePath).readAsBytesSync(); final image = img.decodeImage(bytes); if (image == null) { return BlurResult(score: 0, isBlur: true, rawScore: 0); } // 1. Resize to 300 width for fast calculation (reduces isolate time to <100ms) final resized = img.copyResize(image, width: 300); // 2. Crop the center 60% area to focus purely on text and ignore document borders / backgrounds final cropWidth = (resized.width * 0.60).toInt(); final cropHeight = (resized.height * 0.60).toInt(); final startX = (resized.width - cropWidth) ~/ 2; final startY = (resized.height - cropHeight) ~/ 2; final cropped = img.copyCrop(resized, x: startX, y: startY, width: cropWidth, height: cropHeight); // 3. Apply Gaussian Blur to filter out sensor noise/camera shaking noise final smoothed = img.gaussianBlur(cropped, radius: 1); // 4. Convert to Grayscale final gray = img.grayscale(smoothed); final width = gray.width; final height = gray.height; // 5. Compute Laplacian values final laplacianValues = []; double sum = 0.0; for (int y = 1; y < height - 1; y++) { for (int x = 1; x < width - 1; x++) { final p = gray.getPixel(x, y); final pLeft = gray.getPixel(x - 1, y); final pRight = gray.getPixel(x + 1, y); final pTop = gray.getPixel(x, y - 1); final pBottom = gray.getPixel(x, y + 1); // Get grayscale values directly (r = g = b) final lum = p.r.toDouble(); final lumLeft = pLeft.r.toDouble(); final lumRight = pRight.r.toDouble(); final lumTop = pTop.r.toDouble(); final lumBottom = pBottom.r.toDouble(); // 3x3 Laplacian: 4 * center - left - right - top - bottom final laplacian = (4 * lum) - lumLeft - lumRight - lumTop - lumBottom; laplacianValues.add(laplacian); sum += laplacian; } } final count = laplacianValues.length; if (count == 0) return BlurResult(score: 0, isBlur: true, rawScore: 0); final mean = sum / count; // 6. Calculate Variance of Laplacian double varianceSum = 0.0; for (final val in laplacianValues) { varianceSum += (val - mean) * (val - mean); } final rawScore = varianceSum / count; // 7. Scale score to 0-100 range final scaledScore = (rawScore / maxRawScore * 100).clamp(0.0, 100.0); final isBlur = scaledScore < scaledThreshold; debugPrint('Blur detection: Raw Score: $rawScore, Scaled Score: $scaledScore, IsBlur: $isBlur'); return BlurResult( score: scaledScore, isBlur: isBlur, rawScore: rawScore, ); } catch (e) { debugPrint('Error detecting blur in isolate: $e'); return BlurResult(score: 0, isBlur: true, rawScore: 0); } } }