rn-opencv-doc-perspective-correction 1.0.12 → 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 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,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
- // 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
+ // 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
- react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
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
- for (let i = 0; i < contourMetrics.length; i++) {
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 khi tìm góc tài liệu (OpenCV):', e);
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 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)
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 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, _c, _d, _e, _f;
218
+ var _a, _b;
175
219
  let src = null;
176
220
  let roi = null;
177
- let lab = null;
221
+ let hsv = null;
178
222
  let color3Channel = null;
179
223
  try {
180
224
  src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
@@ -227,38 +271,67 @@ class DocumentScanner {
227
271
  else {
228
272
  color3Channel = target; // Đã là 3 kênh
229
273
  }
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);
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', 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;
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
- 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';
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
337
  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.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
- // 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
+ // 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
- OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
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
- 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;
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 khi tìm góc tài liệu (OpenCV):', e);
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 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)
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 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 lab: OpenCVMat | null = null;
270
+ let hsv: OpenCVMat | null = null;
221
271
  let color3Channel: OpenCVMat | null = null;
222
272
 
223
273
  try {
@@ -273,41 +323,74 @@ export class DocumentScanner {
273
323
  color3Channel = target; // Đã là 3 kênh
274
324
  }
275
325
 
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);
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', 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;
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
- const colorScore = stdA + stdB;
308
- console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
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 > colorThreshold ? 'G' : 'S';
393
+ return colorScore > resolvedThreshold ? 'G' : 'S';
311
394
  } catch (e: any) {
312
395
  console.error('[OpenCV] analyzeColorComplexity lỗi:', e?.message);
313
396
  throw new Error(`[OpenCV analyzeColorComplexity Error]: ${e?.message}`);