react-native-rectangle-doc-scanner 0.71.0 → 1.1.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.
|
@@ -35,6 +35,7 @@ import org.opencv.core.MatOfPoint2f
|
|
|
35
35
|
import org.opencv.core.Point
|
|
36
36
|
import org.opencv.core.Size as MatSize
|
|
37
37
|
import org.opencv.imgproc.Imgproc
|
|
38
|
+
import org.opencv.photo.Photo
|
|
38
39
|
import java.io.File
|
|
39
40
|
import java.nio.ByteBuffer
|
|
40
41
|
import java.text.SimpleDateFormat
|
|
@@ -73,6 +74,8 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
73
74
|
private var currentStableCounter: Int = 0
|
|
74
75
|
private var lastQuad: QuadPoints? = null
|
|
75
76
|
private var lastFrameSize: AndroidSize? = null
|
|
77
|
+
private var missedDetections: Int = 0
|
|
78
|
+
private val maxMissedDetections = 4
|
|
76
79
|
private var capturePromise: Promise? = null
|
|
77
80
|
private var captureInFlight: Boolean = false
|
|
78
81
|
|
|
@@ -198,25 +201,43 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
198
201
|
|
|
199
202
|
private fun emitDetectionResult(quad: QuadPoints?, frameSize: AndroidSize) {
|
|
200
203
|
val reactContext = context as? ReactContext ?: return
|
|
204
|
+
|
|
205
|
+
val effectiveQuad: QuadPoints? = when {
|
|
206
|
+
quad != null -> {
|
|
207
|
+
missedDetections = 0
|
|
208
|
+
lastQuad = quad
|
|
209
|
+
quad
|
|
210
|
+
}
|
|
211
|
+
lastQuad != null && missedDetections < maxMissedDetections -> {
|
|
212
|
+
missedDetections += 1
|
|
213
|
+
lastQuad
|
|
214
|
+
}
|
|
215
|
+
else -> {
|
|
216
|
+
missedDetections = 0
|
|
217
|
+
lastQuad = null
|
|
218
|
+
null
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
val sizeForEvent = if (effectiveQuad === quad) frameSize else lastFrameSize ?: frameSize
|
|
223
|
+
|
|
201
224
|
val event: WritableMap = Arguments.createMap().apply {
|
|
202
|
-
if (
|
|
225
|
+
if (effectiveQuad != null) {
|
|
203
226
|
val quadMap = Arguments.createMap().apply {
|
|
204
|
-
putMap("topLeft",
|
|
205
|
-
putMap("topRight",
|
|
206
|
-
putMap("bottomRight",
|
|
207
|
-
putMap("bottomLeft",
|
|
227
|
+
putMap("topLeft", effectiveQuad.topLeft.toWritable())
|
|
228
|
+
putMap("topRight", effectiveQuad.topRight.toWritable())
|
|
229
|
+
putMap("bottomRight", effectiveQuad.bottomRight.toWritable())
|
|
230
|
+
putMap("bottomLeft", effectiveQuad.bottomLeft.toWritable())
|
|
208
231
|
}
|
|
209
232
|
putMap("rectangleCoordinates", quadMap)
|
|
210
233
|
currentStableCounter = (currentStableCounter + 1).coerceAtMost(detectionCountBeforeCapture)
|
|
211
|
-
lastQuad = quad
|
|
212
234
|
} else {
|
|
213
235
|
putNull("rectangleCoordinates")
|
|
214
236
|
currentStableCounter = 0
|
|
215
|
-
lastQuad = null
|
|
216
237
|
}
|
|
217
238
|
putInt("stableCounter", currentStableCounter)
|
|
218
|
-
putDouble("frameWidth",
|
|
219
|
-
putDouble("frameHeight",
|
|
239
|
+
putDouble("frameWidth", sizeForEvent.width.toDouble())
|
|
240
|
+
putDouble("frameHeight", sizeForEvent.height.toDouble())
|
|
220
241
|
}
|
|
221
242
|
|
|
222
243
|
reactContext
|
|
@@ -387,11 +408,20 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
387
408
|
val gray = Mat()
|
|
388
409
|
Imgproc.cvtColor(mat, gray, Imgproc.COLOR_BGR2GRAY)
|
|
389
410
|
|
|
411
|
+
// Improve contrast for low-light or glossy surfaces
|
|
412
|
+
val clahe = Photo.createCLAHE(2.0, MatSize(8.0, 8.0))
|
|
413
|
+
val enhanced = Mat()
|
|
414
|
+
clahe.apply(gray, enhanced)
|
|
415
|
+
clahe.collectGarbage()
|
|
416
|
+
|
|
390
417
|
val blurred = Mat()
|
|
391
|
-
Imgproc.GaussianBlur(
|
|
418
|
+
Imgproc.GaussianBlur(enhanced, blurred, MatSize(5.0, 5.0), 0.0)
|
|
392
419
|
|
|
393
420
|
val edges = Mat()
|
|
394
|
-
Imgproc.Canny(blurred, edges,
|
|
421
|
+
Imgproc.Canny(blurred, edges, 40.0, 140.0)
|
|
422
|
+
|
|
423
|
+
val morphKernel = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, MatSize(5.0, 5.0))
|
|
424
|
+
Imgproc.morphologyEx(edges, edges, Imgproc.MORPH_CLOSE, morphKernel)
|
|
395
425
|
|
|
396
426
|
val contours = ArrayList<MatOfPoint>()
|
|
397
427
|
val hierarchy = Mat()
|
|
@@ -415,7 +445,7 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
415
445
|
}
|
|
416
446
|
|
|
417
447
|
val area = abs(Imgproc.contourArea(approxCurve))
|
|
418
|
-
if (area < frameArea * 0.
|
|
448
|
+
if (area < frameArea * 0.05 || area > frameArea * 0.98) {
|
|
419
449
|
contour.release()
|
|
420
450
|
contour2f.release()
|
|
421
451
|
continue
|
|
@@ -437,8 +467,10 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
437
467
|
}
|
|
438
468
|
|
|
439
469
|
gray.release()
|
|
470
|
+
enhanced.release()
|
|
440
471
|
blurred.release()
|
|
441
472
|
edges.release()
|
|
473
|
+
morphKernel.release()
|
|
442
474
|
hierarchy.release()
|
|
443
475
|
approxCurve.release()
|
|
444
476
|
mat.release()
|
package/dist/utils/overlay.js
CHANGED
|
@@ -62,6 +62,15 @@ const buildPath = (points) => {
|
|
|
62
62
|
path.close();
|
|
63
63
|
return path;
|
|
64
64
|
};
|
|
65
|
+
const orderQuad = (points) => {
|
|
66
|
+
if (points.length !== 4) {
|
|
67
|
+
return points;
|
|
68
|
+
}
|
|
69
|
+
const sorted = [...points].sort((a, b) => (a.y === b.y ? a.x - b.x : a.y - b.y));
|
|
70
|
+
const top = sorted.slice(0, 2).sort((a, b) => a.x - b.x);
|
|
71
|
+
const bottom = sorted.slice(2, 4).sort((a, b) => a.x - b.x);
|
|
72
|
+
return [top[0], top[1], bottom[1], bottom[0]];
|
|
73
|
+
};
|
|
65
74
|
const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColor = 'rgba(231, 166, 73, 0.35)', gridLineWidth = 2, }) => {
|
|
66
75
|
const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
|
|
67
76
|
const fillColor = (0, react_1.useMemo)(() => withAlpha(color, 0.2), [color]);
|
|
@@ -106,10 +115,11 @@ const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColo
|
|
|
106
115
|
if (!transformedQuad) {
|
|
107
116
|
return { outlinePath: null, gridPaths: [] };
|
|
108
117
|
}
|
|
109
|
-
const
|
|
118
|
+
const normalizedQuad = orderQuad(transformedQuad);
|
|
119
|
+
const skPath = buildPath(normalizedQuad);
|
|
110
120
|
const grid = [];
|
|
111
121
|
if (showGrid) {
|
|
112
|
-
const [topLeft, topRight, bottomRight, bottomLeft] =
|
|
122
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
113
123
|
const steps = [1 / 3, 2 / 3];
|
|
114
124
|
steps.forEach((t) => {
|
|
115
125
|
const start = lerp(topLeft, topRight, t);
|
|
@@ -33,6 +33,8 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
33
33
|
private var isProcessingFrame = false
|
|
34
34
|
private var isCaptureInFlight = false
|
|
35
35
|
private var lastObservation: VNRectangleObservation?
|
|
36
|
+
private var missedDetectionFrames: Int = 0
|
|
37
|
+
private let maxMissedDetections = 4
|
|
36
38
|
private var lastFrameSize: CGSize = .zero
|
|
37
39
|
private var photoCaptureCompletion: ((Result<RNRDocScannerCaptureResult, Error>) -> Void)?
|
|
38
40
|
|
|
@@ -87,6 +89,9 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
87
89
|
if session.canAddOutput(photoOutput) {
|
|
88
90
|
photoOutput.isHighResolutionCaptureEnabled = true
|
|
89
91
|
session.addOutput(photoOutput)
|
|
92
|
+
if let connection = photoOutput.connection(with: .video), connection.isVideoOrientationSupported {
|
|
93
|
+
connection.videoOrientation = .portrait
|
|
94
|
+
}
|
|
90
95
|
}
|
|
91
96
|
|
|
92
97
|
videoOutput.videoSettings = [
|
|
@@ -97,6 +102,9 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
97
102
|
|
|
98
103
|
if session.canAddOutput(videoOutput) {
|
|
99
104
|
session.addOutput(videoOutput)
|
|
105
|
+
if let connection = videoOutput.connection(with: .video), connection.isVideoOrientationSupported {
|
|
106
|
+
connection.videoOrientation = .portrait
|
|
107
|
+
}
|
|
100
108
|
}
|
|
101
109
|
}
|
|
102
110
|
}
|
|
@@ -104,6 +112,9 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
104
112
|
override func layoutSubviews() {
|
|
105
113
|
super.layoutSubviews()
|
|
106
114
|
previewLayer?.frame = bounds
|
|
115
|
+
if let connection = previewLayer?.connection, connection.isVideoOrientationSupported {
|
|
116
|
+
connection.videoOrientation = .portrait
|
|
117
|
+
}
|
|
107
118
|
}
|
|
108
119
|
|
|
109
120
|
private func updateTorchMode() {
|
|
@@ -164,21 +175,27 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
164
175
|
return
|
|
165
176
|
}
|
|
166
177
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
178
|
+
guard let observations = request.results as? [VNRectangleObservation], let observation = observations.max(by: { lhs, rhs in
|
|
179
|
+
lhs.boundingBox.width * lhs.boundingBox.height < rhs.boundingBox.width * rhs.boundingBox.height
|
|
180
|
+
}) else {
|
|
181
|
+
self.lastObservation = nil
|
|
182
|
+
self.handleDetectedRectangle(nil, frameSize: frameSize)
|
|
183
|
+
return
|
|
184
|
+
}
|
|
172
185
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
186
|
+
self.lastObservation = observation
|
|
187
|
+
self.missedDetectionFrames = 0
|
|
188
|
+
self.handleDetectedRectangle(observation, frameSize: frameSize)
|
|
189
|
+
}
|
|
176
190
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
191
|
+
request.maximumObservations = 1
|
|
192
|
+
request.minimumConfidence = 0.5
|
|
193
|
+
request.minimumAspectRatio = 0.15
|
|
194
|
+
request.maximumAspectRatio = 1.75
|
|
195
|
+
request.minimumSize = 0.08
|
|
196
|
+
if #available(iOS 13.0, *) {
|
|
197
|
+
request.quadratureTolerance = 45
|
|
198
|
+
}
|
|
182
199
|
|
|
183
200
|
let handler = VNImageRequestHandler(cvPixelBuffer: pixelBuffer, orientation: orientation, options: [:])
|
|
184
201
|
do {
|
|
@@ -193,13 +210,27 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
193
210
|
func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
|
|
194
211
|
guard let onRectangleDetect else { return }
|
|
195
212
|
|
|
213
|
+
let effectiveObservation: VNRectangleObservation?
|
|
214
|
+
if let rect = rectangle {
|
|
215
|
+
effectiveObservation = rect
|
|
216
|
+
lastObservation = rect
|
|
217
|
+
missedDetectionFrames = 0
|
|
218
|
+
} else if missedDetectionFrames < maxMissedDetections, let cached = lastObservation {
|
|
219
|
+
missedDetectionFrames += 1
|
|
220
|
+
effectiveObservation = cached
|
|
221
|
+
} else {
|
|
222
|
+
lastObservation = nil
|
|
223
|
+
missedDetectionFrames = 0
|
|
224
|
+
effectiveObservation = nil
|
|
225
|
+
}
|
|
226
|
+
|
|
196
227
|
let payload: [String: Any?]
|
|
197
|
-
if let
|
|
228
|
+
if let observation = effectiveObservation {
|
|
198
229
|
let points = [
|
|
199
|
-
pointForOverlay(from:
|
|
200
|
-
pointForOverlay(from:
|
|
201
|
-
pointForOverlay(from:
|
|
202
|
-
pointForOverlay(from:
|
|
230
|
+
pointForOverlay(from: observation.topLeft, frameSize: frameSize),
|
|
231
|
+
pointForOverlay(from: observation.topRight, frameSize: frameSize),
|
|
232
|
+
pointForOverlay(from: observation.bottomRight, frameSize: frameSize),
|
|
233
|
+
pointForOverlay(from: observation.bottomLeft, frameSize: frameSize),
|
|
203
234
|
]
|
|
204
235
|
|
|
205
236
|
currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
|
package/package.json
CHANGED
package/src/utils/overlay.tsx
CHANGED
|
@@ -47,6 +47,18 @@ const buildPath = (points: Point[]) => {
|
|
|
47
47
|
return path;
|
|
48
48
|
};
|
|
49
49
|
|
|
50
|
+
const orderQuad = (points: Point[]): Point[] => {
|
|
51
|
+
if (points.length !== 4) {
|
|
52
|
+
return points;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const sorted = [...points].sort((a, b) => (a.y === b.y ? a.x - b.x : a.y - b.y));
|
|
56
|
+
const top = sorted.slice(0, 2).sort((a, b) => a.x - b.x);
|
|
57
|
+
const bottom = sorted.slice(2, 4).sort((a, b) => a.x - b.x);
|
|
58
|
+
|
|
59
|
+
return [top[0], top[1], bottom[1], bottom[0]];
|
|
60
|
+
};
|
|
61
|
+
|
|
50
62
|
export const Overlay: React.FC<OverlayProps> = ({
|
|
51
63
|
quad,
|
|
52
64
|
color = '#e7a649',
|
|
@@ -105,11 +117,12 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
105
117
|
return { outlinePath: null, gridPaths: [] };
|
|
106
118
|
}
|
|
107
119
|
|
|
108
|
-
const
|
|
120
|
+
const normalizedQuad = orderQuad(transformedQuad);
|
|
121
|
+
const skPath = buildPath(normalizedQuad);
|
|
109
122
|
const grid: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
110
123
|
|
|
111
124
|
if (showGrid) {
|
|
112
|
-
const [topLeft, topRight, bottomRight, bottomLeft] =
|
|
125
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
113
126
|
const steps = [1 / 3, 2 / 3];
|
|
114
127
|
|
|
115
128
|
steps.forEach((t) => {
|