react-native-rectangle-doc-scanner 3.241.0 → 3.242.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.
|
@@ -70,6 +70,8 @@ class CameraController(
|
|
|
70
70
|
.build()
|
|
71
71
|
)
|
|
72
72
|
private var lastRefineTimestamp = 0L
|
|
73
|
+
private var lastRectangle: Rectangle? = null
|
|
74
|
+
private var lastRectangleTimestamp = 0L
|
|
73
75
|
|
|
74
76
|
var onFrameAnalyzed: ((Rectangle?, Int, Int) -> Unit)? = null
|
|
75
77
|
|
|
@@ -213,7 +215,7 @@ class CameraController(
|
|
|
213
215
|
}
|
|
214
216
|
|
|
215
217
|
val previewSizes = streamConfigMap.getOutputSizes(SurfaceTexture::class.java)
|
|
216
|
-
previewSize = chooseBestSize(previewSizes, viewAspect, null)
|
|
218
|
+
previewSize = chooseBestSize(previewSizes, viewAspect, null, preferClosestAspect = true)
|
|
217
219
|
|
|
218
220
|
val analysisSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888)
|
|
219
221
|
analysisSize = chooseBestSize(analysisSizes, viewAspect, ANALYSIS_MAX_AREA)
|
|
@@ -390,7 +392,7 @@ class CameraController(
|
|
|
390
392
|
|
|
391
393
|
val frameWidth = if (rotationDegrees == 90 || rotationDegrees == 270) image.height else image.width
|
|
392
394
|
val frameHeight = if (rotationDegrees == 90 || rotationDegrees == 270) image.width else image.height
|
|
393
|
-
onFrameAnalyzed?.invoke(rectangle, frameWidth, frameHeight)
|
|
395
|
+
onFrameAnalyzed?.invoke(smoothRectangle(rectangle), frameWidth, frameHeight)
|
|
394
396
|
}
|
|
395
397
|
.addOnFailureListener { e ->
|
|
396
398
|
Log.e(TAG, "[CAMERA2] ML Kit detection failed", e)
|
|
@@ -490,26 +492,38 @@ class CameraController(
|
|
|
490
492
|
previewView.setTransform(matrix)
|
|
491
493
|
}
|
|
492
494
|
|
|
493
|
-
private fun chooseBestSize(
|
|
495
|
+
private fun chooseBestSize(
|
|
496
|
+
sizes: Array<Size>?,
|
|
497
|
+
targetAspect: Double,
|
|
498
|
+
maxArea: Int?,
|
|
499
|
+
preferClosestAspect: Boolean = false
|
|
500
|
+
): Size? {
|
|
494
501
|
if (sizes == null || sizes.isEmpty()) return null
|
|
495
502
|
val sorted = sizes.sortedByDescending { it.width * it.height }
|
|
496
503
|
|
|
497
|
-
val matching = sorted.filter {
|
|
498
|
-
val aspect = it.width.toDouble() / it.height.toDouble()
|
|
499
|
-
abs(aspect - targetAspect) <= ANALYSIS_ASPECT_TOLERANCE && (maxArea == null || it.width * it.height <= maxArea)
|
|
500
|
-
}
|
|
501
|
-
|
|
502
|
-
if (matching.isNotEmpty()) {
|
|
503
|
-
return matching.first()
|
|
504
|
-
}
|
|
505
|
-
|
|
506
504
|
val capped = if (maxArea != null) {
|
|
507
505
|
sorted.filter { it.width * it.height <= maxArea }
|
|
508
506
|
} else {
|
|
509
507
|
sorted
|
|
510
508
|
}
|
|
511
509
|
|
|
512
|
-
|
|
510
|
+
if (capped.isEmpty()) {
|
|
511
|
+
return sorted.first()
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (preferClosestAspect) {
|
|
515
|
+
return capped.minWithOrNull(
|
|
516
|
+
compareBy<Size> { abs(it.width.toDouble() / it.height.toDouble() - targetAspect) }
|
|
517
|
+
.thenByDescending { it.width * it.height }
|
|
518
|
+
)
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
val matching = capped.filter {
|
|
522
|
+
val aspect = it.width.toDouble() / it.height.toDouble()
|
|
523
|
+
abs(aspect - targetAspect) <= ANALYSIS_ASPECT_TOLERANCE
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
return matching.firstOrNull() ?: capped.first()
|
|
513
527
|
}
|
|
514
528
|
|
|
515
529
|
private fun rotateAndMirror(bitmap: Bitmap, rotationDegrees: Int, mirror: Boolean): Bitmap {
|
|
@@ -528,7 +542,7 @@ class CameraController(
|
|
|
528
542
|
|
|
529
543
|
private fun shouldRefineWithOpenCv(): Boolean {
|
|
530
544
|
val now = System.currentTimeMillis()
|
|
531
|
-
if (now - lastRefineTimestamp <
|
|
545
|
+
if (now - lastRefineTimestamp < 150) {
|
|
532
546
|
return false
|
|
533
547
|
}
|
|
534
548
|
lastRefineTimestamp = now
|
|
@@ -538,7 +552,16 @@ class CameraController(
|
|
|
538
552
|
private fun refineWithOpenCv(image: Image, rotationDegrees: Int, mlBox: Rect): Rectangle? {
|
|
539
553
|
return try {
|
|
540
554
|
val nv21 = imageToNv21(image)
|
|
541
|
-
val
|
|
555
|
+
val uprightWidth = if (rotationDegrees == 90 || rotationDegrees == 270) image.height else image.width
|
|
556
|
+
val uprightHeight = if (rotationDegrees == 90 || rotationDegrees == 270) image.width else image.height
|
|
557
|
+
val expanded = expandRect(mlBox, uprightWidth, uprightHeight, 0.2f)
|
|
558
|
+
val openCvRect = DocumentDetector.detectRectangleInYUVWithRoi(
|
|
559
|
+
nv21,
|
|
560
|
+
image.width,
|
|
561
|
+
image.height,
|
|
562
|
+
rotationDegrees,
|
|
563
|
+
expanded
|
|
564
|
+
)
|
|
542
565
|
if (openCvRect == null) {
|
|
543
566
|
null
|
|
544
567
|
} else {
|
|
@@ -560,6 +583,48 @@ class CameraController(
|
|
|
560
583
|
)
|
|
561
584
|
}
|
|
562
585
|
|
|
586
|
+
private fun expandRect(box: Rect, maxWidth: Int, maxHeight: Int, ratio: Float): Rect {
|
|
587
|
+
val padX = (box.width() * ratio).toInt()
|
|
588
|
+
val padY = (box.height() * ratio).toInt()
|
|
589
|
+
val left = (box.left - padX).coerceAtLeast(0)
|
|
590
|
+
val top = (box.top - padY).coerceAtLeast(0)
|
|
591
|
+
val right = (box.right + padX).coerceAtMost(maxWidth)
|
|
592
|
+
val bottom = (box.bottom + padY).coerceAtMost(maxHeight)
|
|
593
|
+
return Rect(left, top, right, bottom)
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
private fun smoothRectangle(current: Rectangle?): Rectangle? {
|
|
597
|
+
val now = System.currentTimeMillis()
|
|
598
|
+
val last = lastRectangle
|
|
599
|
+
if (current == null) {
|
|
600
|
+
if (last != null && now - lastRectangleTimestamp < 250) {
|
|
601
|
+
return last
|
|
602
|
+
}
|
|
603
|
+
lastRectangle = null
|
|
604
|
+
return null
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
val smoothed = if (last != null && now - lastRectangleTimestamp < 500) {
|
|
608
|
+
val t = 0.35
|
|
609
|
+
Rectangle(
|
|
610
|
+
Point(lerp(last.topLeft.x, current.topLeft.x, t), lerp(last.topLeft.y, current.topLeft.y, t)),
|
|
611
|
+
Point(lerp(last.topRight.x, current.topRight.x, t), lerp(last.topRight.y, current.topRight.y, t)),
|
|
612
|
+
Point(lerp(last.bottomLeft.x, current.bottomLeft.x, t), lerp(last.bottomLeft.y, current.bottomLeft.y, t)),
|
|
613
|
+
Point(lerp(last.bottomRight.x, current.bottomRight.x, t), lerp(last.bottomRight.y, current.bottomRight.y, t))
|
|
614
|
+
)
|
|
615
|
+
} else {
|
|
616
|
+
current
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
lastRectangle = smoothed
|
|
620
|
+
lastRectangleTimestamp = now
|
|
621
|
+
return smoothed
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
private fun lerp(start: Double, end: Double, t: Double): Double {
|
|
625
|
+
return start + (end - start) * t
|
|
626
|
+
}
|
|
627
|
+
|
|
563
628
|
private fun rectangleBounds(rectangle: Rectangle): Rect {
|
|
564
629
|
val left = listOf(rectangle.topLeft.x, rectangle.bottomLeft.x, rectangle.topRight.x, rectangle.bottomRight.x).minOrNull() ?: 0.0
|
|
565
630
|
val right = listOf(rectangle.topLeft.x, rectangle.bottomLeft.x, rectangle.topRight.x, rectangle.bottomRight.x).maxOrNull() ?: 0.0
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
package com.reactnativerectangledocscanner
|
|
2
2
|
|
|
3
3
|
import android.graphics.Bitmap
|
|
4
|
+
import android.graphics.Rect
|
|
4
5
|
import android.util.Log
|
|
5
6
|
import org.opencv.android.Utils
|
|
6
7
|
import org.opencv.core.*
|
|
@@ -98,6 +99,59 @@ class DocumentDetector {
|
|
|
98
99
|
return rectangle
|
|
99
100
|
}
|
|
100
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Detect rectangle within a region-of-interest (ROI) in YUV image.
|
|
104
|
+
* The ROI is specified in the rotated image coordinate space.
|
|
105
|
+
*/
|
|
106
|
+
fun detectRectangleInYUVWithRoi(
|
|
107
|
+
yuvBytes: ByteArray,
|
|
108
|
+
width: Int,
|
|
109
|
+
height: Int,
|
|
110
|
+
rotation: Int,
|
|
111
|
+
roi: Rect
|
|
112
|
+
): Rectangle? {
|
|
113
|
+
val yuvMat = Mat(height + height / 2, width, CvType.CV_8UC1)
|
|
114
|
+
yuvMat.put(0, 0, yuvBytes)
|
|
115
|
+
|
|
116
|
+
val rgbMat = Mat()
|
|
117
|
+
Imgproc.cvtColor(yuvMat, rgbMat, Imgproc.COLOR_YUV2RGB_NV21)
|
|
118
|
+
|
|
119
|
+
if (rotation != 0) {
|
|
120
|
+
val rotationCode = when (rotation) {
|
|
121
|
+
90 -> Core.ROTATE_90_CLOCKWISE
|
|
122
|
+
180 -> Core.ROTATE_180
|
|
123
|
+
270 -> Core.ROTATE_90_COUNTERCLOCKWISE
|
|
124
|
+
else -> null
|
|
125
|
+
}
|
|
126
|
+
if (rotationCode != null) {
|
|
127
|
+
Core.rotate(rgbMat, rgbMat, rotationCode)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
val x = roi.left.coerceIn(0, rgbMat.cols() - 1)
|
|
132
|
+
val y = roi.top.coerceIn(0, rgbMat.rows() - 1)
|
|
133
|
+
val right = roi.right.coerceIn(x + 1, rgbMat.cols())
|
|
134
|
+
val bottom = roi.bottom.coerceIn(y + 1, rgbMat.rows())
|
|
135
|
+
val w = right - x
|
|
136
|
+
val h = bottom - y
|
|
137
|
+
val roiRect = org.opencv.core.Rect(x, y, w, h)
|
|
138
|
+
|
|
139
|
+
val roiMat = Mat(rgbMat, roiRect)
|
|
140
|
+
val rectangle = detectRectangleInMat(roiMat)
|
|
141
|
+
roiMat.release()
|
|
142
|
+
yuvMat.release()
|
|
143
|
+
rgbMat.release()
|
|
144
|
+
|
|
145
|
+
return rectangle?.let {
|
|
146
|
+
Rectangle(
|
|
147
|
+
Point(it.topLeft.x + x, it.topLeft.y + y),
|
|
148
|
+
Point(it.topRight.x + x, it.topRight.y + y),
|
|
149
|
+
Point(it.bottomLeft.x + x, it.bottomLeft.y + y),
|
|
150
|
+
Point(it.bottomRight.x + x, it.bottomRight.y + y)
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
101
155
|
/**
|
|
102
156
|
* Core detection algorithm using OpenCV
|
|
103
157
|
*/
|
package/package.json
CHANGED