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 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 Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) b* (lam↔vàng)
26
- * - stdA + stdB > threshold → nhiều màu sắc → 'G' (ML Kit, phù hợp thẻ, ảnh màu)
27
- * - stdA + stdB threshold ít màu sắc 'S' (Tesseract, phù hợp giấy trắng, bản scan)
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 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
- // THRESH_BINARY + THRESH_OTSU = 8
36
- react_native_fast_opencv_1.OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
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
- react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
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
- for (let i = 0; i < contourMetrics.length; i++) {
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 khi tìm góc tài liệu (OpenCV):', e);
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 Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) b* (lam↔vàng)
166
- * - stdA + stdB > threshold → nhiều màu sắc → 'G' (ML Kit, phù hợp thẻ, ảnh màu)
167
- * - stdA + stdB threshold ít màu sắc 'S' (Tesseract, phù hợp giấy trắng, bản scan)
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 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, _c, _d, _e, _f;
242
+ var _a, _b;
175
243
  let src = null;
176
244
  let roi = null;
177
- let lab = null;
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 Lab (L=sáng/tối, a*=màu xanh↔đỏ, b*=màu lam↔vàng)
231
- lab = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8UC3);
232
- react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel, lab, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2Lab);
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', lab, meanMat, stdMat);
237
- // Trích xuất stdDev một cách an toàn
238
- const stdJS = react_native_fast_opencv_1.OpenCV.toJSValue(stdMat);
239
- // react-native-fast-opencv thường chuyển `Mat` thành object JS có cấu trúc mảng 1 chiều trong prop `array` hoặc `data`
240
- // hoặc trả về dưới dạng mảng lồng nhau
241
- let stdA = 0;
242
- let stdB = 0;
243
- if (stdJS) {
244
- if (Array.isArray(stdJS)) {
245
- // Nếu đã array
246
- stdA = Array.isArray(stdJS[1]) ? stdJS[1][0] : (_a = stdJS[1]) !== null && _a !== void 0 ? _a : 0;
247
- stdB = Array.isArray(stdJS[2]) ? stdJS[2][0] : (_b = stdJS[2]) !== null && _b !== void 0 ? _b : 0;
248
- }
249
- else if (stdJS.array && Array.isArray(stdJS.array)) {
250
- stdA = (_c = stdJS.array[1]) !== null && _c !== void 0 ? _c : 0;
251
- stdB = (_d = stdJS.array[2]) !== null && _d !== void 0 ? _d : 0;
252
- }
253
- else if (stdJS.data && Array.isArray(stdJS.data)) {
254
- // một số version có định dạng `data`
255
- stdA = (_e = stdJS.data[1]) !== null && _e !== void 0 ? _e : 0;
256
- stdB = (_f = stdJS.data[2]) !== null && _f !== void 0 ? _f : 0;
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
- const colorScore = stdA + stdB;
260
- console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
261
- return colorScore > colorThreshold ? 'G' : 'S';
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.12",
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
- // THRESH_BINARY + THRESH_OTSU = 8
53
- OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
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
- OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
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
- 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;
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 khi tìm góc tài liệu (OpenCV):', e);
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 Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) 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)
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 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 lab: OpenCVMat | null = null;
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 Lab (L=sáng/tối, a*=màu xanh↔đỏ, b*=màu lam↔vàng)
277
- lab = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
278
- OpenCV.invoke('cvtColor', color3Channel, lab, ColorConversionCodes.COLOR_BGR2Lab);
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', lab, meanMat, stdMat);
284
-
285
- // Trích xuất stdDev một cách an toàn
286
- const stdJS = OpenCV.toJSValue(stdMat);
287
- // react-native-fast-opencv thường chuyển `Mat` thành object JS có cấu trúc mảng 1 chiều trong prop `array` hoặc `data`
288
- // hoặc trả về dưới dạng mảng lồng nhau
289
- let stdA = 0;
290
- let stdB = 0;
291
-
292
- if (stdJS) {
293
- if (Array.isArray(stdJS)) {
294
- // Nếu đã là array
295
- stdA = Array.isArray(stdJS[1]) ? stdJS[1][0] : stdJS[1] ?? 0;
296
- stdB = Array.isArray(stdJS[2]) ? stdJS[2][0] : stdJS[2] ?? 0;
297
- } else if (stdJS.array && Array.isArray(stdJS.array)) {
298
- stdA = stdJS.array[1] ?? 0;
299
- stdB = stdJS.array[2] ?? 0;
300
- } else if (stdJS.data && Array.isArray(stdJS.data)) {
301
- // Có một số version có định dạng `data`
302
- stdA = stdJS.data[1] ?? 0;
303
- stdB = stdJS.data[2] ?? 0;
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
- const colorScore = stdA + stdB;
308
- console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
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 > colorThreshold ? 'G' : 'S';
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}`);