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

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 +45 -67
  2. package/package.json +1 -1
  3. package/src/index.ts +74 -91
package/dist/index.js CHANGED
@@ -19,97 +19,70 @@ 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 External sẽ hoạt động hoàn hảo khi Canny chỉ thấy 1 nét vành ngoài.
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, 0 /* RETR_EXTERNAL */, 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 vặt (< 5% diện tích mặt quét)
80
+ if (area > (targetWidth * targetHeight * 0.05)) {
105
81
  contourMetrics.push({ index: i, area, contour });
106
82
  }
107
83
  }
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
84
  contourMetrics.sort((a, b) => b.area - a.area);
110
85
  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
86
  const maxChecks = Math.min(contourMetrics.length, 5);
114
87
  for (let i = 0; i < maxChecks; i++) {
115
88
  const metric = contourMetrics[i];
@@ -117,12 +90,11 @@ class DocumentScanner {
117
90
  const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
118
91
  const peri = periObj ? (periObj.value || 0) : 0;
119
92
  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)
93
+ // Nội suy (approx) giảm bớt gấp khúc với độ dung sai epsilon 0.02 chu vi
121
94
  react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
122
95
  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
96
+ // Nếu quy chiếu thành công ra đúng 4 đỉnh
124
97
  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
98
  try {
127
99
  const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
128
100
  const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
@@ -135,8 +107,14 @@ class DocumentScanner {
135
107
  break;
136
108
  }
137
109
  }
110
+ // BƯỚC 6: Trả về góc và phóng trả lại tỷ lệ kích thước cũ
138
111
  if (largestPoly && largestPoly.length === 4) {
139
- return this.sortCorners(largestPoly);
112
+ const actualRatio = (resized === src) ? 1.0 : ratio;
113
+ const originalCorners = largestPoly.map(p => ({
114
+ x: Math.round(p.x * actualRatio),
115
+ y: Math.round(p.y * actualRatio)
116
+ }));
117
+ return this.sortCorners(originalCorners);
140
118
  }
141
119
  return undefined;
142
120
  }
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.15",
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 External sẽ hoạt động hoàn hảo khi Canny chỉ thấy 1 nét vành ngoài.
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, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
108
90
 
109
91
  const contoursJS = OpenCV.toJSValue(contoursObj);
110
92
  const contoursArray = contoursJS?.array || [];
@@ -114,59 +96,60 @@ 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 vặt (< 5% diện tích mặt quét)
106
+ if (area > (targetWidth * targetHeight * 0.05)) {
126
107
  contourMetrics.push({ index: i, area, contour });
127
108
  }
128
109
  }
129
110
 
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
111
  contourMetrics.sort((a, b) => b.area - a.area);
132
112
 
133
113
  let largestPoly: Point[] | undefined = undefined;
134
114
 
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);
115
+ const maxChecks = Math.min(contourMetrics.length, 5);
116
+ for (let i = 0; i < maxChecks; i++) {
117
+ const metric = contourMetrics[i];
118
+ const contour = metric.contour;
119
+
120
+ const periObj = OpenCV.invoke('arcLength', contour, true);
121
+ const peri = periObj ? (periObj.value || 0) : 0;
122
+ const approx = OpenCV.createObject(ObjectType.PointVector);
123
+
124
+ // Nội suy (approx) giảm bớt gấp khúc với độ dung sai epsilon 0.02 chu vi
125
+ OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
126
+
127
+ const approxJS = OpenCV.toJSValue(approx);
128
+
129
+ // Nếu quy chiếu thành công ra đúng 4 đỉnh
130
+ if (approxJS && approxJS.array && approxJS.array.length === 4) {
150
131
 
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
- }
132
+ try {
133
+ const isConvex = OpenCV.invoke('isContourConvex', approx);
134
+ const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
135
+ if (convexValue === false) {
136
+ continue;
137
+ }
138
+ } catch (convexErr) {}
139
+
140
+ largestPoly = approxJS.array as Point[];
141
+ break;
166
142
  }
143
+ }
167
144
 
145
+ // BƯỚC 6: Trả về góc và phóng trả lại tỷ lệ kích thước cũ
168
146
  if (largestPoly && largestPoly.length === 4) {
169
- return this.sortCorners(largestPoly);
147
+ const actualRatio = (resized === src) ? 1.0 : ratio;
148
+ const originalCorners = largestPoly.map(p => ({
149
+ x: Math.round(p.x * actualRatio),
150
+ y: Math.round(p.y * actualRatio)
151
+ }));
152
+ return this.sortCorners(originalCorners);
170
153
  }
171
154
  return undefined;
172
155
  } catch (e: any) {