capacitor-plugin-camera-forked 3.1.130 → 3.1.131
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/android/build.gradle
CHANGED
|
@@ -76,8 +76,6 @@ dependencies {
|
|
|
76
76
|
implementation 'org.tensorflow:tensorflow-lite-support:0.4.4'
|
|
77
77
|
implementation 'org.tensorflow:tensorflow-lite-gpu:2.14.0'
|
|
78
78
|
|
|
79
|
-
// Google ML Kit dependencies for text recognition and object detection
|
|
80
|
-
|
|
81
|
-
implementation "com.google.mlkit:object-detection:17.0.2"
|
|
82
|
-
implementation "com.google.mlkit:vision-common:17.3.0"
|
|
79
|
+
// Google ML Kit dependencies for text recognition and object detection - REMOVED
|
|
80
|
+
|
|
83
81
|
}
|
|
@@ -115,7 +115,8 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
115
115
|
|
|
116
116
|
// Store the desired JPEG quality, set during initialization
|
|
117
117
|
private int desiredJpegQuality = 95; // Default to high quality
|
|
118
|
-
private BlurDetectionHelper blurDetectionHelper; // TFLite blur detection
|
|
118
|
+
// private BlurDetectionHelper blurDetectionHelper; // TFLite blur detection - REMOVED
|
|
119
|
+
|
|
119
120
|
|
|
120
121
|
@PluginMethod
|
|
121
122
|
public void initialize(PluginCall call) {
|
|
@@ -142,9 +143,10 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
142
143
|
exec = Executors.newSingleThreadExecutor();
|
|
143
144
|
cameraProviderFuture = ProcessCameraProvider.getInstance(getContext());
|
|
144
145
|
|
|
145
|
-
// Initialize TFLite blur detection helper
|
|
146
|
-
blurDetectionHelper = new BlurDetectionHelper();
|
|
147
|
-
boolean tfliteInitialized = blurDetectionHelper.initialize(getContext());
|
|
146
|
+
// Initialize TFLite blur detection helper - REMOVED
|
|
147
|
+
// blurDetectionHelper = new BlurDetectionHelper();
|
|
148
|
+
// boolean tfliteInitialized = blurDetectionHelper.initialize(getContext());
|
|
149
|
+
|
|
148
150
|
|
|
149
151
|
cameraProviderFuture.addListener(() -> {
|
|
150
152
|
try {
|
|
@@ -222,7 +224,11 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
222
224
|
// Only detect blur if checkBlur option is true
|
|
223
225
|
boolean shouldCheckBlur = takeSnapshotCall.getBoolean("checkBlur", false);
|
|
224
226
|
if (shouldCheckBlur) {
|
|
225
|
-
|
|
227
|
+
Log.d("Camera", "Blur detection disabled/removed");
|
|
228
|
+
result.put("isBlur", false);
|
|
229
|
+
result.put("confidence", 0.0);
|
|
230
|
+
// Get blur detection result with bounding boxes in one call
|
|
231
|
+
/* REMOVED BlurDetectionHelper usage
|
|
226
232
|
if (blurDetectionHelper != null && blurDetectionHelper.isInitialized()) {
|
|
227
233
|
java.util.Map<String, Object> blurResult = blurDetectionHelper.detectBlurWithConfidence(bitmap);
|
|
228
234
|
|
|
@@ -286,7 +292,9 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
286
292
|
result.put("boundingBoxes", new java.util.ArrayList<>());
|
|
287
293
|
result.put("detectionMethod", "laplacian_fallback");
|
|
288
294
|
}
|
|
295
|
+
*/
|
|
289
296
|
} else {
|
|
297
|
+
|
|
290
298
|
Log.d("Camera", "Blur detection disabled for performance");
|
|
291
299
|
}
|
|
292
300
|
|
|
@@ -1451,83 +1459,83 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
1451
1459
|
}
|
|
1452
1460
|
|
|
1453
1461
|
// Use the new 3-step pipeline confidence detection method
|
|
1454
|
-
if (blurDetectionHelper != null && blurDetectionHelper.isInitialized()) {
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1462
|
+
// if (blurDetectionHelper != null && blurDetectionHelper.isInitialized()) {
|
|
1463
|
+
// java.util.Map<String, Object> result = blurDetectionHelper.detectBlurWithConfidence(bitmap);
|
|
1464
|
+
|
|
1465
|
+
// JSObject jsResult = new JSObject();
|
|
1466
|
+
// jsResult.put("isBlur", result.get("isBlur"));
|
|
1467
|
+
// jsResult.put("method", result.get("method"));
|
|
1468
|
+
|
|
1469
|
+
// // Handle different confidence values based on detection method
|
|
1470
|
+
// String method = (String) result.get("method");
|
|
1471
|
+
// Double blurConfidence = (Double) result.get("blurConfidence");
|
|
1472
|
+
// Double sharpConfidence = (Double) result.get("sharpConfidence");
|
|
1473
|
+
// Double textConfidence = (Double) result.get("textConfidence");
|
|
1474
|
+
|
|
1475
|
+
// if ("text_detection".equals(method) && textConfidence != null) {
|
|
1476
|
+
// // For text detection, use text confidence as sharp confidence
|
|
1477
|
+
// jsResult.put("sharpConfidence", textConfidence);
|
|
1478
|
+
// jsResult.put("blurConfidence", 1.0 - textConfidence);
|
|
1479
|
+
// } else if ("tflite".equals(method) && sharpConfidence != null) {
|
|
1480
|
+
// // For TFLite model, use the provided confidence values
|
|
1481
|
+
// jsResult.put("sharpConfidence", sharpConfidence);
|
|
1482
|
+
// jsResult.put("blurConfidence", blurConfidence != null ? blurConfidence : (1.0 - sharpConfidence));
|
|
1483
|
+
// } else if ("laplacian".equals(method) && sharpConfidence != null) {
|
|
1484
|
+
// // For Laplacian fallback, use the provided confidence values
|
|
1485
|
+
// jsResult.put("sharpConfidence", sharpConfidence);
|
|
1486
|
+
// jsResult.put("blurConfidence", blurConfidence != null ? blurConfidence : (1.0 - sharpConfidence));
|
|
1487
|
+
// } else if (sharpConfidence != null) {
|
|
1488
|
+
// // Generic fallback for any method with sharp confidence
|
|
1489
|
+
// jsResult.put("sharpConfidence", sharpConfidence);
|
|
1490
|
+
// jsResult.put("blurConfidence", blurConfidence != null ? blurConfidence : (1.0 - sharpConfidence));
|
|
1491
|
+
// } else if (blurConfidence != null) {
|
|
1492
|
+
// // Fallback if only blur confidence is available
|
|
1493
|
+
// jsResult.put("blurConfidence", blurConfidence);
|
|
1494
|
+
// jsResult.put("sharpConfidence", 1.0 - blurConfidence);
|
|
1495
|
+
// } else {
|
|
1496
|
+
// // Final fallback values
|
|
1497
|
+
// Boolean isBlur = (Boolean) result.get("isBlur");
|
|
1498
|
+
// if (isBlur != null) {
|
|
1499
|
+
// jsResult.put("blurConfidence", isBlur ? 1.0 : 0.0);
|
|
1500
|
+
// jsResult.put("sharpConfidence", isBlur ? 0.0 : 1.0);
|
|
1501
|
+
// } else {
|
|
1502
|
+
// jsResult.put("blurConfidence", 0.0);
|
|
1503
|
+
// jsResult.put("sharpConfidence", 0.0);
|
|
1504
|
+
// }
|
|
1505
|
+
// }
|
|
1506
|
+
|
|
1507
|
+
// // Add additional information if available
|
|
1508
|
+
// if (result.containsKey("roiResults")) {
|
|
1509
|
+
// @SuppressWarnings("unchecked")
|
|
1510
|
+
// List<Map<String, Object>> roiResults = (List<Map<String, Object>>) result.get("roiResults");
|
|
1511
|
+
// List<List<Double>> boundingBoxes = new ArrayList<>();
|
|
1504
1512
|
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
} else {
|
|
1513
|
+
// for (Map<String, Object> roi : roiResults) {
|
|
1514
|
+
// Object boundingBoxObj = roi.get("boundingBox");
|
|
1515
|
+
// if (boundingBoxObj instanceof android.graphics.Rect) {
|
|
1516
|
+
// android.graphics.Rect rect = (android.graphics.Rect) boundingBoxObj;
|
|
1517
|
+
// List<Double> box = new ArrayList<>();
|
|
1518
|
+
// box.add((double) rect.left);
|
|
1519
|
+
// box.add((double) rect.top);
|
|
1520
|
+
// box.add((double) rect.right);
|
|
1521
|
+
// box.add((double) rect.bottom);
|
|
1522
|
+
// boundingBoxes.add(box);
|
|
1523
|
+
// }
|
|
1524
|
+
// }
|
|
1525
|
+
// jsResult.put("boundingBoxes", boundingBoxes);
|
|
1526
|
+
// }
|
|
1527
|
+
// if (result.containsKey("objectCount")) {
|
|
1528
|
+
// jsResult.put("objectCount", result.get("objectCount"));
|
|
1529
|
+
// }
|
|
1530
|
+
// if (result.containsKey("wordCount")) {
|
|
1531
|
+
// jsResult.put("wordCount", result.get("wordCount"));
|
|
1532
|
+
// }
|
|
1533
|
+
// if (result.containsKey("readableWords")) {
|
|
1534
|
+
// jsResult.put("readableWords", result.get("readableWords"));
|
|
1535
|
+
// }
|
|
1536
|
+
|
|
1537
|
+
// call.resolve(jsResult);
|
|
1538
|
+
// } else {
|
|
1531
1539
|
// Fallback to Laplacian algorithm with confidence scores
|
|
1532
1540
|
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
1533
1541
|
boolean isBlur = laplacianScore < 150;
|
|
@@ -1542,7 +1550,7 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
1542
1550
|
result.put("method", "laplacian_fallback");
|
|
1543
1551
|
|
|
1544
1552
|
call.resolve(result);
|
|
1545
|
-
}
|
|
1553
|
+
// }
|
|
1546
1554
|
|
|
1547
1555
|
} catch (Exception e) {
|
|
1548
1556
|
call.reject("Failed to process image: " + e.getMessage());
|
|
@@ -1569,13 +1577,15 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
1569
1577
|
if (bitmap == null) return false;
|
|
1570
1578
|
|
|
1571
1579
|
// Use TFLite model if available, otherwise fallback to Laplacian
|
|
1572
|
-
if
|
|
1573
|
-
|
|
1574
|
-
|
|
1580
|
+
// Use TFLite model if available, otherwise fallback to Laplacian
|
|
1581
|
+
// if (blurDetectionHelper != null && blurDetectionHelper.isInitialized()) {
|
|
1582
|
+
// return blurDetectionHelper.isBlurry(bitmap);
|
|
1583
|
+
//} else {
|
|
1575
1584
|
// Fallback to original Laplacian algorithm
|
|
1576
1585
|
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
1577
1586
|
return laplacianScore < 50;
|
|
1578
|
-
}
|
|
1587
|
+
//}
|
|
1588
|
+
|
|
1579
1589
|
}
|
|
1580
1590
|
|
|
1581
1591
|
/**
|
|
@@ -1585,6 +1595,8 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
1585
1595
|
if (bitmap == null) return 0.0;
|
|
1586
1596
|
|
|
1587
1597
|
// Use the new 3-step pipeline blur detection
|
|
1598
|
+
// Use the new 3-step pipeline blur detection - DISABLED
|
|
1599
|
+
/*
|
|
1588
1600
|
if (blurDetectionHelper != null && blurDetectionHelper.isInitialized()) {
|
|
1589
1601
|
java.util.Map<String, Object> result = blurDetectionHelper.detectBlurWithConfidence(bitmap);
|
|
1590
1602
|
String method = (String) result.get("method");
|
|
@@ -1637,11 +1649,13 @@ public class CameraPreviewPlugin extends Plugin {
|
|
|
1637
1649
|
|
|
1638
1650
|
return 0.0;
|
|
1639
1651
|
} else {
|
|
1652
|
+
*/
|
|
1640
1653
|
// Fallback to Laplacian algorithm with confidence calculation
|
|
1641
1654
|
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
1642
1655
|
// Normalize to 0-1 range (higher score = sharper image)
|
|
1643
1656
|
return Math.max(0.0, Math.min(1.0, laplacianScore / 300.0));
|
|
1644
|
-
}
|
|
1657
|
+
//}
|
|
1658
|
+
|
|
1645
1659
|
}
|
|
1646
1660
|
|
|
1647
1661
|
/**
|
package/package.json
CHANGED
|
@@ -1,1029 +0,0 @@
|
|
|
1
|
-
package com.tonyxlh.capacitor.camera;
|
|
2
|
-
|
|
3
|
-
import android.content.Context;
|
|
4
|
-
import android.graphics.Bitmap;
|
|
5
|
-
import android.graphics.Rect;
|
|
6
|
-
import android.util.Log;
|
|
7
|
-
|
|
8
|
-
import com.google.mlkit.vision.common.InputImage;
|
|
9
|
-
import com.google.mlkit.vision.objects.DetectedObject;
|
|
10
|
-
import com.google.mlkit.vision.objects.ObjectDetection;
|
|
11
|
-
import com.google.mlkit.vision.objects.ObjectDetector;
|
|
12
|
-
import com.google.mlkit.vision.objects.defaults.ObjectDetectorOptions;
|
|
13
|
-
import com.google.mlkit.vision.text.Text;
|
|
14
|
-
import com.google.mlkit.vision.text.TextRecognition;
|
|
15
|
-
import com.google.mlkit.vision.text.TextRecognizer;
|
|
16
|
-
import com.google.mlkit.vision.text.latin.TextRecognizerOptions;
|
|
17
|
-
|
|
18
|
-
import org.tensorflow.lite.Interpreter;
|
|
19
|
-
import org.tensorflow.lite.support.common.FileUtil;
|
|
20
|
-
import org.tensorflow.lite.support.image.ImageProcessor;
|
|
21
|
-
import org.tensorflow.lite.support.image.TensorImage;
|
|
22
|
-
import org.tensorflow.lite.support.image.ops.ResizeOp;
|
|
23
|
-
import org.tensorflow.lite.support.image.ops.ResizeWithCropOrPadOp;
|
|
24
|
-
|
|
25
|
-
import org.tensorflow.lite.support.tensorbuffer.TensorBuffer;
|
|
26
|
-
import android.graphics.ColorMatrix;
|
|
27
|
-
import android.graphics.ColorMatrixColorFilter;
|
|
28
|
-
import android.graphics.Paint;
|
|
29
|
-
import android.graphics.Canvas;
|
|
30
|
-
import org.tensorflow.lite.DataType;
|
|
31
|
-
|
|
32
|
-
import java.io.IOException;
|
|
33
|
-
import java.nio.ByteBuffer;
|
|
34
|
-
import java.nio.FloatBuffer;
|
|
35
|
-
import java.nio.MappedByteBuffer;
|
|
36
|
-
import java.nio.ByteOrder;
|
|
37
|
-
import java.util.ArrayList;
|
|
38
|
-
import java.util.Collections;
|
|
39
|
-
import java.util.Comparator;
|
|
40
|
-
import java.util.HashMap;
|
|
41
|
-
import java.util.HashSet;
|
|
42
|
-
import java.util.List;
|
|
43
|
-
import java.util.Map;
|
|
44
|
-
import java.util.Set;
|
|
45
|
-
import java.util.concurrent.CountDownLatch;
|
|
46
|
-
import java.util.concurrent.ExecutorService;
|
|
47
|
-
import java.util.concurrent.Executors;
|
|
48
|
-
import java.util.concurrent.TimeUnit;
|
|
49
|
-
import java.util.concurrent.atomic.AtomicBoolean;
|
|
50
|
-
import java.util.concurrent.atomic.AtomicReference;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Comprehensive Blur Detection Helper
|
|
54
|
-
* Implements the 3-step pipeline: Object Detection -> Text Detection -> Full-Image Blur Detection
|
|
55
|
-
*/
|
|
56
|
-
public class BlurDetectionHelper {
|
|
57
|
-
private static final String TAG = "BlurDetectionHelper";
|
|
58
|
-
private static final String MODEL_FILENAME = "blur_detection_model.tflite";
|
|
59
|
-
private static int INPUT_SIZE = 600; // Will be updated based on actual model input size
|
|
60
|
-
private static final int NUM_CLASSES = 2; // blur, sharp
|
|
61
|
-
|
|
62
|
-
// Timeout settings
|
|
63
|
-
private static final int TIMEOUT_MS = 5000; // 5 second timeout
|
|
64
|
-
|
|
65
|
-
// Text recognition settings
|
|
66
|
-
private static final double MIN_WORD_CONFIDENCE = 0.8; // 80% confidence threshold
|
|
67
|
-
private static final double AT_LEAST_N_PERCENT_OF_WORDS_ARE_READABLE = 0.6; // 60% of words are readable
|
|
68
|
-
private static final double AT_LEAST_N_PERCENT_OF_AVERAGE_CONFIDENCE = 0.85; // 85% of average confidence
|
|
69
|
-
|
|
70
|
-
// Method based confidence threshold
|
|
71
|
-
private static final double MIN_SHARP_CONFIDENCE_FOR_OBJECT_DETECTION = 0.45; // 45% confidence threshold
|
|
72
|
-
private static final double MIN_SHARP_CONFIDENCE_FOR_TEXT_DETECTION = 0.09; // 9% confidence threshold
|
|
73
|
-
private static final double MIN_SHARP_CONFIDENCE_FOR_FULL_IMAGE = 0.65; // 65% confidence threshold
|
|
74
|
-
|
|
75
|
-
// TFLite components
|
|
76
|
-
private Interpreter tflite;
|
|
77
|
-
private ImageProcessor imageProcessor;
|
|
78
|
-
private TensorImage inputImageBuffer;
|
|
79
|
-
private TensorBuffer outputProbabilityBuffer;
|
|
80
|
-
|
|
81
|
-
// ML Kit components
|
|
82
|
-
private ObjectDetector objectDetector;
|
|
83
|
-
private TextRecognizer textRecognizer;
|
|
84
|
-
|
|
85
|
-
// Configuration flags
|
|
86
|
-
private boolean isInitialized = false;
|
|
87
|
-
private boolean useObjectDetection = true;
|
|
88
|
-
private boolean useTextRecognition = true;
|
|
89
|
-
private boolean useDictionaryCheck = false;
|
|
90
|
-
|
|
91
|
-
// Dictionary for text validation
|
|
92
|
-
private Set<String> commonWords;
|
|
93
|
-
|
|
94
|
-
// Executor for parallel processing
|
|
95
|
-
private ExecutorService executorService;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
public BlurDetectionHelper() {
|
|
99
|
-
// Initialize image processor for MobileNetV2 preprocessing with aspect ratio preservation
|
|
100
|
-
imageProcessor = new ImageProcessor.Builder()
|
|
101
|
-
.add(new ResizeWithCropOrPadOp(INPUT_SIZE, INPUT_SIZE)) // This preserves aspect ratio by cropping/padding
|
|
102
|
-
.build();
|
|
103
|
-
|
|
104
|
-
// Initialize common words dictionary
|
|
105
|
-
initializeCommonWords();
|
|
106
|
-
|
|
107
|
-
// Initialize executor service for parallel processing
|
|
108
|
-
executorService = Executors.newFixedThreadPool(3);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Initialize all components (TFLite, Object Detection, Text Recognition)
|
|
113
|
-
* @param context Android context to access assets
|
|
114
|
-
* @return true if initialization successful
|
|
115
|
-
*/
|
|
116
|
-
public boolean initialize(Context context) {
|
|
117
|
-
boolean tfliteInitialized = false;
|
|
118
|
-
boolean objectDetectorInitialized = false;
|
|
119
|
-
boolean textRecognizerInitialized = false;
|
|
120
|
-
|
|
121
|
-
// Initialize TFLite model
|
|
122
|
-
try {
|
|
123
|
-
// Load model from assets
|
|
124
|
-
MappedByteBuffer tfliteModel = FileUtil.loadMappedFile(context, MODEL_FILENAME);
|
|
125
|
-
|
|
126
|
-
// Configure interpreter options for better performance
|
|
127
|
-
Interpreter.Options options = new Interpreter.Options();
|
|
128
|
-
options.setNumThreads(4); // Use multiple threads for better performance
|
|
129
|
-
|
|
130
|
-
// Try to use GPU acceleration if available
|
|
131
|
-
try {
|
|
132
|
-
options.setUseXNNPACK(true);
|
|
133
|
-
} catch (Exception e) {
|
|
134
|
-
// XNNPACK not available, using CPU
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
tflite = new Interpreter(tfliteModel, options);
|
|
138
|
-
|
|
139
|
-
// Initialize input and output buffers
|
|
140
|
-
inputImageBuffer = new TensorImage(tflite.getInputTensor(0).dataType());
|
|
141
|
-
outputProbabilityBuffer = TensorBuffer.createFixedSize(
|
|
142
|
-
tflite.getOutputTensor(0).shape(),
|
|
143
|
-
tflite.getOutputTensor(0).dataType()
|
|
144
|
-
);
|
|
145
|
-
|
|
146
|
-
// Update INPUT_SIZE based on actual model input shape
|
|
147
|
-
int[] inputShape = tflite.getInputTensor(0).shape();
|
|
148
|
-
if (inputShape.length >= 3) {
|
|
149
|
-
int modelInputSize = inputShape[1]; // height dimension
|
|
150
|
-
if (modelInputSize != INPUT_SIZE) {
|
|
151
|
-
INPUT_SIZE = modelInputSize;
|
|
152
|
-
|
|
153
|
-
// Recreate image processor with correct size and aspect ratio preservation
|
|
154
|
-
imageProcessor = new ImageProcessor.Builder()
|
|
155
|
-
.add(new ResizeWithCropOrPadOp(INPUT_SIZE, INPUT_SIZE)) // Preserves aspect ratio
|
|
156
|
-
.build();
|
|
157
|
-
}
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
tfliteInitialized = true;
|
|
161
|
-
|
|
162
|
-
} catch (Exception e) {
|
|
163
|
-
Log.e(TAG, "Failed to initialize TFLite model: " + e.getMessage());
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
// Initialize Object Detector
|
|
167
|
-
try {
|
|
168
|
-
ObjectDetectorOptions detectorOptions = new ObjectDetectorOptions.Builder()
|
|
169
|
-
.setDetectorMode(ObjectDetectorOptions.SINGLE_IMAGE_MODE)
|
|
170
|
-
.enableClassification() // Enable classification to get confidence scores
|
|
171
|
-
.build();
|
|
172
|
-
|
|
173
|
-
objectDetector = ObjectDetection.getClient(detectorOptions);
|
|
174
|
-
objectDetectorInitialized = true;
|
|
175
|
-
|
|
176
|
-
} catch (Exception e) {
|
|
177
|
-
Log.e(TAG, "Failed to initialize Object Detector: " + e.getMessage());
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Initialize Text Recognizer
|
|
181
|
-
try {
|
|
182
|
-
textRecognizer = TextRecognition.getClient(TextRecognizerOptions.DEFAULT_OPTIONS);
|
|
183
|
-
textRecognizerInitialized = true;
|
|
184
|
-
|
|
185
|
-
} catch (Exception e) {
|
|
186
|
-
Log.e(TAG, "Failed to initialize Text Recognizer: " + e.getMessage());
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
// Set initialization status
|
|
190
|
-
isInitialized = tfliteInitialized || objectDetectorInitialized || textRecognizerInitialized;
|
|
191
|
-
|
|
192
|
-
Log.d(TAG, "Initialization status - TFLite: " + tfliteInitialized +
|
|
193
|
-
", Object Detection: " + objectDetectorInitialized +
|
|
194
|
-
", Text Recognition: " + textRecognizerInitialized);
|
|
195
|
-
|
|
196
|
-
return isInitialized;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
/**
|
|
200
|
-
* Main blur detection method implementing the 3-step pipeline
|
|
201
|
-
* @param bitmap Input image bitmap
|
|
202
|
-
* @return Blur confidence score (0.0 = sharp, 1.0 = very blurry)
|
|
203
|
-
*/
|
|
204
|
-
public double detectBlur(Bitmap bitmap) {
|
|
205
|
-
Map<String, Object> result = detectBlurWithConfidence(bitmap);
|
|
206
|
-
Boolean isBlur = (Boolean) result.get("isBlur");
|
|
207
|
-
return (isBlur != null && isBlur) ? 1.0 : 0.0;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Detect blur with detailed confidence scores using the 3-step pipeline
|
|
212
|
-
* @param bitmap Input image bitmap
|
|
213
|
-
* @return Map with comprehensive blur detection results
|
|
214
|
-
*/
|
|
215
|
-
public Map<String, Object> detectBlurWithConfidence(Bitmap bitmap) {
|
|
216
|
-
Map<String, Object> result = new HashMap<>();
|
|
217
|
-
|
|
218
|
-
if (!isInitialized) {
|
|
219
|
-
result.put("isBlur", false);
|
|
220
|
-
result.put("method", "none");
|
|
221
|
-
result.put("error", "Blur detector not initialized");
|
|
222
|
-
return result;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
try {
|
|
226
|
-
// Step 1: Object Detection (Preferred Path)
|
|
227
|
-
if (useObjectDetection && objectDetector != null) {
|
|
228
|
-
Log.d(TAG, "Step 1: Trying Object Detection");
|
|
229
|
-
List<DetectedObject> objects = detectObjects(bitmap);
|
|
230
|
-
|
|
231
|
-
if (!objects.isEmpty()) {
|
|
232
|
-
Log.d(TAG, "Objects detected: " + objects.size());
|
|
233
|
-
|
|
234
|
-
// Process ROIs from detected objects
|
|
235
|
-
List<Map<String, Object>> roiResults = new ArrayList<>();
|
|
236
|
-
boolean anyBlurry = false;
|
|
237
|
-
|
|
238
|
-
for (DetectedObject object : objects) {
|
|
239
|
-
Rect boundingBox = object.getBoundingBox();
|
|
240
|
-
if (boundingBox != null) {
|
|
241
|
-
// Crop ROI from original image
|
|
242
|
-
Bitmap roi = cropBitmap(bitmap, boundingBox);
|
|
243
|
-
if (roi != null) {
|
|
244
|
-
// Run TFLite blur detection on ROI
|
|
245
|
-
Map<String, Object> roiResult = detectBlurWithTFLiteConfidence(roi, MIN_SHARP_CONFIDENCE_FOR_OBJECT_DETECTION);
|
|
246
|
-
|
|
247
|
-
Log.d(TAG, "Object Detection ROI Result isBlur: " + roiResult.get("isBlur"));
|
|
248
|
-
roiResult.put("boundingBox", boundingBox);
|
|
249
|
-
roiResults.add(roiResult);
|
|
250
|
-
|
|
251
|
-
Boolean isBlur = (Boolean) roiResult.get("isBlur");
|
|
252
|
-
if (isBlur != null && isBlur) {
|
|
253
|
-
anyBlurry = true;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
result.put("method", "object_detection");
|
|
260
|
-
result.put("isBlur", anyBlurry);
|
|
261
|
-
result.put("roiResults", roiResults);
|
|
262
|
-
result.put("objectCount", objects.size());
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
// Step 2: Text Detection (Fallback if Object Not Found)
|
|
268
|
-
if (useTextRecognition && textRecognizer != null) {
|
|
269
|
-
Log.d(TAG, "Step 2: Trying Text Detection");
|
|
270
|
-
TextRecognitionResult textResult = detectTextWithBoundingBoxes(bitmap);
|
|
271
|
-
|
|
272
|
-
if (textResult.totalWords > 0) {
|
|
273
|
-
Log.d(TAG, "Text detected: " + textResult.totalWords + " words");
|
|
274
|
-
|
|
275
|
-
// Combine all detected text areas into a single bounding box
|
|
276
|
-
List<Rect> topTextAreas = combineAllTextAreas(textResult.boundingBoxes);
|
|
277
|
-
|
|
278
|
-
if (!topTextAreas.isEmpty()) {
|
|
279
|
-
// Process ROIs from text areas
|
|
280
|
-
List<Map<String, Object>> roiResults = processROIsInParallel(bitmap, topTextAreas);
|
|
281
|
-
|
|
282
|
-
// Check if any ROI is blurry
|
|
283
|
-
boolean anyBlurry = false;
|
|
284
|
-
for (Map<String, Object> roiResult : roiResults) {
|
|
285
|
-
Boolean isBlur = (Boolean) roiResult.get("isBlur");
|
|
286
|
-
if (isBlur != null && isBlur) {
|
|
287
|
-
anyBlurry = true;
|
|
288
|
-
break;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
result.put("method", "text_detection");
|
|
293
|
-
result.put("isBlur", anyBlurry);
|
|
294
|
-
result.put("roiResults", roiResults);
|
|
295
|
-
result.put("textConfidence", textResult.averageConfidence);
|
|
296
|
-
result.put("wordCount", textResult.totalWords);
|
|
297
|
-
result.put("readableWords", textResult.readableWords);
|
|
298
|
-
return result;
|
|
299
|
-
}
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
// Step 3: Full-Image Blur Detection (Final Fallback)
|
|
304
|
-
Log.d(TAG, "Step 3: Using Full-Image Blur Detection");
|
|
305
|
-
return detectBlurWithTFLiteConfidence(bitmap, MIN_SHARP_CONFIDENCE_FOR_FULL_IMAGE);
|
|
306
|
-
|
|
307
|
-
} catch (Exception e) {
|
|
308
|
-
Log.e(TAG, "Error in blur detection pipeline: " + e.getMessage());
|
|
309
|
-
result.put("isBlur", false);
|
|
310
|
-
result.put("method", "error");
|
|
311
|
-
result.put("error", e.getMessage());
|
|
312
|
-
return result;
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
/**
|
|
317
|
-
* Detect blur in image using TFLite model only
|
|
318
|
-
* @param bitmap Input image bitmap
|
|
319
|
-
* @return Blur confidence score (0.0 = sharp, 1.0 = very blurry)
|
|
320
|
-
*/
|
|
321
|
-
public double detectBlurWithTFLite(Bitmap bitmap) {
|
|
322
|
-
if (!isInitialized || tflite == null) {
|
|
323
|
-
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
324
|
-
return laplacianScore;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
try {
|
|
329
|
-
// Use the original bitmap directly (no image enhancement)
|
|
330
|
-
Bitmap processedBitmap = bitmap;
|
|
331
|
-
|
|
332
|
-
// Preprocess image for model (resize and potential enhancement)
|
|
333
|
-
inputImageBuffer.load(processedBitmap);
|
|
334
|
-
inputImageBuffer = imageProcessor.process(inputImageBuffer);
|
|
335
|
-
|
|
336
|
-
// Ensure black padding for better accuracy (matches iOS implementation)
|
|
337
|
-
ensureBlackPadding(inputImageBuffer);
|
|
338
|
-
|
|
339
|
-
// Get tensor buffer
|
|
340
|
-
ByteBuffer tensorBuffer = inputImageBuffer.getBuffer();
|
|
341
|
-
|
|
342
|
-
// Check if we need normalization based on data types
|
|
343
|
-
ByteBuffer inferenceBuffer;
|
|
344
|
-
if (inputImageBuffer.getDataType() == DataType.UINT8 && tflite.getInputTensor(0).dataType() == DataType.FLOAT32) {
|
|
345
|
-
inferenceBuffer = normalizeImageBuffer(tensorBuffer);
|
|
346
|
-
} else if (inputImageBuffer.getDataType() == DataType.FLOAT32) {
|
|
347
|
-
// Check if values are in [0,1] range or [0,255] range
|
|
348
|
-
inferenceBuffer = checkAndNormalizeFloat32Buffer(tensorBuffer);
|
|
349
|
-
} else {
|
|
350
|
-
inferenceBuffer = tensorBuffer;
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
// Run inference
|
|
354
|
-
tflite.run(inferenceBuffer, outputProbabilityBuffer.getBuffer().rewind());
|
|
355
|
-
|
|
356
|
-
// Get output probabilities
|
|
357
|
-
float[] probabilities = outputProbabilityBuffer.getFloatArray();
|
|
358
|
-
|
|
359
|
-
// probabilities[0] = blur probability, probabilities[1] = sharp probability
|
|
360
|
-
double blurConfidence = probabilities.length > 0 ? probabilities[0] : 0.0;
|
|
361
|
-
double sharpConfidence = probabilities.length > 1 ? probabilities[1] : 0.0;
|
|
362
|
-
|
|
363
|
-
// Determine if image is blurry using TFLite confidence
|
|
364
|
-
boolean isBlur = sharpConfidence < MIN_SHARP_CONFIDENCE_FOR_FULL_IMAGE;
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
// Return 1.0 for blur, 0.0 for sharp (to maintain double return type)
|
|
368
|
-
return isBlur ? 1.0 : 0.0;
|
|
369
|
-
|
|
370
|
-
} catch (Exception e) {
|
|
371
|
-
// Fallback to Laplacian algorithm
|
|
372
|
-
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
373
|
-
boolean isBlur = laplacianScore < 150;
|
|
374
|
-
return isBlur ? 1.0 : 0.0;
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Check if float32 buffer needs normalization and normalize if needed
|
|
380
|
-
* @param float32Buffer Input buffer with float32 pixel values
|
|
381
|
-
* @return Normalized float32 buffer (values in [0,1] range)
|
|
382
|
-
*/
|
|
383
|
-
private ByteBuffer checkAndNormalizeFloat32Buffer(ByteBuffer float32Buffer) {
|
|
384
|
-
float32Buffer.rewind();
|
|
385
|
-
FloatBuffer floatBuffer = float32Buffer.asFloatBuffer();
|
|
386
|
-
|
|
387
|
-
// Sample a few values to check if they're in [0,1] or [0,255] range
|
|
388
|
-
float maxSample = 0.0f;
|
|
389
|
-
int sampleCount = Math.min(100, floatBuffer.remaining());
|
|
390
|
-
|
|
391
|
-
for (int i = 0; i < sampleCount; i++) {
|
|
392
|
-
float value = Math.abs(floatBuffer.get());
|
|
393
|
-
maxSample = Math.max(maxSample, value);
|
|
394
|
-
}
|
|
395
|
-
|
|
396
|
-
// If max value is > 1.5, assume it's in [0,255] range and needs normalization
|
|
397
|
-
if (maxSample > 1.5f) {
|
|
398
|
-
return normalizeFloat32Buffer(float32Buffer);
|
|
399
|
-
} else {
|
|
400
|
-
float32Buffer.rewind();
|
|
401
|
-
return float32Buffer;
|
|
402
|
-
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
/**
|
|
406
|
-
* Normalize float32 buffer from [0,255] to [0,1]
|
|
407
|
-
* @param float32Buffer Input buffer with float32 pixel values in [0,255] range
|
|
408
|
-
* @return Normalized float32 buffer
|
|
409
|
-
*/
|
|
410
|
-
private ByteBuffer normalizeFloat32Buffer(ByteBuffer float32Buffer) {
|
|
411
|
-
int pixelCount = INPUT_SIZE * INPUT_SIZE * 3;
|
|
412
|
-
ByteBuffer normalizedBuffer = ByteBuffer.allocateDirect(pixelCount * 4);
|
|
413
|
-
normalizedBuffer.order(ByteOrder.nativeOrder());
|
|
414
|
-
FloatBuffer normalizedFloats = normalizedBuffer.asFloatBuffer();
|
|
415
|
-
|
|
416
|
-
float32Buffer.rewind();
|
|
417
|
-
FloatBuffer sourceFloats = float32Buffer.asFloatBuffer();
|
|
418
|
-
|
|
419
|
-
while (sourceFloats.hasRemaining() && normalizedFloats.hasRemaining()) {
|
|
420
|
-
float pixelValue = sourceFloats.get();
|
|
421
|
-
float normalizedValue = pixelValue / 255.0f;
|
|
422
|
-
normalizedFloats.put(normalizedValue);
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
normalizedBuffer.rewind();
|
|
426
|
-
return normalizedBuffer;
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Ensure black padding in the processed image buffer for better accuracy
|
|
431
|
-
* @param tensorImage Processed tensor image
|
|
432
|
-
*/
|
|
433
|
-
private void ensureBlackPadding(TensorImage tensorImage) {
|
|
434
|
-
ByteBuffer buffer = tensorImage.getBuffer();
|
|
435
|
-
DataType dataType = tensorImage.getDataType();
|
|
436
|
-
|
|
437
|
-
if (dataType == DataType.FLOAT32) {
|
|
438
|
-
// For float32, ensure padding areas are 0.0 (black)
|
|
439
|
-
FloatBuffer floatBuffer = buffer.asFloatBuffer();
|
|
440
|
-
int totalPixels = INPUT_SIZE * INPUT_SIZE * 3;
|
|
441
|
-
|
|
442
|
-
// Check if we need to fill with zeros (black)
|
|
443
|
-
for (int i = 0; i < totalPixels; i++) {
|
|
444
|
-
if (floatBuffer.get(i) < 0.001f) { // Near zero values
|
|
445
|
-
floatBuffer.put(i, 0.0f); // Ensure exact zero (black)
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
} else if (dataType == DataType.UINT8) {
|
|
449
|
-
// For uint8, ensure padding areas are 0 (black)
|
|
450
|
-
buffer.rewind();
|
|
451
|
-
while (buffer.hasRemaining()) {
|
|
452
|
-
byte value = buffer.get();
|
|
453
|
-
if (value == 0) {
|
|
454
|
-
buffer.put(buffer.position() - 1, (byte) 0); // Ensure exact zero
|
|
455
|
-
}
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
/**
|
|
461
|
-
* Normalize image buffer from uint8 [0,255] to float32 [0,1]
|
|
462
|
-
* @param uint8Buffer Input buffer with uint8 pixel values
|
|
463
|
-
* @return Normalized float32 buffer
|
|
464
|
-
*/
|
|
465
|
-
private ByteBuffer normalizeImageBuffer(ByteBuffer uint8Buffer) {
|
|
466
|
-
// Create float32 buffer with proper size and byte order
|
|
467
|
-
int pixelCount = INPUT_SIZE * INPUT_SIZE * 3;
|
|
468
|
-
ByteBuffer float32Buffer = ByteBuffer.allocateDirect(pixelCount * 4); // 4 bytes per float
|
|
469
|
-
float32Buffer.order(ByteOrder.nativeOrder());
|
|
470
|
-
FloatBuffer floatBuffer = float32Buffer.asFloatBuffer();
|
|
471
|
-
|
|
472
|
-
// Reset uint8 buffer position
|
|
473
|
-
uint8Buffer.rewind();
|
|
474
|
-
|
|
475
|
-
// Convert each uint8 pixel to normalized float32
|
|
476
|
-
while (uint8Buffer.hasRemaining() && floatBuffer.hasRemaining()) {
|
|
477
|
-
int pixelValue = uint8Buffer.get() & 0xFF; // Convert to unsigned int
|
|
478
|
-
float normalizedValue = pixelValue / 255.0f; // Normalize to [0,1]
|
|
479
|
-
floatBuffer.put(normalizedValue);
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
float32Buffer.rewind();
|
|
483
|
-
return float32Buffer;
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
/**
|
|
487
|
-
* Fallback Laplacian blur detection (from original implementation)
|
|
488
|
-
*/
|
|
489
|
-
private double calculateLaplacianBlurScore(Bitmap bitmap) {
|
|
490
|
-
if (bitmap == null) return 0.0;
|
|
491
|
-
|
|
492
|
-
int width = bitmap.getWidth();
|
|
493
|
-
int height = bitmap.getHeight();
|
|
494
|
-
|
|
495
|
-
// Convert to grayscale for better blur detection
|
|
496
|
-
int[] pixels = new int[width * height];
|
|
497
|
-
bitmap.getPixels(pixels, 0, width, 0, 0, width, height);
|
|
498
|
-
|
|
499
|
-
double[] grayscale = new double[width * height];
|
|
500
|
-
for (int i = 0; i < pixels.length; i++) {
|
|
501
|
-
int pixel = pixels[i];
|
|
502
|
-
int r = (pixel >> 16) & 0xFF;
|
|
503
|
-
int g = (pixel >> 8) & 0xFF;
|
|
504
|
-
int b = pixel & 0xFF;
|
|
505
|
-
grayscale[i] = 0.299 * r + 0.587 * g + 0.114 * b;
|
|
506
|
-
}
|
|
507
|
-
|
|
508
|
-
// Apply Laplacian kernel for edge detection
|
|
509
|
-
double variance = 0.0;
|
|
510
|
-
int count = 0;
|
|
511
|
-
|
|
512
|
-
// Sample every 4th pixel for performance
|
|
513
|
-
int step = 4;
|
|
514
|
-
for (int y = step; y < height - step; y += step) {
|
|
515
|
-
for (int x = step; x < width - step; x += step) {
|
|
516
|
-
int idx = y * width + x;
|
|
517
|
-
|
|
518
|
-
// 3x3 Laplacian kernel
|
|
519
|
-
double laplacian =
|
|
520
|
-
-grayscale[idx - width - 1] - grayscale[idx - width] - grayscale[idx - width + 1] +
|
|
521
|
-
-grayscale[idx - 1] + 8 * grayscale[idx] - grayscale[idx + 1] +
|
|
522
|
-
-grayscale[idx + width - 1] - grayscale[idx + width] - grayscale[idx + width + 1];
|
|
523
|
-
|
|
524
|
-
variance += laplacian * laplacian;
|
|
525
|
-
count++;
|
|
526
|
-
}
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
return count > 0 ? variance / count : 0.0;
|
|
530
|
-
}
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
/**
|
|
534
|
-
* Detect blur with detailed confidence scores using TFLite only
|
|
535
|
-
* @param bitmap Input image bitmap
|
|
536
|
-
* @return Map with isBlur, blurConfidence, and sharpConfidence
|
|
537
|
-
*/
|
|
538
|
-
public java.util.Map<String, Object> detectBlurWithTFLiteConfidence(Bitmap bitmap, double minSharpConfidence) {
|
|
539
|
-
java.util.Map<String, Object> result = new java.util.HashMap<>();
|
|
540
|
-
|
|
541
|
-
if (!isInitialized || tflite == null) {
|
|
542
|
-
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
543
|
-
boolean isBlur = laplacianScore < 150;
|
|
544
|
-
double normalizedScore = Math.max(0.0, Math.min(1.0, laplacianScore / 300.0));
|
|
545
|
-
double sharpConfidence = normalizedScore;
|
|
546
|
-
double blurConfidence = 1.0 - normalizedScore;
|
|
547
|
-
|
|
548
|
-
result.put("method", "laplacian");
|
|
549
|
-
result.put("isBlur", isBlur);
|
|
550
|
-
result.put("blurConfidence", blurConfidence);
|
|
551
|
-
result.put("sharpConfidence", sharpConfidence);
|
|
552
|
-
result.put("laplacianScore", laplacianScore);
|
|
553
|
-
result.put("hasText", false);
|
|
554
|
-
result.put("boundingBoxes", new java.util.ArrayList<>());
|
|
555
|
-
return result;
|
|
556
|
-
}
|
|
557
|
-
|
|
558
|
-
try {
|
|
559
|
-
// Use the original bitmap directly (no image enhancement)
|
|
560
|
-
Bitmap processedBitmap = bitmap;
|
|
561
|
-
|
|
562
|
-
// Preprocess image for model (resize and potential enhancement)
|
|
563
|
-
inputImageBuffer.load(processedBitmap);
|
|
564
|
-
inputImageBuffer = imageProcessor.process(inputImageBuffer);
|
|
565
|
-
|
|
566
|
-
// Ensure black padding for better accuracy (matches iOS implementation)
|
|
567
|
-
ensureBlackPadding(inputImageBuffer);
|
|
568
|
-
|
|
569
|
-
// Get tensor buffer
|
|
570
|
-
ByteBuffer tensorBuffer = inputImageBuffer.getBuffer();
|
|
571
|
-
|
|
572
|
-
// Check if we need normalization based on data types
|
|
573
|
-
ByteBuffer inferenceBuffer;
|
|
574
|
-
if (inputImageBuffer.getDataType() == DataType.UINT8 && tflite.getInputTensor(0).dataType() == DataType.FLOAT32) {
|
|
575
|
-
inferenceBuffer = normalizeImageBuffer(tensorBuffer);
|
|
576
|
-
} else if (inputImageBuffer.getDataType() == DataType.FLOAT32) {
|
|
577
|
-
// Check if values are in [0,1] range or [0,255] range
|
|
578
|
-
inferenceBuffer = checkAndNormalizeFloat32Buffer(tensorBuffer);
|
|
579
|
-
} else {
|
|
580
|
-
inferenceBuffer = tensorBuffer;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
// Run inference
|
|
584
|
-
tflite.run(inferenceBuffer, outputProbabilityBuffer.getBuffer().rewind());
|
|
585
|
-
|
|
586
|
-
// Get output probabilities
|
|
587
|
-
float[] probabilities = outputProbabilityBuffer.getFloatArray();
|
|
588
|
-
|
|
589
|
-
// probabilities[0] = blur probability, probabilities[1] = sharp probability
|
|
590
|
-
double blurConfidence = probabilities.length > 0 ? probabilities[0] : 0.0;
|
|
591
|
-
double sharpConfidence = probabilities.length > 1 ? probabilities[1] : 0.0;
|
|
592
|
-
|
|
593
|
-
Log.d(TAG, "TFLite Blur confidence: " + blurConfidence + " Sharp confidence: " + sharpConfidence);
|
|
594
|
-
|
|
595
|
-
// Determine if image is blurry using TFLite confidence
|
|
596
|
-
boolean isBlur = sharpConfidence < minSharpConfidence;
|
|
597
|
-
|
|
598
|
-
Log.d(TAG, "TFLite Blur detection isBlur: " + isBlur);
|
|
599
|
-
|
|
600
|
-
result.put("isBlur", isBlur);
|
|
601
|
-
result.put("method", "tflite");
|
|
602
|
-
result.put("blurConfidence", blurConfidence);
|
|
603
|
-
result.put("sharpConfidence", sharpConfidence);
|
|
604
|
-
result.put("boundingBoxes", new java.util.ArrayList<>());
|
|
605
|
-
return result;
|
|
606
|
-
|
|
607
|
-
} catch (Exception e) {
|
|
608
|
-
// Fallback to Laplacian algorithm
|
|
609
|
-
double laplacianScore = calculateLaplacianBlurScore(bitmap);
|
|
610
|
-
boolean isBlur = laplacianScore < 150;
|
|
611
|
-
double normalizedScore = Math.max(0.0, Math.min(1.0, laplacianScore / 300.0));
|
|
612
|
-
double sharpConfidence = normalizedScore;
|
|
613
|
-
double blurConfidence = 1.0 - normalizedScore;
|
|
614
|
-
|
|
615
|
-
result.put("isBlur", isBlur);
|
|
616
|
-
result.put("blurConfidence", blurConfidence);
|
|
617
|
-
result.put("sharpConfidence", sharpConfidence);
|
|
618
|
-
result.put("boundingBoxes", new java.util.ArrayList<>());
|
|
619
|
-
return result;
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
/**
|
|
624
|
-
* Check if image is blurry
|
|
625
|
-
* @param bitmap Input image
|
|
626
|
-
* @return true if image is blurry, false if sharp
|
|
627
|
-
*/
|
|
628
|
-
public boolean isBlurry(Bitmap bitmap) {
|
|
629
|
-
double result = detectBlur(bitmap);
|
|
630
|
-
return result == 1.0;
|
|
631
|
-
}
|
|
632
|
-
|
|
633
|
-
/**
|
|
634
|
-
* Get blur percentage (0-100%) - Deprecated, use isBlurry() instead
|
|
635
|
-
* @param bitmap Input image
|
|
636
|
-
* @return Blur percentage where 0% = sharp, 100% = very blurry
|
|
637
|
-
*/
|
|
638
|
-
@Deprecated
|
|
639
|
-
public double getBlurPercentage(Bitmap bitmap) {
|
|
640
|
-
// Convert boolean result to percentage for backward compatibility
|
|
641
|
-
return isBlurry(bitmap) ? 100.0 : 0.0;
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
/**
|
|
646
|
-
* Detect objects in the image using ML Kit Object Detection
|
|
647
|
-
* @param bitmap Input image
|
|
648
|
-
* @return List of detected objects
|
|
649
|
-
*/
|
|
650
|
-
private List<DetectedObject> detectObjects(Bitmap bitmap) {
|
|
651
|
-
if (objectDetector == null) {
|
|
652
|
-
return new ArrayList<>();
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
InputImage image = InputImage.fromBitmap(bitmap, 0);
|
|
656
|
-
|
|
657
|
-
CountDownLatch latch = new CountDownLatch(1);
|
|
658
|
-
AtomicReference<List<DetectedObject>> resultRef = new AtomicReference<>(new ArrayList<>());
|
|
659
|
-
|
|
660
|
-
objectDetector.process(image)
|
|
661
|
-
.addOnSuccessListener(detectedObjects -> {
|
|
662
|
-
resultRef.set(detectedObjects);
|
|
663
|
-
latch.countDown();
|
|
664
|
-
})
|
|
665
|
-
.addOnFailureListener(e -> {
|
|
666
|
-
Log.e(TAG, "Object detection failed: " + e.getMessage());
|
|
667
|
-
latch.countDown();
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
try {
|
|
671
|
-
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
|
672
|
-
} catch (InterruptedException e) {
|
|
673
|
-
Thread.currentThread().interrupt();
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
return resultRef.get();
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
/**
|
|
680
|
-
* Detect text in image with bounding boxes
|
|
681
|
-
* @param bitmap Input image
|
|
682
|
-
* @return TextRecognitionResult with bounding boxes
|
|
683
|
-
*/
|
|
684
|
-
private TextRecognitionResult detectTextWithBoundingBoxes(Bitmap bitmap) {
|
|
685
|
-
if (textRecognizer == null) {
|
|
686
|
-
return new TextRecognitionResult(false, 0.0, 0, 0, new ArrayList<>());
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
InputImage image = InputImage.fromBitmap(bitmap, 0);
|
|
690
|
-
|
|
691
|
-
CountDownLatch latch = new CountDownLatch(1);
|
|
692
|
-
AtomicReference<TextRecognitionResult> resultRef = new AtomicReference<>();
|
|
693
|
-
AtomicBoolean hasError = new AtomicBoolean(false);
|
|
694
|
-
|
|
695
|
-
textRecognizer.process(image)
|
|
696
|
-
.addOnSuccessListener(visionText -> {
|
|
697
|
-
try {
|
|
698
|
-
TextRecognitionResult result = analyzeTextConfidence(visionText);
|
|
699
|
-
resultRef.set(result);
|
|
700
|
-
} catch (Exception e) {
|
|
701
|
-
hasError.set(true);
|
|
702
|
-
} finally {
|
|
703
|
-
latch.countDown();
|
|
704
|
-
}
|
|
705
|
-
})
|
|
706
|
-
.addOnFailureListener(e -> {
|
|
707
|
-
hasError.set(true);
|
|
708
|
-
latch.countDown();
|
|
709
|
-
});
|
|
710
|
-
|
|
711
|
-
try {
|
|
712
|
-
boolean completed = latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
|
713
|
-
if (!completed || hasError.get()) {
|
|
714
|
-
return new TextRecognitionResult(false, 0.0, 0, 0, new ArrayList<>());
|
|
715
|
-
}
|
|
716
|
-
|
|
717
|
-
TextRecognitionResult result = resultRef.get();
|
|
718
|
-
return result != null ? result : new TextRecognitionResult(false, 0.0, 0, 0, new ArrayList<>());
|
|
719
|
-
|
|
720
|
-
} catch (InterruptedException e) {
|
|
721
|
-
Thread.currentThread().interrupt();
|
|
722
|
-
return new TextRecognitionResult(false, 0.0, 0, 0, new ArrayList<>());
|
|
723
|
-
}
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
/**
|
|
727
|
-
* Analyze text confidence and extract bounding boxes
|
|
728
|
-
* @param visionText Recognized text from ML Kit
|
|
729
|
-
* @return TextRecognitionResult with analysis
|
|
730
|
-
*/
|
|
731
|
-
private TextRecognitionResult analyzeTextConfidence(Text visionText) {
|
|
732
|
-
int totalWords = 0;
|
|
733
|
-
int readableWords = 0;
|
|
734
|
-
double totalConfidence = 0.0;
|
|
735
|
-
List<Rect> boundingBoxes = new ArrayList<>();
|
|
736
|
-
|
|
737
|
-
for (Text.TextBlock block : visionText.getTextBlocks()) {
|
|
738
|
-
Rect blockBoundingBox = block.getBoundingBox();
|
|
739
|
-
if (blockBoundingBox != null) {
|
|
740
|
-
boundingBoxes.add(blockBoundingBox);
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
for (Text.Line line : block.getLines()) {
|
|
744
|
-
for (Text.Element element : line.getElements()) {
|
|
745
|
-
String text = element.getText().trim();
|
|
746
|
-
if (!text.isEmpty()) {
|
|
747
|
-
totalWords++;
|
|
748
|
-
|
|
749
|
-
// Estimate word confidence
|
|
750
|
-
double confidence = estimateWordConfidence(element, text);
|
|
751
|
-
totalConfidence += confidence;
|
|
752
|
-
|
|
753
|
-
if (confidence >= MIN_WORD_CONFIDENCE) {
|
|
754
|
-
if (!useDictionaryCheck || isInDictionary(text)) {
|
|
755
|
-
readableWords++;
|
|
756
|
-
}
|
|
757
|
-
}
|
|
758
|
-
}
|
|
759
|
-
}
|
|
760
|
-
}
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
double averageConfidence = totalWords > 0 ? totalConfidence / totalWords : 0.0;
|
|
764
|
-
|
|
765
|
-
// Image is readable if we have text and sufficient readable words or high confidence
|
|
766
|
-
boolean isReadable = totalWords > 0 &&
|
|
767
|
-
(readableWords >= Math.max(1, totalWords * AT_LEAST_N_PERCENT_OF_WORDS_ARE_READABLE) ||
|
|
768
|
-
averageConfidence >= AT_LEAST_N_PERCENT_OF_AVERAGE_CONFIDENCE);
|
|
769
|
-
|
|
770
|
-
return new TextRecognitionResult(isReadable, averageConfidence, totalWords, readableWords, boundingBoxes);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
/**
|
|
774
|
-
* Combine all detected text areas into a single bounding box
|
|
775
|
-
* @param boundingBoxes List of bounding boxes
|
|
776
|
-
* @return List containing a single combined bounding box
|
|
777
|
-
*/
|
|
778
|
-
private List<Rect> combineAllTextAreas(List<Rect> boundingBoxes) {
|
|
779
|
-
if (boundingBoxes.isEmpty()) {
|
|
780
|
-
return new ArrayList<>();
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
// Find the minimum and maximum coordinates across all boxes
|
|
784
|
-
int minLeft = Integer.MAX_VALUE;
|
|
785
|
-
int minTop = Integer.MAX_VALUE;
|
|
786
|
-
int maxRight = Integer.MIN_VALUE;
|
|
787
|
-
int maxBottom = Integer.MIN_VALUE;
|
|
788
|
-
|
|
789
|
-
for (Rect rect : boundingBoxes) {
|
|
790
|
-
if (rect == null) continue;
|
|
791
|
-
minLeft = Math.min(minLeft, rect.left);
|
|
792
|
-
minTop = Math.min(minTop, rect.top);
|
|
793
|
-
maxRight = Math.max(maxRight, rect.right);
|
|
794
|
-
maxBottom = Math.max(maxBottom, rect.bottom);
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
if (minLeft == Integer.MAX_VALUE) {
|
|
798
|
-
return new ArrayList<>();
|
|
799
|
-
}
|
|
800
|
-
|
|
801
|
-
Rect combinedRect = new Rect(minLeft, minTop, maxRight, maxBottom);
|
|
802
|
-
|
|
803
|
-
Log.d(TAG, "Combined " + boundingBoxes.size() + " text areas into single bounding box: " +
|
|
804
|
-
"(" + minLeft + ", " + minTop + ", " + maxRight + ", " + maxBottom + ")");
|
|
805
|
-
|
|
806
|
-
// Minimum width and height
|
|
807
|
-
int minWidth = 100;
|
|
808
|
-
int minHeight = 40;
|
|
809
|
-
if (combinedRect.width() < minWidth || combinedRect.height() < minHeight) {
|
|
810
|
-
return new ArrayList<>();
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
List<Rect> result = new ArrayList<>();
|
|
814
|
-
result.add(combinedRect);
|
|
815
|
-
return result;
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Process multiple ROIs in parallel for blur detection
|
|
820
|
-
* @param bitmap Original bitmap
|
|
821
|
-
* @param rois List of regions of interest
|
|
822
|
-
* @return List of blur detection results for each ROI
|
|
823
|
-
*/
|
|
824
|
-
private List<Map<String, Object>> processROIsInParallel(Bitmap bitmap, List<Rect> rois) {
|
|
825
|
-
List<Map<String, Object>> results = Collections.synchronizedList(new ArrayList<>());
|
|
826
|
-
CountDownLatch latch = new CountDownLatch(rois.size());
|
|
827
|
-
|
|
828
|
-
for (Rect roi : rois) {
|
|
829
|
-
executorService.execute(() -> {
|
|
830
|
-
try {
|
|
831
|
-
Bitmap cropped = cropBitmap(bitmap, roi);
|
|
832
|
-
if (cropped != null) {
|
|
833
|
-
Map<String, Object> result = detectBlurWithTFLiteConfidence(cropped, MIN_SHARP_CONFIDENCE_FOR_TEXT_DETECTION);
|
|
834
|
-
result.put("boundingBox", roi);
|
|
835
|
-
results.add(result);
|
|
836
|
-
}
|
|
837
|
-
} catch (Exception e) {
|
|
838
|
-
Log.e(TAG, "Error processing ROI: " + e.getMessage());
|
|
839
|
-
} finally {
|
|
840
|
-
latch.countDown();
|
|
841
|
-
}
|
|
842
|
-
});
|
|
843
|
-
}
|
|
844
|
-
|
|
845
|
-
try {
|
|
846
|
-
latch.await(TIMEOUT_MS, TimeUnit.MILLISECONDS);
|
|
847
|
-
} catch (InterruptedException e) {
|
|
848
|
-
Thread.currentThread().interrupt();
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
return results;
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
/**
|
|
855
|
-
* Crop a bitmap to the specified bounding box
|
|
856
|
-
* @param source Source bitmap
|
|
857
|
-
* @param rect Bounding box to crop
|
|
858
|
-
* @return Cropped bitmap or null if invalid
|
|
859
|
-
*/
|
|
860
|
-
private Bitmap cropBitmap(Bitmap source, Rect rect) {
|
|
861
|
-
try {
|
|
862
|
-
// Ensure bounds are within image
|
|
863
|
-
int left = Math.max(0, rect.left);
|
|
864
|
-
int top = Math.max(0, rect.top);
|
|
865
|
-
int right = Math.min(source.getWidth(), rect.right);
|
|
866
|
-
int bottom = Math.min(source.getHeight(), rect.bottom);
|
|
867
|
-
|
|
868
|
-
int width = right - left;
|
|
869
|
-
int height = bottom - top;
|
|
870
|
-
|
|
871
|
-
if (width > 0 && height > 0) {
|
|
872
|
-
return Bitmap.createBitmap(source, left, top, width, height);
|
|
873
|
-
}
|
|
874
|
-
} catch (Exception e) {
|
|
875
|
-
Log.e(TAG, "Error cropping bitmap: " + e.getMessage());
|
|
876
|
-
}
|
|
877
|
-
return null;
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
/**
|
|
881
|
-
* Estimate word confidence based on element properties
|
|
882
|
-
* @param element Text element from ML Kit
|
|
883
|
-
* @param text The actual text content
|
|
884
|
-
* @return Estimated confidence score
|
|
885
|
-
*/
|
|
886
|
-
private double estimateWordConfidence(Text.Element element, String text) {
|
|
887
|
-
double confidence = 0.55; // Base confidence
|
|
888
|
-
|
|
889
|
-
// Text length check
|
|
890
|
-
if (text.length() >= 3 && text.length() <= 15) {
|
|
891
|
-
confidence += 0.2;
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
// Common patterns check
|
|
895
|
-
if (text.matches("[a-zA-Z0-9\\s\\-\\.]+")) {
|
|
896
|
-
confidence += 0.15;
|
|
897
|
-
}
|
|
898
|
-
|
|
899
|
-
// Mixed case check
|
|
900
|
-
if (text.matches(".*[a-z].*") && text.matches(".*[A-Z].*")) {
|
|
901
|
-
confidence += 0.1;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
// Numbers check
|
|
905
|
-
if (text.matches(".*\\d.*")) {
|
|
906
|
-
confidence += 0.1;
|
|
907
|
-
}
|
|
908
|
-
|
|
909
|
-
// Special characters penalty
|
|
910
|
-
if (text.matches(".*[^a-zA-Z0-9\\s\\-\\.].*")) {
|
|
911
|
-
confidence -= 0.1;
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
return Math.max(0.0, Math.min(1.0, confidence));
|
|
915
|
-
}
|
|
916
|
-
|
|
917
|
-
/**
|
|
918
|
-
* Check if word is in common dictionary
|
|
919
|
-
* @param word Word to check
|
|
920
|
-
* @return true if word is in dictionary
|
|
921
|
-
*/
|
|
922
|
-
private boolean isInDictionary(String word) {
|
|
923
|
-
if (commonWords == null || word == null) {
|
|
924
|
-
return false;
|
|
925
|
-
}
|
|
926
|
-
|
|
927
|
-
String cleanWord = word.toLowerCase().replaceAll("[^a-zA-Z]", "");
|
|
928
|
-
return commonWords.contains(cleanWord);
|
|
929
|
-
}
|
|
930
|
-
|
|
931
|
-
/**
|
|
932
|
-
* Initialize common English words for dictionary check
|
|
933
|
-
*/
|
|
934
|
-
private void initializeCommonWords() {
|
|
935
|
-
commonWords = new HashSet<>();
|
|
936
|
-
|
|
937
|
-
// Add common English words
|
|
938
|
-
String[] words = {
|
|
939
|
-
"the", "be", "to", "of", "and", "a", "in", "that", "have", "i", "it", "for", "not", "on", "with",
|
|
940
|
-
"he", "as", "you", "do", "at", "this", "but", "his", "by", "from", "they", "we", "say", "her",
|
|
941
|
-
"she", "or", "an", "will", "my", "one", "all", "would", "there", "their", "what", "so", "up",
|
|
942
|
-
"out", "if", "about", "who", "get", "which", "go", "me", "when", "make", "can", "like", "time",
|
|
943
|
-
"no", "just", "him", "know", "take", "people", "into", "year", "your", "good", "some", "could",
|
|
944
|
-
"them", "see", "other", "than", "then", "now", "look", "only", "come", "its", "over", "think",
|
|
945
|
-
"also", "back", "after", "use", "two", "how", "our", "work", "first", "well", "way", "even",
|
|
946
|
-
"new", "want", "because", "any", "these", "give", "day", "most", "us", "is", "was", "are",
|
|
947
|
-
"were", "been", "has", "had", "having", "does", "did", "doing", "can", "could", "should",
|
|
948
|
-
"would", "may", "might", "must", "shall", "will", "am", "being", "became", "become", "becomes"
|
|
949
|
-
};
|
|
950
|
-
|
|
951
|
-
for (String word : words) {
|
|
952
|
-
commonWords.add(word);
|
|
953
|
-
}
|
|
954
|
-
}
|
|
955
|
-
|
|
956
|
-
/**
|
|
957
|
-
* Clean up resources
|
|
958
|
-
*/
|
|
959
|
-
public void close() {
|
|
960
|
-
if (tflite != null) {
|
|
961
|
-
tflite.close();
|
|
962
|
-
tflite = null;
|
|
963
|
-
}
|
|
964
|
-
if (objectDetector != null) {
|
|
965
|
-
objectDetector.close();
|
|
966
|
-
objectDetector = null;
|
|
967
|
-
}
|
|
968
|
-
if (textRecognizer != null) {
|
|
969
|
-
textRecognizer.close();
|
|
970
|
-
textRecognizer = null;
|
|
971
|
-
}
|
|
972
|
-
if (executorService != null) {
|
|
973
|
-
executorService.shutdown();
|
|
974
|
-
executorService = null;
|
|
975
|
-
}
|
|
976
|
-
isInitialized = false;
|
|
977
|
-
}
|
|
978
|
-
|
|
979
|
-
/**
|
|
980
|
-
* Check if blur detector is properly initialized
|
|
981
|
-
*/
|
|
982
|
-
public boolean isInitialized() {
|
|
983
|
-
return isInitialized;
|
|
984
|
-
}
|
|
985
|
-
|
|
986
|
-
/**
|
|
987
|
-
* Enable or disable object detection
|
|
988
|
-
* @param enable true to enable object detection
|
|
989
|
-
*/
|
|
990
|
-
public void setObjectDetectionEnabled(boolean enable) {
|
|
991
|
-
this.useObjectDetection = enable;
|
|
992
|
-
}
|
|
993
|
-
|
|
994
|
-
/**
|
|
995
|
-
* Enable or disable text recognition
|
|
996
|
-
* @param enable true to enable text recognition
|
|
997
|
-
*/
|
|
998
|
-
public void setTextRecognitionEnabled(boolean enable) {
|
|
999
|
-
this.useTextRecognition = enable;
|
|
1000
|
-
}
|
|
1001
|
-
|
|
1002
|
-
/**
|
|
1003
|
-
* Enable or disable dictionary check in text recognition
|
|
1004
|
-
* @param enable true to enable dictionary check
|
|
1005
|
-
*/
|
|
1006
|
-
public void setDictionaryCheckEnabled(boolean enable) {
|
|
1007
|
-
this.useDictionaryCheck = enable;
|
|
1008
|
-
}
|
|
1009
|
-
|
|
1010
|
-
/**
|
|
1011
|
-
* Result class for text recognition analysis
|
|
1012
|
-
*/
|
|
1013
|
-
private static class TextRecognitionResult {
|
|
1014
|
-
final boolean isReadable;
|
|
1015
|
-
final double averageConfidence;
|
|
1016
|
-
final int totalWords;
|
|
1017
|
-
final int readableWords;
|
|
1018
|
-
final List<Rect> boundingBoxes;
|
|
1019
|
-
|
|
1020
|
-
TextRecognitionResult(boolean isReadable, double averageConfidence, int totalWords,
|
|
1021
|
-
int readableWords, List<Rect> boundingBoxes) {
|
|
1022
|
-
this.isReadable = isReadable;
|
|
1023
|
-
this.averageConfidence = averageConfidence;
|
|
1024
|
-
this.totalWords = totalWords;
|
|
1025
|
-
this.readableWords = readableWords;
|
|
1026
|
-
this.boundingBoxes = boundingBoxes;
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
}
|