react-native-rectangle-doc-scanner 13.2.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 +6 -4
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +48 -19
- 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 {
|
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,18 +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
|
-
val left = 0
|
|
158
|
-
// Center vertically; allows top/bottom crop when targetHeight > viewHeight.
|
|
159
|
-
val top = (viewHeight - targetHeight) / 2
|
|
160
|
-
val right = left + targetWidth
|
|
161
|
-
val bottom = top + targetHeight
|
|
162
|
-
|
|
163
|
-
previewView.layout(left, top, right, bottom)
|
|
164
|
-
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)
|
|
165
158
|
}
|
|
166
159
|
|
|
167
160
|
private fun initializeCameraWhenReady() {
|
|
@@ -264,36 +257,72 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
264
257
|
}
|
|
265
258
|
}
|
|
266
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
|
+
|
|
267
264
|
private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
|
|
268
265
|
if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
|
|
269
266
|
lastSmoothedRectangleOnScreen = null
|
|
267
|
+
consecutiveStableFrames = 0
|
|
270
268
|
return null
|
|
271
269
|
}
|
|
272
270
|
|
|
273
271
|
val prev = lastSmoothedRectangleOnScreen
|
|
274
272
|
if (prev == null) {
|
|
275
273
|
lastSmoothedRectangleOnScreen = rectangle
|
|
274
|
+
consecutiveStableFrames = 0
|
|
276
275
|
return rectangle
|
|
277
276
|
}
|
|
278
277
|
|
|
279
278
|
val minDim = min(viewWidth, viewHeight).toDouble()
|
|
280
|
-
|
|
281
|
-
val
|
|
282
|
-
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(
|
|
283
285
|
maxCornerDistance(prev, rectangle),
|
|
284
286
|
maxCornerDistance(rectangle, prev)
|
|
285
287
|
)
|
|
286
288
|
|
|
289
|
+
// Enhanced snap logic: Check individual corner distances
|
|
290
|
+
val allCornersStable = areAllCornersStable(prev, rectangle, snapThreshold)
|
|
291
|
+
|
|
287
292
|
val result = when {
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
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
|
+
}
|
|
291
310
|
}
|
|
292
311
|
|
|
293
312
|
lastSmoothedRectangleOnScreen = result
|
|
294
313
|
return result
|
|
295
314
|
}
|
|
296
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
|
+
|
|
297
326
|
private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
|
|
298
327
|
return max(
|
|
299
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
|
*/
|