rn-opencv-doc-perspective-correction 1.0.7 → 1.0.9
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/dist/index.d.ts +3 -3
- package/dist/index.js +3 -15
- package/package.json +1 -1
- package/src/index.ts +288 -209
package/dist/index.d.ts
CHANGED
|
@@ -5,10 +5,10 @@ export type Point = {
|
|
|
5
5
|
export declare class DocumentScanner {
|
|
6
6
|
private static getDistance;
|
|
7
7
|
private static sortCorners;
|
|
8
|
-
static detectPageCorners(imageBase64: string
|
|
9
|
-
static applyPerspectiveCorrection(imageBase64: string, corners: Point[]
|
|
8
|
+
static detectPageCorners(imageBase64: string): Point[] | undefined;
|
|
9
|
+
static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined;
|
|
10
10
|
/**
|
|
11
11
|
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
12
12
|
*/
|
|
13
|
-
static rotateImage(imageBase64: string, angle: 90 | -90 | 180
|
|
13
|
+
static rotateImage(imageBase64: string, angle: 90 | -90 | 180): string | undefined;
|
|
14
14
|
}
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ class DocumentScanner {
|
|
|
17
17
|
return angleA - angleB;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
-
static detectPageCorners(imageBase64
|
|
20
|
+
static detectPageCorners(imageBase64) {
|
|
21
21
|
let src = null;
|
|
22
22
|
let gray = null;
|
|
23
23
|
let blurred = null;
|
|
@@ -45,8 +45,6 @@ class DocumentScanner {
|
|
|
45
45
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
46
46
|
const contoursSize = contoursArray.length;
|
|
47
47
|
if (contoursSize === 0) {
|
|
48
|
-
if (onLog)
|
|
49
|
-
onLog(`[OpenCV] Không tìm thấy contours.`);
|
|
50
48
|
return undefined;
|
|
51
49
|
}
|
|
52
50
|
// First pass: extract all areas to sort them and minimize JSI calls
|
|
@@ -76,10 +74,6 @@ class DocumentScanner {
|
|
|
76
74
|
break; // Stop at the first 4-point polygon like python script
|
|
77
75
|
}
|
|
78
76
|
}
|
|
79
|
-
const logMsg = `[OpenCV] Contours: ${contoursSize}. Metrics pass: ${contourMetrics.length}. Poly detect: ${largestPoly ? 'Thành công' : 'Thất bại'}.`;
|
|
80
|
-
console.log(logMsg);
|
|
81
|
-
if (onLog)
|
|
82
|
-
onLog(logMsg);
|
|
83
77
|
if (largestPoly && largestPoly.length === 4) {
|
|
84
78
|
return this.sortCorners(largestPoly);
|
|
85
79
|
}
|
|
@@ -87,15 +81,13 @@ class DocumentScanner {
|
|
|
87
81
|
}
|
|
88
82
|
catch (e) {
|
|
89
83
|
console.error('Lỗi khi dò tìm góc tài liệu (OpenCV):', e);
|
|
90
|
-
if (onLog)
|
|
91
|
-
onLog(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
92
84
|
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
93
85
|
}
|
|
94
86
|
finally {
|
|
95
87
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
|
96
88
|
}
|
|
97
89
|
}
|
|
98
|
-
static applyPerspectiveCorrection(imageBase64, corners
|
|
90
|
+
static applyPerspectiveCorrection(imageBase64, corners) {
|
|
99
91
|
let src = null;
|
|
100
92
|
let dst = null;
|
|
101
93
|
try {
|
|
@@ -138,8 +130,6 @@ class DocumentScanner {
|
|
|
138
130
|
}
|
|
139
131
|
catch (e) {
|
|
140
132
|
console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
|
|
141
|
-
if (onLog)
|
|
142
|
-
onLog(`[OpenCV Perspective Correction Error]: ${e.message || e}`);
|
|
143
133
|
return undefined;
|
|
144
134
|
}
|
|
145
135
|
finally {
|
|
@@ -149,7 +139,7 @@ class DocumentScanner {
|
|
|
149
139
|
/**
|
|
150
140
|
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
151
141
|
*/
|
|
152
|
-
static rotateImage(imageBase64, angle
|
|
142
|
+
static rotateImage(imageBase64, angle) {
|
|
153
143
|
let src = null;
|
|
154
144
|
let dst = null;
|
|
155
145
|
try {
|
|
@@ -169,8 +159,6 @@ class DocumentScanner {
|
|
|
169
159
|
}
|
|
170
160
|
catch (e) {
|
|
171
161
|
console.error('Lỗi khi xoay ảnh (OpenCV):', e);
|
|
172
|
-
if (onLog)
|
|
173
|
-
onLog(`[OpenCV Rotate Error]: ${e.message}`);
|
|
174
162
|
return undefined;
|
|
175
163
|
}
|
|
176
164
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rn-opencv-doc-perspective-correction",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.9",
|
|
4
4
|
"description": "A React Native library for document corner detection and perspective correction using react-native-fast-opencv",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/src/index.ts
CHANGED
|
@@ -1,209 +1,288 @@
|
|
|
1
|
-
// @ts-nocheck
|
|
2
|
-
import { OpenCV, OpenCVMat, ObjectType, DataTypes, ColorConversionCodes } from 'react-native-fast-opencv';
|
|
3
|
-
|
|
4
|
-
export type Point = { x: number; y: number };
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
OpenCV.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
OpenCV.
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
OpenCV.
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
const
|
|
71
|
-
const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
contourMetrics.sort((a, b) => b.area - a.area);
|
|
79
|
-
|
|
80
|
-
let largestPoly: Point[] | undefined = undefined;
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
const
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
OpenCV.createObject(ObjectType.Point2f,
|
|
144
|
-
OpenCV.createObject(ObjectType.Point2f,
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { OpenCV, OpenCVMat, ObjectType, DataTypes, ColorConversionCodes } from 'react-native-fast-opencv';
|
|
3
|
+
|
|
4
|
+
export type Point = { x: number; y: number };
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Gợi ý phương thức OCR nên dùng dựa trên phân tích màu sắc.
|
|
8
|
+
* 'G' = ML Kit (nhiều màu, ảnh thẻ, màu sắc phong phú)
|
|
9
|
+
* 'S' = Tesseract (ít màu, bản scan, giấy trắng)
|
|
10
|
+
*/
|
|
11
|
+
export type OcrMethodHint = 'G' | 'S';
|
|
12
|
+
|
|
13
|
+
export class DocumentScanner {
|
|
14
|
+
private static getDistance(p1: Point, p2: Point) {
|
|
15
|
+
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
private static sortCorners(corners: Point[]): Point[] {
|
|
19
|
+
if (corners.length !== 4) return corners;
|
|
20
|
+
|
|
21
|
+
const center = corners.reduce(
|
|
22
|
+
(acc, cur) => ({ x: acc.x + cur.x / 4, y: acc.y + cur.y / 4 }),
|
|
23
|
+
{ x: 0, y: 0 }
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return corners.sort((a, b) => {
|
|
27
|
+
const angleA = Math.atan2(a.y - center.y, a.x - center.x);
|
|
28
|
+
const angleB = Math.atan2(b.y - center.y, b.x - center.x);
|
|
29
|
+
return angleA - angleB;
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public static detectPageCorners(imageBase64: string): Point[] | undefined {
|
|
34
|
+
let src: OpenCVMat | null = null;
|
|
35
|
+
let gray: OpenCVMat | null = null;
|
|
36
|
+
let blurred: OpenCVMat | null = null;
|
|
37
|
+
let edges: OpenCVMat | null = null;
|
|
38
|
+
let contoursObj: any = null;
|
|
39
|
+
let hierarchyObj: any = null;
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
src = OpenCV.base64ToMat(imageBase64);
|
|
43
|
+
gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
44
|
+
blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
45
|
+
edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
46
|
+
|
|
47
|
+
OpenCV.invoke('cvtColor', src, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
48
|
+
|
|
49
|
+
const ksize = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
50
|
+
OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
51
|
+
|
|
52
|
+
// THRESH_BINARY + THRESH_OTSU = 8
|
|
53
|
+
OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
|
|
54
|
+
|
|
55
|
+
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
56
|
+
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
57
|
+
|
|
58
|
+
OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
59
|
+
|
|
60
|
+
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
61
|
+
const contoursArray = contoursJS?.array || [];
|
|
62
|
+
const contoursSize = contoursArray.length;
|
|
63
|
+
|
|
64
|
+
if (contoursSize === 0) {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
let contourMetrics = [];
|
|
69
|
+
for (let i = 0; i < contoursSize; i++) {
|
|
70
|
+
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
71
|
+
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
72
|
+
const area = areaObj ? areaObj.value : 0;
|
|
73
|
+
if (area > 5000) {
|
|
74
|
+
contourMetrics.push({ index: i, area, contour });
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
contourMetrics.sort((a, b) => b.area - a.area);
|
|
79
|
+
|
|
80
|
+
let largestPoly: Point[] | undefined = undefined;
|
|
81
|
+
|
|
82
|
+
for (let i = 0; i < contourMetrics.length; i++) {
|
|
83
|
+
const metric = contourMetrics[i];
|
|
84
|
+
const contour = metric.contour;
|
|
85
|
+
|
|
86
|
+
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
87
|
+
const peri = periObj ? periObj.value : 0;
|
|
88
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
89
|
+
OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
90
|
+
|
|
91
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
92
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
93
|
+
largestPoly = approxJS.array as Point[];
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (largestPoly && largestPoly.length === 4) {
|
|
99
|
+
return this.sortCorners(largestPoly);
|
|
100
|
+
}
|
|
101
|
+
return undefined;
|
|
102
|
+
} catch (e: any) {
|
|
103
|
+
console.error('Lỗi khi dò tìm góc tài liệu (OpenCV):', e);
|
|
104
|
+
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
105
|
+
} finally {
|
|
106
|
+
OpenCV.clearBuffers();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
public static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined {
|
|
111
|
+
let src: OpenCVMat | null = null;
|
|
112
|
+
let dst: OpenCVMat | null = null;
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
if (!corners || corners.length !== 4) throw new Error("Cần truyền vào đúng 4 điểm góc.");
|
|
116
|
+
|
|
117
|
+
const sortedCorners = this.sortCorners([...corners]);
|
|
118
|
+
const [tl, tr, br, bl] = sortedCorners;
|
|
119
|
+
|
|
120
|
+
const widthA = this.getDistance(br, bl);
|
|
121
|
+
const widthB = this.getDistance(tr, tl);
|
|
122
|
+
const maxWidth = Math.max(Math.round(widthA), Math.round(widthB));
|
|
123
|
+
|
|
124
|
+
const heightA = this.getDistance(tr, br);
|
|
125
|
+
const heightB = this.getDistance(tl, bl);
|
|
126
|
+
const maxHeight = Math.max(Math.round(heightA), Math.round(heightB));
|
|
127
|
+
|
|
128
|
+
if (maxWidth === 0 || maxHeight === 0) return undefined;
|
|
129
|
+
|
|
130
|
+
src = OpenCV.base64ToMat(imageBase64);
|
|
131
|
+
dst = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
|
|
132
|
+
|
|
133
|
+
const srcPoints = OpenCV.createObject(ObjectType.Point2fVector,
|
|
134
|
+
[
|
|
135
|
+
OpenCV.createObject(ObjectType.Point2f, tl.x, tl.y),
|
|
136
|
+
OpenCV.createObject(ObjectType.Point2f, tr.x, tr.y),
|
|
137
|
+
OpenCV.createObject(ObjectType.Point2f, br.x, br.y),
|
|
138
|
+
OpenCV.createObject(ObjectType.Point2f, bl.x, bl.y)
|
|
139
|
+
]
|
|
140
|
+
);
|
|
141
|
+
const dstPoints = OpenCV.createObject(ObjectType.Point2fVector,
|
|
142
|
+
[
|
|
143
|
+
OpenCV.createObject(ObjectType.Point2f, 0, 0),
|
|
144
|
+
OpenCV.createObject(ObjectType.Point2f, maxWidth - 1, 0),
|
|
145
|
+
OpenCV.createObject(ObjectType.Point2f, maxWidth - 1, maxHeight - 1),
|
|
146
|
+
OpenCV.createObject(ObjectType.Point2f, 0, maxHeight - 1)
|
|
147
|
+
]
|
|
148
|
+
);
|
|
149
|
+
|
|
150
|
+
const perspectiveMatrix = OpenCV.invoke('getPerspectiveTransform', srcPoints, dstPoints, 0);
|
|
151
|
+
|
|
152
|
+
const size = OpenCV.createObject(ObjectType.Size, maxWidth, maxHeight);
|
|
153
|
+
const borderValue = OpenCV.createObject(ObjectType.Scalar, 0);
|
|
154
|
+
|
|
155
|
+
OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size, 1 /* INTER_LINEAR */, 0 /* BORDER_CONSTANT */, borderValue);
|
|
156
|
+
|
|
157
|
+
const dstValue = OpenCV.toJSValue(dst);
|
|
158
|
+
|
|
159
|
+
if (dstValue && dstValue.base64) {
|
|
160
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
} catch (e) {
|
|
164
|
+
console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
|
|
165
|
+
return undefined;
|
|
166
|
+
} finally {
|
|
167
|
+
OpenCV.clearBuffers();
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
173
|
+
*/
|
|
174
|
+
public static rotateImage(imageBase64: string, angle: 90 | -90 | 180): string | undefined {
|
|
175
|
+
let src: OpenCVMat | null = null;
|
|
176
|
+
let dst: OpenCVMat | null = null;
|
|
177
|
+
try {
|
|
178
|
+
src = OpenCV.base64ToMat(imageBase64);
|
|
179
|
+
dst = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
180
|
+
|
|
181
|
+
let rotateCode = 0; // ROTATE_90_CLOCKWISE
|
|
182
|
+
if (angle === -90) rotateCode = 2; // ROTATE_90_COUNTERCLOCKWISE
|
|
183
|
+
else if (angle === 180) rotateCode = 1; // ROTATE_180
|
|
184
|
+
|
|
185
|
+
OpenCV.invoke('rotate', src, dst, rotateCode);
|
|
186
|
+
|
|
187
|
+
const dstValue = OpenCV.toJSValue(dst);
|
|
188
|
+
if (dstValue && dstValue.base64) {
|
|
189
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
190
|
+
}
|
|
191
|
+
return undefined;
|
|
192
|
+
} catch (e: any) {
|
|
193
|
+
console.error('Lỗi khi xoay ảnh (OpenCV):', e);
|
|
194
|
+
return undefined;
|
|
195
|
+
} finally {
|
|
196
|
+
OpenCV.clearBuffers();
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Phân tích độ phức tạp màu sắc để gợi ý phương thức OCR phù hợp.
|
|
202
|
+
*
|
|
203
|
+
* - Nếu corners phát hiện thành công → crop vùng tài liệu và phân tích vùng đó
|
|
204
|
+
* - Nếu không có corners (phát hiện thất bại) → phân tích toàn bộ ảnh
|
|
205
|
+
* - Dùng không gian màu Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) và b* (lam↔vàng)
|
|
206
|
+
* - stdA + stdB > threshold → nhiều màu sắc → 'G' (ML Kit, phù hợp thẻ, ảnh màu)
|
|
207
|
+
* - stdA + stdB ≤ threshold → ít màu sắc → 'S' (Tesseract, phù hợp giấy trắng, bản scan)
|
|
208
|
+
*
|
|
209
|
+
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
210
|
+
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
|
211
|
+
* @param colorThreshold Ngưỡng phân biệt (mặc định 18.0, điều chỉnh nếu cần tinh chỉnh)
|
|
212
|
+
*/
|
|
213
|
+
public static analyzeColorComplexity(
|
|
214
|
+
imageBase64: string,
|
|
215
|
+
corners?: Point[],
|
|
216
|
+
colorThreshold: number = 18.0
|
|
217
|
+
): OcrMethodHint {
|
|
218
|
+
let src: OpenCVMat | null = null;
|
|
219
|
+
let roi: OpenCVMat | null = null;
|
|
220
|
+
let lab: OpenCVMat | null = null;
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
src = OpenCV.base64ToMat(imageBase64);
|
|
224
|
+
|
|
225
|
+
// Nếu có corners hợp lệ → crop vùng tài liệu để phân tích chính xác hơn
|
|
226
|
+
if (corners && corners.length === 4) {
|
|
227
|
+
try {
|
|
228
|
+
const sortedCorners = this.sortCorners([...corners]);
|
|
229
|
+
const [tl, tr, br, bl] = sortedCorners;
|
|
230
|
+
const widthA = this.getDistance(br, bl);
|
|
231
|
+
const widthB = this.getDistance(tr, tl);
|
|
232
|
+
const maxWidth = Math.max(Math.round(widthA), Math.round(widthB));
|
|
233
|
+
const heightA = this.getDistance(tr, br);
|
|
234
|
+
const heightB = this.getDistance(tl, bl);
|
|
235
|
+
const maxHeight = Math.max(Math.round(heightA), Math.round(heightB));
|
|
236
|
+
|
|
237
|
+
if (maxWidth > 0 && maxHeight > 0) {
|
|
238
|
+
const srcPts = OpenCV.createObject(ObjectType.Point2fVector, [
|
|
239
|
+
OpenCV.createObject(ObjectType.Point2f, tl.x, tl.y),
|
|
240
|
+
OpenCV.createObject(ObjectType.Point2f, tr.x, tr.y),
|
|
241
|
+
OpenCV.createObject(ObjectType.Point2f, br.x, br.y),
|
|
242
|
+
OpenCV.createObject(ObjectType.Point2f, bl.x, bl.y),
|
|
243
|
+
]);
|
|
244
|
+
const dstPts = OpenCV.createObject(ObjectType.Point2fVector, [
|
|
245
|
+
OpenCV.createObject(ObjectType.Point2f, 0, 0),
|
|
246
|
+
OpenCV.createObject(ObjectType.Point2f, maxWidth - 1, 0),
|
|
247
|
+
OpenCV.createObject(ObjectType.Point2f, maxWidth - 1, maxHeight - 1),
|
|
248
|
+
OpenCV.createObject(ObjectType.Point2f, 0, maxHeight - 1),
|
|
249
|
+
]);
|
|
250
|
+
const perspM = OpenCV.invoke('getPerspectiveTransform', srcPts, dstPts, 0);
|
|
251
|
+
const sz = OpenCV.createObject(ObjectType.Size, maxWidth, maxHeight);
|
|
252
|
+
const borderVal = OpenCV.createObject(ObjectType.Scalar, 0);
|
|
253
|
+
roi = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
|
|
254
|
+
OpenCV.invoke('warpPerspective', src, roi, perspM, sz, 1, 0, borderVal);
|
|
255
|
+
}
|
|
256
|
+
} catch (_) {
|
|
257
|
+
roi = null; // Crop lỗi → dùng toàn ảnh
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const target = roi ?? src; // Dùng vùng crop nếu có, ngược lại toàn ảnh
|
|
262
|
+
|
|
263
|
+
// Chuyển sang Lab (L=sáng/tối, a*=màu xanh↔đỏ, b*=màu lam↔vàng)
|
|
264
|
+
lab = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
|
|
265
|
+
OpenCV.invoke('cvtColor', target, lab, ColorConversionCodes.COLOR_BGR2Lab);
|
|
266
|
+
|
|
267
|
+
// Đo mean và stddev từng kênh
|
|
268
|
+
const meanMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
269
|
+
const stdMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
270
|
+
OpenCV.invoke('meanStdDev', lab, meanMat, stdMat);
|
|
271
|
+
|
|
272
|
+
const stdJS = OpenCV.toJSValue(stdMat);
|
|
273
|
+
const stdArray: number[] = stdJS?.array ?? [];
|
|
274
|
+
const stdA = stdArray[1] ?? 0; // a*: xanh lá ↔ đỏ
|
|
275
|
+
const stdB = stdArray[2] ?? 0; // b*: lam ↔ vàng
|
|
276
|
+
|
|
277
|
+
const colorScore = stdA + stdB;
|
|
278
|
+
console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
|
|
279
|
+
|
|
280
|
+
return colorScore > colorThreshold ? 'G' : 'S';
|
|
281
|
+
} catch (e: any) {
|
|
282
|
+
console.warn('[OpenCV] analyzeColorComplexity lỗi, fallback G:', e?.message);
|
|
283
|
+
return 'G'; // Fallback an toàn về ML Kit
|
|
284
|
+
} finally {
|
|
285
|
+
OpenCV.clearBuffers();
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|