rn-opencv-doc-perspective-correction 1.0.15 → 1.0.17
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 +9 -0
- package/dist/index.js +93 -32
- package/package.json +1 -1
- package/src/index.ts +95 -38
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,15 @@ export type OcrMethodHint = 'G' | 'S';
|
|
|
11
11
|
export declare class DocumentScanner {
|
|
12
12
|
private static getDistance;
|
|
13
13
|
private static sortCorners;
|
|
14
|
+
/**
|
|
15
|
+
* Tính góc (độ) giữa vector (p2→p1) và vector (p2→p3).
|
|
16
|
+
*/
|
|
17
|
+
private static getAngleBetween;
|
|
18
|
+
/**
|
|
19
|
+
* Dải góc nội tứ giác (max - min). Nếu > 40° → quá méo, không phải tài liệu.
|
|
20
|
+
* Tham chiếu: andrewdcampbell/OpenCV-Document-Scanner MAX_QUAD_ANGLE_RANGE=40
|
|
21
|
+
*/
|
|
22
|
+
private static getAngleRange;
|
|
14
23
|
static detectPageCorners(imageBase64: string): Point[] | undefined;
|
|
15
24
|
static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined;
|
|
16
25
|
/**
|
package/dist/index.js
CHANGED
|
@@ -17,6 +17,35 @@ class DocumentScanner {
|
|
|
17
17
|
return angleA - angleB;
|
|
18
18
|
});
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Tính góc (độ) giữa vector (p2→p1) và vector (p2→p3).
|
|
22
|
+
*/
|
|
23
|
+
static getAngleBetween(p1, p2, p3) {
|
|
24
|
+
const v1x = p1.x - p2.x, v1y = p1.y - p2.y;
|
|
25
|
+
const v2x = p3.x - p2.x, v2y = p3.y - p2.y;
|
|
26
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
27
|
+
const mag1 = Math.sqrt(v1x * v1x + v1y * v1y);
|
|
28
|
+
const mag2 = Math.sqrt(v2x * v2x + v2y * v2y);
|
|
29
|
+
if (mag1 === 0 || mag2 === 0)
|
|
30
|
+
return 0;
|
|
31
|
+
return Math.acos(Math.max(-1, Math.min(1, dot / (mag1 * mag2)))) * (180 / Math.PI);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Dải góc nội tứ giác (max - min). Nếu > 40° → quá méo, không phải tài liệu.
|
|
35
|
+
* Tham chiếu: andrewdcampbell/OpenCV-Document-Scanner MAX_QUAD_ANGLE_RANGE=40
|
|
36
|
+
*/
|
|
37
|
+
static getAngleRange(quad) {
|
|
38
|
+
if (quad.length !== 4)
|
|
39
|
+
return 999;
|
|
40
|
+
const [tl, tr, br, bl] = quad;
|
|
41
|
+
const angles = [
|
|
42
|
+
this.getAngleBetween(bl, tl, tr),
|
|
43
|
+
this.getAngleBetween(tl, tr, br),
|
|
44
|
+
this.getAngleBetween(tr, br, bl),
|
|
45
|
+
this.getAngleBetween(br, bl, tl),
|
|
46
|
+
];
|
|
47
|
+
return Math.max(...angles) - Math.min(...angles);
|
|
48
|
+
}
|
|
20
49
|
static detectPageCorners(imageBase64) {
|
|
21
50
|
let src = null;
|
|
22
51
|
let resized = null;
|
|
@@ -24,7 +53,7 @@ class DocumentScanner {
|
|
|
24
53
|
let blurred = null;
|
|
25
54
|
let closed = null;
|
|
26
55
|
let edges = null;
|
|
27
|
-
let
|
|
56
|
+
let dilated = null;
|
|
28
57
|
let contoursObj = null;
|
|
29
58
|
let hierarchyObj = null;
|
|
30
59
|
try {
|
|
@@ -46,25 +75,41 @@ class DocumentScanner {
|
|
|
46
75
|
// BƯỚC 1: Chuyển sang ảnh xám (Grayscale)
|
|
47
76
|
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
77
|
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', resized, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
|
|
49
|
-
// BƯỚC 2:
|
|
78
|
+
// BƯỚC 2: GaussianBlur 7x7 (theo andrewdcampbell)
|
|
50
79
|
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
80
|
const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 7, 7);
|
|
52
81
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
53
|
-
// BƯỚC 3:
|
|
54
|
-
//
|
|
82
|
+
// BƯỚC 3: MORPH_CLOSE (kernel RECT 9x9) – nối các lỗ trống giữa cạnh viền
|
|
83
|
+
// Thiếu bước này là nguyên nhân chính khiến contour bị đứt đoạn
|
|
84
|
+
const MORPH_SIZE = 9;
|
|
85
|
+
const morphKernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, MORPH_SIZE, MORPH_SIZE);
|
|
86
|
+
const morphKernel = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, morphKernelSize);
|
|
55
87
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
88
|
+
try {
|
|
89
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, morphKernel);
|
|
90
|
+
}
|
|
91
|
+
catch (_morphErr) {
|
|
92
|
+
closed = blurred; // Fallback nếu thiết bị không hỗ trợ
|
|
93
|
+
}
|
|
94
|
+
// BƯỚC 4: Canny (0, 84) – ngưỡng thấp theo andrewdcampbell
|
|
95
|
+
// threshold1=0: chấp nhận TẤT CẢ cạnh yếu nếu liên kết với cạnh mạnh
|
|
96
|
+
// threshold2=84: ngưỡng cạnh mạnh vừa phải
|
|
61
97
|
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
98
|
react_native_fast_opencv_1.OpenCV.invoke('Canny', closed, edges, 0, 84);
|
|
63
|
-
// BƯỚC 5:
|
|
64
|
-
|
|
99
|
+
// BƯỚC 5: Dilate 3x3 – nối các pixel viền lân cận bị đứt
|
|
100
|
+
const dilKernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 3, 3);
|
|
101
|
+
const dilKernel = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
102
|
+
dilated = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
103
|
+
try {
|
|
104
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', edges, dilated, 1 /* MORPH_DILATE */, dilKernel);
|
|
105
|
+
}
|
|
106
|
+
catch (_dilErr) {
|
|
107
|
+
dilated = edges; // Fallback
|
|
108
|
+
}
|
|
109
|
+
// BƯỚC 6: Tìm Contours trên ảnh đã dilate
|
|
65
110
|
contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
66
111
|
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);
|
|
67
|
-
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy',
|
|
112
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj, 1 /* RETR_LIST */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
68
113
|
const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
|
|
69
114
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
70
115
|
const contoursSize = contoursArray.length;
|
|
@@ -72,45 +117,61 @@ class DocumentScanner {
|
|
|
72
117
|
return undefined;
|
|
73
118
|
}
|
|
74
119
|
let contourMetrics = [];
|
|
120
|
+
const imgArea = targetWidth * targetHeight;
|
|
121
|
+
// Diện tích tối thiểu 15% (gốc andrewdcampbell dùng 25%, ta dùng 15% linh hoạt hơn)
|
|
122
|
+
const MIN_AREA_RATIO = 0.15;
|
|
75
123
|
for (let i = 0; i < contoursSize; i++) {
|
|
76
124
|
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
77
125
|
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
78
126
|
const area = areaObj ? areaObj.value : 0;
|
|
79
|
-
|
|
80
|
-
if (area > (targetWidth * targetHeight * 0.05)) {
|
|
127
|
+
if (area > (imgArea * MIN_AREA_RATIO)) {
|
|
81
128
|
contourMetrics.push({ index: i, area, contour });
|
|
82
129
|
}
|
|
83
130
|
}
|
|
84
131
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
85
|
-
|
|
132
|
+
// Thu thập TẤT CẢ candidates hợp lệ, chọn tốt nhất (không dừng sớm)
|
|
133
|
+
let bestPoly = undefined;
|
|
134
|
+
let bestArea = 0;
|
|
135
|
+
let bestAngleRange = 999;
|
|
86
136
|
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
87
137
|
for (let i = 0; i < maxChecks; i++) {
|
|
88
138
|
const metric = contourMetrics[i];
|
|
89
139
|
const contour = metric.contour;
|
|
90
140
|
const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
|
|
91
141
|
const peri = periObj ? (periObj.value || 0) : 0;
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
142
|
+
// Vòng lặp epsilon (2%→10%, bước 1%) – tìm tứ giác 4 đỉnh
|
|
143
|
+
for (let ep = 0.02; ep <= 0.1; ep += 0.01) {
|
|
144
|
+
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
145
|
+
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
|
|
146
|
+
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
147
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
148
|
+
try {
|
|
149
|
+
const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
|
|
150
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
151
|
+
if (convexValue === false)
|
|
152
|
+
continue;
|
|
153
|
+
const pts = approxJS.array;
|
|
154
|
+
const sorted = this.sortCorners([...pts]);
|
|
155
|
+
const angleRange = this.getAngleRange(sorted);
|
|
156
|
+
// Validate: dải góc nội < 40° (loại hình thoi/bình hành méo)
|
|
157
|
+
if (angleRange < 40) {
|
|
158
|
+
// Ưu tiên: lớn nhất + vuông nhất
|
|
159
|
+
if (metric.area > bestArea || (metric.area === bestArea && angleRange < bestAngleRange)) {
|
|
160
|
+
bestPoly = pts;
|
|
161
|
+
bestArea = metric.area;
|
|
162
|
+
bestAngleRange = angleRange;
|
|
163
|
+
}
|
|
164
|
+
break; // Đã tìm quad tốt cho contour này, sang contour tiếp
|
|
165
|
+
}
|
|
103
166
|
}
|
|
167
|
+
catch (_convexErr) { }
|
|
104
168
|
}
|
|
105
|
-
catch (convexErr) { }
|
|
106
|
-
largestPoly = approxJS.array;
|
|
107
|
-
break;
|
|
108
169
|
}
|
|
109
170
|
}
|
|
110
|
-
// BƯỚC
|
|
111
|
-
if (
|
|
171
|
+
// BƯỚC 7: Trả về góc, phóng trả tỷ lệ kích thước gốc
|
|
172
|
+
if (bestPoly && bestPoly.length === 4) {
|
|
112
173
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
113
|
-
const originalCorners =
|
|
174
|
+
const originalCorners = bestPoly.map(p => ({
|
|
114
175
|
x: Math.round(p.x * actualRatio),
|
|
115
176
|
y: Math.round(p.y * actualRatio)
|
|
116
177
|
}));
|
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.17",
|
|
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
|
@@ -30,6 +30,35 @@ export class DocumentScanner {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Tính góc (độ) giữa vector (p2→p1) và vector (p2→p3).
|
|
35
|
+
*/
|
|
36
|
+
private static getAngleBetween(p1: Point, p2: Point, p3: Point): number {
|
|
37
|
+
const v1x = p1.x - p2.x, v1y = p1.y - p2.y;
|
|
38
|
+
const v2x = p3.x - p2.x, v2y = p3.y - p2.y;
|
|
39
|
+
const dot = v1x * v2x + v1y * v2y;
|
|
40
|
+
const mag1 = Math.sqrt(v1x * v1x + v1y * v1y);
|
|
41
|
+
const mag2 = Math.sqrt(v2x * v2x + v2y * v2y);
|
|
42
|
+
if (mag1 === 0 || mag2 === 0) return 0;
|
|
43
|
+
return Math.acos(Math.max(-1, Math.min(1, dot / (mag1 * mag2)))) * (180 / Math.PI);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Dải góc nội tứ giác (max - min). Nếu > 40° → quá méo, không phải tài liệu.
|
|
48
|
+
* Tham chiếu: andrewdcampbell/OpenCV-Document-Scanner MAX_QUAD_ANGLE_RANGE=40
|
|
49
|
+
*/
|
|
50
|
+
private static getAngleRange(quad: Point[]): number {
|
|
51
|
+
if (quad.length !== 4) return 999;
|
|
52
|
+
const [tl, tr, br, bl] = quad;
|
|
53
|
+
const angles = [
|
|
54
|
+
this.getAngleBetween(bl, tl, tr),
|
|
55
|
+
this.getAngleBetween(tl, tr, br),
|
|
56
|
+
this.getAngleBetween(tr, br, bl),
|
|
57
|
+
this.getAngleBetween(br, bl, tl),
|
|
58
|
+
];
|
|
59
|
+
return Math.max(...angles) - Math.min(...angles);
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
public static detectPageCorners(imageBase64: string): Point[] | undefined {
|
|
34
63
|
let src: OpenCVMat | null = null;
|
|
35
64
|
let resized: OpenCVMat | null = null;
|
|
@@ -37,7 +66,7 @@ export class DocumentScanner {
|
|
|
37
66
|
let blurred: OpenCVMat | null = null;
|
|
38
67
|
let closed: OpenCVMat | null = null;
|
|
39
68
|
let edges: OpenCVMat | null = null;
|
|
40
|
-
let
|
|
69
|
+
let dilated: OpenCVMat | null = null;
|
|
41
70
|
let contoursObj: any = null;
|
|
42
71
|
let hierarchyObj: any = null;
|
|
43
72
|
|
|
@@ -65,28 +94,43 @@ export class DocumentScanner {
|
|
|
65
94
|
gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
66
95
|
OpenCV.invoke('cvtColor', resized, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
67
96
|
|
|
68
|
-
// BƯỚC 2:
|
|
97
|
+
// BƯỚC 2: GaussianBlur 7x7 (theo andrewdcampbell)
|
|
69
98
|
blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
70
99
|
const ksize = OpenCV.createObject(ObjectType.Size, 7, 7);
|
|
71
100
|
OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
72
101
|
|
|
73
|
-
// BƯỚC 3:
|
|
74
|
-
//
|
|
102
|
+
// BƯỚC 3: MORPH_CLOSE (kernel RECT 9x9) – nối các lỗ trống giữa cạnh viền
|
|
103
|
+
// Thiếu bước này là nguyên nhân chính khiến contour bị đứt đoạn
|
|
104
|
+
const MORPH_SIZE = 9;
|
|
105
|
+
const morphKernelSize = OpenCV.createObject(ObjectType.Size, MORPH_SIZE, MORPH_SIZE);
|
|
106
|
+
const morphKernel = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, morphKernelSize);
|
|
75
107
|
closed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
108
|
+
try {
|
|
109
|
+
OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, morphKernel);
|
|
110
|
+
} catch (_morphErr) {
|
|
111
|
+
closed = blurred; // Fallback nếu thiết bị không hỗ trợ
|
|
112
|
+
}
|
|
79
113
|
|
|
80
|
-
// BƯỚC 4:
|
|
81
|
-
//
|
|
114
|
+
// BƯỚC 4: Canny (0, 84) – ngưỡng thấp theo andrewdcampbell
|
|
115
|
+
// threshold1=0: chấp nhận TẤT CẢ cạnh yếu nếu liên kết với cạnh mạnh
|
|
116
|
+
// threshold2=84: ngưỡng cạnh mạnh vừa phải
|
|
82
117
|
edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
83
118
|
OpenCV.invoke('Canny', closed, edges, 0, 84);
|
|
84
119
|
|
|
85
|
-
// BƯỚC 5:
|
|
86
|
-
|
|
120
|
+
// BƯỚC 5: Dilate 3x3 – nối các pixel viền lân cận bị đứt
|
|
121
|
+
const dilKernelSize = OpenCV.createObject(ObjectType.Size, 3, 3);
|
|
122
|
+
const dilKernel = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
123
|
+
dilated = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
124
|
+
try {
|
|
125
|
+
OpenCV.invoke('morphologyEx', edges, dilated, 1 /* MORPH_DILATE */, dilKernel);
|
|
126
|
+
} catch (_dilErr) {
|
|
127
|
+
dilated = edges; // Fallback
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// BƯỚC 6: Tìm Contours trên ảnh đã dilate
|
|
87
131
|
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
88
132
|
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
89
|
-
OpenCV.invoke('findContoursWithHierarchy',
|
|
133
|
+
OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj, 1 /* RETR_LIST */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
90
134
|
|
|
91
135
|
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
92
136
|
const contoursArray = contoursJS?.array || [];
|
|
@@ -97,20 +141,24 @@ export class DocumentScanner {
|
|
|
97
141
|
}
|
|
98
142
|
|
|
99
143
|
let contourMetrics = [];
|
|
144
|
+
const imgArea = targetWidth * targetHeight;
|
|
145
|
+
// Diện tích tối thiểu 15% (gốc andrewdcampbell dùng 25%, ta dùng 15% linh hoạt hơn)
|
|
146
|
+
const MIN_AREA_RATIO = 0.15;
|
|
100
147
|
for (let i = 0; i < contoursSize; i++) {
|
|
101
148
|
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
102
149
|
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
103
150
|
const area = areaObj ? areaObj.value : 0;
|
|
104
|
-
|
|
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)) {
|
|
151
|
+
if (area > (imgArea * MIN_AREA_RATIO)) {
|
|
107
152
|
contourMetrics.push({ index: i, area, contour });
|
|
108
153
|
}
|
|
109
154
|
}
|
|
110
155
|
|
|
111
156
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
112
157
|
|
|
113
|
-
|
|
158
|
+
// Thu thập TẤT CẢ candidates hợp lệ, chọn tốt nhất (không dừng sớm)
|
|
159
|
+
let bestPoly: Point[] | undefined = undefined;
|
|
160
|
+
let bestArea = 0;
|
|
161
|
+
let bestAngleRange = 999;
|
|
114
162
|
|
|
115
163
|
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
116
164
|
for (let i = 0; i < maxChecks; i++) {
|
|
@@ -119,33 +167,42 @@ export class DocumentScanner {
|
|
|
119
167
|
|
|
120
168
|
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
121
169
|
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
170
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
171
|
+
// Vòng lặp epsilon (2%→10%, bước 1%) – tìm tứ giác 4 đỉnh
|
|
172
|
+
for (let ep = 0.02; ep <= 0.1; ep += 0.01) {
|
|
173
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
174
|
+
OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
|
|
175
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
176
|
+
|
|
177
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
178
|
+
try {
|
|
179
|
+
const isConvex = OpenCV.invoke('isContourConvex', approx);
|
|
180
|
+
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
181
|
+
if (convexValue === false) continue;
|
|
182
|
+
|
|
183
|
+
const pts = approxJS.array as Point[];
|
|
184
|
+
const sorted = this.sortCorners([...pts]);
|
|
185
|
+
const angleRange = this.getAngleRange(sorted);
|
|
186
|
+
|
|
187
|
+
// Validate: dải góc nội < 40° (loại hình thoi/bình hành méo)
|
|
188
|
+
if (angleRange < 40) {
|
|
189
|
+
// Ưu tiên: lớn nhất + vuông nhất
|
|
190
|
+
if (metric.area > bestArea || (metric.area === bestArea && angleRange < bestAngleRange)) {
|
|
191
|
+
bestPoly = pts;
|
|
192
|
+
bestArea = metric.area;
|
|
193
|
+
bestAngleRange = angleRange;
|
|
194
|
+
}
|
|
195
|
+
break; // Đã tìm quad tốt cho contour này, sang contour tiếp
|
|
196
|
+
}
|
|
197
|
+
} catch (_convexErr) {}
|
|
198
|
+
}
|
|
142
199
|
}
|
|
143
200
|
}
|
|
144
201
|
|
|
145
|
-
// BƯỚC
|
|
146
|
-
if (
|
|
202
|
+
// BƯỚC 7: Trả về góc, phóng trả tỷ lệ kích thước gốc
|
|
203
|
+
if (bestPoly && bestPoly.length === 4) {
|
|
147
204
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
148
|
-
const originalCorners =
|
|
205
|
+
const originalCorners = bestPoly.map(p => ({
|
|
149
206
|
x: Math.round(p.x * actualRatio),
|
|
150
207
|
y: Math.round(p.y * actualRatio)
|
|
151
208
|
}));
|