react-native-rectangle-doc-scanner 0.43.0 → 0.45.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/README.md +35 -0
- package/dist/DocScanner.js +24 -19
- package/package.json +1 -1
- package/src/DocScanner.tsx +24 -19
package/README.md
CHANGED
|
@@ -5,6 +5,10 @@ VisionCamera + Fast-OpenCV powered document scanner template built for React Nat
|
|
|
5
5
|
## Features
|
|
6
6
|
- Real-time quad detection using `react-native-fast-opencv`
|
|
7
7
|
- Frame processor worklet executed on the UI thread via `react-native-vision-camera`
|
|
8
|
+
- High-resolution processing (1280p) for accurate corner detection
|
|
9
|
+
- Advanced anchor locking system maintains corner positions during camera movement
|
|
10
|
+
- Intelligent edge detection with optimized Canny parameters (50/150 thresholds)
|
|
11
|
+
- Adaptive smoothing with weighted averaging across multiple frames
|
|
8
12
|
- Resize plugin keeps frame processing fast on lower-end devices
|
|
9
13
|
- Skia overlay visualises detected document contours
|
|
10
14
|
- Stability tracker enables auto-capture once the document is steady
|
|
@@ -107,6 +111,37 @@ Passing `children` lets you render any UI on top of the camera preview, so you c
|
|
|
107
111
|
- The internal frame processor handles document detection; do not override `frameProcessor` in `cameraProps`.
|
|
108
112
|
- Adjust `minStableFrames` or tweak lighting conditions if auto capture is too sensitive or too slow.
|
|
109
113
|
|
|
114
|
+
## Detection Algorithm
|
|
115
|
+
|
|
116
|
+
The scanner uses a sophisticated multi-stage pipeline:
|
|
117
|
+
|
|
118
|
+
1. **Pre-processing** (1280p resolution for accuracy)
|
|
119
|
+
- Converts frame to grayscale
|
|
120
|
+
- Applies morphological opening to reduce noise
|
|
121
|
+
- Gaussian blur for smoother edges
|
|
122
|
+
- Canny edge detection with 50/150 thresholds
|
|
123
|
+
|
|
124
|
+
2. **Contour Detection**
|
|
125
|
+
- Finds external contours using CHAIN_APPROX_SIMPLE
|
|
126
|
+
- Applies convex hull for better corner detection
|
|
127
|
+
- Tests multiple epsilon values (0.1%-10%) for approxPolyDP
|
|
128
|
+
- Validates quadrilaterals for convexity
|
|
129
|
+
|
|
130
|
+
3. **Anchor Locking System**
|
|
131
|
+
- Once corners are detected, they "snap" to stable positions
|
|
132
|
+
- Maintains lock even when camera moves (up to 200px center delta)
|
|
133
|
+
- Holds anchor for up to 20 missed detections
|
|
134
|
+
- Adaptive blending between old and new positions for smooth transitions
|
|
135
|
+
- Builds confidence over time (max 30 frames) for stronger locking
|
|
136
|
+
|
|
137
|
+
4. **Quad Validation**
|
|
138
|
+
- Checks area ratio (0.02%-90% of frame)
|
|
139
|
+
- Validates minimum edge lengths
|
|
140
|
+
- Ensures reasonable aspect ratios
|
|
141
|
+
- Filters out non-convex shapes
|
|
142
|
+
|
|
143
|
+
This approach ensures corners remain locked once detected, allowing you to move the camera slightly without losing the document boundary.
|
|
144
|
+
|
|
110
145
|
## Build
|
|
111
146
|
```sh
|
|
112
147
|
yarn build
|
package/dist/DocScanner.js
CHANGED
|
@@ -100,19 +100,19 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
100
100
|
const lastMeasurementRef = (0, react_1.useRef)(null);
|
|
101
101
|
const frameSizeRef = (0, react_1.useRef)(null);
|
|
102
102
|
const MAX_HISTORY = 5;
|
|
103
|
-
const SNAP_DISTANCE =
|
|
104
|
-
const SNAP_CENTER_DISTANCE =
|
|
105
|
-
const BLEND_DISTANCE =
|
|
106
|
-
const MAX_CENTER_DELTA =
|
|
107
|
-
const REJECT_CENTER_DELTA =
|
|
108
|
-
const MAX_AREA_SHIFT = 0.
|
|
109
|
-
const HISTORY_RESET_DISTANCE =
|
|
110
|
-
const MIN_AREA_RATIO = 0.
|
|
103
|
+
const SNAP_DISTANCE = 8; // pixels; keep corners locked when movement is tiny (increased for stronger lock)
|
|
104
|
+
const SNAP_CENTER_DISTANCE = 18;
|
|
105
|
+
const BLEND_DISTANCE = 80; // pixels; softly ease between similar shapes (increased)
|
|
106
|
+
const MAX_CENTER_DELTA = 120; // increased to allow more camera movement
|
|
107
|
+
const REJECT_CENTER_DELTA = 200; // increased to maintain lock during camera movement
|
|
108
|
+
const MAX_AREA_SHIFT = 0.55; // more tolerance for perspective changes
|
|
109
|
+
const HISTORY_RESET_DISTANCE = 90;
|
|
110
|
+
const MIN_AREA_RATIO = 0.0002;
|
|
111
111
|
const MAX_AREA_RATIO = 0.9;
|
|
112
|
-
const MIN_EDGE_RATIO = 0.
|
|
113
|
-
const MAX_ANCHOR_MISSES =
|
|
112
|
+
const MIN_EDGE_RATIO = 0.015;
|
|
113
|
+
const MAX_ANCHOR_MISSES = 20; // increased to hold anchor longer when detection temporarily fails
|
|
114
114
|
const MIN_CONFIDENCE_TO_HOLD = 2;
|
|
115
|
-
const MAX_ANCHOR_CONFIDENCE =
|
|
115
|
+
const MAX_ANCHOR_CONFIDENCE = 30; // increased max confidence for stronger anchoring
|
|
116
116
|
const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
|
|
117
117
|
if (__DEV__) {
|
|
118
118
|
console.log('[DocScanner] quad', value);
|
|
@@ -189,10 +189,13 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
189
189
|
const centerDelta = Math.hypot(candidateCenter.x - anchorCenter.x, candidateCenter.y - anchorCenter.y);
|
|
190
190
|
const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
|
|
191
191
|
if (centerDelta >= REJECT_CENTER_DELTA || areaShift > 1.2) {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
smoothingBufferRef.current = [sanitized];
|
|
193
|
+
lastMeasurementRef.current = sanitized;
|
|
194
|
+
anchorQuadRef.current = candidate;
|
|
195
|
+
anchorConfidenceRef.current = 1;
|
|
196
|
+
anchorMissesRef.current = 0;
|
|
197
|
+
lastQuadRef.current = candidate;
|
|
198
|
+
setQuad(candidate);
|
|
196
199
|
return;
|
|
197
200
|
}
|
|
198
201
|
if (delta <= SNAP_DISTANCE && centerDelta <= SNAP_CENTER_DISTANCE && areaShift <= 0.08) {
|
|
@@ -246,8 +249,8 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
246
249
|
try {
|
|
247
250
|
// Report frame size for coordinate transformation
|
|
248
251
|
updateFrameSize(frame.width, frame.height);
|
|
249
|
-
// Use higher resolution for better accuracy -
|
|
250
|
-
const ratio =
|
|
252
|
+
// Use higher resolution for better accuracy - 1280p for improved corner detection
|
|
253
|
+
const ratio = 1280 / frame.width;
|
|
251
254
|
const width = Math.floor(frame.width * ratio);
|
|
252
255
|
const height = Math.floor(frame.height * ratio);
|
|
253
256
|
step = 'resize';
|
|
@@ -276,7 +279,9 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
276
279
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', mat, mat, gaussianKernel, 0);
|
|
277
280
|
step = 'Canny';
|
|
278
281
|
reportStage(step);
|
|
279
|
-
|
|
282
|
+
// Improved Canny parameters for better edge detection accuracy
|
|
283
|
+
// Lower threshold (50 -> more edges detected) and higher threshold (150 -> stronger edges kept)
|
|
284
|
+
react_native_fast_opencv_1.OpenCV.invoke('Canny', mat, mat, 50, 150);
|
|
280
285
|
step = 'createContours';
|
|
281
286
|
reportStage(step);
|
|
282
287
|
const contours = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVectorOfVectors);
|
|
@@ -310,7 +315,7 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
310
315
|
console.log('[DocScanner] area', area, 'ratio', areaRatio);
|
|
311
316
|
}
|
|
312
317
|
// Skip if area ratio is too small or too large
|
|
313
|
-
if (areaRatio < 0.
|
|
318
|
+
if (areaRatio < 0.0002 || areaRatio > 0.99) {
|
|
314
319
|
continue;
|
|
315
320
|
}
|
|
316
321
|
// Try to use convex hull for better corner detection
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -116,19 +116,19 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
116
116
|
const frameSizeRef = useRef<{ width: number; height: number } | null>(null);
|
|
117
117
|
|
|
118
118
|
const MAX_HISTORY = 5;
|
|
119
|
-
const SNAP_DISTANCE =
|
|
120
|
-
const SNAP_CENTER_DISTANCE =
|
|
121
|
-
const BLEND_DISTANCE =
|
|
122
|
-
const MAX_CENTER_DELTA =
|
|
123
|
-
const REJECT_CENTER_DELTA =
|
|
124
|
-
const MAX_AREA_SHIFT = 0.
|
|
125
|
-
const HISTORY_RESET_DISTANCE =
|
|
126
|
-
const MIN_AREA_RATIO = 0.
|
|
119
|
+
const SNAP_DISTANCE = 8; // pixels; keep corners locked when movement is tiny (increased for stronger lock)
|
|
120
|
+
const SNAP_CENTER_DISTANCE = 18;
|
|
121
|
+
const BLEND_DISTANCE = 80; // pixels; softly ease between similar shapes (increased)
|
|
122
|
+
const MAX_CENTER_DELTA = 120; // increased to allow more camera movement
|
|
123
|
+
const REJECT_CENTER_DELTA = 200; // increased to maintain lock during camera movement
|
|
124
|
+
const MAX_AREA_SHIFT = 0.55; // more tolerance for perspective changes
|
|
125
|
+
const HISTORY_RESET_DISTANCE = 90;
|
|
126
|
+
const MIN_AREA_RATIO = 0.0002;
|
|
127
127
|
const MAX_AREA_RATIO = 0.9;
|
|
128
|
-
const MIN_EDGE_RATIO = 0.
|
|
129
|
-
const MAX_ANCHOR_MISSES =
|
|
128
|
+
const MIN_EDGE_RATIO = 0.015;
|
|
129
|
+
const MAX_ANCHOR_MISSES = 20; // increased to hold anchor longer when detection temporarily fails
|
|
130
130
|
const MIN_CONFIDENCE_TO_HOLD = 2;
|
|
131
|
-
const MAX_ANCHOR_CONFIDENCE =
|
|
131
|
+
const MAX_ANCHOR_CONFIDENCE = 30; // increased max confidence for stronger anchoring
|
|
132
132
|
|
|
133
133
|
const updateQuad = useRunOnJS((value: Point[] | null) => {
|
|
134
134
|
if (__DEV__) {
|
|
@@ -224,10 +224,13 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
224
224
|
const areaShift = anchorArea > 0 ? Math.abs(anchorArea - candidateArea) / anchorArea : 0;
|
|
225
225
|
|
|
226
226
|
if (centerDelta >= REJECT_CENTER_DELTA || areaShift > 1.2) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
227
|
+
smoothingBufferRef.current = [sanitized];
|
|
228
|
+
lastMeasurementRef.current = sanitized;
|
|
229
|
+
anchorQuadRef.current = candidate;
|
|
230
|
+
anchorConfidenceRef.current = 1;
|
|
231
|
+
anchorMissesRef.current = 0;
|
|
232
|
+
lastQuadRef.current = candidate;
|
|
233
|
+
setQuad(candidate);
|
|
231
234
|
return;
|
|
232
235
|
}
|
|
233
236
|
|
|
@@ -287,8 +290,8 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
287
290
|
// Report frame size for coordinate transformation
|
|
288
291
|
updateFrameSize(frame.width, frame.height);
|
|
289
292
|
|
|
290
|
-
// Use higher resolution for better accuracy -
|
|
291
|
-
const ratio =
|
|
293
|
+
// Use higher resolution for better accuracy - 1280p for improved corner detection
|
|
294
|
+
const ratio = 1280 / frame.width;
|
|
292
295
|
const width = Math.floor(frame.width * ratio);
|
|
293
296
|
const height = Math.floor(frame.height * ratio);
|
|
294
297
|
step = 'resize';
|
|
@@ -321,7 +324,9 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
321
324
|
OpenCV.invoke('GaussianBlur', mat, mat, gaussianKernel, 0);
|
|
322
325
|
step = 'Canny';
|
|
323
326
|
reportStage(step);
|
|
324
|
-
|
|
327
|
+
// Improved Canny parameters for better edge detection accuracy
|
|
328
|
+
// Lower threshold (50 -> more edges detected) and higher threshold (150 -> stronger edges kept)
|
|
329
|
+
OpenCV.invoke('Canny', mat, mat, 50, 150);
|
|
325
330
|
|
|
326
331
|
step = 'createContours';
|
|
327
332
|
reportStage(step);
|
|
@@ -365,7 +370,7 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
365
370
|
}
|
|
366
371
|
|
|
367
372
|
// Skip if area ratio is too small or too large
|
|
368
|
-
if (areaRatio < 0.
|
|
373
|
+
if (areaRatio < 0.0002 || areaRatio > 0.99) {
|
|
369
374
|
continue;
|
|
370
375
|
}
|
|
371
376
|
|