rn-opencv-doc-perspective-correction 1.0.12 → 1.0.14
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 +136 -39
- package/package.json +1 -1
- package/src/index.ts +153 -50
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,108 @@ 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
|
+
// Vì invoke('threshold',...) bị lỗi C++ JSI Argument Index(3) out of bounds trên thư viện này,
|
|
39
|
+
// ta chuyển sang dùng meanStdDev để tính Median Pixel Value.
|
|
40
|
+
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);
|
|
41
|
+
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);
|
|
42
|
+
react_native_fast_opencv_1.OpenCV.invoke('meanStdDev', blurred, meanMat, stdMat);
|
|
43
|
+
const getFirstVal = (jsObj) => {
|
|
44
|
+
if (!jsObj)
|
|
45
|
+
return 128;
|
|
46
|
+
if (typeof jsObj === 'number')
|
|
47
|
+
return jsObj;
|
|
48
|
+
if (Array.isArray(jsObj))
|
|
49
|
+
return jsObj[0] || 128;
|
|
50
|
+
if (jsObj.array && Array.isArray(jsObj.array))
|
|
51
|
+
return jsObj.array[0] || 128;
|
|
52
|
+
if (jsObj.data && Array.isArray(jsObj.data))
|
|
53
|
+
return jsObj.data[0] || 128;
|
|
54
|
+
if (jsObj.value !== undefined) {
|
|
55
|
+
if (typeof jsObj.value === 'number')
|
|
56
|
+
return jsObj.value;
|
|
57
|
+
if (Array.isArray(jsObj.value))
|
|
58
|
+
return jsObj.value[0] || 128;
|
|
59
|
+
}
|
|
60
|
+
return 128;
|
|
61
|
+
};
|
|
62
|
+
const meanVal = getFirstVal(react_native_fast_opencv_1.OpenCV.toJSValue(meanMat));
|
|
63
|
+
// Công thức Zero-Parameter Canny Edge Detection (áp dụng Sigma = 0.33)
|
|
64
|
+
const sigma = 0.33;
|
|
65
|
+
let lowThresh = Math.max(0, (1.0 - sigma) * meanVal);
|
|
66
|
+
let highThresh = Math.min(255, (1.0 + sigma) * meanVal);
|
|
67
|
+
// BƯỚC 4: Dò tìm cạnh viền Canny với cảm biến tự động
|
|
68
|
+
react_native_fast_opencv_1.OpenCV.invoke('Canny', blurred, edges, lowThresh, highThresh);
|
|
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 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
72
|
+
try {
|
|
73
|
+
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);
|
|
74
|
+
const anchor = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point, -1, -1);
|
|
75
|
+
// Tạo gía trị biên ảo (morphology filter defaults) để tránh lỗi Index out of bounds
|
|
76
|
+
const borderValue = react_native_fast_opencv_1.OpenCV.invoke('morphologyDefaultBorderValue');
|
|
77
|
+
// dilate yêu cầu đúng 8 tham số (gồm tên hàm): name, src, dst, kernel, anchor, iterations, borderType, borderValue
|
|
78
|
+
react_native_fast_opencv_1.OpenCV.invoke('dilate', edges, dilatedEdges, emptyKernel, anchor, 1, 0 /* BORDER_CONSTANT */, borderValue);
|
|
79
|
+
}
|
|
80
|
+
catch (dilateErr) {
|
|
81
|
+
react_native_fast_opencv_1.OpenCV.invoke('copyTo', edges, dilatedEdges); // Fallback an toàn nếu dilate tàng hình fail
|
|
82
|
+
}
|
|
83
|
+
// BƯỚC 6: Tìm khối đa giác liên kết (Contours)
|
|
37
84
|
contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
38
85
|
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
|
-
|
|
86
|
+
// BẮT BUỘC DÙNG RETR_EXTERNAL để ngăn OOM (Out of Memory):
|
|
87
|
+
// Canny làm lộ ra TẤT CẢ mép bao gồm cả hàng trăm chữ cái trên CCCD.
|
|
88
|
+
// 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!
|
|
89
|
+
// 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).
|
|
90
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', dilatedEdges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
40
91
|
const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
|
|
41
92
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
42
93
|
const contoursSize = contoursArray.length;
|
|
43
94
|
if (contoursSize === 0) {
|
|
44
95
|
return undefined;
|
|
45
96
|
}
|
|
97
|
+
// Thu thập và đo đạc diện tích
|
|
46
98
|
let contourMetrics = [];
|
|
47
99
|
for (let i = 0; i < contoursSize; i++) {
|
|
48
100
|
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
49
101
|
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
50
102
|
const area = areaObj ? areaObj.value : 0;
|
|
103
|
+
// 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
104
|
if (area > 5000) {
|
|
52
105
|
contourMetrics.push({ index: i, area, contour });
|
|
53
106
|
}
|
|
54
107
|
}
|
|
108
|
+
// 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
109
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
56
110
|
let largestPoly = undefined;
|
|
57
|
-
|
|
111
|
+
// BƯỚC 7: Thẩm định hình học
|
|
112
|
+
// Quét qua max 5 contour lớn nhất để tiết kiệm thao tác mảng
|
|
113
|
+
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
114
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
58
115
|
const metric = contourMetrics[i];
|
|
59
116
|
const contour = metric.contour;
|
|
60
117
|
const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
|
|
61
|
-
const peri = periObj ? periObj.value : 0;
|
|
118
|
+
const peri = periObj ? (periObj.value || 0) : 0;
|
|
62
119
|
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
120
|
+
// Tính nội suy đa giác (Epsilon = 2% của chu vi)
|
|
63
121
|
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
64
122
|
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
123
|
+
// Nếu quy chiếu thành công ra đúng 4 góc
|
|
65
124
|
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
125
|
+
// Thẩm định: Phải là đa giác lồi Convex (Loại bóng râm)
|
|
126
|
+
try {
|
|
127
|
+
const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
|
|
128
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
129
|
+
if (convexValue === false) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
catch (convexErr) { }
|
|
66
134
|
largestPoly = approxJS.array;
|
|
67
135
|
break;
|
|
68
136
|
}
|
|
@@ -73,7 +141,7 @@ class DocumentScanner {
|
|
|
73
141
|
return undefined;
|
|
74
142
|
}
|
|
75
143
|
catch (e) {
|
|
76
|
-
console.error('Lỗi
|
|
144
|
+
console.error('[OpenCV] Lỗi vòng quét nhận diện mép tài liệu:', e);
|
|
77
145
|
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
78
146
|
}
|
|
79
147
|
finally {
|
|
@@ -162,19 +230,19 @@ class DocumentScanner {
|
|
|
162
230
|
*
|
|
163
231
|
* - 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
232
|
* - 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
|
-
* -
|
|
233
|
+
* - 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).
|
|
234
|
+
* - 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ẻ)
|
|
235
|
+
* - 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
236
|
*
|
|
169
237
|
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
170
238
|
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
|
171
239
|
* @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
240
|
*/
|
|
173
241
|
static analyzeColorComplexity(imageBase64, corners, colorThreshold = 18.0) {
|
|
174
|
-
var _a, _b
|
|
242
|
+
var _a, _b;
|
|
175
243
|
let src = null;
|
|
176
244
|
let roi = null;
|
|
177
|
-
let
|
|
245
|
+
let hsv = null;
|
|
178
246
|
let color3Channel = null;
|
|
179
247
|
try {
|
|
180
248
|
src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
@@ -227,38 +295,67 @@ class DocumentScanner {
|
|
|
227
295
|
else {
|
|
228
296
|
color3Channel = target; // Đã là 3 kênh
|
|
229
297
|
}
|
|
230
|
-
// Chuyển sang
|
|
231
|
-
|
|
232
|
-
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel,
|
|
298
|
+
// Phương án 2.3: Chuyển sang Không gian màu HSV (Hue, Saturation, Value)
|
|
299
|
+
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);
|
|
300
|
+
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel, hsv, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2HSV);
|
|
233
301
|
// Đo mean và stddev từng kênh
|
|
234
302
|
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
303
|
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
|
-
|
|
304
|
+
react_native_fast_opencv_1.OpenCV.invoke('meanStdDev', hsv, meanMat, stdMat);
|
|
305
|
+
// 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)
|
|
306
|
+
const extractValues = (jsObj) => {
|
|
307
|
+
if (!jsObj)
|
|
308
|
+
return [0, 0, 0];
|
|
309
|
+
// JSI/C++ bridge có thể trả về TypedArray thay vì JS Array thuần
|
|
310
|
+
const isList = (obj) => Array.isArray(obj) || (obj && obj.buffer instanceof ArrayBuffer) || ArrayBuffer.isView(obj);
|
|
311
|
+
const parseList = (list) => {
|
|
312
|
+
const result = [];
|
|
313
|
+
// Đảm bảo lấy ít nhất 3 tham số (Hue, Saturation, Value)
|
|
314
|
+
const len = Math.max(list.length || 0, 3);
|
|
315
|
+
for (let i = 0; i < len; i++) {
|
|
316
|
+
let item = list[i];
|
|
317
|
+
if (Array.isArray(item))
|
|
318
|
+
item = item[0];
|
|
319
|
+
else if (item && typeof item === 'object' && 'value' in item)
|
|
320
|
+
item = item.value;
|
|
321
|
+
const num = Number(item);
|
|
322
|
+
result.push(isNaN(num) ? 0 : num);
|
|
323
|
+
}
|
|
324
|
+
return result;
|
|
325
|
+
};
|
|
326
|
+
if (isList(jsObj))
|
|
327
|
+
return parseList(jsObj);
|
|
328
|
+
if (jsObj.array && isList(jsObj.array))
|
|
329
|
+
return parseList(jsObj.array);
|
|
330
|
+
if (jsObj.data && isList(jsObj.data))
|
|
331
|
+
return parseList(jsObj.data);
|
|
332
|
+
if (jsObj.value && isList(jsObj.value))
|
|
333
|
+
return parseList(jsObj.value);
|
|
334
|
+
// Fallback cho object có dạng Dictionary Object Array-like (vd: {'0': 15, '1': 30})
|
|
335
|
+
if (typeof jsObj === 'object') {
|
|
336
|
+
if (jsObj[0] !== undefined || jsObj[1] !== undefined) {
|
|
337
|
+
return parseList(jsObj);
|
|
338
|
+
}
|
|
339
|
+
// Quét các key để xem có chứa Array không
|
|
340
|
+
const possibleArrayKey = Object.keys(jsObj).find(k => isList(jsObj[k]));
|
|
341
|
+
if (possibleArrayKey)
|
|
342
|
+
return parseList(jsObj[possibleArrayKey]);
|
|
257
343
|
}
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
344
|
+
console.warn('[OCR Auto HSV] extractValues không thể parse object:', JSON.stringify(jsObj));
|
|
345
|
+
return [0, 0, 0];
|
|
346
|
+
};
|
|
347
|
+
const meanArr = extractValues(react_native_fast_opencv_1.OpenCV.toJSValue(meanMat));
|
|
348
|
+
const stdArr = extractValues(react_native_fast_opencv_1.OpenCV.toJSValue(stdMat));
|
|
349
|
+
// Kênh S (Saturation) nằm ở index 1, biểu thị độ bão hòa (đậm nhạt) của màu sắc
|
|
350
|
+
const meanS = (_a = meanArr[1]) !== null && _a !== void 0 ? _a : 0;
|
|
351
|
+
const stdS = (_b = stdArr[1]) !== null && _b !== void 0 ? _b : 0;
|
|
352
|
+
// Ảnh đen trắng (Scan/Document) sẽ có Mean và Std của Saturation tiến sát về 0.
|
|
353
|
+
// 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.
|
|
354
|
+
const colorScore = meanS + stdS;
|
|
355
|
+
// 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)
|
|
356
|
+
const resolvedThreshold = colorThreshold === 18.0 ? 10.0 : colorThreshold;
|
|
357
|
+
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)'}`);
|
|
358
|
+
return colorScore > resolvedThreshold ? 'G' : 'S';
|
|
262
359
|
}
|
|
263
360
|
catch (e) {
|
|
264
361
|
console.error('[OpenCV] analyzeColorComplexity lỗi:', e === null || e === void 0 ? void 0 : e.message);
|
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.14",
|
|
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,67 @@ 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
|
+
// Vì invoke('threshold',...) bị lỗi C++ JSI Argument Index(3) out of bounds trên thư viện này,
|
|
56
|
+
// ta chuyển sang dùng meanStdDev để tính Median Pixel Value.
|
|
57
|
+
const meanMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
58
|
+
const stdMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
59
|
+
OpenCV.invoke('meanStdDev', blurred, meanMat, stdMat);
|
|
60
|
+
|
|
61
|
+
const getFirstVal = (jsObj: any): number => {
|
|
62
|
+
if (!jsObj) return 128;
|
|
63
|
+
if (typeof jsObj === 'number') return jsObj;
|
|
64
|
+
if (Array.isArray(jsObj)) return jsObj[0] || 128;
|
|
65
|
+
if (jsObj.array && Array.isArray(jsObj.array)) return jsObj.array[0] || 128;
|
|
66
|
+
if (jsObj.data && Array.isArray(jsObj.data)) return jsObj.data[0] || 128;
|
|
67
|
+
if (jsObj.value !== undefined) {
|
|
68
|
+
if (typeof jsObj.value === 'number') return jsObj.value;
|
|
69
|
+
if (Array.isArray(jsObj.value)) return jsObj.value[0] || 128;
|
|
70
|
+
}
|
|
71
|
+
return 128;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const meanVal = getFirstVal(OpenCV.toJSValue(meanMat));
|
|
75
|
+
|
|
76
|
+
// Công thức Zero-Parameter Canny Edge Detection (áp dụng Sigma = 0.33)
|
|
77
|
+
const sigma = 0.33;
|
|
78
|
+
let lowThresh = Math.max(0, (1.0 - sigma) * meanVal);
|
|
79
|
+
let highThresh = Math.min(255, (1.0 + sigma) * meanVal);
|
|
80
|
+
|
|
81
|
+
// BƯỚC 4: Dò tìm cạnh viền Canny với cảm biến tự động
|
|
82
|
+
OpenCV.invoke('Canny', blurred, edges, lowThresh, highThresh);
|
|
83
|
+
|
|
84
|
+
// BƯỚC 5: Liền sẹo nét viền (Morphology Dilate)
|
|
85
|
+
// Giúp nối liền các đứt gãy do bóng đổ chia cắt nét viền
|
|
86
|
+
const dilatedEdges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
87
|
+
try {
|
|
88
|
+
const emptyKernel = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
89
|
+
const anchor = OpenCV.createObject(ObjectType.Point, -1, -1);
|
|
90
|
+
// Tạo gía trị biên ảo (morphology filter defaults) để tránh lỗi Index out of bounds
|
|
91
|
+
const borderValue = OpenCV.invoke('morphologyDefaultBorderValue');
|
|
92
|
+
|
|
93
|
+
// dilate yêu cầu đúng 8 tham số (gồm tên hàm): name, src, dst, kernel, anchor, iterations, borderType, borderValue
|
|
94
|
+
OpenCV.invoke('dilate', edges, dilatedEdges, emptyKernel, anchor, 1, 0 /* BORDER_CONSTANT */, borderValue);
|
|
95
|
+
} catch (dilateErr) {
|
|
96
|
+
OpenCV.invoke('copyTo', edges, dilatedEdges); // Fallback an toàn nếu dilate tàng hình fail
|
|
97
|
+
}
|
|
54
98
|
|
|
99
|
+
// BƯỚC 6: Tìm khối đa giác liên kết (Contours)
|
|
55
100
|
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
56
101
|
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
57
102
|
|
|
58
|
-
|
|
103
|
+
// BẮT BUỘC DÙNG RETR_EXTERNAL để ngăn OOM (Out of Memory):
|
|
104
|
+
// Canny làm lộ ra TẤT CẢ mép bao gồm cả hàng trăm chữ cái trên CCCD.
|
|
105
|
+
// 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!
|
|
106
|
+
// 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).
|
|
107
|
+
OpenCV.invoke('findContoursWithHierarchy', dilatedEdges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
59
108
|
|
|
60
109
|
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
61
110
|
const contoursArray = contoursJS?.array || [];
|
|
@@ -65,42 +114,63 @@ export class DocumentScanner {
|
|
|
65
114
|
return undefined;
|
|
66
115
|
}
|
|
67
116
|
|
|
117
|
+
// Thu thập và đo đạc diện tích
|
|
68
118
|
let contourMetrics = [];
|
|
69
119
|
for (let i = 0; i < contoursSize; i++) {
|
|
70
120
|
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
71
121
|
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
72
122
|
const area = areaObj ? areaObj.value : 0;
|
|
123
|
+
|
|
124
|
+
// 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
125
|
if (area > 5000) {
|
|
74
126
|
contourMetrics.push({ index: i, area, contour });
|
|
75
127
|
}
|
|
76
128
|
}
|
|
77
129
|
|
|
130
|
+
// 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
131
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
79
132
|
|
|
80
133
|
let largestPoly: Point[] | undefined = undefined;
|
|
81
134
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
135
|
+
// BƯỚC 7: Thẩm định hình học
|
|
136
|
+
// Quét qua max 5 contour lớn nhất để tiết kiệm thao tác mảng
|
|
137
|
+
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
138
|
+
for (let i = 0; i < maxChecks; i++) {
|
|
139
|
+
const metric = contourMetrics[i];
|
|
140
|
+
const contour = metric.contour;
|
|
141
|
+
|
|
142
|
+
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
143
|
+
const peri = periObj ? (periObj.value || 0) : 0;
|
|
144
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
145
|
+
|
|
146
|
+
// Tính nội suy đa giác (Epsilon = 2% của chu vi)
|
|
147
|
+
OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
148
|
+
|
|
149
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
150
|
+
|
|
151
|
+
// Nếu quy chiếu thành công ra đúng 4 góc
|
|
152
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
153
|
+
|
|
154
|
+
// Thẩm định: Phải là đa giác lồi Convex (Loại bóng râm)
|
|
155
|
+
try {
|
|
156
|
+
const isConvex = OpenCV.invoke('isContourConvex', approx);
|
|
157
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
158
|
+
if (convexValue === false) {
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
} catch (convexErr) {}
|
|
162
|
+
|
|
163
|
+
largestPoly = approxJS.array as Point[];
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
95
166
|
}
|
|
96
|
-
}
|
|
97
167
|
|
|
98
168
|
if (largestPoly && largestPoly.length === 4) {
|
|
99
169
|
return this.sortCorners(largestPoly);
|
|
100
170
|
}
|
|
101
171
|
return undefined;
|
|
102
172
|
} catch (e: any) {
|
|
103
|
-
console.error('Lỗi
|
|
173
|
+
console.error('[OpenCV] Lỗi vòng quét nhận diện mép tài liệu:', e);
|
|
104
174
|
throw new Error(`[OpenCV Corner Detection Error]: ${e.message}`);
|
|
105
175
|
} finally {
|
|
106
176
|
OpenCV.clearBuffers();
|
|
@@ -202,9 +272,9 @@ export class DocumentScanner {
|
|
|
202
272
|
*
|
|
203
273
|
* - 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
274
|
* - 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
|
-
* -
|
|
275
|
+
* - 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).
|
|
276
|
+
* - 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ẻ)
|
|
277
|
+
* - 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
278
|
*
|
|
209
279
|
* @param imageBase64 Ảnh gốc base64 JPEG
|
|
210
280
|
* @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
|
|
@@ -217,7 +287,7 @@ export class DocumentScanner {
|
|
|
217
287
|
): OcrMethodHint {
|
|
218
288
|
let src: OpenCVMat | null = null;
|
|
219
289
|
let roi: OpenCVMat | null = null;
|
|
220
|
-
let
|
|
290
|
+
let hsv: OpenCVMat | null = null;
|
|
221
291
|
let color3Channel: OpenCVMat | null = null;
|
|
222
292
|
|
|
223
293
|
try {
|
|
@@ -273,41 +343,74 @@ export class DocumentScanner {
|
|
|
273
343
|
color3Channel = target; // Đã là 3 kênh
|
|
274
344
|
}
|
|
275
345
|
|
|
276
|
-
// Chuyển sang
|
|
277
|
-
|
|
278
|
-
OpenCV.invoke('cvtColor', color3Channel,
|
|
346
|
+
// Phương án 2.3: Chuyển sang Không gian màu HSV (Hue, Saturation, Value)
|
|
347
|
+
hsv = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
|
|
348
|
+
OpenCV.invoke('cvtColor', color3Channel, hsv, ColorConversionCodes.COLOR_BGR2HSV);
|
|
279
349
|
|
|
280
350
|
// Đo mean và stddev từng kênh
|
|
281
351
|
const meanMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
|
|
282
352
|
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
|
-
|
|
353
|
+
OpenCV.invoke('meanStdDev', hsv, meanMat, stdMat);
|
|
354
|
+
|
|
355
|
+
// 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)
|
|
356
|
+
const extractValues = (jsObj: any): number[] => {
|
|
357
|
+
if (!jsObj) return [0, 0, 0];
|
|
358
|
+
|
|
359
|
+
// JSI/C++ bridge có thể trả về TypedArray thay vì JS Array thuần
|
|
360
|
+
const isList = (obj: any): boolean =>
|
|
361
|
+
Array.isArray(obj) || (obj && obj.buffer instanceof ArrayBuffer) || ArrayBuffer.isView(obj);
|
|
362
|
+
|
|
363
|
+
const parseList = (list: any): number[] => {
|
|
364
|
+
const result: number[] = [];
|
|
365
|
+
// Đảm bảo lấy ít nhất 3 tham số (Hue, Saturation, Value)
|
|
366
|
+
const len = Math.max(list.length || 0, 3);
|
|
367
|
+
for (let i = 0; i < len; i++) {
|
|
368
|
+
let item = list[i];
|
|
369
|
+
if (Array.isArray(item)) item = item[0];
|
|
370
|
+
else if (item && typeof item === 'object' && 'value' in item) item = item.value;
|
|
371
|
+
|
|
372
|
+
const num = Number(item);
|
|
373
|
+
result.push(isNaN(num) ? 0 : num);
|
|
374
|
+
}
|
|
375
|
+
return result;
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
if (isList(jsObj)) return parseList(jsObj);
|
|
379
|
+
if (jsObj.array && isList(jsObj.array)) return parseList(jsObj.array);
|
|
380
|
+
if (jsObj.data && isList(jsObj.data)) return parseList(jsObj.data);
|
|
381
|
+
if (jsObj.value && isList(jsObj.value)) return parseList(jsObj.value);
|
|
382
|
+
|
|
383
|
+
// Fallback cho object có dạng Dictionary Object Array-like (vd: {'0': 15, '1': 30})
|
|
384
|
+
if (typeof jsObj === 'object') {
|
|
385
|
+
if (jsObj[0] !== undefined || jsObj[1] !== undefined) {
|
|
386
|
+
return parseList(jsObj);
|
|
387
|
+
}
|
|
388
|
+
// Quét các key để xem có chứa Array không
|
|
389
|
+
const possibleArrayKey = Object.keys(jsObj).find(k => isList(jsObj[k]));
|
|
390
|
+
if (possibleArrayKey) return parseList(jsObj[possibleArrayKey]);
|
|
304
391
|
}
|
|
305
|
-
}
|
|
306
392
|
|
|
307
|
-
|
|
308
|
-
|
|
393
|
+
console.warn('[OCR Auto HSV] extractValues không thể parse object:', JSON.stringify(jsObj));
|
|
394
|
+
return [0, 0, 0];
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const meanArr = extractValues(OpenCV.toJSValue(meanMat));
|
|
398
|
+
const stdArr = extractValues(OpenCV.toJSValue(stdMat));
|
|
399
|
+
|
|
400
|
+
// Kênh S (Saturation) nằm ở index 1, biểu thị độ bão hòa (đậm nhạt) của màu sắc
|
|
401
|
+
const meanS = meanArr[1] ?? 0;
|
|
402
|
+
const stdS = stdArr[1] ?? 0;
|
|
403
|
+
|
|
404
|
+
// Ảnh đen trắng (Scan/Document) sẽ có Mean và Std của Saturation tiến sát về 0.
|
|
405
|
+
// 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.
|
|
406
|
+
const colorScore = meanS + stdS;
|
|
407
|
+
|
|
408
|
+
// 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)
|
|
409
|
+
const resolvedThreshold = colorThreshold === 18.0 ? 10.0 : colorThreshold;
|
|
410
|
+
|
|
411
|
+
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
412
|
|
|
310
|
-
return colorScore >
|
|
413
|
+
return colorScore > resolvedThreshold ? 'G' : 'S';
|
|
311
414
|
} catch (e: any) {
|
|
312
415
|
console.error('[OpenCV] analyzeColorComplexity lỗi:', e?.message);
|
|
313
416
|
throw new Error(`[OpenCV analyzeColorComplexity Error]: ${e?.message}`);
|