react-native-rectangle-doc-scanner 10.30.0 → 10.32.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.
|
@@ -433,36 +433,83 @@ class CameraController(
|
|
|
433
433
|
Log.d(TAG, "[CAMERAX] Transform refresh requested - handled automatically")
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
-
//
|
|
436
|
+
// Coordinate mapping aligned with TextureView transform
|
|
437
437
|
fun mapRectangleToView(rectangle: Rectangle?, imageWidth: Int, imageHeight: Int): Rectangle? {
|
|
438
438
|
if (rectangle == null || imageWidth <= 0 || imageHeight <= 0) return null
|
|
439
439
|
|
|
440
|
-
// Fit-center scaling to avoid zoom/crop and keep mapping aligned with preview.
|
|
441
440
|
val viewWidth = textureView.width.toFloat()
|
|
442
441
|
val viewHeight = textureView.height.toFloat()
|
|
443
442
|
|
|
444
443
|
if (viewWidth <= 0 || viewHeight <= 0) return null
|
|
445
444
|
|
|
446
|
-
|
|
447
|
-
val
|
|
448
|
-
val
|
|
449
|
-
|
|
450
|
-
|
|
445
|
+
// Get sensor orientation to match transform rotation
|
|
446
|
+
val sensorOrientation = getCameraSensorOrientation()
|
|
447
|
+
val displayRotationDegrees = when (textureView.display?.rotation ?: Surface.ROTATION_0) {
|
|
448
|
+
Surface.ROTATION_0 -> 0
|
|
449
|
+
Surface.ROTATION_90 -> 90
|
|
450
|
+
Surface.ROTATION_180 -> 180
|
|
451
|
+
Surface.ROTATION_270 -> 270
|
|
452
|
+
else -> 0
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
val tabletUpsideDownFix = if (sensorOrientation == 0 && displayRotationDegrees == 90) 180 else 0
|
|
456
|
+
val rotationDegrees = ((displayRotationDegrees + tabletUpsideDownFix) % 360).toFloat()
|
|
457
|
+
|
|
458
|
+
// First, apply rotation to point coordinates
|
|
459
|
+
fun rotatePoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
460
|
+
return when (rotationDegrees.toInt()) {
|
|
461
|
+
90 -> org.opencv.core.Point(
|
|
462
|
+
imageHeight - point.y,
|
|
463
|
+
point.x
|
|
464
|
+
)
|
|
465
|
+
180 -> org.opencv.core.Point(
|
|
466
|
+
imageWidth - point.x,
|
|
467
|
+
imageHeight - point.y
|
|
468
|
+
)
|
|
469
|
+
270 -> org.opencv.core.Point(
|
|
470
|
+
point.y,
|
|
471
|
+
imageWidth - point.x
|
|
472
|
+
)
|
|
473
|
+
else -> point // 0 degrees, no rotation
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// Determine rotated dimensions (same as transform)
|
|
478
|
+
val rotatedImageWidth = if (rotationDegrees == 90f || rotationDegrees == 270f) {
|
|
479
|
+
imageHeight
|
|
480
|
+
} else {
|
|
481
|
+
imageWidth
|
|
482
|
+
}
|
|
483
|
+
val rotatedImageHeight = if (rotationDegrees == 90f || rotationDegrees == 270f) {
|
|
484
|
+
imageWidth
|
|
485
|
+
} else {
|
|
486
|
+
imageHeight
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// Use same fit-scaling as transform
|
|
490
|
+
val scaleX = viewWidth / rotatedImageWidth.toFloat()
|
|
491
|
+
val scaleY = viewHeight / rotatedImageHeight.toFloat()
|
|
492
|
+
val scale = scaleX.coerceAtMost(scaleY) // Fit (not fill)
|
|
493
|
+
|
|
494
|
+
val scaledWidth = rotatedImageWidth * scale
|
|
495
|
+
val scaledHeight = rotatedImageHeight * scale
|
|
451
496
|
val offsetX = (viewWidth - scaledWidth) / 2f
|
|
452
497
|
val offsetY = (viewHeight - scaledHeight) / 2f
|
|
453
498
|
|
|
454
|
-
|
|
499
|
+
// Apply rotation first, then scale and offset
|
|
500
|
+
fun transformPoint(point: org.opencv.core.Point): org.opencv.core.Point {
|
|
501
|
+
val rotated = rotatePoint(point)
|
|
455
502
|
return org.opencv.core.Point(
|
|
456
|
-
|
|
457
|
-
|
|
503
|
+
rotated.x * scale + offsetX,
|
|
504
|
+
rotated.y * scale + offsetY
|
|
458
505
|
)
|
|
459
506
|
}
|
|
460
507
|
|
|
461
508
|
return Rectangle(
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
509
|
+
transformPoint(rectangle.topLeft),
|
|
510
|
+
transformPoint(rectangle.topRight),
|
|
511
|
+
transformPoint(rectangle.bottomLeft),
|
|
512
|
+
transformPoint(rectangle.bottomRight)
|
|
466
513
|
)
|
|
467
514
|
}
|
|
468
515
|
|
|
@@ -285,15 +285,24 @@ class DocumentDetector {
|
|
|
285
285
|
|
|
286
286
|
if (quad.total() == 4L && Imgproc.isContourConvex(MatOfPoint(*quad.toArray()))) {
|
|
287
287
|
val points = quad.toArray()
|
|
288
|
+
val ordered = orderPoints(points)
|
|
288
289
|
val rect = Imgproc.minAreaRect(MatOfPoint2f(*points))
|
|
289
290
|
val rectArea = rect.size.area()
|
|
290
291
|
val rectangularity = if (rectArea > 1.0) contourArea / rectArea else 0.0
|
|
291
|
-
if (rectangularity >= 0.6) {
|
|
292
|
+
if (rectangularity >= 0.6 && isCandidateValid(ordered, srcMat)) {
|
|
292
293
|
debugStats.candidates += 1
|
|
293
294
|
val score = contourArea * rectangularity
|
|
294
295
|
if (score > bestScore) {
|
|
296
|
+
val width = distance(ordered.topLeft, ordered.topRight)
|
|
297
|
+
val height = distance(ordered.topLeft, ordered.bottomLeft)
|
|
298
|
+
val aspect = if (height > 0.0) width / height else 0.0
|
|
295
299
|
bestScore = score
|
|
296
|
-
|
|
300
|
+
debugStats.bestArea = contourArea
|
|
301
|
+
debugStats.bestRectangularity = rectangularity
|
|
302
|
+
debugStats.bestWidth = width
|
|
303
|
+
debugStats.bestHeight = height
|
|
304
|
+
debugStats.bestAspect = aspect
|
|
305
|
+
largestRectangle = refineRectangle(grayMat, ordered)
|
|
297
306
|
}
|
|
298
307
|
}
|
|
299
308
|
} else {
|
|
@@ -308,10 +317,22 @@ class DocumentDetector {
|
|
|
308
317
|
debugStats.candidates += 1
|
|
309
318
|
val boxPoints = Array(4) { Point() }
|
|
310
319
|
rotated.points(boxPoints)
|
|
320
|
+
val ordered = orderPoints(boxPoints)
|
|
321
|
+
if (!isCandidateValid(ordered, srcMat)) {
|
|
322
|
+
continue
|
|
323
|
+
}
|
|
311
324
|
val score = contourArea * rectangularity
|
|
312
325
|
if (score > bestScore) {
|
|
326
|
+
val width = distance(ordered.topLeft, ordered.topRight)
|
|
327
|
+
val height = distance(ordered.topLeft, ordered.bottomLeft)
|
|
328
|
+
val aspect = if (height > 0.0) width / height else 0.0
|
|
313
329
|
bestScore = score
|
|
314
|
-
|
|
330
|
+
debugStats.bestArea = contourArea
|
|
331
|
+
debugStats.bestRectangularity = rectangularity
|
|
332
|
+
debugStats.bestWidth = width
|
|
333
|
+
debugStats.bestHeight = height
|
|
334
|
+
debugStats.bestAspect = aspect
|
|
335
|
+
largestRectangle = refineRectangle(grayMat, ordered)
|
|
315
336
|
}
|
|
316
337
|
}
|
|
317
338
|
}
|
|
@@ -355,7 +376,12 @@ class DocumentDetector {
|
|
|
355
376
|
TAG,
|
|
356
377
|
"[DEBUG] cannyLow=$cannyLow cannyHigh=$cannyHigh " +
|
|
357
378
|
"contours=${debugStats.contours} candidates=${debugStats.candidates} " +
|
|
358
|
-
"bestScore=${String.format(
|
|
379
|
+
"bestScore=${String.format("%.1f", debugStats.bestScore)} " +
|
|
380
|
+
"bestArea=${String.format("%.1f", debugStats.bestArea)} " +
|
|
381
|
+
"bestRect=${String.format("%.2f", debugStats.bestRectangularity)} " +
|
|
382
|
+
"bestW=${String.format("%.1f", debugStats.bestWidth)} " +
|
|
383
|
+
"bestH=${String.format("%.1f", debugStats.bestHeight)} " +
|
|
384
|
+
"bestAspect=${String.format("%.2f", debugStats.bestAspect)} " +
|
|
359
385
|
"hasRect=${rectangle != null}"
|
|
360
386
|
)
|
|
361
387
|
}
|
|
@@ -374,7 +400,12 @@ class DocumentDetector {
|
|
|
374
400
|
private data class DebugStats(
|
|
375
401
|
var contours: Int = 0,
|
|
376
402
|
var candidates: Int = 0,
|
|
377
|
-
var bestScore: Double = 0.0
|
|
403
|
+
var bestScore: Double = 0.0,
|
|
404
|
+
var bestArea: Double = 0.0,
|
|
405
|
+
var bestRectangularity: Double = 0.0,
|
|
406
|
+
var bestWidth: Double = 0.0,
|
|
407
|
+
var bestHeight: Double = 0.0,
|
|
408
|
+
var bestAspect: Double = 0.0
|
|
378
409
|
)
|
|
379
410
|
|
|
380
411
|
/**
|
|
@@ -393,6 +424,21 @@ class DocumentDetector {
|
|
|
393
424
|
return Rectangle(topLeft, topRight, bottomLeft, bottomRight)
|
|
394
425
|
}
|
|
395
426
|
|
|
427
|
+
private fun isCandidateValid(rectangle: Rectangle, srcMat: Mat): Boolean {
|
|
428
|
+
val width = distance(rectangle.topLeft, rectangle.topRight)
|
|
429
|
+
val height = distance(rectangle.topLeft, rectangle.bottomLeft)
|
|
430
|
+
val minDim = min(srcMat.cols(), srcMat.rows()).toDouble()
|
|
431
|
+
val minEdge = max(60.0, minDim * 0.08)
|
|
432
|
+
if (width < minEdge || height < minEdge) {
|
|
433
|
+
return false
|
|
434
|
+
}
|
|
435
|
+
val aspect = if (height > 0) width / height else 0.0
|
|
436
|
+
if (aspect < 0.45 || aspect > 2.8) {
|
|
437
|
+
return false
|
|
438
|
+
}
|
|
439
|
+
return true
|
|
440
|
+
}
|
|
441
|
+
|
|
396
442
|
/**
|
|
397
443
|
* Evaluate rectangle quality (matching iOS logic)
|
|
398
444
|
*/
|
|
@@ -437,7 +483,7 @@ class DocumentDetector {
|
|
|
437
483
|
}
|
|
438
484
|
|
|
439
485
|
val minDim = kotlin.math.min(viewWidth.toDouble(), viewHeight.toDouble())
|
|
440
|
-
val angleThreshold = max(
|
|
486
|
+
val angleThreshold = max(140.0, minDim * 0.18)
|
|
441
487
|
|
|
442
488
|
val topYDiff = abs(rectangle.topRight.y - rectangle.topLeft.y)
|
|
443
489
|
val bottomYDiff = abs(rectangle.bottomLeft.y - rectangle.bottomRight.y)
|
|
@@ -448,7 +494,7 @@ class DocumentDetector {
|
|
|
448
494
|
return RectangleQuality.BAD_ANGLE
|
|
449
495
|
}
|
|
450
496
|
|
|
451
|
-
val margin = max(
|
|
497
|
+
val margin = max(50.0, minDim * 0.05)
|
|
452
498
|
if (rectangle.topLeft.y > margin ||
|
|
453
499
|
rectangle.topRight.y > margin ||
|
|
454
500
|
rectangle.bottomLeft.y < (viewHeight - margin) ||
|
package/package.json
CHANGED