react-native-rectangle-doc-scanner 10.22.0 → 10.24.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 +26 -7
- package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +74 -9
- package/android/src/common/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +5 -5
- package/dist/DocScanner.js +15 -5
- package/package.json +1 -1
- package/src/DocScanner.tsx +20 -5
|
@@ -355,7 +355,22 @@ class CameraController(
|
|
|
355
355
|
mlBox: android.graphics.Rect?
|
|
356
356
|
): Rectangle? {
|
|
357
357
|
return try {
|
|
358
|
-
|
|
358
|
+
if (mlBox != null) {
|
|
359
|
+
val frameWidth = if (rotation == 90 || rotation == 270) height else width
|
|
360
|
+
val frameHeight = if (rotation == 90 || rotation == 270) width else height
|
|
361
|
+
val padX = (mlBox.width() * 0.15f).toInt().coerceAtLeast(24)
|
|
362
|
+
val padY = (mlBox.height() * 0.15f).toInt().coerceAtLeast(24)
|
|
363
|
+
val roi = android.graphics.Rect(
|
|
364
|
+
(mlBox.left - padX).coerceAtLeast(0),
|
|
365
|
+
(mlBox.top - padY).coerceAtLeast(0),
|
|
366
|
+
(mlBox.right + padX).coerceAtMost(frameWidth),
|
|
367
|
+
(mlBox.bottom + padY).coerceAtMost(frameHeight)
|
|
368
|
+
)
|
|
369
|
+
DocumentDetector.detectRectangleInYUVWithRoi(nv21, width, height, rotation, roi)
|
|
370
|
+
?: DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
|
|
371
|
+
} else {
|
|
372
|
+
DocumentDetector.detectRectangleInYUV(nv21, width, height, rotation)
|
|
373
|
+
}
|
|
359
374
|
} catch (e: Exception) {
|
|
360
375
|
Log.w(TAG, "[CAMERAX] OpenCV detection failed", e)
|
|
361
376
|
null
|
|
@@ -421,20 +436,24 @@ class CameraController(
|
|
|
421
436
|
fun mapRectangleToView(rectangle: Rectangle?, imageWidth: Int, imageHeight: Int): Rectangle? {
|
|
422
437
|
if (rectangle == null || imageWidth <= 0 || imageHeight <= 0) return null
|
|
423
438
|
|
|
424
|
-
//
|
|
439
|
+
// Fit-center scaling to avoid zoom/crop and keep mapping aligned with preview.
|
|
425
440
|
val viewWidth = textureView.width.toFloat()
|
|
426
441
|
val viewHeight = textureView.height.toFloat()
|
|
427
442
|
|
|
428
443
|
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
429
444
|
|
|
430
|
-
// Simple proportional scaling
|
|
431
445
|
val scaleX = viewWidth / imageWidth.toFloat()
|
|
432
446
|
val scaleY = viewHeight / imageHeight.toFloat()
|
|
447
|
+
val scale = scaleX.coerceAtMost(scaleY)
|
|
448
|
+
val scaledWidth = imageWidth * scale
|
|
449
|
+
val scaledHeight = imageHeight * scale
|
|
450
|
+
val offsetX = (viewWidth - scaledWidth) / 2f
|
|
451
|
+
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
433
452
|
|
|
434
453
|
fun scalePoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
435
454
|
return org.opencv.core.Point(
|
|
436
|
-
point.x *
|
|
437
|
-
point.y *
|
|
455
|
+
point.x * scale + offsetX,
|
|
456
|
+
point.y * scale + offsetY
|
|
438
457
|
)
|
|
439
458
|
}
|
|
440
459
|
|
|
@@ -509,10 +528,10 @@ class CameraController(
|
|
|
509
528
|
bufferHeight
|
|
510
529
|
}
|
|
511
530
|
|
|
512
|
-
// Scale to
|
|
531
|
+
// Scale to fit within the view while maintaining aspect ratio (no zoom/crop)
|
|
513
532
|
val scaleX = viewWidth.toFloat() / rotatedBufferWidth.toFloat()
|
|
514
533
|
val scaleY = viewHeight.toFloat() / rotatedBufferHeight.toFloat()
|
|
515
|
-
val scale = scaleX.
|
|
534
|
+
val scale = scaleX.coerceAtMost(scaleY) // Use min to fit
|
|
516
535
|
|
|
517
536
|
Log.d(TAG, "[TRANSFORM] Rotated buffer: ${rotatedBufferWidth}x${rotatedBufferHeight}, ScaleX: $scaleX, ScaleY: $scaleY, Using: $scale")
|
|
518
537
|
|
package/android/src/camera2/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt
CHANGED
|
@@ -25,6 +25,7 @@ import kotlinx.coroutines.delay
|
|
|
25
25
|
import java.io.File
|
|
26
26
|
import kotlin.math.min
|
|
27
27
|
import kotlin.math.max
|
|
28
|
+
import kotlin.math.hypot
|
|
28
29
|
|
|
29
30
|
class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), LifecycleOwner {
|
|
30
31
|
private val themedContext = context
|
|
@@ -58,6 +59,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
58
59
|
private var lastDetectedImageWidth = 0
|
|
59
60
|
private var lastDetectedImageHeight = 0
|
|
60
61
|
private var lastRectangleOnScreen: Rectangle? = null
|
|
62
|
+
private var lastSmoothedRectangleOnScreen: Rectangle? = null
|
|
61
63
|
|
|
62
64
|
// Coroutine scope for async operations
|
|
63
65
|
private val scope = CoroutineScope(Dispatchers.Main + SupervisorJob())
|
|
@@ -215,19 +217,82 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
215
217
|
} else {
|
|
216
218
|
null
|
|
217
219
|
}
|
|
218
|
-
|
|
220
|
+
val smoothedRectangleOnScreen = smoothRectangle(rectangleOnScreen, width, height)
|
|
221
|
+
lastRectangleOnScreen = smoothedRectangleOnScreen
|
|
219
222
|
val quality = when {
|
|
220
|
-
|
|
221
|
-
DocumentDetector.evaluateRectangleQualityInView(
|
|
223
|
+
smoothedRectangleOnScreen != null && width > 0 && height > 0 ->
|
|
224
|
+
DocumentDetector.evaluateRectangleQualityInView(smoothedRectangleOnScreen, width, height)
|
|
222
225
|
rectangle != null -> DocumentDetector.evaluateRectangleQuality(rectangle, imageWidth, imageHeight)
|
|
223
226
|
else -> RectangleQuality.TOO_FAR
|
|
224
227
|
}
|
|
225
228
|
|
|
226
229
|
post {
|
|
227
|
-
onRectangleDetected(
|
|
230
|
+
onRectangleDetected(smoothedRectangleOnScreen, rectangle, quality, imageWidth, imageHeight)
|
|
228
231
|
}
|
|
229
232
|
}
|
|
230
233
|
|
|
234
|
+
private fun smoothRectangle(rectangle: Rectangle?, viewWidth: Int, viewHeight: Int): Rectangle? {
|
|
235
|
+
if (rectangle == null || viewWidth <= 0 || viewHeight <= 0) {
|
|
236
|
+
lastSmoothedRectangleOnScreen = null
|
|
237
|
+
return null
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
val prev = lastSmoothedRectangleOnScreen
|
|
241
|
+
if (prev == null) {
|
|
242
|
+
lastSmoothedRectangleOnScreen = rectangle
|
|
243
|
+
return rectangle
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
val minDim = min(viewWidth, viewHeight).toDouble()
|
|
247
|
+
val snapThreshold = max(12.0, minDim * 0.015)
|
|
248
|
+
val smoothThreshold = max(24.0, minDim * 0.06)
|
|
249
|
+
val maxCornerDistance = max(
|
|
250
|
+
maxCornerDistance(prev, rectangle),
|
|
251
|
+
maxCornerDistance(rectangle, prev)
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
val result = when {
|
|
255
|
+
maxCornerDistance <= snapThreshold -> prev
|
|
256
|
+
maxCornerDistance <= smoothThreshold -> blendRectangle(prev, rectangle, 0.35)
|
|
257
|
+
else -> rectangle
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
lastSmoothedRectangleOnScreen = result
|
|
261
|
+
return result
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private fun maxCornerDistance(a: Rectangle, b: Rectangle): Double {
|
|
265
|
+
return max(
|
|
266
|
+
max(
|
|
267
|
+
distance(a.topLeft, b.topLeft),
|
|
268
|
+
distance(a.topRight, b.topRight)
|
|
269
|
+
),
|
|
270
|
+
max(
|
|
271
|
+
distance(a.bottomLeft, b.bottomLeft),
|
|
272
|
+
distance(a.bottomRight, b.bottomRight)
|
|
273
|
+
)
|
|
274
|
+
)
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
private fun distance(p1: Point, p2: Point): Double {
|
|
278
|
+
return hypot(p1.x - p2.x, p1.y - p2.y)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
private fun blendRectangle(a: Rectangle, b: Rectangle, t: Double): Rectangle {
|
|
282
|
+
fun lerp(p1: Point, p2: Point): Point {
|
|
283
|
+
val x = p1.x + (p2.x - p1.x) * t
|
|
284
|
+
val y = p1.y + (p2.y - p1.y) * t
|
|
285
|
+
return Point(x, y)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return Rectangle(
|
|
289
|
+
lerp(a.topLeft, b.topLeft),
|
|
290
|
+
lerp(a.topRight, b.topRight),
|
|
291
|
+
lerp(a.bottomLeft, b.bottomLeft),
|
|
292
|
+
lerp(a.bottomRight, b.bottomRight)
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
231
296
|
private fun onRectangleDetected(
|
|
232
297
|
rectangleOnScreen: Rectangle?,
|
|
233
298
|
rectangleCoordinates: Rectangle?,
|
|
@@ -684,18 +749,18 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
684
749
|
if (viewWidth == 0 || viewHeight == 0 || imageWidth == 0 || imageHeight == 0) {
|
|
685
750
|
return rectangle
|
|
686
751
|
}
|
|
687
|
-
val scale =
|
|
752
|
+
val scale = min(
|
|
688
753
|
viewWidth.toDouble() / imageWidth.toDouble(),
|
|
689
754
|
viewHeight.toDouble() / imageHeight.toDouble()
|
|
690
755
|
)
|
|
691
756
|
val scaledImageWidth = imageWidth.toDouble() * scale
|
|
692
757
|
val scaledImageHeight = imageHeight.toDouble() * scale
|
|
693
|
-
val offsetX = (
|
|
694
|
-
val offsetY = (
|
|
758
|
+
val offsetX = (viewWidth - scaledImageWidth) / 2.0
|
|
759
|
+
val offsetY = (viewHeight - scaledImageHeight) / 2.0
|
|
695
760
|
|
|
696
761
|
fun mapPoint(point: Point): Point {
|
|
697
|
-
val x = (point.x
|
|
698
|
-
val y = (point.y
|
|
762
|
+
val x = (point.x - offsetX) / scale
|
|
763
|
+
val y = (point.y - offsetY) / scale
|
|
699
764
|
return Point(
|
|
700
765
|
x.coerceIn(0.0, imageWidth.toDouble()),
|
|
701
766
|
y.coerceIn(0.0, imageHeight.toDouble())
|
|
@@ -421,19 +421,19 @@ class DocumentDetector {
|
|
|
421
421
|
return rectangle
|
|
422
422
|
}
|
|
423
423
|
|
|
424
|
-
val scale =
|
|
424
|
+
val scale = min(
|
|
425
425
|
viewWidth.toDouble() / imageWidth.toDouble(),
|
|
426
426
|
viewHeight.toDouble() / imageHeight.toDouble()
|
|
427
427
|
)
|
|
428
428
|
|
|
429
429
|
val scaledImageWidth = imageWidth * scale
|
|
430
430
|
val scaledImageHeight = imageHeight * scale
|
|
431
|
-
val offsetX = (
|
|
432
|
-
val offsetY = (
|
|
431
|
+
val offsetX = (viewWidth - scaledImageWidth) / 2.0
|
|
432
|
+
val offsetY = (viewHeight - scaledImageHeight) / 2.0
|
|
433
433
|
|
|
434
434
|
fun mapPoint(point: Point): Point {
|
|
435
|
-
val x = (point.x * scale)
|
|
436
|
-
val y = (point.y * scale)
|
|
435
|
+
val x = (point.x * scale) + offsetX
|
|
436
|
+
val y = (point.y * scale) + offsetY
|
|
437
437
|
return Point(
|
|
438
438
|
x.coerceIn(0.0, viewWidth.toDouble()),
|
|
439
439
|
y.coerceIn(0.0, viewHeight.toDouble())
|
package/dist/DocScanner.js
CHANGED
|
@@ -121,14 +121,24 @@ const mirrorRectangleHorizontally = (rectangle, imageWidth) => ({
|
|
|
121
121
|
const mapRectangleToView = (rectangle, imageWidth, imageHeight, viewWidth, viewHeight, density) => {
|
|
122
122
|
const viewWidthPx = viewWidth * density;
|
|
123
123
|
const viewHeightPx = viewHeight * density;
|
|
124
|
-
const scale =
|
|
124
|
+
const scale = react_native_1.Platform.OS === 'ios'
|
|
125
|
+
? Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight)
|
|
126
|
+
: Math.min(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
|
|
125
127
|
const scaledImageWidth = imageWidth * scale;
|
|
126
128
|
const scaledImageHeight = imageHeight * scale;
|
|
127
|
-
const offsetX =
|
|
128
|
-
|
|
129
|
+
const offsetX = react_native_1.Platform.OS === 'ios'
|
|
130
|
+
? (scaledImageWidth - viewWidthPx) / 2
|
|
131
|
+
: (viewWidthPx - scaledImageWidth) / 2;
|
|
132
|
+
const offsetY = react_native_1.Platform.OS === 'ios'
|
|
133
|
+
? (scaledImageHeight - viewHeightPx) / 2
|
|
134
|
+
: (viewHeightPx - scaledImageHeight) / 2;
|
|
129
135
|
const mapPoint = (point) => ({
|
|
130
|
-
x:
|
|
131
|
-
|
|
136
|
+
x: react_native_1.Platform.OS === 'ios'
|
|
137
|
+
? (point.x * scale - offsetX) / density
|
|
138
|
+
: (point.x * scale + offsetX) / density,
|
|
139
|
+
y: react_native_1.Platform.OS === 'ios'
|
|
140
|
+
? (point.y * scale - offsetY) / density
|
|
141
|
+
: (point.y * scale + offsetY) / density,
|
|
132
142
|
});
|
|
133
143
|
return {
|
|
134
144
|
topLeft: mapPoint(rectangle.topLeft),
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -188,15 +188,30 @@ const mapRectangleToView = (
|
|
|
188
188
|
): Rectangle => {
|
|
189
189
|
const viewWidthPx = viewWidth * density;
|
|
190
190
|
const viewHeightPx = viewHeight * density;
|
|
191
|
-
const scale =
|
|
191
|
+
const scale =
|
|
192
|
+
Platform.OS === 'ios'
|
|
193
|
+
? Math.max(viewWidthPx / imageWidth, viewHeightPx / imageHeight)
|
|
194
|
+
: Math.min(viewWidthPx / imageWidth, viewHeightPx / imageHeight);
|
|
192
195
|
const scaledImageWidth = imageWidth * scale;
|
|
193
196
|
const scaledImageHeight = imageHeight * scale;
|
|
194
|
-
const offsetX =
|
|
195
|
-
|
|
197
|
+
const offsetX =
|
|
198
|
+
Platform.OS === 'ios'
|
|
199
|
+
? (scaledImageWidth - viewWidthPx) / 2
|
|
200
|
+
: (viewWidthPx - scaledImageWidth) / 2;
|
|
201
|
+
const offsetY =
|
|
202
|
+
Platform.OS === 'ios'
|
|
203
|
+
? (scaledImageHeight - viewHeightPx) / 2
|
|
204
|
+
: (viewHeightPx - scaledImageHeight) / 2;
|
|
196
205
|
|
|
197
206
|
const mapPoint = (point: Point): Point => ({
|
|
198
|
-
x:
|
|
199
|
-
|
|
207
|
+
x:
|
|
208
|
+
Platform.OS === 'ios'
|
|
209
|
+
? (point.x * scale - offsetX) / density
|
|
210
|
+
: (point.x * scale + offsetX) / density,
|
|
211
|
+
y:
|
|
212
|
+
Platform.OS === 'ios'
|
|
213
|
+
? (point.y * scale - offsetY) / density
|
|
214
|
+
: (point.y * scale + offsetY) / density,
|
|
200
215
|
});
|
|
201
216
|
|
|
202
217
|
return {
|