react-native-rectangle-doc-scanner 4.1.0 → 4.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/main/kotlin/com/reactnativerectangledocscanner/CameraController.kt +17 -9
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentDetector.kt +31 -6
- package/android/src/main/kotlin/com/reactnativerectangledocscanner/DocumentScannerView.kt +37 -19
- package/package.json +1 -1
|
@@ -34,6 +34,7 @@ import java.util.concurrent.atomic.AtomicReference
|
|
|
34
34
|
import java.util.concurrent.atomic.AtomicBoolean
|
|
35
35
|
import kotlin.math.abs
|
|
36
36
|
import kotlin.math.max
|
|
37
|
+
import kotlin.math.min
|
|
37
38
|
|
|
38
39
|
class CameraController(
|
|
39
40
|
private val context: Context,
|
|
@@ -217,11 +218,13 @@ class CameraController(
|
|
|
217
218
|
val previewSizes = streamConfigMap.getOutputSizes(SurfaceTexture::class.java)
|
|
218
219
|
previewSize = chooseBestSize(previewSizes, viewAspect, null, preferClosestAspect = true)
|
|
219
220
|
|
|
221
|
+
val previewAspect = previewSize?.let { it.width.toDouble() / it.height.toDouble() } ?: viewAspect
|
|
220
222
|
val analysisSizes = streamConfigMap.getOutputSizes(ImageFormat.YUV_420_888)
|
|
221
|
-
analysisSize = chooseBestSize(analysisSizes,
|
|
223
|
+
analysisSize = chooseBestSize(analysisSizes, previewAspect, null, preferClosestAspect = true)
|
|
222
224
|
|
|
223
225
|
val captureSizes = streamConfigMap.getOutputSizes(ImageFormat.JPEG)
|
|
224
|
-
captureSize = captureSizes
|
|
226
|
+
captureSize = chooseBestSize(captureSizes, previewAspect, null, preferClosestAspect = true)
|
|
227
|
+
?: captureSizes?.maxByOrNull { it.width * it.height }
|
|
225
228
|
|
|
226
229
|
setupImageReaders()
|
|
227
230
|
|
|
@@ -540,17 +543,22 @@ class CameraController(
|
|
|
540
543
|
return sorted.first()
|
|
541
544
|
}
|
|
542
545
|
|
|
546
|
+
fun aspectDiff(size: Size): Double {
|
|
547
|
+
val w = size.width.toDouble()
|
|
548
|
+
val h = size.height.toDouble()
|
|
549
|
+
val direct = abs(w / h - targetAspect)
|
|
550
|
+
val inverted = abs(h / w - targetAspect)
|
|
551
|
+
return min(direct, inverted)
|
|
552
|
+
}
|
|
553
|
+
|
|
543
554
|
if (preferClosestAspect) {
|
|
544
555
|
return capped.minWithOrNull(
|
|
545
|
-
compareBy<Size> {
|
|
556
|
+
compareBy<Size> { aspectDiff(it) }
|
|
546
557
|
.thenByDescending { it.width * it.height }
|
|
547
558
|
)
|
|
548
559
|
}
|
|
549
560
|
|
|
550
|
-
val matching = capped.filter {
|
|
551
|
-
val aspect = it.width.toDouble() / it.height.toDouble()
|
|
552
|
-
abs(aspect - targetAspect) <= ANALYSIS_ASPECT_TOLERANCE
|
|
553
|
-
}
|
|
561
|
+
val matching = capped.filter { aspectDiff(it) <= ANALYSIS_ASPECT_TOLERANCE }
|
|
554
562
|
|
|
555
563
|
return matching.firstOrNull() ?: capped.first()
|
|
556
564
|
}
|
|
@@ -580,7 +588,7 @@ class CameraController(
|
|
|
580
588
|
val uprightWidth = if (rotationDegrees == 90 || rotationDegrees == 270) imageHeight else imageWidth
|
|
581
589
|
val uprightHeight = if (rotationDegrees == 90 || rotationDegrees == 270) imageWidth else imageHeight
|
|
582
590
|
val openCvRect = if (mlBox != null) {
|
|
583
|
-
val expanded = expandRect(mlBox, uprightWidth, uprightHeight, 0.
|
|
591
|
+
val expanded = expandRect(mlBox, uprightWidth, uprightHeight, 0.25f)
|
|
584
592
|
DocumentDetector.detectRectangleInYUVWithRoi(
|
|
585
593
|
nv21,
|
|
586
594
|
imageWidth,
|
|
@@ -592,7 +600,7 @@ class CameraController(
|
|
|
592
600
|
DocumentDetector.detectRectangleInYUV(nv21, imageWidth, imageHeight, rotationDegrees)
|
|
593
601
|
}
|
|
594
602
|
if (openCvRect == null) {
|
|
595
|
-
mlBox?.let { boxToRectangle(insetBox(it, 0.
|
|
603
|
+
mlBox?.let { boxToRectangle(insetBox(it, 0.9f)) }
|
|
596
604
|
} else {
|
|
597
605
|
openCvRect
|
|
598
606
|
}
|
|
@@ -173,8 +173,8 @@ class DocumentDetector {
|
|
|
173
173
|
// Apply a light blur to reduce noise without killing small edges.
|
|
174
174
|
Imgproc.GaussianBlur(grayMat, blurredMat, Size(3.0, 3.0), 0.0)
|
|
175
175
|
|
|
176
|
-
// Apply Canny edge detection with
|
|
177
|
-
Imgproc.Canny(blurredMat, cannyMat,
|
|
176
|
+
// Apply Canny edge detection with lower thresholds for small, low-contrast documents.
|
|
177
|
+
Imgproc.Canny(blurredMat, cannyMat, 40.0, 120.0)
|
|
178
178
|
val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
|
|
179
179
|
Imgproc.morphologyEx(cannyMat, morphMat, Imgproc.MORPH_CLOSE, kernel)
|
|
180
180
|
kernel.release()
|
|
@@ -192,7 +192,7 @@ class DocumentDetector {
|
|
|
192
192
|
|
|
193
193
|
var largestRectangle: Rectangle? = null
|
|
194
194
|
var largestArea = 0.0
|
|
195
|
-
val minArea = max(
|
|
195
|
+
val minArea = max(300.0, (srcMat.rows() * srcMat.cols()) * 0.0005)
|
|
196
196
|
|
|
197
197
|
for (contour in contours) {
|
|
198
198
|
val contourArea = Imgproc.contourArea(contour)
|
|
@@ -200,18 +200,43 @@ class DocumentDetector {
|
|
|
200
200
|
|
|
201
201
|
val approx = MatOfPoint2f()
|
|
202
202
|
val contour2f = MatOfPoint2f(*contour.toArray())
|
|
203
|
-
val
|
|
203
|
+
val arcLength = Imgproc.arcLength(contour2f, true)
|
|
204
|
+
val epsilon = 0.018 * arcLength
|
|
204
205
|
Imgproc.approxPolyDP(contour2f, approx, epsilon, true)
|
|
206
|
+
val relaxed = if (approx.total() != 4L) {
|
|
207
|
+
MatOfPoint2f().apply {
|
|
208
|
+
Imgproc.approxPolyDP(contour2f, this, 0.03 * arcLength, true)
|
|
209
|
+
}
|
|
210
|
+
} else {
|
|
211
|
+
null
|
|
212
|
+
}
|
|
213
|
+
val quad = if (relaxed?.total() == 4L) relaxed else approx
|
|
205
214
|
|
|
206
|
-
if (
|
|
207
|
-
val points =
|
|
215
|
+
if (quad.total() == 4L && Imgproc.isContourConvex(MatOfPoint(*quad.toArray()))) {
|
|
216
|
+
val points = quad.toArray()
|
|
208
217
|
if (contourArea > largestArea) {
|
|
209
218
|
largestArea = contourArea
|
|
210
219
|
largestRectangle = orderPoints(points)
|
|
211
220
|
}
|
|
221
|
+
} else {
|
|
222
|
+
// Fallback: use rotated bounding box when contour is near-rectangular.
|
|
223
|
+
val contour2fForRect = MatOfPoint2f(*contour.toArray())
|
|
224
|
+
val rotated = Imgproc.minAreaRect(contour2fForRect)
|
|
225
|
+
contour2fForRect.release()
|
|
226
|
+
val rectArea = rotated.size.area()
|
|
227
|
+
if (rectArea > 1.0) {
|
|
228
|
+
val rectangularity = contourArea / rectArea
|
|
229
|
+
if (rectangularity >= 0.6 && contourArea > largestArea) {
|
|
230
|
+
val boxPoints = Array(4) { Point() }
|
|
231
|
+
rotated.points(boxPoints)
|
|
232
|
+
largestArea = contourArea
|
|
233
|
+
largestRectangle = orderPoints(boxPoints)
|
|
234
|
+
}
|
|
235
|
+
}
|
|
212
236
|
}
|
|
213
237
|
|
|
214
238
|
approx.release()
|
|
239
|
+
relaxed?.release()
|
|
215
240
|
contour2f.release()
|
|
216
241
|
}
|
|
217
242
|
|
|
@@ -30,6 +30,7 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
30
30
|
private val themedContext = context
|
|
31
31
|
private val previewView: TextureView
|
|
32
32
|
private val overlayView: OverlayView
|
|
33
|
+
private val useNativeOverlay = false
|
|
33
34
|
private var cameraController: CameraController? = null
|
|
34
35
|
private val lifecycleRegistry = LifecycleRegistry(this)
|
|
35
36
|
|
|
@@ -224,8 +225,10 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
224
225
|
imageWidth: Int,
|
|
225
226
|
imageHeight: Int
|
|
226
227
|
) {
|
|
227
|
-
// Update overlay
|
|
228
|
-
|
|
228
|
+
// Update native overlay only when explicitly enabled to avoid double-rendering with JS overlay
|
|
229
|
+
if (useNativeOverlay) {
|
|
230
|
+
overlayView.setRectangle(rectangleOnScreen, overlayColor)
|
|
231
|
+
}
|
|
229
232
|
|
|
230
233
|
// Update stable counter based on quality
|
|
231
234
|
if (rectangleCoordinates == null) {
|
|
@@ -327,14 +330,9 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
327
330
|
// Detect rectangle in captured image
|
|
328
331
|
val bitmap = BitmapFactory.decodeFile(imageFile.absolutePath)
|
|
329
332
|
?: throw IllegalStateException("decode_failed")
|
|
330
|
-
var detectedRectangle =
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
Log.w(TAG, "Rectangle detection failed, using original image", e)
|
|
334
|
-
null
|
|
335
|
-
}
|
|
336
|
-
if (detectedRectangle == null && lastDetectedImageWidth > 0 && lastDetectedImageHeight > 0) {
|
|
337
|
-
val rectangleFromView = lastRectangleOnScreen?.let {
|
|
333
|
+
var detectedRectangle: Rectangle? = null
|
|
334
|
+
val rectangleFromView = if (lastDetectedImageWidth > 0 && lastDetectedImageHeight > 0) {
|
|
335
|
+
lastRectangleOnScreen?.let {
|
|
338
336
|
viewToImageRectangle(
|
|
339
337
|
it,
|
|
340
338
|
width,
|
|
@@ -343,15 +341,35 @@ class DocumentScannerView(context: ThemedReactContext) : FrameLayout(context), L
|
|
|
343
341
|
lastDetectedImageHeight
|
|
344
342
|
)
|
|
345
343
|
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
344
|
+
} else {
|
|
345
|
+
null
|
|
346
|
+
}
|
|
347
|
+
if (rectangleFromView != null) {
|
|
348
|
+
detectedRectangle = scaleRectangleToBitmap(
|
|
349
|
+
rectangleFromView,
|
|
350
|
+
lastDetectedImageWidth,
|
|
351
|
+
lastDetectedImageHeight,
|
|
352
|
+
bitmap.width,
|
|
353
|
+
bitmap.height
|
|
354
|
+
)
|
|
355
|
+
} else {
|
|
356
|
+
detectedRectangle = try {
|
|
357
|
+
DocumentDetector.detectRectangle(bitmap)
|
|
358
|
+
} catch (e: Exception) {
|
|
359
|
+
Log.w(TAG, "Rectangle detection failed, using original image", e)
|
|
360
|
+
null
|
|
361
|
+
}
|
|
362
|
+
if (detectedRectangle == null && lastDetectedImageWidth > 0 && lastDetectedImageHeight > 0) {
|
|
363
|
+
val fallbackRect = lastDetectedRectangle
|
|
364
|
+
if (fallbackRect != null) {
|
|
365
|
+
detectedRectangle = scaleRectangleToBitmap(
|
|
366
|
+
fallbackRect,
|
|
367
|
+
lastDetectedImageWidth,
|
|
368
|
+
lastDetectedImageHeight,
|
|
369
|
+
bitmap.width,
|
|
370
|
+
bitmap.height
|
|
371
|
+
)
|
|
372
|
+
}
|
|
355
373
|
}
|
|
356
374
|
}
|
|
357
375
|
|