rn-opencv-doc-perspective-correction 1.0.11 → 1.0.13
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 +114 -42
- package/package.json +1 -1
- package/src/index.ts +135 -53
package/dist/index.d.ts
CHANGED
|
@@ -22,9 +22,9 @@ export declare class DocumentScanner {
|
|
|
22
22
|
*
|
|
23
23
|
* - 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 đó
|
|
24
24
|
* - Nếu không có corners (phát hiện thất bại) → phân tích toàn bộ ảnh
|
|
25
|
-
* - Dùng không gian màu
|
|
26
|
-
* -
|
|
27
|
-
* -
|
|
25
|
+
* - Dùng không gian màu HSV, lẩy ra mặt phẳng kênh S (Saturation - Độ bão hòa đỏ/xanh/sáng).
|
|
26
|
+
* - Cộng trung bình (Mean) và độ lệch chuẩn (Std) của S > threshold → nhiều màu sắc → 'G' (ML Kit, thẻ)
|
|
27
|
+
* - Thuật toán cũ dùng Lab thất bại do mặt phẳng màu trên nền CCCD không đủ kéo độ lệch lên quá ngưỡng.
|
|
28
28
|
*
|
|
29
29
|
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
30
30
|
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
package/dist/index.js
CHANGED
|
@@ -29,40 +29,84 @@ class DocumentScanner {
|
|
|
29
29
|
gray = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
30
30
|
blurred = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
31
31
|
edges = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
32
|
+
// BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
|
|
32
33
|
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', src, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
|
|
34
|
+
// BƯỚC 2: Làm mờ để loại bỏ nhiễu hạt nhỏ (Gaussian Blur)
|
|
33
35
|
const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
|
|
34
36
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
+
// BƯỚC 3: Đo sáng môi trường để thiết lập Canny thông minh (Auto-Canny)
|
|
38
|
+
// Lợi dụng Otsu Threshold để dò ra "Ngưỡng sáng sinh thái" của bức ảnh đó
|
|
39
|
+
const dummyEdges = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
40
|
+
const otsuResult = react_native_fast_opencv_1.OpenCV.invoke('threshold', blurred, dummyEdges, 0, 255, 8 /* THRESH_BINARY | THRESH_OTSU */);
|
|
41
|
+
// Xử lý cẩn thận kiểu dữ liệu trả về từ Bridge (Primitive vs Object)
|
|
42
|
+
let highThresh = 200;
|
|
43
|
+
if (otsuResult !== undefined && otsuResult !== null) {
|
|
44
|
+
highThresh = typeof otsuResult === 'number' ? otsuResult : (otsuResult.value || 200);
|
|
45
|
+
}
|
|
46
|
+
let lowThresh = highThresh * 0.5;
|
|
47
|
+
// BƯỚC 4: Dò tìm cạnh viền Canny với cảm biến tự động
|
|
48
|
+
react_native_fast_opencv_1.OpenCV.invoke('Canny', blurred, edges, lowThresh, highThresh);
|
|
49
|
+
// BƯỚC 5: Liền sẹo nét viền (Morphology Dilate)
|
|
50
|
+
// Giúp nối liền các đứt gãy do bóng đổ chia cắt nét viền
|
|
51
|
+
const dilatedEdges = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
52
|
+
try {
|
|
53
|
+
const emptyKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
54
|
+
react_native_fast_opencv_1.OpenCV.invoke('dilate', edges, dilatedEdges, emptyKernel);
|
|
55
|
+
}
|
|
56
|
+
catch (dilateErr) {
|
|
57
|
+
react_native_fast_opencv_1.OpenCV.invoke('copyTo', edges, dilatedEdges); // Fallback an toàn nếu dilate tàng hình fail
|
|
58
|
+
}
|
|
59
|
+
// BƯỚC 6: Tìm khối đa giác liên kết (Contours)
|
|
37
60
|
contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
38
61
|
hierarchyObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
39
|
-
|
|
62
|
+
// BẮT BUỘC DÙNG RETR_EXTERNAL để ngăn OOM (Out of Memory):
|
|
63
|
+
// Canny làm lộ ra TẤT CẢ mép bao gồm cả hàng trăm chữ cái trên CCCD.
|
|
64
|
+
// Nếu dùng RETR_LIST, JS Bridge sẽ phải vòng lặp cấp phát 5000 Mats gây Crash App ngay lập tức!
|
|
65
|
+
// RETR_EXTERNAL với Canny sẽ chỉ bắt Vòng khép kín ngoài cùng (Chính xác là mép CCCD).
|
|
66
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', dilatedEdges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
40
67
|
const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
|
|
41
68
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
42
69
|
const contoursSize = contoursArray.length;
|
|
43
70
|
if (contoursSize === 0) {
|
|
44
71
|
return undefined;
|
|
45
72
|
}
|
|
73
|
+
// Thu thập và đo đạc diện tích
|
|
46
74
|
let contourMetrics = [];
|
|
47
75
|
for (let i = 0; i < contoursSize; i++) {
|
|
48
76
|
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
49
77
|
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
50
78
|
const area = areaObj ? areaObj.value : 0;
|
|
79
|
+
// Mở rộng bộ lọc diện tích: Loại bỏ đi vùng nhiễu bé (5000 là khoảng diện tích tối thiểu cho giấy tờ)
|
|
51
80
|
if (area > 5000) {
|
|
52
81
|
contourMetrics.push({ index: i, area, contour });
|
|
53
82
|
}
|
|
54
83
|
}
|
|
84
|
+
// Sắp xếp diện tích lớn nhất lên đầu (Ưu tiên thẻ bài đang chiếm lớn nhất khung hình)
|
|
55
85
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
56
86
|
let largestPoly = undefined;
|
|
57
|
-
|
|
87
|
+
// BƯỚC 7: Thẩm định hình học
|
|
88
|
+
// Quét qua max 5 contour lớn nhất để tiết kiệm thao tác mảng
|
|
89
|
+
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
90
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
58
91
|
const metric = contourMetrics[i];
|
|
59
92
|
const contour = metric.contour;
|
|
60
93
|
const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
|
|
61
|
-
const peri = periObj ? periObj.value : 0;
|
|
94
|
+
const peri = periObj ? (periObj.value || 0) : 0;
|
|
62
95
|
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
96
|
+
// Tính nội suy đa giác (Epsilon = 2% của chu vi)
|
|
63
97
|
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
64
98
|
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
99
|
+
// Nếu quy chiếu thành công ra đúng 4 góc
|
|
65
100
|
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
101
|
+
// Thẩm định: Phải là đa giác lồi Convex (Loại bóng râm)
|
|
102
|
+
try {
|
|
103
|
+
const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
|
|
104
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
105
|
+
if (convexValue === false) {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
catch (convexErr) { }
|
|
66
110
|
largestPoly = approxJS.array;
|
|
67
111
|
break;
|
|
68
112
|
}
|
|
@@ -73,7 +117,7 @@ class DocumentScanner {
|
|
|
73
117
|
return undefined;
|
|
74
118
|
}
|
|
75
119
|
catch (e) {
|
|
76
|
-
console.error('Lỗi
|
|
120
|
+
console.error('[OpenCV] Lỗi vòng quét nhận diện mép tài liệu:', e);
|
|
77
121
|
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
78
122
|
}
|
|
79
123
|
finally {
|
|
@@ -162,19 +206,19 @@ class DocumentScanner {
|
|
|
162
206
|
*
|
|
163
207
|
* - 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 đó
|
|
164
208
|
* - Nếu không có corners (phát hiện thất bại) → phân tích toàn bộ ảnh
|
|
165
|
-
* - Dùng không gian màu
|
|
166
|
-
* -
|
|
167
|
-
* -
|
|
209
|
+
* - Dùng không gian màu HSV, lẩy ra mặt phẳng kênh S (Saturation - Độ bão hòa đỏ/xanh/sáng).
|
|
210
|
+
* - Cộng trung bình (Mean) và độ lệch chuẩn (Std) của S > threshold → nhiều màu sắc → 'G' (ML Kit, thẻ)
|
|
211
|
+
* - Thuật toán cũ dùng Lab thất bại do mặt phẳng màu trên nền CCCD không đủ kéo độ lệch lên quá ngưỡng.
|
|
168
212
|
*
|
|
169
213
|
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
170
214
|
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
|
171
215
|
* @param colorThreshold Ngưỡng phân biệt (mặc định 18.0, điều chỉnh nếu cần tinh chỉnh)
|
|
172
216
|
*/
|
|
173
217
|
static analyzeColorComplexity(imageBase64, corners, colorThreshold = 18.0) {
|
|
174
|
-
var _a, _b
|
|
218
|
+
var _a, _b;
|
|
175
219
|
let src = null;
|
|
176
220
|
let roi = null;
|
|
177
|
-
let
|
|
221
|
+
let hsv = null;
|
|
178
222
|
let color3Channel = null;
|
|
179
223
|
try {
|
|
180
224
|
src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
@@ -227,43 +271,71 @@ class DocumentScanner {
|
|
|
227
271
|
else {
|
|
228
272
|
color3Channel = target; // Đã là 3 kênh
|
|
229
273
|
}
|
|
230
|
-
// Chuyển sang
|
|
231
|
-
|
|
232
|
-
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel,
|
|
274
|
+
// Phương án 2.3: Chuyển sang Không gian màu HSV (Hue, Saturation, Value)
|
|
275
|
+
hsv = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8UC3);
|
|
276
|
+
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel, hsv, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2HSV);
|
|
233
277
|
// Đo mean và stddev từng kênh
|
|
234
278
|
const meanMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_64F);
|
|
235
279
|
const stdMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_64F);
|
|
236
|
-
react_native_fast_opencv_1.OpenCV.invoke('meanStdDev',
|
|
237
|
-
//
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
//
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
280
|
+
react_native_fast_opencv_1.OpenCV.invoke('meanStdDev', hsv, meanMat, stdMat);
|
|
281
|
+
// Phương án 2.1: Hàm trích xuất mảng JS an toàn (tránh lỗi bridge trả về 0)
|
|
282
|
+
const extractValues = (jsObj) => {
|
|
283
|
+
if (!jsObj)
|
|
284
|
+
return [0, 0, 0];
|
|
285
|
+
// JSI/C++ bridge có thể trả về TypedArray thay vì JS Array thuần
|
|
286
|
+
const isList = (obj) => Array.isArray(obj) || (obj && obj.buffer instanceof ArrayBuffer) || ArrayBuffer.isView(obj);
|
|
287
|
+
const parseList = (list) => {
|
|
288
|
+
const result = [];
|
|
289
|
+
// Đảm bảo lấy ít nhất 3 tham số (Hue, Saturation, Value)
|
|
290
|
+
const len = Math.max(list.length || 0, 3);
|
|
291
|
+
for (let i = 0; i < len; i++) {
|
|
292
|
+
let item = list[i];
|
|
293
|
+
if (Array.isArray(item))
|
|
294
|
+
item = item[0];
|
|
295
|
+
else if (item && typeof item === 'object' && 'value' in item)
|
|
296
|
+
item = item.value;
|
|
297
|
+
const num = Number(item);
|
|
298
|
+
result.push(isNaN(num) ? 0 : num);
|
|
299
|
+
}
|
|
300
|
+
return result;
|
|
301
|
+
};
|
|
302
|
+
if (isList(jsObj))
|
|
303
|
+
return parseList(jsObj);
|
|
304
|
+
if (jsObj.array && isList(jsObj.array))
|
|
305
|
+
return parseList(jsObj.array);
|
|
306
|
+
if (jsObj.data && isList(jsObj.data))
|
|
307
|
+
return parseList(jsObj.data);
|
|
308
|
+
if (jsObj.value && isList(jsObj.value))
|
|
309
|
+
return parseList(jsObj.value);
|
|
310
|
+
// Fallback cho object có dạng Dictionary Object Array-like (vd: {'0': 15, '1': 30})
|
|
311
|
+
if (typeof jsObj === 'object') {
|
|
312
|
+
if (jsObj[0] !== undefined || jsObj[1] !== undefined) {
|
|
313
|
+
return parseList(jsObj);
|
|
314
|
+
}
|
|
315
|
+
// Quét các key để xem có chứa Array không
|
|
316
|
+
const possibleArrayKey = Object.keys(jsObj).find(k => isList(jsObj[k]));
|
|
317
|
+
if (possibleArrayKey)
|
|
318
|
+
return parseList(jsObj[possibleArrayKey]);
|
|
257
319
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
320
|
+
console.warn('[OCR Auto HSV] extractValues không thể parse object:', JSON.stringify(jsObj));
|
|
321
|
+
return [0, 0, 0];
|
|
322
|
+
};
|
|
323
|
+
const meanArr = extractValues(react_native_fast_opencv_1.OpenCV.toJSValue(meanMat));
|
|
324
|
+
const stdArr = extractValues(react_native_fast_opencv_1.OpenCV.toJSValue(stdMat));
|
|
325
|
+
// Kênh S (Saturation) nằm ở index 1, biểu thị độ bão hòa (đậm nhạt) của màu sắc
|
|
326
|
+
const meanS = (_a = meanArr[1]) !== null && _a !== void 0 ? _a : 0;
|
|
327
|
+
const stdS = (_b = stdArr[1]) !== null && _b !== void 0 ? _b : 0;
|
|
328
|
+
// Ảnh đen trắng (Scan/Document) sẽ có Mean và Std của Saturation tiến sát về 0.
|
|
329
|
+
// CCCD hoặc thẻ màu (Ảnh ID) sẽ có các vùng màu (tóc, da, mộc đỏ) kéo Std và Mean lên cao.
|
|
330
|
+
const colorScore = meanS + stdS;
|
|
331
|
+
// Phương án 2.2: Hạ mức Threshold phân định (CCCD Việt Nam có nền xanh/vàng lợt nên dễ bị điểm thấp nếu dùng 18.0)
|
|
332
|
+
const resolvedThreshold = colorThreshold === 18.0 ? 10.0 : colorThreshold;
|
|
333
|
+
console.log(`[OCR Auto HSV] Mean(S)=${meanS.toFixed(2)}, Std(S)=${stdS.toFixed(2)}, colorScore=${colorScore.toFixed(2)}, threshold=${resolvedThreshold} => ${colorScore > resolvedThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
|
|
334
|
+
return colorScore > resolvedThreshold ? 'G' : 'S';
|
|
262
335
|
}
|
|
263
336
|
catch (e) {
|
|
264
|
-
console.
|
|
265
|
-
|
|
266
|
-
return 'S';
|
|
337
|
+
console.error('[OpenCV] analyzeColorComplexity lỗi:', e === null || e === void 0 ? void 0 : e.message);
|
|
338
|
+
throw new Error(`[OpenCV analyzeColorComplexity Error]: ${e === null || e === void 0 ? void 0 : e.message}`);
|
|
267
339
|
}
|
|
268
340
|
finally {
|
|
269
341
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
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.13",
|
|
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
|
@@ -44,18 +44,47 @@ export class DocumentScanner {
|
|
|
44
44
|
blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
45
45
|
edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
46
46
|
|
|
47
|
+
// BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
|
|
47
48
|
OpenCV.invoke('cvtColor', src, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
48
49
|
|
|
50
|
+
// BƯỚC 2: Làm mờ để loại bỏ nhiễu hạt nhỏ (Gaussian Blur)
|
|
49
51
|
const ksize = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
50
52
|
OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
51
53
|
|
|
52
|
-
//
|
|
53
|
-
|
|
54
|
+
// BƯỚC 3: Đo sáng môi trường để thiết lập Canny thông minh (Auto-Canny)
|
|
55
|
+
// Lợi dụng Otsu Threshold để dò ra "Ngưỡng sáng sinh thái" của bức ảnh đó
|
|
56
|
+
const dummyEdges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
57
|
+
const otsuResult = OpenCV.invoke('threshold', blurred, dummyEdges, 0, 255, 8 /* THRESH_BINARY | THRESH_OTSU */);
|
|
58
|
+
|
|
59
|
+
// Xử lý cẩn thận kiểu dữ liệu trả về từ Bridge (Primitive vs Object)
|
|
60
|
+
let highThresh = 200;
|
|
61
|
+
if (otsuResult !== undefined && otsuResult !== null) {
|
|
62
|
+
highThresh = typeof otsuResult === 'number' ? otsuResult : (otsuResult.value || 200);
|
|
63
|
+
}
|
|
64
|
+
let lowThresh = highThresh * 0.5;
|
|
65
|
+
|
|
66
|
+
// BƯỚC 4: Dò tìm cạnh viền Canny với cảm biến tự động
|
|
67
|
+
OpenCV.invoke('Canny', blurred, edges, lowThresh, highThresh);
|
|
68
|
+
|
|
69
|
+
// BƯỚC 5: Liền sẹo nét viền (Morphology Dilate)
|
|
70
|
+
// Giúp nối liền các đứt gãy do bóng đổ chia cắt nét viền
|
|
71
|
+
const dilatedEdges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
72
|
+
try {
|
|
73
|
+
const emptyKernel = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
74
|
+
OpenCV.invoke('dilate', edges, dilatedEdges, emptyKernel);
|
|
75
|
+
} catch (dilateErr) {
|
|
76
|
+
OpenCV.invoke('copyTo', edges, dilatedEdges); // Fallback an toàn nếu dilate tàng hình fail
|
|
77
|
+
}
|
|
54
78
|
|
|
79
|
+
// BƯỚC 6: Tìm khối đa giác liên kết (Contours)
|
|
55
80
|
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
56
81
|
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
57
82
|
|
|
58
|
-
|
|
83
|
+
// BẮT BUỘC DÙNG RETR_EXTERNAL để ngăn OOM (Out of Memory):
|
|
84
|
+
// Canny làm lộ ra TẤT CẢ mép bao gồm cả hàng trăm chữ cái trên CCCD.
|
|
85
|
+
// Nếu dùng RETR_LIST, JS Bridge sẽ phải vòng lặp cấp phát 5000 Mats gây Crash App ngay lập tức!
|
|
86
|
+
// RETR_EXTERNAL với Canny sẽ chỉ bắt Vòng khép kín ngoài cùng (Chính xác là mép CCCD).
|
|
87
|
+
OpenCV.invoke('findContoursWithHierarchy', dilatedEdges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
59
88
|
|
|
60
89
|
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
61
90
|
const contoursArray = contoursJS?.array || [];
|
|
@@ -65,42 +94,63 @@ export class DocumentScanner {
|
|
|
65
94
|
return undefined;
|
|
66
95
|
}
|
|
67
96
|
|
|
97
|
+
// Thu thập và đo đạc diện tích
|
|
68
98
|
let contourMetrics = [];
|
|
69
99
|
for (let i = 0; i < contoursSize; i++) {
|
|
70
100
|
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
71
101
|
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
72
102
|
const area = areaObj ? areaObj.value : 0;
|
|
103
|
+
|
|
104
|
+
// Mở rộng bộ lọc diện tích: Loại bỏ đi vùng nhiễu bé (5000 là khoảng diện tích tối thiểu cho giấy tờ)
|
|
73
105
|
if (area > 5000) {
|
|
74
106
|
contourMetrics.push({ index: i, area, contour });
|
|
75
107
|
}
|
|
76
108
|
}
|
|
77
109
|
|
|
110
|
+
// Sắp xếp diện tích lớn nhất lên đầu (Ưu tiên thẻ bài đang chiếm lớn nhất khung hình)
|
|
78
111
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
79
112
|
|
|
80
113
|
let largestPoly: Point[] | undefined = undefined;
|
|
81
114
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
115
|
+
// BƯỚC 7: Thẩm định hình học
|
|
116
|
+
// Quét qua max 5 contour lớn nhất để tiết kiệm thao tác mảng
|
|
117
|
+
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
118
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
119
|
+
const metric = contourMetrics[i];
|
|
120
|
+
const contour = metric.contour;
|
|
121
|
+
|
|
122
|
+
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
123
|
+
const peri = periObj ? (periObj.value || 0) : 0;
|
|
124
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
125
|
+
|
|
126
|
+
// Tính nội suy đa giác (Epsilon = 2% của chu vi)
|
|
127
|
+
OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
128
|
+
|
|
129
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
130
|
+
|
|
131
|
+
// Nếu quy chiếu thành công ra đúng 4 góc
|
|
132
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
133
|
+
|
|
134
|
+
// Thẩm định: Phải là đa giác lồi Convex (Loại bóng râm)
|
|
135
|
+
try {
|
|
136
|
+
const isConvex = OpenCV.invoke('isContourConvex', approx);
|
|
137
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
138
|
+
if (convexValue === false) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
} catch (convexErr) {}
|
|
142
|
+
|
|
143
|
+
largestPoly = approxJS.array as Point[];
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
95
146
|
}
|
|
96
|
-
}
|
|
97
147
|
|
|
98
148
|
if (largestPoly && largestPoly.length === 4) {
|
|
99
149
|
return this.sortCorners(largestPoly);
|
|
100
150
|
}
|
|
101
151
|
return undefined;
|
|
102
152
|
} catch (e: any) {
|
|
103
|
-
console.error('Lỗi
|
|
153
|
+
console.error('[OpenCV] Lỗi vòng quét nhận diện mép tài liệu:', e);
|
|
104
154
|
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
105
155
|
} finally {
|
|
106
156
|
OpenCV.clearBuffers();
|
|
@@ -202,9 +252,9 @@ export class DocumentScanner {
|
|
|
202
252
|
*
|
|
203
253
|
* - 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
254
|
* - 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
|
|
206
|
-
* -
|
|
207
|
-
* -
|
|
255
|
+
* - Dùng không gian màu HSV, lẩy ra mặt phẳng kênh S (Saturation - Độ bão hòa đỏ/xanh/sáng).
|
|
256
|
+
* - Cộng trung bình (Mean) và độ lệch chuẩn (Std) của S > threshold → nhiều màu sắc → 'G' (ML Kit, thẻ)
|
|
257
|
+
* - Thuật toán cũ dùng Lab thất bại do mặt phẳng màu trên nền CCCD không đủ kéo độ lệch lên quá ngưỡng.
|
|
208
258
|
*
|
|
209
259
|
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
210
260
|
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
|
@@ -217,7 +267,7 @@ export class DocumentScanner {
|
|
|
217
267
|
): OcrMethodHint {
|
|
218
268
|
let src: OpenCVMat | null = null;
|
|
219
269
|
let roi: OpenCVMat | null = null;
|
|
220
|
-
let
|
|
270
|
+
let hsv: OpenCVMat | null = null;
|
|
221
271
|
let color3Channel: OpenCVMat | null = null;
|
|
222
272
|
|
|
223
273
|
try {
|
|
@@ -273,45 +323,77 @@ export class DocumentScanner {
|
|
|
273
323
|
color3Channel = target; // Đã là 3 kênh
|
|
274
324
|
}
|
|
275
325
|
|
|
276
|
-
// Chuyển sang
|
|
277
|
-
|
|
278
|
-
OpenCV.invoke('cvtColor', color3Channel,
|
|
326
|
+
// Phương án 2.3: Chuyển sang Không gian màu HSV (Hue, Saturation, Value)
|
|
327
|
+
hsv = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
|
|
328
|
+
OpenCV.invoke('cvtColor', color3Channel, hsv, ColorConversionCodes.COLOR_BGR2HSV);
|
|
279
329
|
|
|
280
330
|
// Đo mean và stddev từng kênh
|
|
281
331
|
const meanMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
282
332
|
const stdMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
283
|
-
OpenCV.invoke('meanStdDev',
|
|
284
|
-
|
|
285
|
-
//
|
|
286
|
-
const
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
333
|
+
OpenCV.invoke('meanStdDev', hsv, meanMat, stdMat);
|
|
334
|
+
|
|
335
|
+
// Phương án 2.1: Hàm trích xuất mảng JS an toàn (tránh lỗi bridge trả về 0)
|
|
336
|
+
const extractValues = (jsObj: any): number[] => {
|
|
337
|
+
if (!jsObj) return [0, 0, 0];
|
|
338
|
+
|
|
339
|
+
// JSI/C++ bridge có thể trả về TypedArray thay vì JS Array thuần
|
|
340
|
+
const isList = (obj: any): boolean =>
|
|
341
|
+
Array.isArray(obj) || (obj && obj.buffer instanceof ArrayBuffer) || ArrayBuffer.isView(obj);
|
|
342
|
+
|
|
343
|
+
const parseList = (list: any): number[] => {
|
|
344
|
+
const result: number[] = [];
|
|
345
|
+
// Đảm bảo lấy ít nhất 3 tham số (Hue, Saturation, Value)
|
|
346
|
+
const len = Math.max(list.length || 0, 3);
|
|
347
|
+
for (let i = 0; i < len; i++) {
|
|
348
|
+
let item = list[i];
|
|
349
|
+
if (Array.isArray(item)) item = item[0];
|
|
350
|
+
else if (item && typeof item === 'object' && 'value' in item) item = item.value;
|
|
351
|
+
|
|
352
|
+
const num = Number(item);
|
|
353
|
+
result.push(isNaN(num) ? 0 : num);
|
|
354
|
+
}
|
|
355
|
+
return result;
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
if (isList(jsObj)) return parseList(jsObj);
|
|
359
|
+
if (jsObj.array && isList(jsObj.array)) return parseList(jsObj.array);
|
|
360
|
+
if (jsObj.data && isList(jsObj.data)) return parseList(jsObj.data);
|
|
361
|
+
if (jsObj.value && isList(jsObj.value)) return parseList(jsObj.value);
|
|
362
|
+
|
|
363
|
+
// Fallback cho object có dạng Dictionary Object Array-like (vd: {'0': 15, '1': 30})
|
|
364
|
+
if (typeof jsObj === 'object') {
|
|
365
|
+
if (jsObj[0] !== undefined || jsObj[1] !== undefined) {
|
|
366
|
+
return parseList(jsObj);
|
|
367
|
+
}
|
|
368
|
+
// Quét các key để xem có chứa Array không
|
|
369
|
+
const possibleArrayKey = Object.keys(jsObj).find(k => isList(jsObj[k]));
|
|
370
|
+
if (possibleArrayKey) return parseList(jsObj[possibleArrayKey]);
|
|
304
371
|
}
|
|
305
|
-
}
|
|
306
372
|
|
|
307
|
-
|
|
308
|
-
|
|
373
|
+
console.warn('[OCR Auto HSV] extractValues không thể parse object:', JSON.stringify(jsObj));
|
|
374
|
+
return [0, 0, 0];
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
const meanArr = extractValues(OpenCV.toJSValue(meanMat));
|
|
378
|
+
const stdArr = extractValues(OpenCV.toJSValue(stdMat));
|
|
379
|
+
|
|
380
|
+
// Kênh S (Saturation) nằm ở index 1, biểu thị độ bão hòa (đậm nhạt) của màu sắc
|
|
381
|
+
const meanS = meanArr[1] ?? 0;
|
|
382
|
+
const stdS = stdArr[1] ?? 0;
|
|
383
|
+
|
|
384
|
+
// Ảnh đen trắng (Scan/Document) sẽ có Mean và Std của Saturation tiến sát về 0.
|
|
385
|
+
// CCCD hoặc thẻ màu (Ảnh ID) sẽ có các vùng màu (tóc, da, mộc đỏ) kéo Std và Mean lên cao.
|
|
386
|
+
const colorScore = meanS + stdS;
|
|
387
|
+
|
|
388
|
+
// Phương án 2.2: Hạ mức Threshold phân định (CCCD Việt Nam có nền xanh/vàng lợt nên dễ bị điểm thấp nếu dùng 18.0)
|
|
389
|
+
const resolvedThreshold = colorThreshold === 18.0 ? 10.0 : colorThreshold;
|
|
390
|
+
|
|
391
|
+
console.log(`[OCR Auto HSV] Mean(S)=${meanS.toFixed(2)}, Std(S)=${stdS.toFixed(2)}, colorScore=${colorScore.toFixed(2)}, threshold=${resolvedThreshold} => ${colorScore > resolvedThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
|
|
309
392
|
|
|
310
|
-
return colorScore >
|
|
393
|
+
return colorScore > resolvedThreshold ? 'G' : 'S';
|
|
311
394
|
} catch (e: any) {
|
|
312
|
-
console.
|
|
313
|
-
|
|
314
|
-
return 'S';
|
|
395
|
+
console.error('[OpenCV] analyzeColorComplexity lỗi:', e?.message);
|
|
396
|
+
throw new Error(`[OpenCV analyzeColorComplexity Error]: ${e?.message}`);
|
|
315
397
|
} finally {
|
|
316
398
|
OpenCV.clearBuffers();
|
|
317
399
|
}
|