react-native-rectangle-doc-scanner 11.3.0 → 13.0.0
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.
|
@@ -486,32 +486,11 @@ class CameraController(
|
|
|
486
486
|
|
|
487
487
|
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
488
488
|
|
|
489
|
-
//
|
|
490
|
-
|
|
491
|
-
val
|
|
492
|
-
val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
|
|
493
|
-
Surface.ROTATION_0 -> 0
|
|
494
|
-
Surface.ROTATION_90 -> 90
|
|
495
|
-
Surface.ROTATION_180 -> 180
|
|
496
|
-
Surface.ROTATION_270 -> 270
|
|
497
|
-
else -> 0
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
fun rotatePoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
501
|
-
return if (sensorOrientation == 90) {
|
|
502
|
-
org.opencv.core.Point(
|
|
503
|
-
point.y,
|
|
504
|
-
imageWidth - point.x
|
|
505
|
-
)
|
|
506
|
-
} else {
|
|
507
|
-
point
|
|
508
|
-
}
|
|
509
|
-
}
|
|
510
|
-
|
|
511
|
-
val finalWidth = if (sensorOrientation == 90) imageHeight else imageWidth
|
|
512
|
-
val finalHeight = if (sensorOrientation == 90) imageWidth else imageHeight
|
|
489
|
+
// Rectangle coordinates are already in the rotated image space (effective rotation applied).
|
|
490
|
+
val finalWidth = imageWidth
|
|
491
|
+
val finalHeight = imageHeight
|
|
513
492
|
|
|
514
|
-
//
|
|
493
|
+
// Apply fit-center scaling to match TextureView display.
|
|
515
494
|
val scaleX = viewWidth / finalWidth.toFloat()
|
|
516
495
|
val scaleY = viewHeight / finalHeight.toFloat()
|
|
517
496
|
val scale = scaleX.coerceAtMost(scaleY)
|
|
@@ -522,10 +501,9 @@ class CameraController(
|
|
|
522
501
|
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
523
502
|
|
|
524
503
|
fun transformPoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
525
|
-
val rotated = rotatePoint(point)
|
|
526
504
|
return org.opencv.core.Point(
|
|
527
|
-
|
|
528
|
-
|
|
505
|
+
point.x * scale + offsetX,
|
|
506
|
+
point.y * scale + offsetY
|
|
529
507
|
)
|
|
530
508
|
}
|
|
531
509
|
|
|
@@ -536,10 +514,9 @@ class CameraController(
|
|
|
536
514
|
transformPoint(rectangle.bottomRight)
|
|
537
515
|
)
|
|
538
516
|
|
|
539
|
-
Log.d(TAG, "[MAPPING]
|
|
517
|
+
Log.d(TAG, "[MAPPING] Image: ${imageWidth}x${imageHeight} → Final: ${finalWidth}x${finalHeight}")
|
|
540
518
|
Log.d(TAG, "[MAPPING] View: ${viewWidth.toInt()}x${viewHeight.toInt()}, Scale: $scale, Offset: ($offsetX, $offsetY)")
|
|
541
519
|
Log.d(TAG, "[MAPPING] TL: (${rectangle.topLeft.x}, ${rectangle.topLeft.y}) → " +
|
|
542
|
-
"Rotated: (${rotatePoint(rectangle.topLeft).x}, ${rotatePoint(rectangle.topLeft).y}) → " +
|
|
543
520
|
"Final: (${result.topLeft.x}, ${result.topLeft.y})")
|
|
544
521
|
|
|
545
522
|
return result
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -60,6 +60,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
60
60
|
private var lastDetectedImageHeight = 0
|
|
61
61
|
private var lastRectangleOnScreen: Rectangle? = null
|
|
62
62
|
private var lastSmoothedRectangleOnScreen: Rectangle? = null
|
|
63
|
+
private val iouHistory = ArrayDeque<Rectangle>()
|
|
63
64
|
|
|
64
65
|
// Coroutine scope for async operations
|
|
65
66
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
@@ -175,6 +176,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
175
176
|
}
|
|
176
177
|
}
|
|
177
178
|
|
|
179
|
+
|
|
178
180
|
private fun setupCamera() {
|
|
179
181
|
try {
|
|
180
182
|
Log.d(TAG, "[SETUP] Creating CameraController...")
|
|
@@ -305,23 +307,33 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
305
307
|
overlayView.setRectangle(rectangleOnScreen, overlayColor)
|
|
306
308
|
}
|
|
307
309
|
|
|
308
|
-
// Update stable counter based on quality
|
|
310
|
+
// Update stable counter based on quality + IOU stability
|
|
309
311
|
if (rectangleCoordinates == null) {
|
|
310
312
|
if (stableCounter != 0) {
|
|
311
313
|
Log.d(TAG, "Rectangle lost, resetting stableCounter")
|
|
312
314
|
}
|
|
313
315
|
stableCounter = 0
|
|
316
|
+
clearIouHistory()
|
|
314
317
|
} else {
|
|
315
318
|
when (quality) {
|
|
316
319
|
RectangleQuality.GOOD -> {
|
|
317
|
-
|
|
318
|
-
|
|
320
|
+
val isStable = rectangleOnScreen?.let { updateIouHistory(it) } ?: false
|
|
321
|
+
if (isStable) {
|
|
322
|
+
stableCounter = min(stableCounter + 1, detectionCountBeforeCapture)
|
|
323
|
+
Log.d(TAG, "Good rectangle detected, stableCounter: $stableCounter/$detectionCountBeforeCapture")
|
|
324
|
+
} else {
|
|
325
|
+
if (stableCounter > 0) {
|
|
326
|
+
stableCounter--
|
|
327
|
+
}
|
|
328
|
+
Log.d(TAG, "Rectangle unstable (IOU), stableCounter: $stableCounter")
|
|
329
|
+
}
|
|
319
330
|
}
|
|
320
331
|
RectangleQuality.BAD_ANGLE, RectangleQuality.TOO_FAR -> {
|
|
321
332
|
if (stableCounter > 0) {
|
|
322
333
|
stableCounter--
|
|
323
334
|
}
|
|
324
335
|
Log.d(TAG, "Bad rectangle detected (type: $quality), stableCounter: $stableCounter")
|
|
336
|
+
clearIouHistory()
|
|
325
337
|
}
|
|
326
338
|
}
|
|
327
339
|
}
|
|
@@ -337,6 +349,52 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
337
349
|
}
|
|
338
350
|
}
|
|
339
351
|
|
|
352
|
+
private fun updateIouHistory(rectangle: Rectangle): Boolean {
|
|
353
|
+
if (iouHistory.size >= 3) {
|
|
354
|
+
iouHistory.removeFirst()
|
|
355
|
+
}
|
|
356
|
+
iouHistory.addLast(rectangle)
|
|
357
|
+
if (iouHistory.size < 3) {
|
|
358
|
+
return false
|
|
359
|
+
}
|
|
360
|
+
val r0 = iouHistory.elementAt(0)
|
|
361
|
+
val r1 = iouHistory.elementAt(1)
|
|
362
|
+
val r2 = iouHistory.elementAt(2)
|
|
363
|
+
val iou01 = rectangleIou(r0, r1)
|
|
364
|
+
val iou12 = rectangleIou(r1, r2)
|
|
365
|
+
val iou02 = rectangleIou(r0, r2)
|
|
366
|
+
return iou01 >= 0.85 && iou12 >= 0.85 && iou02 >= 0.85
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
private fun clearIouHistory() {
|
|
370
|
+
iouHistory.clear()
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
private fun rectangleIou(a: Rectangle, b: Rectangle): Double {
|
|
374
|
+
fun bounds(r: Rectangle): DoubleArray {
|
|
375
|
+
val minX = min(min(r.topLeft.x, r.topRight.x), min(r.bottomLeft.x, r.bottomRight.x))
|
|
376
|
+
val maxX = max(max(r.topLeft.x, r.topRight.x), max(r.bottomLeft.x, r.bottomRight.x))
|
|
377
|
+
val minY = min(min(r.topLeft.y, r.topRight.y), min(r.bottomLeft.y, r.bottomRight.y))
|
|
378
|
+
val maxY = max(max(r.topLeft.y, r.topRight.y), max(r.bottomLeft.y, r.bottomRight.y))
|
|
379
|
+
return doubleArrayOf(minX, minY, maxX, maxY)
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
val ab = bounds(a)
|
|
383
|
+
val bb = bounds(b)
|
|
384
|
+
val interLeft = max(ab[0], bb[0])
|
|
385
|
+
val interTop = max(ab[1], bb[1])
|
|
386
|
+
val interRight = min(ab[2], bb[2])
|
|
387
|
+
val interBottom = min(ab[3], bb[3])
|
|
388
|
+
val interW = max(0.0, interRight - interLeft)
|
|
389
|
+
val interH = max(0.0, interBottom - interTop)
|
|
390
|
+
val interArea = interW * interH
|
|
391
|
+
val areaA = max(0.0, (ab[2] - ab[0])) * max(0.0, (ab[3] - ab[1]))
|
|
392
|
+
val areaB = max(0.0, (bb[2] - bb[0])) * max(0.0, (bb[3] - bb[1]))
|
|
393
|
+
val union = areaA + areaB - interArea
|
|
394
|
+
if (union <= 0.0) return 0.0
|
|
395
|
+
return interArea / union
|
|
396
|
+
}
|
|
397
|
+
|
|
340
398
|
fun capture() {
|
|
341
399
|
captureWithPromise(null)
|
|
342
400
|
}
|
package/dist/DocScanner.js
CHANGED
|
@@ -715,7 +715,7 @@ const NativeScanner = (0, react_1.forwardRef)(({ onCapture, overlayColor = DEFAU
|
|
|
715
715
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
716
716
|
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
717
717
|
return (react_1.default.createElement(react_native_1.View, { style: styles.container },
|
|
718
|
-
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly:
|
|
718
|
+
react_1.default.createElement(react_native_document_scanner_1.default, { ref: scannerRef, style: styles.scanner, detectionCountBeforeCapture: detectionThreshold, overlayColor: overlayColor, enableTorch: enableTorch, quality: normalizedQuality, useBase64: useBase64, manualOnly: react_native_1.Platform.OS === 'android', detectionConfig: detectionConfig, onPictureTaken: handlePictureTaken, onError: handleError, onRectangleDetect: handleRectangleDetect }),
|
|
719
719
|
showGrid && overlayPolygon && (react_1.default.createElement(overlay_1.ScannerOverlay, { active: overlayIsActive, color: gridColor ?? overlayColor, lineWidth: gridLineWidth, polygon: overlayPolygon, clipRect: react_native_1.Platform.OS === 'android' ? null : (detectedRectangle?.previewViewport ?? null) })),
|
|
720
720
|
showManualCaptureButton && (react_1.default.createElement(react_native_1.TouchableOpacity, { style: styles.button, onPress: handleManualCapture })),
|
|
721
721
|
children));
|
|
@@ -725,16 +725,11 @@ exports.DocScanner = (0, react_1.forwardRef)((props, ref) => {
|
|
|
725
725
|
if (react_native_1.Platform.OS !== 'android') {
|
|
726
726
|
return;
|
|
727
727
|
}
|
|
728
|
-
|
|
729
|
-
console.log('[DocScanner] Using VisionCamera pipeline');
|
|
730
|
-
}
|
|
731
|
-
else {
|
|
732
|
-
console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
|
|
733
|
-
hasVisionCameraModule: Boolean(visionCameraModule),
|
|
734
|
-
hasReanimated: Boolean(reanimatedModule),
|
|
735
|
-
});
|
|
736
|
-
}
|
|
728
|
+
console.log('[DocScanner] Using native CameraX pipeline on Android');
|
|
737
729
|
}, []);
|
|
730
|
+
if (react_native_1.Platform.OS === 'android') {
|
|
731
|
+
return react_1.default.createElement(NativeScanner, { ref: ref, ...props });
|
|
732
|
+
}
|
|
738
733
|
if (hasVisionCamera) {
|
|
739
734
|
return react_1.default.createElement(VisionCameraScanner, { ref: ref, ...props });
|
|
740
735
|
}
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -994,7 +994,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
994
994
|
: detectedRectangle?.rectangleOnScreen ?? detectedRectangle?.rectangleCoordinates ?? null;
|
|
995
995
|
const overlayIsActive = autoCapture ? isAutoCapturing : (detectedRectangle?.stableCounter ?? 0) > 0;
|
|
996
996
|
|
|
997
|
-
|
|
997
|
+
const detectionThreshold = autoCapture ? minStableFrames : 99999;
|
|
998
998
|
|
|
999
999
|
return (
|
|
1000
1000
|
<View style={styles.container}>
|
|
@@ -1006,7 +1006,7 @@ const NativeScanner = forwardRef<DocScannerHandle, Props>(
|
|
|
1006
1006
|
enableTorch={enableTorch}
|
|
1007
1007
|
quality={normalizedQuality}
|
|
1008
1008
|
useBase64={useBase64}
|
|
1009
|
-
manualOnly={
|
|
1009
|
+
manualOnly={Platform.OS === 'android'}
|
|
1010
1010
|
detectionConfig={detectionConfig}
|
|
1011
1011
|
onPictureTaken={handlePictureTaken}
|
|
1012
1012
|
onError={handleError}
|
|
@@ -1035,16 +1035,13 @@ export const DocScanner = forwardRef<DocScannerHandle, Props>((props, ref) => {
|
|
|
1035
1035
|
if (Platform.OS !== 'android') {
|
|
1036
1036
|
return;
|
|
1037
1037
|
}
|
|
1038
|
-
|
|
1039
|
-
console.log('[DocScanner] Using VisionCamera pipeline');
|
|
1040
|
-
} else {
|
|
1041
|
-
console.warn('[DocScanner] VisionCamera pipeline unavailable, falling back to native view.', {
|
|
1042
|
-
hasVisionCameraModule: Boolean(visionCameraModule),
|
|
1043
|
-
hasReanimated: Boolean(reanimatedModule),
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1038
|
+
console.log('[DocScanner] Using native CameraX pipeline on Android');
|
|
1046
1039
|
}, []);
|
|
1047
1040
|
|
|
1041
|
+
if (Platform.OS === 'android') {
|
|
1042
|
+
return <NativeScanner ref={ref} {...props} />;
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1048
1045
|
if (hasVisionCamera) {
|
|
1049
1046
|
return <VisionCameraScanner ref={ref} {...props} />;
|
|
1050
1047
|
}
|