Files
pfm-ocr/lib/features/camera/blur_detector.dart
T

113 lines
3.9 KiB
Dart

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<BlurResult> 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>[];
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);
}
}
}