react-native-rectangle-doc-scanner 10.31.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
- // Simplified coordinate mapping for TextureView
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
- val scaleX = viewWidth / imageWidth.toFloat()
447
- val scaleY = viewHeight / imageHeight.toFloat()
448
- val scale = scaleX.coerceAtMost(scaleY)
449
- val scaledWidth = imageWidth * scale
450
- val scaledHeight = imageHeight * scale
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
- fun scalePoint(point: org.opencv.core.Point): org.opencv.core.Point {
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
- point.x * scale + offsetX,
457
- point.y * scale + offsetY
503
+ rotated.x * scale + offsetX,
504
+ rotated.y * scale + offsetY
458
505
  )
459
506
  }
460
507
 
461
508
  return Rectangle(
462
- scalePoint(rectangle.topLeft),
463
- scalePoint(rectangle.topRight),
464
- scalePoint(rectangle.bottomLeft),
465
- scalePoint(rectangle.bottomRight)
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
- largestRectangle = refineRectangle(grayMat, orderPoints(points))
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
- largestRectangle = refineRectangle(grayMat, orderPoints(boxPoints))
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
  }
@@ -356,6 +377,11 @@ class DocumentDetector {
356
377
  "[DEBUG] cannyLow=$cannyLow cannyHigh=$cannyHigh " +
357
378
  "contours=${debugStats.contours} candidates=${debugStats.candidates} " +
358
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(90.0, minDim * 0.12)
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(80.0, minDim * 0.08)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "10.31.0",
3
+ "version": "10.32.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",