No content changes: git diff --ignore-all-space over these files is empty. The churn came from editing on Windows against a repo checked out with LF.
113 lines
4.0 KiB
Dart
113 lines
4.0 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);
|
|
}
|
|
}
|
|
}
|