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.
- package/dist/index.js +45 -67
- package/package.json +1 -1
- 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
|
-
|
|
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 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
|
-
|
|
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
|
|
104
|
-
if (area >
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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 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
|
|
125
|
-
if (area >
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
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
|
-
|
|
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) {
|