react-native-rectangle-doc-scanner 10.23.0 → 10.25.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.
@@ -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
- lastRectangleOnScreen = rectangleOnScreen
220
+ val smoothedRectangleOnScreen = smoothRectangle(rectangleOnScreen, width, height)
221
+ lastRectangleOnScreen = smoothedRectangleOnScreen
219
222
  val quality = when {
220
- rectangleOnScreen != null && width > 0 && height > 0 ->
221
- DocumentDetector.evaluateRectangleQualityInView(rectangleOnScreen, width, height)
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(rectangleOnScreen, rectangle, quality, imageWidth, imageHeight)
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?,
@@ -173,8 +173,40 @@ class DocumentDetector {
173
173
  // Apply a light blur to reduce noise without killing small edges.
174
174
  Imgproc.GaussianBlur(grayMat, blurredMat, Size(5.0, 5.0), 0.0)
175
175
 
176
- // Apply Canny edge detection with lower thresholds for better corner detection.
177
- Imgproc.Canny(blurredMat, cannyMat, 30.0, 90.0)
176
+ fun computeMedian(mat: Mat): Double {
177
+ val hist = Mat()
178
+ return try {
179
+ Imgproc.calcHist(
180
+ listOf(mat),
181
+ MatOfInt(0),
182
+ Mat(),
183
+ hist,
184
+ MatOfInt(256),
185
+ MatOfFloat(0f, 256f)
186
+ )
187
+ val total = mat.total().toDouble()
188
+ var cumulative = 0.0
189
+ var median = 0.0
190
+ for (i in 0 until 256) {
191
+ cumulative += hist.get(i, 0)[0]
192
+ if (cumulative >= total * 0.5) {
193
+ median = i.toDouble()
194
+ break
195
+ }
196
+ }
197
+ median
198
+ } finally {
199
+ hist.release()
200
+ }
201
+ }
202
+
203
+ val median = computeMedian(blurredMat)
204
+ val sigma = 0.33
205
+ val cannyLow = max(20.0, (1.0 - sigma) * median)
206
+ val cannyHigh = max(60.0, (1.0 + sigma) * median)
207
+
208
+ // Apply Canny edge detection with adaptive thresholds for better corner detection.
209
+ Imgproc.Canny(blurredMat, cannyMat, cannyLow, cannyHigh)
178
210
  val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, Size(3.0, 3.0))
179
211
  Imgproc.morphologyEx(cannyMat, morphMat, Imgproc.MORPH_CLOSE, kernel)
180
212
  kernel.release()
@@ -218,8 +250,8 @@ class DocumentDetector {
218
250
  )
219
251
 
220
252
  var largestRectangle: Rectangle? = null
221
- var largestArea = 0.0
222
- val minArea = max(300.0, (srcMat.rows() * srcMat.cols()) * 0.0005)
253
+ var bestScore = 0.0
254
+ val minArea = max(800.0, (srcMat.rows() * srcMat.cols()) * 0.001)
223
255
 
224
256
  for (contour in contours) {
225
257
  val contourArea = Imgproc.contourArea(contour)
@@ -243,9 +275,15 @@ class DocumentDetector {
243
275
 
244
276
  if (quad.total() == 4L && Imgproc.isContourConvex(MatOfPoint(*quad.toArray()))) {
245
277
  val points = quad.toArray()
246
- if (contourArea > largestArea) {
247
- largestArea = contourArea
248
- largestRectangle = refineRectangle(grayMat, orderPoints(points))
278
+ val rect = Imgproc.minAreaRect(MatOfPoint2f(*points))
279
+ val rectArea = rect.size.area()
280
+ val rectangularity = if (rectArea > 1.0) contourArea / rectArea else 0.0
281
+ if (rectangularity >= 0.6) {
282
+ val score = contourArea * rectangularity
283
+ if (score > bestScore) {
284
+ bestScore = score
285
+ largestRectangle = refineRectangle(grayMat, orderPoints(points))
286
+ }
249
287
  }
250
288
  } else {
251
289
  // Fallback: use rotated bounding box when contour is near-rectangular.
@@ -255,11 +293,14 @@ class DocumentDetector {
255
293
  val rectArea = rotated.size.area()
256
294
  if (rectArea > 1.0) {
257
295
  val rectangularity = contourArea / rectArea
258
- if (rectangularity >= 0.6 && contourArea > largestArea) {
296
+ if (rectangularity >= 0.6) {
259
297
  val boxPoints = Array(4) { Point() }
260
298
  rotated.points(boxPoints)
261
- largestArea = contourArea
262
- largestRectangle = refineRectangle(grayMat, orderPoints(boxPoints))
299
+ val score = contourArea * rectangularity
300
+ if (score > bestScore) {
301
+ bestScore = score
302
+ largestRectangle = refineRectangle(grayMat, orderPoints(boxPoints))
303
+ }
263
304
  }
264
305
  }
265
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "10.23.0",
3
+ "version": "10.25.0",
4
4
  "description": "Native-backed document scanner for React Native with customizable overlays.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",