react-native-rectangle-doc-scanner 1.0.0 → 1.2.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.
|
@@ -74,6 +74,8 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
74
74
|
private var currentStableCounter: Int = 0
|
|
75
75
|
private var lastQuad: QuadPoints? = null
|
|
76
76
|
private var lastFrameSize: AndroidSize? = null
|
|
77
|
+
private var missedDetections: Int = 0
|
|
78
|
+
private val maxMissedDetections = 4
|
|
77
79
|
private var capturePromise: Promise? = null
|
|
78
80
|
private var captureInFlight: Boolean = false
|
|
79
81
|
|
|
@@ -199,25 +201,43 @@ class RNRDocScannerView @JvmOverloads constructor(
|
|
|
199
201
|
|
|
200
202
|
private fun emitDetectionResult(quad: QuadPoints?, frameSize: AndroidSize) {
|
|
201
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
|
+
|
|
202
224
|
val event: WritableMap = Arguments.createMap().apply {
|
|
203
|
-
if (
|
|
225
|
+
if (effectiveQuad != null) {
|
|
204
226
|
val quadMap = Arguments.createMap().apply {
|
|
205
|
-
putMap("topLeft",
|
|
206
|
-
putMap("topRight",
|
|
207
|
-
putMap("bottomRight",
|
|
208
|
-
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())
|
|
209
231
|
}
|
|
210
232
|
putMap("rectangleCoordinates", quadMap)
|
|
211
233
|
currentStableCounter = (currentStableCounter + 1).coerceAtMost(detectionCountBeforeCapture)
|
|
212
|
-
lastQuad = quad
|
|
213
234
|
} else {
|
|
214
235
|
putNull("rectangleCoordinates")
|
|
215
236
|
currentStableCounter = 0
|
|
216
|
-
lastQuad = null
|
|
217
237
|
}
|
|
218
238
|
putInt("stableCounter", currentStableCounter)
|
|
219
|
-
putDouble("frameWidth",
|
|
220
|
-
putDouble("frameHeight",
|
|
239
|
+
putDouble("frameWidth", sizeForEvent.width.toDouble())
|
|
240
|
+
putDouble("frameHeight", sizeForEvent.height.toDouble())
|
|
221
241
|
}
|
|
222
242
|
|
|
223
243
|
reactContext
|
package/dist/utils/overlay.js
CHANGED
|
@@ -62,6 +62,23 @@ 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 sum = (p) => p.x + p.y;
|
|
70
|
+
const diff = (p) => p.x - p.y;
|
|
71
|
+
const topLeft = points.reduce((prev, curr) => (sum(curr) < sum(prev) ? curr : prev));
|
|
72
|
+
const bottomRight = points.reduce((prev, curr) => (sum(curr) > sum(prev) ? curr : prev));
|
|
73
|
+
const remaining = points.filter((p) => p !== topLeft && p !== bottomRight);
|
|
74
|
+
if (remaining.length !== 2) {
|
|
75
|
+
return [topLeft, bottomRight, ...remaining];
|
|
76
|
+
}
|
|
77
|
+
const [candidate1, candidate2] = remaining;
|
|
78
|
+
const topRight = diff(candidate1) > diff(candidate2) ? candidate1 : candidate2;
|
|
79
|
+
const bottomLeft = topRight === candidate1 ? candidate2 : candidate1;
|
|
80
|
+
return [topLeft, topRight, bottomRight, bottomLeft];
|
|
81
|
+
};
|
|
65
82
|
const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColor = 'rgba(231, 166, 73, 0.35)', gridLineWidth = 2, }) => {
|
|
66
83
|
const { width: screenWidth, height: screenHeight } = (0, react_native_1.useWindowDimensions)();
|
|
67
84
|
const fillColor = (0, react_1.useMemo)(() => withAlpha(color, 0.2), [color]);
|
|
@@ -106,10 +123,11 @@ const Overlay = ({ quad, color = '#e7a649', frameSize, showGrid = true, gridColo
|
|
|
106
123
|
if (!transformedQuad) {
|
|
107
124
|
return { outlinePath: null, gridPaths: [] };
|
|
108
125
|
}
|
|
109
|
-
const
|
|
126
|
+
const normalizedQuad = orderQuad(transformedQuad);
|
|
127
|
+
const skPath = buildPath(normalizedQuad);
|
|
110
128
|
const grid = [];
|
|
111
129
|
if (showGrid) {
|
|
112
|
-
const [topLeft, topRight, bottomRight, bottomLeft] =
|
|
130
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
113
131
|
const steps = [1 / 3, 2 / 3];
|
|
114
132
|
steps.forEach((t) => {
|
|
115
133
|
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
|
|
|
@@ -173,15 +175,18 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
173
175
|
return
|
|
174
176
|
}
|
|
175
177
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
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
|
+
}
|
|
181
185
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
186
|
+
self.lastObservation = observation
|
|
187
|
+
self.missedDetectionFrames = 0
|
|
188
|
+
self.handleDetectedRectangle(observation, frameSize: frameSize)
|
|
189
|
+
}
|
|
185
190
|
|
|
186
191
|
request.maximumObservations = 1
|
|
187
192
|
request.minimumConfidence = 0.5
|
|
@@ -205,13 +210,27 @@ class RNRDocScannerView: UIView, AVCaptureVideoDataOutputSampleBufferDelegate, A
|
|
|
205
210
|
func handleDetectedRectangle(_ rectangle: VNRectangleObservation?, frameSize: CGSize) {
|
|
206
211
|
guard let onRectangleDetect else { return }
|
|
207
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
|
+
|
|
208
227
|
let payload: [String: Any?]
|
|
209
|
-
if let
|
|
228
|
+
if let observation = effectiveObservation {
|
|
210
229
|
let points = [
|
|
211
|
-
pointForOverlay(from:
|
|
212
|
-
pointForOverlay(from:
|
|
213
|
-
pointForOverlay(from:
|
|
214
|
-
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),
|
|
215
234
|
]
|
|
216
235
|
|
|
217
236
|
currentStableCounter = min(currentStableCounter + 1, Int(truncating: detectionCountBeforeCapture))
|
package/package.json
CHANGED
package/src/utils/overlay.tsx
CHANGED
|
@@ -47,6 +47,29 @@ 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 sum = (p: Point) => p.x + p.y;
|
|
56
|
+
const diff = (p: Point) => p.x - p.y;
|
|
57
|
+
|
|
58
|
+
const topLeft = points.reduce((prev, curr) => (sum(curr) < sum(prev) ? curr : prev));
|
|
59
|
+
const bottomRight = points.reduce((prev, curr) => (sum(curr) > sum(prev) ? curr : prev));
|
|
60
|
+
|
|
61
|
+
const remaining = points.filter((p) => p !== topLeft && p !== bottomRight);
|
|
62
|
+
if (remaining.length !== 2) {
|
|
63
|
+
return [topLeft, bottomRight, ...remaining];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [candidate1, candidate2] = remaining;
|
|
67
|
+
const topRight = diff(candidate1) > diff(candidate2) ? candidate1 : candidate2;
|
|
68
|
+
const bottomLeft = topRight === candidate1 ? candidate2 : candidate1;
|
|
69
|
+
|
|
70
|
+
return [topLeft, topRight, bottomRight, bottomLeft];
|
|
71
|
+
};
|
|
72
|
+
|
|
50
73
|
export const Overlay: React.FC<OverlayProps> = ({
|
|
51
74
|
quad,
|
|
52
75
|
color = '#e7a649',
|
|
@@ -105,11 +128,12 @@ export const Overlay: React.FC<OverlayProps> = ({
|
|
|
105
128
|
return { outlinePath: null, gridPaths: [] };
|
|
106
129
|
}
|
|
107
130
|
|
|
108
|
-
const
|
|
131
|
+
const normalizedQuad = orderQuad(transformedQuad);
|
|
132
|
+
const skPath = buildPath(normalizedQuad);
|
|
109
133
|
const grid: ReturnType<typeof Skia.Path.Make>[] = [];
|
|
110
134
|
|
|
111
135
|
if (showGrid) {
|
|
112
|
-
const [topLeft, topRight, bottomRight, bottomLeft] =
|
|
136
|
+
const [topLeft, topRight, bottomRight, bottomLeft] = normalizedQuad;
|
|
113
137
|
const steps = [1 / 3, 2 / 3];
|
|
114
138
|
|
|
115
139
|
steps.forEach((t) => {
|