rn-opencv-doc-perspective-correction 1.0.14 → 1.0.16

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.
Files changed (3) hide show
  1. package/dist/index.js +65 -78
  2. package/package.json +1 -1
  3. package/src/index.ts +74 -83
package/dist/index.js CHANGED
@@ -19,124 +19,111 @@ class DocumentScanner {
19
19
  }
20
20
  static detectPageCorners(imageBase64) {
21
21
  let src = null;
22
+ let resized = null;
22
23
  let gray = null;
23
24
  let blurred = null;
25
+ let closed = null;
24
26
  let edges = null;
27
+ let kernelDims = null;
25
28
  let contoursObj = null;
26
29
  let hierarchyObj = null;
27
30
  try {
28
31
  src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
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
- 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
- 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)
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)
35
- const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
36
- react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
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);
32
+ const jsSrc = react_native_fast_opencv_1.OpenCV.toJSValue(src);
33
+ const origCols = jsSrc.cols || 1000;
34
+ const origRows = jsSrc.rows || 1000;
35
+ const targetHeight = 500.0;
36
+ const ratio = origRows / targetHeight;
37
+ const targetWidth = Math.max(1, Math.round(origCols / ratio));
38
+ resized = 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
+ const dstSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, targetWidth, targetHeight);
72
40
  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);
41
+ react_native_fast_opencv_1.OpenCV.invoke('resize', src, resized, dstSize, 0, 0, 1 /* INTER_LINEAR */);
79
42
  }
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
43
+ catch (err) {
44
+ resized = src; // Fallback an toàn
82
45
  }
83
- // BƯỚC 6: Tìm khối đa giác liên kết (Contours)
46
+ // BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
47
+ 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);
48
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', resized, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
49
+ // BƯỚC 2: Làm mờ để loại bỏ nhiễu hạt (Gaussian Blur 7x7)
50
+ 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);
51
+ const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 7, 7);
52
+ react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
53
+ // BƯỚC 3: Dùng hình thái học lấp các chữ/chi tiết trên CCCD bằng MORPH_CLOSE
54
+ // Giúp mảng CCCD thành khối liền (mặt chữ lặn đi), Canny sẽ bắt vòng viền chuẩn xác
55
+ closed = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
56
+ const kernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 9, 9);
57
+ kernelDims = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, kernelSize);
58
+ react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, kernelDims);
59
+ // BƯỚC 4: Dò tìm cạnh viền Canny
60
+ // Không cần Canny-Auto nữa vì MORPH_CLOSE đã dọn dẹp mặt thẻ quá mượt.
61
+ 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);
62
+ react_native_fast_opencv_1.OpenCV.invoke('Canny', closed, edges, 0, 84);
63
+ // BƯỚC 5: Tìm khối đa giác liên kết (Contours)
64
+ // Lệnh bắt RETR_LIST (1) để tóm được tài liệu nằm bên trong các viền nền lớn
84
65
  contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
85
66
  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);
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 */);
67
+ react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 1 /* RETR_LIST */, 2 /* CHAIN_APPROX_SIMPLE */);
91
68
  const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
92
69
  const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
93
70
  const contoursSize = contoursArray.length;
94
71
  if (contoursSize === 0) {
95
72
  return undefined;
96
73
  }
97
- // Thu thập và đo đạc diện tích
98
74
  let contourMetrics = [];
99
75
  for (let i = 0; i < contoursSize; i++) {
100
76
  const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
101
77
  const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
102
78
  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 (5000 khoảng diện tích tối thiểu cho giấy tờ)
104
- if (area > 5000) {
79
+ // Mở rộng bộ lọc diện tích: Loại bỏ đi vùng nhiễu vụn (< 5% diện tích mặt quét)
80
+ // Và loại bỏ những vùng quá lớn ( dụ toàn bộ ảnh nền > 98%)
81
+ const imgArea = targetWidth * targetHeight;
82
+ if (area > (imgArea * 0.05) && area < (imgArea * 0.98)) {
105
83
  contourMetrics.push({ index: i, area, contour });
106
84
  }
107
85
  }
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)
109
86
  contourMetrics.sort((a, b) => b.area - a.area);
110
87
  let largestPoly = undefined;
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
88
  const maxChecks = Math.min(contourMetrics.length, 5);
114
89
  for (let i = 0; i < maxChecks; i++) {
115
90
  const metric = contourMetrics[i];
116
91
  const contour = metric.contour;
117
92
  const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
118
93
  const peri = periObj ? (periObj.value || 0) : 0;
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)
121
- react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
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
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;
94
+ let found4 = false;
95
+ // Vòng lặp tăng dần độ dung sai Epsilon (từ 2% -> 10% chu vi)
96
+ // Giúp bắt gọn 4 đỉnh kể cả khi đường mép giấy tờ bị nhăn nheo, lởm chởm
97
+ for (let ep = 0.02; ep <= 0.1; ep += 0.02) {
98
+ const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
99
+ react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
100
+ const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
101
+ // Nếu nội suy thành công ra đúng 4 đỉnh
102
+ if (approxJS && approxJS.array && approxJS.array.length === 4) {
103
+ try {
104
+ const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
105
+ const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
106
+ if (convexValue !== false) {
107
+ largestPoly = approxJS.array;
108
+ found4 = true;
109
+ break;
110
+ }
131
111
  }
112
+ catch (convexErr) { }
132
113
  }
133
- catch (convexErr) { }
134
- largestPoly = approxJS.array;
114
+ }
115
+ if (found4) {
135
116
  break;
136
117
  }
137
118
  }
119
+ // BƯỚC 6: Trả về góc và phóng trả lại tỷ lệ kích thước cũ
138
120
  if (largestPoly && largestPoly.length === 4) {
139
- return this.sortCorners(largestPoly);
121
+ const actualRatio = (resized === src) ? 1.0 : ratio;
122
+ const originalCorners = largestPoly.map(p => ({
123
+ x: Math.round(p.x * actualRatio),
124
+ y: Math.round(p.y * actualRatio)
125
+ }));
126
+ return this.sortCorners(originalCorners);
140
127
  }
141
128
  return undefined;
142
129
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rn-opencv-doc-perspective-correction",
3
- "version": "1.0.14",
3
+ "version": "1.0.16",
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
@@ -32,79 +32,61 @@ export class DocumentScanner {
32
32
 
33
33
  public static detectPageCorners(imageBase64: string): Point[] | undefined {
34
34
  let src: OpenCVMat | null = null;
35
+ let resized: OpenCVMat | null = null;
35
36
  let gray: OpenCVMat | null = null;
36
37
  let blurred: OpenCVMat | null = null;
38
+ let closed: OpenCVMat | null = null;
37
39
  let edges: OpenCVMat | null = null;
40
+ let kernelDims: any = null;
38
41
  let contoursObj: any = null;
39
42
  let hierarchyObj: any = null;
40
43
 
41
44
  try {
42
45
  src = OpenCV.base64ToMat(imageBase64);
43
- gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
44
- blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
45
- edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
46
+
47
+ const jsSrc = OpenCV.toJSValue(src);
48
+ const origCols = jsSrc.cols || 1000;
49
+ const origRows = jsSrc.rows || 1000;
46
50
 
47
- // BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
48
- OpenCV.invoke('cvtColor', src, gray, ColorConversionCodes.COLOR_BGR2GRAY);
51
+ const targetHeight = 500.0;
52
+ const ratio = origRows / targetHeight;
53
+ const targetWidth = Math.max(1, Math.round(origCols / ratio));
49
54
 
50
- // BƯỚC 2: Làm mờ để loại bỏ nhiễu hạt nhỏ (Gaussian Blur)
51
- const ksize = OpenCV.createObject(ObjectType.Size, 5, 5);
52
- OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
55
+ resized = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
56
+ const dstSize = OpenCV.createObject(ObjectType.Size, targetWidth, targetHeight);
57
+
58
+ try {
59
+ OpenCV.invoke('resize', src, resized, dstSize, 0, 0, 1 /* INTER_LINEAR */);
60
+ } catch (err) {
61
+ resized = src; // Fallback an toàn
62
+ }
53
63
 
54
- // BƯỚC 3: Đo sáng môi trường để thiết lập Canny thông minh (Auto-Canny)
55
- // 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
- };
64
+ // BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
65
+ gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
66
+ OpenCV.invoke('cvtColor', resized, gray, ColorConversionCodes.COLOR_BGR2GRAY);
73
67
 
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);
68
+ // BƯỚC 2: Làm mờ để loại bỏ nhiễu hạt (Gaussian Blur 7x7)
69
+ blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
70
+ const ksize = OpenCV.createObject(ObjectType.Size, 7, 7);
71
+ OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
80
72
 
81
- // BƯỚC 4: tìm cạnh viền Canny với cảm biến tự động
82
- OpenCV.invoke('Canny', blurred, edges, lowThresh, highThresh);
73
+ // BƯỚC 3: Dùng hình thái học lấp các chữ/chi tiết trên CCCD bằng MORPH_CLOSE
74
+ // Giúp mảng CCCD thành khối liền (mặt chữ lặn đi), Canny sẽ bắt vòng viền chuẩn xác
75
+ closed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
76
+ const kernelSize = OpenCV.createObject(ObjectType.Size, 9, 9);
77
+ kernelDims = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, kernelSize);
78
+ OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, kernelDims);
83
79
 
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
- }
80
+ // BƯỚC 4: tìm cạnh viền Canny
81
+ // Không cần Canny-Auto nữa MORPH_CLOSE đã dọn dẹp mặt thẻ quá mượt.
82
+ edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
83
+ OpenCV.invoke('Canny', closed, edges, 0, 84);
98
84
 
99
- // BƯỚC 6: Tìm khối đa giác liên kết (Contours)
85
+ // BƯỚC 5: Tìm khối đa giác liên kết (Contours)
86
+ // Lệnh bắt RETR_LIST (1) để tóm được tài liệu nằm bên trong các viền nền lớn
100
87
  contoursObj = OpenCV.createObject(ObjectType.MatVector);
101
88
  hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
102
-
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 */);
89
+ OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 1 /* RETR_LIST */, 2 /* CHAIN_APPROX_SIMPLE */);
108
90
 
109
91
  const contoursJS = OpenCV.toJSValue(contoursObj);
110
92
  const contoursArray = contoursJS?.array || [];
@@ -114,59 +96,68 @@ export class DocumentScanner {
114
96
  return undefined;
115
97
  }
116
98
 
117
- // Thu thập và đo đạc diện tích
118
99
  let contourMetrics = [];
119
100
  for (let i = 0; i < contoursSize; i++) {
120
101
  const contour = OpenCV.copyObjectFromVector(contoursObj, i);
121
102
  const areaObj = OpenCV.invoke('contourArea', contour);
122
103
  const area = areaObj ? areaObj.value : 0;
123
104
 
124
- // Mở rộng bộ lọc diện tích: Loại bỏ đi vùng nhiễu (5000 khoảng diện tích tối thiểu cho giấy tờ)
125
- if (area > 5000) {
105
+ // Mở rộng bộ lọc diện tích: Loại bỏ đi vùng nhiễu vụn (< 5% diện tích mặt quét)
106
+ // Và loại bỏ những vùng quá lớn ( dụ toàn bộ ảnh nền > 98%)
107
+ const imgArea = targetWidth * targetHeight;
108
+ if (area > (imgArea * 0.05) && area < (imgArea * 0.98)) {
126
109
  contourMetrics.push({ index: i, area, contour });
127
110
  }
128
111
  }
129
112
 
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)
131
113
  contourMetrics.sort((a, b) => b.area - a.area);
132
114
 
133
115
  let largestPoly: Point[] | undefined = undefined;
134
116
 
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;
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
+
125
+ let found4 = false;
126
+
127
+ // Vòng lặp tăng dần độ dung sai Epsilon (từ 2% -> 10% chu vi)
128
+ // Giúp bắt gọn 4 đỉnh kể cả khi đường mép giấy tờ bị nhăn nheo, lởm chởm
129
+ for (let ep = 0.02; ep <= 0.1; ep += 0.02) {
144
130
  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
-
131
+ OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
149
132
  const approxJS = OpenCV.toJSValue(approx);
150
-
151
- // Nếu quy chiếu thành công ra đúng 4 góc
133
+
134
+ // Nếu nội suy thành công ra đúng 4 đỉnh
152
135
  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
136
  try {
156
137
  const isConvex = OpenCV.invoke('isContourConvex', approx);
157
138
  const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
158
- if (convexValue === false) {
159
- continue;
139
+ if (convexValue !== false) {
140
+ largestPoly = approxJS.array as Point[];
141
+ found4 = true;
142
+ break;
160
143
  }
161
144
  } catch (convexErr) {}
162
-
163
- largestPoly = approxJS.array as Point[];
164
- break;
165
145
  }
166
146
  }
147
+
148
+ if (found4) {
149
+ break;
150
+ }
151
+ }
167
152
 
153
+ // BƯỚC 6: Trả về góc và phóng trả lại tỷ lệ kích thước cũ
168
154
  if (largestPoly && largestPoly.length === 4) {
169
- return this.sortCorners(largestPoly);
155
+ const actualRatio = (resized === src) ? 1.0 : ratio;
156
+ const originalCorners = largestPoly.map(p => ({
157
+ x: Math.round(p.x * actualRatio),
158
+ y: Math.round(p.y * actualRatio)
159
+ }));
160
+ return this.sortCorners(originalCorners);
170
161
  }
171
162
  return undefined;
172
163
  } catch (e: any) {