react-native-rectangle-doc-scanner 0.3.0 → 0.5.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.
@@ -38,10 +38,32 @@ const react_1 = __importStar(require("react"));
38
38
  const react_native_1 = require("react-native");
39
39
  const react_native_vision_camera_1 = require("react-native-vision-camera");
40
40
  const vision_camera_resize_plugin_1 = require("vision-camera-resize-plugin");
41
- const react_native_reanimated_1 = require("react-native-reanimated");
41
+ const react_native_worklets_core_1 = require("react-native-worklets-core");
42
42
  const react_native_fast_opencv_1 = require("react-native-fast-opencv");
43
43
  const overlay_1 = require("./utils/overlay");
44
44
  const stability_1 = require("./utils/stability");
45
+ const isConvexQuadrilateral = (points) => {
46
+ if (points.length !== 4) {
47
+ return false;
48
+ }
49
+ let previous = 0;
50
+ for (let i = 0; i < 4; i++) {
51
+ const p0 = points[i];
52
+ const p1 = points[(i + 1) % 4];
53
+ const p2 = points[(i + 2) % 4];
54
+ const cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
55
+ if (Math.abs(cross) < 1e-3) {
56
+ return false;
57
+ }
58
+ if (i === 0) {
59
+ previous = cross;
60
+ }
61
+ else if (previous * cross < 0) {
62
+ return false;
63
+ }
64
+ }
65
+ return true;
66
+ };
45
67
  const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, minStableFrames = 8, cameraProps, children, }) => {
46
68
  const device = (0, react_native_vision_camera_1.useCameraDevice)('back');
47
69
  const { hasPermission, requestPermission } = (0, react_native_vision_camera_1.useCameraPermission)();
@@ -55,52 +77,89 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
55
77
  (0, react_1.useEffect)(() => {
56
78
  requestPermission();
57
79
  }, [requestPermission]);
80
+ const updateQuad = (0, react_native_worklets_core_1.useRunOnJS)((value) => {
81
+ setQuad(value);
82
+ }, []);
83
+ const reportError = (0, react_native_worklets_core_1.useRunOnJS)((step, error) => {
84
+ const message = error instanceof Error ? error.message : `${error}`;
85
+ console.warn(`[DocScanner] frame error at ${step}: ${message}`);
86
+ }, []);
58
87
  const frameProcessor = (0, react_native_vision_camera_1.useFrameProcessor)((frame) => {
59
88
  'worklet';
60
- const ratio = 480 / frame.width;
61
- const w = Math.floor(frame.width * ratio);
62
- const h = Math.floor(frame.height * ratio);
63
- const resized = resize(frame, {
64
- dataType: 'uint8',
65
- pixelFormat: 'bgr',
66
- scale: { width: w, height: h },
67
- });
68
- const mat = react_native_fast_opencv_1.OpenCV.frameBufferToMat(h, w, 3, resized);
69
- react_native_fast_opencv_1.OpenCV.invoke('cvtColor', mat, mat, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
70
- const kernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 4, 4);
71
- const element = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', react_native_fast_opencv_1.MorphShapes.MORPH_RECT, kernel);
72
- react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_OPEN, element);
73
- react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', mat, mat, kernel, 0);
74
- react_native_fast_opencv_1.OpenCV.invoke('Canny', mat, mat, 75, 100);
75
- const contours = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVectorOfVectors);
76
- react_native_fast_opencv_1.OpenCV.invoke('findContours', mat, contours, react_native_fast_opencv_1.RetrievalModes.RETR_LIST, react_native_fast_opencv_1.ContourApproximationModes.CHAIN_APPROX_SIMPLE);
77
- let best = null;
78
- let maxArea = 0;
79
- const arr = react_native_fast_opencv_1.OpenCV.toJSValue(contours).array;
80
- for (let i = 0; i < arr.length; i++) {
81
- const c = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contours, i);
82
- const { value: area } = react_native_fast_opencv_1.OpenCV.invoke('contourArea', c, false);
83
- if (area < w * h * 0.1) {
84
- continue;
85
- }
86
- const { value: peri } = react_native_fast_opencv_1.OpenCV.invoke('arcLength', c, true);
87
- const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
88
- react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', c, approx, 0.02 * peri, true);
89
- const size = react_native_fast_opencv_1.OpenCV.invokeWithOutParam('size', approx);
90
- const { value: convex } = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
91
- if (convex && size === 4 && area > maxArea) {
92
- const pts = [];
93
- for (let j = 0; j < 4; j++) {
94
- const p = react_native_fast_opencv_1.OpenCV.invoke('atPoint', approx, j, 0);
95
- pts.push({ x: p.x / ratio, y: p.y / ratio });
89
+ let step = 'start';
90
+ try {
91
+ const ratio = 480 / frame.width;
92
+ const width = Math.floor(frame.width * ratio);
93
+ const height = Math.floor(frame.height * ratio);
94
+ step = 'resize';
95
+ const resized = resize(frame, {
96
+ dataType: 'uint8',
97
+ pixelFormat: 'bgr',
98
+ scale: { width: width, height: height },
99
+ });
100
+ step = 'frameBufferToMat';
101
+ const mat = react_native_fast_opencv_1.OpenCV.frameBufferToMat(height, width, 3, resized);
102
+ step = 'cvtColor';
103
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', mat, mat, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
104
+ const morphologyKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
105
+ step = 'getStructuringElement';
106
+ const element = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', react_native_fast_opencv_1.MorphShapes.MORPH_RECT, morphologyKernel);
107
+ step = 'morphologyEx';
108
+ react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_OPEN, element);
109
+ const gaussianKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
110
+ step = 'GaussianBlur';
111
+ react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', mat, mat, gaussianKernel, 0);
112
+ step = 'Canny';
113
+ react_native_fast_opencv_1.OpenCV.invoke('Canny', mat, mat, 75, 100);
114
+ step = 'createContours';
115
+ const contours = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVectorOfVectors);
116
+ react_native_fast_opencv_1.OpenCV.invoke('findContours', mat, contours, react_native_fast_opencv_1.RetrievalModes.RETR_LIST, react_native_fast_opencv_1.ContourApproximationModes.CHAIN_APPROX_SIMPLE);
117
+ let best = null;
118
+ let maxArea = 0;
119
+ step = 'toJSValue';
120
+ const contourVector = react_native_fast_opencv_1.OpenCV.toJSValue(contours);
121
+ const contourArray = Array.isArray(contourVector?.array) ? contourVector.array : [];
122
+ for (let i = 0; i < contourArray.length; i += 1) {
123
+ step = `contour_${i}_copy`;
124
+ const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contours, i);
125
+ step = `contour_${i}_area`;
126
+ const { value: area } = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour, false);
127
+ if (area < width * height * 0.1) {
128
+ continue;
129
+ }
130
+ step = `contour_${i}_arcLength`;
131
+ const { value: perimeter } = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
132
+ const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
133
+ step = `contour_${i}_approxPolyDP`;
134
+ react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * perimeter, true);
135
+ step = `contour_${i}_toJS`;
136
+ const approxValue = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
137
+ const approxArray = Array.isArray(approxValue?.array) ? approxValue.array : [];
138
+ if (approxArray.length !== 4) {
139
+ continue;
140
+ }
141
+ step = `contour_${i}_convex`;
142
+ const points = approxArray.map((pt) => ({
143
+ x: pt.x / ratio,
144
+ y: pt.y / ratio,
145
+ }));
146
+ if (!isConvexQuadrilateral(points)) {
147
+ continue;
148
+ }
149
+ if (area > maxArea) {
150
+ best = points;
151
+ maxArea = area;
96
152
  }
97
- best = pts;
98
- maxArea = area;
99
153
  }
154
+ step = 'clearBuffers';
155
+ react_native_fast_opencv_1.OpenCV.clearBuffers();
156
+ step = 'updateQuad';
157
+ updateQuad(best);
158
+ }
159
+ catch (error) {
160
+ reportError(step, error);
100
161
  }
101
- react_native_fast_opencv_1.OpenCV.clearBuffers();
102
- (0, react_native_reanimated_1.runOnJS)(setQuad)(best);
103
- }, [resize]);
162
+ }, [resize, reportError, updateQuad]);
104
163
  (0, react_1.useEffect)(() => {
105
164
  const s = (0, stability_1.checkStability)(quad);
106
165
  setStable(s);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-rectangle-doc-scanner",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "repository": {
@@ -29,5 +29,8 @@
29
29
  "@types/react": "^18.2.41",
30
30
  "@types/react-native": "0.73.0",
31
31
  "typescript": "^5.3.3"
32
+ },
33
+ "dependencies": {
34
+ "react-native-worklets-core": "^1.6.2"
32
35
  }
33
36
  }
@@ -2,7 +2,7 @@ import React, { ReactNode, useCallback, useEffect, useRef, useState } from 'reac
2
2
  import { View, TouchableOpacity, StyleSheet } from 'react-native';
3
3
  import { Camera, useCameraDevice, useCameraPermission, useFrameProcessor } from 'react-native-vision-camera';
4
4
  import { useResizePlugin } from 'vision-camera-resize-plugin';
5
- import { runOnJS } from 'react-native-reanimated';
5
+ import { useRunOnJS } from 'react-native-worklets-core';
6
6
  import {
7
7
  OpenCV,
8
8
  ColorConversionCodes,
@@ -16,6 +16,33 @@ import { Overlay } from './utils/overlay';
16
16
  import { checkStability } from './utils/stability';
17
17
  import type { Point } from './types';
18
18
 
19
+ const isConvexQuadrilateral = (points: Point[]) => {
20
+ if (points.length !== 4) {
21
+ return false;
22
+ }
23
+
24
+ let previous = 0;
25
+
26
+ for (let i = 0; i < 4; i++) {
27
+ const p0 = points[i];
28
+ const p1 = points[(i + 1) % 4];
29
+ const p2 = points[(i + 2) % 4];
30
+ const cross = (p1.x - p0.x) * (p2.y - p1.y) - (p1.y - p0.y) * (p2.x - p1.x);
31
+
32
+ if (Math.abs(cross) < 1e-3) {
33
+ return false;
34
+ }
35
+
36
+ if (i === 0) {
37
+ previous = cross;
38
+ } else if (previous * cross < 0) {
39
+ return false;
40
+ }
41
+ }
42
+
43
+ return true;
44
+ };
45
+
19
46
  type CameraRef = {
20
47
  takePhoto: (options: { qualityPrioritization: 'balanced' | 'quality' | 'speed' }) => Promise<{
21
48
  path: string;
@@ -55,65 +82,110 @@ export const DocScanner: React.FC<Props> = ({
55
82
  requestPermission();
56
83
  }, [requestPermission]);
57
84
 
58
- const frameProcessor = useFrameProcessor((frame) => {
59
- 'worklet';
60
-
61
- const ratio = 480 / frame.width;
62
- const w = Math.floor(frame.width * ratio);
63
- const h = Math.floor(frame.height * ratio);
64
- const resized = resize(frame, {
65
- dataType: 'uint8',
66
- pixelFormat: 'bgr',
67
- scale: { width: w, height: h },
68
- });
85
+ const updateQuad = useRunOnJS((value: Point[] | null) => {
86
+ setQuad(value);
87
+ }, []);
69
88
 
70
- const mat = OpenCV.frameBufferToMat(h, w, 3, resized);
89
+ const reportError = useRunOnJS((step: string, error: unknown) => {
90
+ const message = error instanceof Error ? error.message : `${error}`;
91
+ console.warn(`[DocScanner] frame error at ${step}: ${message}`);
92
+ }, []);
71
93
 
72
- OpenCV.invoke('cvtColor', mat, mat, ColorConversionCodes.COLOR_BGR2GRAY);
94
+ const frameProcessor = useFrameProcessor((frame) => {
95
+ 'worklet';
73
96
 
74
- const kernel = OpenCV.createObject(ObjectType.Size, 4, 4);
75
- const element = OpenCV.invoke('getStructuringElement', MorphShapes.MORPH_RECT, kernel);
76
- OpenCV.invoke('morphologyEx', mat, mat, MorphTypes.MORPH_OPEN, element);
97
+ let step = 'start';
98
+
99
+ try {
100
+ const ratio = 480 / frame.width;
101
+ const width = Math.floor(frame.width * ratio);
102
+ const height = Math.floor(frame.height * ratio);
103
+ step = 'resize';
104
+ const resized = resize(frame, {
105
+ dataType: 'uint8',
106
+ pixelFormat: 'bgr',
107
+ scale: { width: width, height: height },
108
+ });
109
+
110
+ step = 'frameBufferToMat';
111
+ const mat = OpenCV.frameBufferToMat(height, width, 3, resized);
112
+
113
+ step = 'cvtColor';
114
+ OpenCV.invoke('cvtColor', mat, mat, ColorConversionCodes.COLOR_BGR2GRAY);
115
+
116
+ const morphologyKernel = OpenCV.createObject(ObjectType.Size, 5, 5);
117
+ step = 'getStructuringElement';
118
+ const element = OpenCV.invoke('getStructuringElement', MorphShapes.MORPH_RECT, morphologyKernel);
119
+ step = 'morphologyEx';
120
+ OpenCV.invoke('morphologyEx', mat, mat, MorphTypes.MORPH_OPEN, element);
121
+
122
+ const gaussianKernel = OpenCV.createObject(ObjectType.Size, 5, 5);
123
+ step = 'GaussianBlur';
124
+ OpenCV.invoke('GaussianBlur', mat, mat, gaussianKernel, 0);
125
+ step = 'Canny';
126
+ OpenCV.invoke('Canny', mat, mat, 75, 100);
127
+
128
+ step = 'createContours';
129
+ const contours = OpenCV.createObject(ObjectType.PointVectorOfVectors);
130
+ OpenCV.invoke('findContours', mat, contours, RetrievalModes.RETR_LIST, ContourApproximationModes.CHAIN_APPROX_SIMPLE);
131
+
132
+ let best: Point[] | null = null;
133
+ let maxArea = 0;
134
+
135
+ step = 'toJSValue';
136
+ const contourVector = OpenCV.toJSValue(contours);
137
+ const contourArray = Array.isArray(contourVector?.array) ? contourVector.array : [];
138
+
139
+ for (let i = 0; i < contourArray.length; i += 1) {
140
+ step = `contour_${i}_copy`;
141
+ const contour = OpenCV.copyObjectFromVector(contours, i);
142
+
143
+ step = `contour_${i}_area`;
144
+ const { value: area } = OpenCV.invoke('contourArea', contour, false);
145
+
146
+ if (area < width * height * 0.1) {
147
+ continue;
148
+ }
77
149
 
78
- OpenCV.invoke('GaussianBlur', mat, mat, kernel, 0);
79
- OpenCV.invoke('Canny', mat, mat, 75, 100);
150
+ step = `contour_${i}_arcLength`;
151
+ const { value: perimeter } = OpenCV.invoke('arcLength', contour, true);
152
+ const approx = OpenCV.createObject(ObjectType.PointVector);
80
153
 
81
- const contours = OpenCV.createObject(ObjectType.PointVectorOfVectors);
82
- OpenCV.invoke('findContours', mat, contours, RetrievalModes.RETR_LIST, ContourApproximationModes.CHAIN_APPROX_SIMPLE);
154
+ step = `contour_${i}_approxPolyDP`;
155
+ OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * perimeter, true);
83
156
 
84
- let best: Point[] | null = null;
85
- let maxArea = 0;
157
+ step = `contour_${i}_toJS`;
158
+ const approxValue = OpenCV.toJSValue(approx);
159
+ const approxArray = Array.isArray(approxValue?.array) ? approxValue.array : [];
86
160
 
87
- const arr = OpenCV.toJSValue(contours).array;
161
+ if (approxArray.length !== 4) {
162
+ continue;
163
+ }
88
164
 
89
- for (let i = 0; i < arr.length; i++) {
90
- const c = OpenCV.copyObjectFromVector(contours, i);
91
- const { value: area } = OpenCV.invoke('contourArea', c, false);
165
+ step = `contour_${i}_convex`;
166
+ const points: Point[] = approxArray.map((pt: { x: number; y: number }) => ({
167
+ x: pt.x / ratio,
168
+ y: pt.y / ratio,
169
+ }));
92
170
 
93
- if (area < w * h * 0.1) {
94
- continue;
95
- }
171
+ if (!isConvexQuadrilateral(points)) {
172
+ continue;
173
+ }
96
174
 
97
- const { value: peri } = OpenCV.invoke('arcLength', c, true);
98
- const approx = OpenCV.createObject(ObjectType.PointVector);
99
- OpenCV.invoke('approxPolyDP', c, approx, 0.02 * peri, true);
100
- const size = OpenCV.invokeWithOutParam('size', approx);
101
- const { value: convex } = OpenCV.invoke('isContourConvex', approx);
102
-
103
- if (convex && size === 4 && area > maxArea) {
104
- const pts: Point[] = [];
105
- for (let j = 0; j < 4; j++) {
106
- const p = OpenCV.invoke('atPoint', approx, j, 0);
107
- pts.push({ x: p.x / ratio, y: p.y / ratio });
175
+ if (area > maxArea) {
176
+ best = points;
177
+ maxArea = area;
108
178
  }
109
- best = pts;
110
- maxArea = area;
111
179
  }
112
- }
113
180
 
114
- OpenCV.clearBuffers();
115
- runOnJS(setQuad)(best);
116
- }, [resize]);
181
+ step = 'clearBuffers';
182
+ OpenCV.clearBuffers();
183
+ step = 'updateQuad';
184
+ updateQuad(best);
185
+ } catch (error) {
186
+ reportError(step, error);
187
+ }
188
+ }, [resize, reportError, updateQuad]);
117
189
 
118
190
  useEffect(() => {
119
191
  const s = checkStability(quad);
@@ -0,0 +1,8 @@
1
+ import type { DependencyList } from 'react';
2
+
3
+ declare module 'react-native-worklets-core' {
4
+ export function useRunOnJS<T extends (...args: any[]) => any>(
5
+ callback: T,
6
+ deps: DependencyList
7
+ ): (...args: Parameters<T>) => void;
8
+ }