react-native-rectangle-doc-scanner 13.1.0 → 13.3.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.
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/CameraController.kt +12 -19
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +48 -25
- package/android/src/common/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +92 -0
- package/package.json +1 -1
|
@@ -53,7 +53,9 @@ class CameraController(
|
|
|
53
53
|
|
|
54
54
|
companion object {
|
|
55
55
|
private const val TAG = "CameraController"
|
|
56
|
-
|
|
56
|
+
// Increased resolution for better corner detection (was causing 640x480 downscale)
|
|
57
|
+
private const val ANALYSIS_TARGET_WIDTH = 1280
|
|
58
|
+
private const val ANALYSIS_TARGET_HEIGHT = 960
|
|
57
59
|
}
|
|
58
60
|
|
|
59
61
|
private fun getCameraSensorOrientation(): Int {
|
|
@@ -183,11 +185,11 @@ class CameraController(
|
|
|
183
185
|
Log.d(TAG, "[CAMERAX] SurfaceProvider set successfully")
|
|
184
186
|
}
|
|
185
187
|
|
|
186
|
-
// ImageAnalysis UseCase for document detection
|
|
188
|
+
// ImageAnalysis UseCase for document detection with fixed high resolution
|
|
187
189
|
imageAnalyzer = ImageAnalysis.Builder()
|
|
188
190
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
|
189
|
-
//
|
|
190
|
-
.
|
|
191
|
+
// Use explicit resolution instead of aspect ratio to prevent 640x480 downscale
|
|
192
|
+
.setTargetResolution(Size(ANALYSIS_TARGET_WIDTH, ANALYSIS_TARGET_HEIGHT))
|
|
191
193
|
.setTargetRotation(targetRotation) // Match preview rotation
|
|
192
194
|
.build()
|
|
193
195
|
.also {
|
|
@@ -494,10 +496,10 @@ class CameraController(
|
|
|
494
496
|
val finalWidth = imageWidth
|
|
495
497
|
val finalHeight = imageHeight
|
|
496
498
|
|
|
497
|
-
// Apply
|
|
499
|
+
// Apply center-crop scaling to match TextureView display.
|
|
498
500
|
val scaleX = viewWidth / finalWidth.toFloat()
|
|
499
501
|
val scaleY = viewHeight / finalHeight.toFloat()
|
|
500
|
-
val scale = scaleX.
|
|
502
|
+
val scale = scaleX.coerceAtLeast(scaleY)
|
|
501
503
|
|
|
502
504
|
val scaledWidth = finalWidth * scale
|
|
503
505
|
val scaledHeight = finalHeight * scale
|
|
@@ -583,26 +585,17 @@ class CameraController(
|
|
|
583
585
|
bufferHeight
|
|
584
586
|
}
|
|
585
587
|
|
|
586
|
-
// Scale to
|
|
588
|
+
// Scale to fill the view while maintaining aspect ratio (center-crop)
|
|
587
589
|
val scaleX = viewWidth.toFloat() / rotatedBufferWidth.toFloat()
|
|
588
590
|
val scaleY = viewHeight.toFloat() / rotatedBufferHeight.toFloat()
|
|
589
|
-
val scale = scaleX.
|
|
591
|
+
val scale = scaleX.coerceAtLeast(scaleY) // Use max to fill
|
|
590
592
|
|
|
591
593
|
Log.d(TAG, "[TRANSFORM] Rotated buffer: ${rotatedBufferWidth}x${rotatedBufferHeight}, ScaleX: $scaleX, ScaleY: $scaleY, Using: $scale")
|
|
592
594
|
|
|
593
595
|
matrix.postScale(scale, scale, centerX, centerY)
|
|
594
596
|
|
|
595
|
-
//
|
|
596
|
-
|
|
597
|
-
val scaledHeight = rotatedBufferHeight * scale
|
|
598
|
-
val offsetX = (viewWidth - scaledWidth) / 2f
|
|
599
|
-
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
600
|
-
previewViewport = android.graphics.RectF(
|
|
601
|
-
offsetX,
|
|
602
|
-
offsetY,
|
|
603
|
-
offsetX + scaledWidth,
|
|
604
|
-
offsetY + scaledHeight
|
|
605
|
-
)
|
|
597
|
+
// With center-crop, the preview fully covers the view bounds.
|
|
598
|
+
previewViewport = android.graphics.RectF(0f, 0f, viewWidth.toFloat(), viewHeight.toFloat())
|
|
606
599
|
|
|
607
600
|
textureView.setTransform(matrix)
|
|
608
601
|
Log.d(TAG, "[TRANSFORM] Transform applied successfully")
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -67,7 +67,8 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
67
67
|
|
|
68
68
|
companion object {
|
|
69
69
|
private const val TAG = "DocumentScannerView"
|
|
70
|
-
|
|
70
|
+
// Removed fixed aspect ratio to prevent squished preview on tablets
|
|
71
|
+
// Preview will now properly fill the view using center-crop scaling
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
override val lifecycle: Lifecycle
|
|
@@ -150,24 +151,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
150
151
|
private fun layoutPreviewAndOverlay(viewWidth: Int, viewHeight: Int) {
|
|
151
152
|
if (viewWidth <= 0 || viewHeight <= 0) return
|
|
152
153
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
targetWidth = viewWidth
|
|
158
|
-
targetHeight = aspectHeight
|
|
159
|
-
} else {
|
|
160
|
-
targetWidth = (viewHeight * PREVIEW_ASPECT_RATIO).toInt()
|
|
161
|
-
targetHeight = viewHeight
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
val left = (viewWidth - targetWidth) / 2
|
|
165
|
-
val top = if (targetHeight < viewHeight) 0 else (viewHeight - targetHeight) / 2
|
|
166
|
-
val right = left + targetWidth
|
|
167
|
-
val bottom = top + targetHeight
|
|
168
|
-
|
|
169
|
-
previewView.layout(left, top, right, bottom)
|
|
170
|
-
overlayView.layout(left, top, right, bottom)
|
|
154
|
+
// Fill entire view to prevent squished appearance on tablets
|
|
155
|
+
// CameraX will handle aspect ratio via center-crop in updateTextureViewTransform
|
|
156
|
+
previewView.layout(0, 0, viewWidth, viewHeight)
|
|
157
|
+
overlayView.layout(0, 0, viewWidth, viewHeight)
|
|
171
158
|
}
|
|
172
159
|
|
|
173
160
|
private fun initializeCameraWhenReady() {
|
|
@@ -270,36 +257,72 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
270
257
|
}
|
|
271
258
|
}
|
|
272
259
|
|
|
260
|
+
// Enhanced snap: Track corner movement per frame
|
|
261
|
+
private var consecutiveStableFrames = 0
|
|
262
|
+
private val requiredStableFrames = 3 // Require 3 consecutive stable frames for snap
|
|
263
|
+
|
|
273
264
|
private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
|
|
274
265
|
if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
|
|
275
266
|
lastSmoothedRectangleOnScreen = null
|
|
267
|
+
consecutiveStableFrames = 0
|
|
276
268
|
return null
|
|
277
269
|
}
|
|
278
270
|
|
|
279
271
|
val prev = lastSmoothedRectangleOnScreen
|
|
280
272
|
if (prev == null) {
|
|
281
273
|
lastSmoothedRectangleOnScreen = rectangle
|
|
274
|
+
consecutiveStableFrames = 0
|
|
282
275
|
return rectangle
|
|
283
276
|
}
|
|
284
277
|
|
|
285
278
|
val minDim = min(viewWidth, viewHeight).toDouble()
|
|
286
|
-
|
|
287
|
-
val
|
|
288
|
-
val
|
|
279
|
+
// Tighter snap threshold for better stability
|
|
280
|
+
val snapThreshold = max(10.0, minDim * 0.012)
|
|
281
|
+
val smoothThreshold = max(20.0, minDim * 0.05)
|
|
282
|
+
|
|
283
|
+
// Calculate max corner movement
|
|
284
|
+
val maxCornerDist = max(
|
|
289
285
|
maxCornerDistance(prev, rectangle),
|
|
290
286
|
maxCornerDistance(rectangle, prev)
|
|
291
287
|
)
|
|
292
288
|
|
|
289
|
+
// Enhanced snap logic: Check individual corner distances
|
|
290
|
+
val allCornersStable = areAllCornersStable(prev, rectangle, snapThreshold)
|
|
291
|
+
|
|
293
292
|
val result = when {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
293
|
+
// Snap only if all corners are stable AND we have consecutive stable frames
|
|
294
|
+
allCornersStable && maxCornerDist <= snapThreshold -> {
|
|
295
|
+
consecutiveStableFrames++
|
|
296
|
+
if (consecutiveStableFrames >= requiredStableFrames) {
|
|
297
|
+
prev // Lock to previous position
|
|
298
|
+
} else {
|
|
299
|
+
blendRectangle(prev, rectangle, 0.25) // Small blend while building stability
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
maxCornerDist <= smoothThreshold -> {
|
|
303
|
+
consecutiveStableFrames = 0
|
|
304
|
+
blendRectangle(prev, rectangle, 0.35)
|
|
305
|
+
}
|
|
306
|
+
else -> {
|
|
307
|
+
consecutiveStableFrames = 0
|
|
308
|
+
rectangle
|
|
309
|
+
}
|
|
297
310
|
}
|
|
298
311
|
|
|
299
312
|
lastSmoothedRectangleOnScreen = result
|
|
300
313
|
return result
|
|
301
314
|
}
|
|
302
315
|
|
|
316
|
+
/**
|
|
317
|
+
* Check if all 4 corners are within the snap threshold (not just max distance)
|
|
318
|
+
*/
|
|
319
|
+
private fun areAllCornersStable(prev: Rectangle, current: Rectangle, threshold: Double): Boolean {
|
|
320
|
+
return distance(prev.topLeft, current.topLeft) <= threshold &&
|
|
321
|
+
distance(prev.topRight, current.topRight) <= threshold &&
|
|
322
|
+
distance(prev.bottomLeft, current.bottomLeft) <= threshold &&
|
|
323
|
+
distance(prev.bottomRight, current.bottomRight) <= threshold
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
|
|
304
327
|
return max(
|
|
305
328
|
max(
|
|
@@ -436,9 +436,101 @@ class DocumentDetector {
|
|
|
436
436
|
if (aspect < 0.45 || aspect > 2.8) {
|
|
437
437
|
return false
|
|
438
438
|
}
|
|
439
|
+
|
|
440
|
+
// Enhanced validation: Check corner angles (should be close to 90°)
|
|
441
|
+
if (!hasValidCornerAngles(rectangle)) {
|
|
442
|
+
return false
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// Enhanced validation: Check edge straightness
|
|
446
|
+
if (!hasValidEdgeStraightness(rectangle, srcMat)) {
|
|
447
|
+
return false
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Enhanced validation: Check margin from view edges (avoid detecting screen borders)
|
|
451
|
+
val margin = minDim * 0.05 // 5% margin
|
|
452
|
+
if (rectangle.topLeft.x < margin || rectangle.topLeft.y < margin ||
|
|
453
|
+
rectangle.topRight.x > srcMat.cols() - margin || rectangle.topRight.y < margin ||
|
|
454
|
+
rectangle.bottomLeft.x < margin || rectangle.bottomLeft.y > srcMat.rows() - margin ||
|
|
455
|
+
rectangle.bottomRight.x > srcMat.cols() - margin || rectangle.bottomRight.y > srcMat.rows() - margin) {
|
|
456
|
+
return false
|
|
457
|
+
}
|
|
458
|
+
|
|
439
459
|
return true
|
|
440
460
|
}
|
|
441
461
|
|
|
462
|
+
/**
|
|
463
|
+
* Check if all corners have angles close to 90° (within 60°-120° range)
|
|
464
|
+
*/
|
|
465
|
+
private fun hasValidCornerAngles(rectangle: Rectangle): Boolean {
|
|
466
|
+
fun angleAt(p1: Point, vertex: Point, p2: Point): Double {
|
|
467
|
+
val v1x = p1.x - vertex.x
|
|
468
|
+
val v1y = p1.y - vertex.y
|
|
469
|
+
val v2x = p2.x - vertex.x
|
|
470
|
+
val v2y = p2.y - vertex.y
|
|
471
|
+
|
|
472
|
+
val dot = v1x * v2x + v1y * v2y
|
|
473
|
+
val len1 = sqrt(v1x * v1x + v1y * v1y)
|
|
474
|
+
val len2 = sqrt(v2x * v2x + v2y * v2y)
|
|
475
|
+
|
|
476
|
+
if (len1 < 1.0 || len2 < 1.0) return 90.0
|
|
477
|
+
|
|
478
|
+
val cosAngle = (dot / (len1 * len2)).coerceIn(-1.0, 1.0)
|
|
479
|
+
return Math.toDegrees(kotlin.math.acos(cosAngle))
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
val angleTL = angleAt(rectangle.topRight, rectangle.topLeft, rectangle.bottomLeft)
|
|
483
|
+
val angleTR = angleAt(rectangle.topLeft, rectangle.topRight, rectangle.bottomRight)
|
|
484
|
+
val angleBL = angleAt(rectangle.topLeft, rectangle.bottomLeft, rectangle.bottomRight)
|
|
485
|
+
val angleBR = angleAt(rectangle.topRight, rectangle.bottomRight, rectangle.bottomLeft)
|
|
486
|
+
|
|
487
|
+
// All angles should be within 60°-120° (allow ±30° from 90°)
|
|
488
|
+
return angleTL in 60.0..120.0 && angleTR in 60.0..120.0 &&
|
|
489
|
+
angleBL in 60.0..120.0 && angleBR in 60.0..120.0
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Check if edges are sufficiently straight (low deviation from fitted line)
|
|
494
|
+
*/
|
|
495
|
+
private fun hasValidEdgeStraightness(rectangle: Rectangle, srcMat: Mat): Boolean {
|
|
496
|
+
val minDim = min(srcMat.cols(), srcMat.rows()).toDouble()
|
|
497
|
+
val maxDeviation = max(10.0, minDim * 0.02) // Allow 2% deviation
|
|
498
|
+
|
|
499
|
+
fun checkEdgeStraightness(p1: Point, p2: Point, p3: Point, p4: Point): Boolean {
|
|
500
|
+
// Check if points p1-p2 and p3-p4 form roughly parallel lines
|
|
501
|
+
val dx1 = p2.x - p1.x
|
|
502
|
+
val dy1 = p2.y - p1.y
|
|
503
|
+
val dx2 = p4.x - p3.x
|
|
504
|
+
val dy2 = p4.y - p3.y
|
|
505
|
+
|
|
506
|
+
val len1 = sqrt(dx1 * dx1 + dy1 * dy1)
|
|
507
|
+
val len2 = sqrt(dx2 * dx2 + dy2 * dy2)
|
|
508
|
+
|
|
509
|
+
if (len1 < 1.0 || len2 < 1.0) return true
|
|
510
|
+
|
|
511
|
+
// Normalize and compute angle difference
|
|
512
|
+
val dot = (dx1 * dx2 + dy1 * dy2) / (len1 * len2)
|
|
513
|
+
val angleDiff = Math.toDegrees(kotlin.math.acos(dot.coerceIn(-1.0, 1.0)))
|
|
514
|
+
|
|
515
|
+
// Parallel lines should have angle diff close to 0° or 180°
|
|
516
|
+
return angleDiff < 15.0 || angleDiff > 165.0
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Check top vs bottom edges
|
|
520
|
+
val topBottomOk = checkEdgeStraightness(
|
|
521
|
+
rectangle.topLeft, rectangle.topRight,
|
|
522
|
+
rectangle.bottomLeft, rectangle.bottomRight
|
|
523
|
+
)
|
|
524
|
+
|
|
525
|
+
// Check left vs right edges
|
|
526
|
+
val leftRightOk = checkEdgeStraightness(
|
|
527
|
+
rectangle.topLeft, rectangle.bottomLeft,
|
|
528
|
+
rectangle.topRight, rectangle.bottomRight
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
return topBottomOk && leftRightOk
|
|
532
|
+
}
|
|
533
|
+
|
|
442
534
|
/**
|
|
443
535
|
* Evaluate rectangle quality (matching iOS logic)
|
|
444
536
|
*/
|