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.
- package/dist/index.js +65 -78
- package/package.json +1 -1
- 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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
react_native_fast_opencv_1.OpenCV.
|
|
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
|
-
|
|
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 (
|
|
81
|
-
|
|
43
|
+
catch (err) {
|
|
44
|
+
resized = src; // Fallback an toàn
|
|
82
45
|
}
|
|
83
|
-
// BƯỚC
|
|
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
|
-
|
|
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
|
|
104
|
-
|
|
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 (ví 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
|
-
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
-
|
|
134
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
+
|
|
47
|
+
const jsSrc = OpenCV.toJSValue(src);
|
|
48
|
+
const origCols = jsSrc.cols || 1000;
|
|
49
|
+
const origRows = jsSrc.rows || 1000;
|
|
46
50
|
|
|
47
|
-
|
|
48
|
-
|
|
51
|
+
const targetHeight = 500.0;
|
|
52
|
+
const ratio = origRows / targetHeight;
|
|
53
|
+
const targetWidth = Math.max(1, Math.round(origCols / ratio));
|
|
49
54
|
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
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
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
|
82
|
-
|
|
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
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
|
|
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: Dò tìm cạnh viền Canny
|
|
81
|
+
// Không cần Canny-Auto nữa vì 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
|
|
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
|
|
125
|
-
|
|
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 (ví 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
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
|
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
|
|
159
|
-
|
|
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
|
-
|
|
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) {
|