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.
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?,
|
|
@@ -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
|
-
|
|
177
|
-
|
|
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
|
|
222
|
-
val minArea = max(
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
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
|
|
296
|
+
if (rectangularity >= 0.6) {
|
|
259
297
|
val boxPoints = Array(4) { Point() }
|
|
260
298
|
rotated.points(boxPoints)
|
|
261
|
-
|
|
262
|
-
|
|
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