rn-opencv-doc-perspective-correction 1.0.1

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 ADDED
@@ -0,0 +1,107 @@
1
+ # rn-opencv-doc-perspective-correction
2
+
3
+ Thư viện React Native chuyên biệt cho **Page Corner Detection** và **Perspective Correction** — chạy hoàn toàn offline (on-device) thông qua `react-native-fast-opencv` và OpenCV C++ JSI.
4
+
5
+ ## Tính năng
6
+ - **Page Corner Detection:** Tự động nhận diện 4 góc của tờ giấy/tài liệu trong ảnh bất kỳ.
7
+ - **Perspective Correction:** Bóp phẳng ảnh nghiêng dựa trên 4 góc nhận diện, xuất ra ảnh tài liệu chữ nhật hoàn hảo.
8
+
9
+ ## Cài đặt
10
+
11
+ Thư viện này là wrapper của `react-native-fast-opencv`. Đảm bảo peer dependency đã được cài trong dự án chính.
12
+
13
+ ```bash
14
+ # Cài thư viện (local development)
15
+ npm install react-native-fast-opencv
16
+ npm install file:../rn-opencv-doc-perspective-correction
17
+ ```
18
+
19
+ Hoặc thêm trực tiếp vào `package.json`:
20
+ ```json
21
+ {
22
+ "dependencies": {
23
+ "rn-opencv-doc-perspective-correction": "file:../rn-opencv-doc-perspective-correction",
24
+ "react-native-fast-opencv": "^0.4.8"
25
+ }
26
+ }
27
+ ```
28
+
29
+ ## Cách sử dụng
30
+
31
+ ### Luồng hoàn chỉnh (Auto-detect + Warp)
32
+
33
+ ```typescript
34
+ import { DocumentScanner } from 'rn-opencv-doc-perspective-correction';
35
+ import RNFS from 'react-native-fs';
36
+
37
+ // 1. Đọc ảnh thành Base64
38
+ const imageBase64 = await RNFS.readFile('/path/to/image.jpg', 'base64');
39
+
40
+ // 2. Auto-detect 4 góc giấy
41
+ const corners = DocumentScanner.detectPageCorners(imageBase64);
42
+
43
+ if (corners) {
44
+ console.log('Tìm thấy góc:', corners);
45
+ // corners = [
46
+ // { x: 45, y: 120 }, // Top-Left
47
+ // { x: 940, y: 100 }, // Top-Right
48
+ // { x: 960, y: 1280 }, // Bottom-Right
49
+ // { x: 30, y: 1300 }, // Bottom-Left
50
+ // ]
51
+
52
+ // 3. Bóp phẳng ảnh theo 4 góc tìm được (hoặc góc sau khi user chỉnh tay)
53
+ const scannedBase64 = DocumentScanner.applyPerspectiveCorrection(imageBase64, corners);
54
+
55
+ if (scannedBase64) {
56
+ // 4. Lưu lại file kết quả
57
+ await RNFS.writeFile('/path/to/scanned.jpg', scannedBase64, 'base64');
58
+ }
59
+ }
60
+ ```
61
+
62
+ ### Kết hợp với UI tuỳ chỉnh (Bán tự động)
63
+
64
+ ```typescript
65
+ import { DocumentScanner, Point } from 'rn-opencv-doc-perspective-correction';
66
+
67
+ // i. Máy đoán góc trước
68
+ const autoCorners = DocumentScanner.detectPageCorners(imageBase64);
69
+
70
+ // ii. Hiển thị ảnh + 4 nút draggable tại vị trí autoCorners (ví dụ dùng rn-image-crop-skew)
71
+ // Người dùng có thể kéo thả để tinh chỉnh
72
+
73
+ // iii. Sau khi người dùng ấn Done, lấy finalCorners từ UI component
74
+ const finalCorners: Point[] = getFinalCornersFromUI();
75
+
76
+ // iv. Warp với góc đã tinh chỉnh
77
+ const result = DocumentScanner.applyPerspectiveCorrection(imageBase64, finalCorners);
78
+ ```
79
+
80
+ ## Thuật toán OpenCV bên dưới
81
+
82
+ ### Phase 1 — Detect Corners:
83
+ ```
84
+ Input Image
85
+ → cvtColor (BGR2GRAY)
86
+ → GaussianBlur (5x5 kernel)
87
+ → Canny Edge Detection (threshold: 75, 200)
88
+ → findContours (RETR_LIST, CHAIN_APPROX_SIMPLE)
89
+ → Lọc contour diện tích lớn nhất có đúng 4 đỉnh (approxPolyDP)
90
+ → Sắp xếp 4 góc theo hướng kim đồng hồ (TL → TR → BR → BL)
91
+ ```
92
+
93
+ ### Phase 2 — Perspective Correction:
94
+ ```
95
+ [4 góc nguồn] + Input Image
96
+ → Tính maxWidth & maxHeight bằng khoảng cách Euclidean
97
+ → Tạo srcPoints (góc nghiêng gốc) và dstPoints (hình chữ nhật phẳng)
98
+ → getPerspectiveTransform → Ma trận biến dạng
99
+ → warpPerspective → Ảnh đã phẳng
100
+ → toBase64 → Output
101
+ ```
102
+
103
+ ## Lưu ý quan trọng
104
+
105
+ - Hàm **`OpenCV.clearBuffers()`** được gọi tự động trong `finally` block của mỗi hàm để giải phóng bộ nhớ C++ JSI, tránh memory leak.
106
+ - Kết quả `detectPageCorners` trả về **`undefined`** nếu không tìm thấy vật thể 4 góc rõ ràng — trong trường hợp đó, bạn nên yêu cầu User chỉnh tay.
107
+ - Nên dùng ảnh **độ phân giải vừa phải** (1000-2000px) để thuật toán hoạt động chính xác nhất. Ảnh quá nhỏ sẽ mất chi tiết viền; ảnh quá lớn sẽ tốn RAM.
@@ -0,0 +1,22 @@
1
+ export type Point = {
2
+ x: number;
3
+ y: number;
4
+ };
5
+ export declare class DocumentScanner {
6
+ /**
7
+ * Tính khoảng cách Euclidean giữa 2 điểm
8
+ */
9
+ private static getDistance;
10
+ /**
11
+ * Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
12
+ */
13
+ private static sortCorners;
14
+ /**
15
+ * Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
16
+ */
17
+ static detectPageCorners(imageBase64: string): Point[] | undefined;
18
+ /**
19
+ * Bước 2: Perspective Correction
20
+ */
21
+ static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined;
22
+ }
package/dist/index.js ADDED
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DocumentScanner = void 0;
4
+ // @ts-nocheck
5
+ const react_native_fast_opencv_1 = require("react-native-fast-opencv");
6
+ class DocumentScanner {
7
+ /**
8
+ * Tính khoảng cách Euclidean giữa 2 điểm
9
+ */
10
+ static getDistance(p1, p2) {
11
+ return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
12
+ }
13
+ /**
14
+ * Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
15
+ */
16
+ static sortCorners(corners) {
17
+ if (corners.length !== 4)
18
+ return corners;
19
+ const center = corners.reduce((acc, cur) => ({ x: acc.x + cur.x / 4, y: acc.y + cur.y / 4 }), { x: 0, y: 0 });
20
+ return corners.sort((a, b) => {
21
+ const angleA = Math.atan2(a.y - center.y, a.x - center.x);
22
+ const angleB = Math.atan2(b.y - center.y, b.x - center.x);
23
+ return angleA - angleB;
24
+ });
25
+ }
26
+ /**
27
+ * Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
28
+ */
29
+ static detectPageCorners(imageBase64) {
30
+ let src = null;
31
+ let gray = null;
32
+ let blurred = null;
33
+ let edges = null;
34
+ let contoursObj = null;
35
+ let hierarchyObj = null;
36
+ try {
37
+ src = react_native_fast_opencv_1.OpenCV.invoke('Mat', react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64));
38
+ gray = react_native_fast_opencv_1.OpenCV.invoke('Mat');
39
+ blurred = react_native_fast_opencv_1.OpenCV.invoke('Mat');
40
+ edges = react_native_fast_opencv_1.OpenCV.invoke('Mat');
41
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', src, gray, 6); // 6 is BGR2GRAY
42
+ const ksize = react_native_fast_opencv_1.OpenCV.createObject('Size', 5, 5);
43
+ react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
44
+ react_native_fast_opencv_1.OpenCV.invoke('Canny', blurred, edges, 75, 200, 3, false);
45
+ contoursObj = react_native_fast_opencv_1.OpenCV.createObject('MatVector');
46
+ hierarchyObj = react_native_fast_opencv_1.OpenCV.invoke('Mat');
47
+ react_native_fast_opencv_1.OpenCV.invoke('findContours', edges, contoursObj, hierarchyObj, 1, 2);
48
+ const contoursSize = react_native_fast_opencv_1.OpenCV.invoke('size', contoursObj) || 0;
49
+ let maxArea = 0;
50
+ let largestPoly = undefined;
51
+ for (let i = 0; i < contoursSize; i++) {
52
+ const contour = react_native_fast_opencv_1.OpenCV.invoke('get', contoursObj, i);
53
+ const area = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
54
+ if (area > maxArea) {
55
+ const peri = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
56
+ const approx = react_native_fast_opencv_1.OpenCV.invoke('Mat');
57
+ react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
58
+ const vertices = react_native_fast_opencv_1.OpenCV.invoke('rows', approx);
59
+ if (vertices === 4) {
60
+ maxArea = area;
61
+ const points = [];
62
+ for (let v = 0; v < 4; v++) {
63
+ try {
64
+ const pt = react_native_fast_opencv_1.OpenCV.invoke('row', approx, v);
65
+ if (pt && typeof pt === 'object' && 'x' in pt)
66
+ points.push(pt);
67
+ }
68
+ catch (err) { }
69
+ }
70
+ if (points.length === 4)
71
+ largestPoly = points;
72
+ }
73
+ }
74
+ }
75
+ if (largestPoly && largestPoly.length === 4) {
76
+ return this.sortCorners(largestPoly);
77
+ }
78
+ return undefined;
79
+ }
80
+ catch (e) {
81
+ console.error('Lỗi khi dò tìm góc tài liệu (OpenCV):', e);
82
+ return undefined;
83
+ }
84
+ finally {
85
+ react_native_fast_opencv_1.OpenCV.clearBuffers();
86
+ }
87
+ }
88
+ /**
89
+ * Bước 2: Perspective Correction
90
+ */
91
+ static applyPerspectiveCorrection(imageBase64, corners) {
92
+ let src = null;
93
+ let dst = null;
94
+ try {
95
+ if (!corners || corners.length !== 4)
96
+ throw new Error("Cần truyền vào đúng 4 điểm góc.");
97
+ const sortedCorners = this.sortCorners([...corners]);
98
+ const [tl, tr, br, bl] = sortedCorners;
99
+ const widthA = this.getDistance(br, bl);
100
+ const widthB = this.getDistance(tr, tl);
101
+ const maxWidth = Math.max(Math.round(widthA), Math.round(widthB));
102
+ const heightA = this.getDistance(tr, br);
103
+ const heightB = this.getDistance(tl, bl);
104
+ const maxHeight = Math.max(Math.round(heightA), Math.round(heightB));
105
+ if (maxWidth === 0 || maxHeight === 0)
106
+ return undefined;
107
+ src = react_native_fast_opencv_1.OpenCV.invoke('Mat', react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64));
108
+ dst = react_native_fast_opencv_1.OpenCV.invoke('Mat');
109
+ const srcPoints = react_native_fast_opencv_1.OpenCV.createObject('Point2fVector', tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y);
110
+ const dstPoints = react_native_fast_opencv_1.OpenCV.createObject('Point2fVector', 0, 0, maxWidth - 1, 0, maxWidth - 1, maxHeight - 1, 0, maxHeight - 1);
111
+ const perspectiveMatrix = react_native_fast_opencv_1.OpenCV.invoke('getPerspectiveTransform', srcPoints, dstPoints);
112
+ const size = react_native_fast_opencv_1.OpenCV.createObject('Size', maxWidth, maxHeight);
113
+ react_native_fast_opencv_1.OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size);
114
+ return react_native_fast_opencv_1.OpenCV.invoke('toBase64', dst);
115
+ }
116
+ catch (e) {
117
+ console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
118
+ return undefined;
119
+ }
120
+ finally {
121
+ react_native_fast_opencv_1.OpenCV.clearBuffers();
122
+ }
123
+ }
124
+ }
125
+ exports.DocumentScanner = DocumentScanner;
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "rn-opencv-doc-perspective-correction",
3
+ "version": "1.0.1",
4
+ "description": "A React Native library for document corner detection and perspective correction using react-native-fast-opencv",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc",
9
+ "clean": "rm -rf dist"
10
+ },
11
+ "keywords": [
12
+ "react-native",
13
+ "opencv",
14
+ "document-scanner",
15
+ "perspective-correction"
16
+ ],
17
+ "author": "",
18
+ "license": "MIT",
19
+ "peerDependencies": {
20
+ "react": "*",
21
+ "react-native": "*",
22
+ "react-native-fast-opencv": "*"
23
+ },
24
+ "devDependencies": {
25
+ "typescript": "^5.0.0"
26
+ }
27
+ }
package/src/index.ts ADDED
@@ -0,0 +1,150 @@
1
+ // @ts-nocheck
2
+ import { OpenCV, OpenCVMat } from 'react-native-fast-opencv';
3
+
4
+ export type Point = { x: number; y: number };
5
+
6
+ export class DocumentScanner {
7
+ /**
8
+ * Tính khoảng cách Euclidean giữa 2 điểm
9
+ */
10
+ private static getDistance(p1: Point, p2: Point) {
11
+ return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
12
+ }
13
+
14
+ /**
15
+ * Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
16
+ */
17
+ private static sortCorners(corners: Point[]): Point[] {
18
+ if (corners.length !== 4) return corners;
19
+
20
+ const center = corners.reduce(
21
+ (acc, cur) => ({ x: acc.x + cur.x / 4, y: acc.y + cur.y / 4 }),
22
+ { x: 0, y: 0 }
23
+ );
24
+
25
+ return corners.sort((a, b) => {
26
+ const angleA = Math.atan2(a.y - center.y, a.x - center.x);
27
+ const angleB = Math.atan2(b.y - center.y, b.x - center.x);
28
+ return angleA - angleB;
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
34
+ */
35
+ public static detectPageCorners(imageBase64: string): Point[] | undefined {
36
+ let src: OpenCVMat | null = null;
37
+ let gray: OpenCVMat | null = null;
38
+ let blurred: OpenCVMat | null = null;
39
+ let edges: OpenCVMat | null = null;
40
+ let contoursObj: any = null;
41
+ let hierarchyObj: any = null;
42
+
43
+ try {
44
+ src = OpenCV.invoke('Mat', OpenCV.base64ToMat(imageBase64));
45
+ gray = OpenCV.invoke('Mat');
46
+ blurred = OpenCV.invoke('Mat');
47
+ edges = OpenCV.invoke('Mat');
48
+
49
+ OpenCV.invoke('cvtColor', src, gray, 6); // 6 is BGR2GRAY
50
+
51
+ const ksize = OpenCV.createObject('Size', 5, 5);
52
+ OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
53
+
54
+ OpenCV.invoke('Canny', blurred, edges, 75, 200, 3, false);
55
+
56
+ contoursObj = OpenCV.createObject('MatVector');
57
+ hierarchyObj = OpenCV.invoke('Mat');
58
+
59
+ OpenCV.invoke('findContours', edges, contoursObj, hierarchyObj, 1, 2);
60
+
61
+ const contoursSize = OpenCV.invoke('size', contoursObj) || 0;
62
+ let maxArea = 0;
63
+ let largestPoly: Point[] | undefined = undefined;
64
+
65
+ for (let i = 0; i < contoursSize; i++) {
66
+ const contour = OpenCV.invoke('get', contoursObj, i);
67
+ const area = OpenCV.invoke('contourArea', contour);
68
+
69
+ if (area > maxArea) {
70
+ const peri = OpenCV.invoke('arcLength', contour, true);
71
+ const approx = OpenCV.invoke('Mat');
72
+ OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
73
+
74
+ const vertices = OpenCV.invoke('rows', approx);
75
+ if (vertices === 4) {
76
+ maxArea = area;
77
+
78
+ const points: Point[] = [];
79
+ for (let v = 0; v < 4; v++) {
80
+ try {
81
+ const pt = OpenCV.invoke('row', approx, v);
82
+ if (pt && typeof pt === 'object' && 'x' in pt) points.push(pt as Point);
83
+ } catch (err) { }
84
+ }
85
+ if (points.length === 4) largestPoly = points;
86
+ }
87
+ }
88
+ }
89
+
90
+ if (largestPoly && largestPoly.length === 4) {
91
+ return this.sortCorners(largestPoly);
92
+ }
93
+ return undefined;
94
+ } catch (e) {
95
+ console.error('Lỗi khi dò tìm góc tài liệu (OpenCV):', e);
96
+ return undefined;
97
+ } finally {
98
+ OpenCV.clearBuffers();
99
+ }
100
+ }
101
+
102
+ /**
103
+ * Bước 2: Perspective Correction
104
+ */
105
+ public static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined {
106
+ let src: OpenCVMat | null = null;
107
+ let dst: OpenCVMat | null = null;
108
+
109
+ try {
110
+ if (!corners || corners.length !== 4) throw new Error("Cần truyền vào đúng 4 điểm góc.");
111
+
112
+ const sortedCorners = this.sortCorners([...corners]);
113
+ const [tl, tr, br, bl] = sortedCorners;
114
+
115
+ const widthA = this.getDistance(br, bl);
116
+ const widthB = this.getDistance(tr, tl);
117
+ const maxWidth = Math.max(Math.round(widthA), Math.round(widthB));
118
+
119
+ const heightA = this.getDistance(tr, br);
120
+ const heightB = this.getDistance(tl, bl);
121
+ const maxHeight = Math.max(Math.round(heightA), Math.round(heightB));
122
+
123
+ if (maxWidth === 0 || maxHeight === 0) return undefined;
124
+
125
+ src = OpenCV.invoke('Mat', OpenCV.base64ToMat(imageBase64));
126
+ dst = OpenCV.invoke('Mat');
127
+
128
+ const srcPoints = OpenCV.createObject('Point2fVector', tl.x, tl.y, tr.x, tr.y, br.x, br.y, bl.x, bl.y);
129
+ const dstPoints = OpenCV.createObject(
130
+ 'Point2fVector',
131
+ 0, 0,
132
+ maxWidth - 1, 0,
133
+ maxWidth - 1, maxHeight - 1,
134
+ 0, maxHeight - 1
135
+ );
136
+
137
+ const perspectiveMatrix = OpenCV.invoke('getPerspectiveTransform', srcPoints, dstPoints);
138
+
139
+ const size = OpenCV.createObject('Size', maxWidth, maxHeight);
140
+ OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size);
141
+
142
+ return OpenCV.invoke('toBase64', dst);
143
+ } catch (e) {
144
+ console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
145
+ return undefined;
146
+ } finally {
147
+ OpenCV.clearBuffers();
148
+ }
149
+ }
150
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "es2015",
4
+ "module": "commonjs",
5
+ "declaration": true,
6
+ "outDir": "./dist",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true
11
+ },
12
+ "include": [
13
+ "src"
14
+ ]
15
+ }