rn-opencv-doc-perspective-correction 1.0.16 → 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 +85 -33
- package/package.json +1 -1
- package/src/index.ts +86 -37
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,54 +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
|
-
// 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)) {
|
|
127
|
+
if (area > (imgArea * MIN_AREA_RATIO)) {
|
|
83
128
|
contourMetrics.push({ index: i, area, contour });
|
|
84
129
|
}
|
|
85
130
|
}
|
|
86
131
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
87
|
-
|
|
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;
|
|
88
136
|
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
89
137
|
for (let i = 0; i < maxChecks; i++) {
|
|
90
138
|
const metric = contourMetrics[i];
|
|
91
139
|
const contour = metric.contour;
|
|
92
140
|
const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
|
|
93
141
|
const peri = periObj ? (periObj.value || 0) : 0;
|
|
94
|
-
|
|
95
|
-
|
|
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) {
|
|
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) {
|
|
98
144
|
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
99
145
|
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
|
|
100
146
|
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
147
|
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
103
148
|
try {
|
|
104
149
|
const isConvex = react_native_fast_opencv_1.OpenCV.invoke('isContourConvex', approx);
|
|
105
150
|
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
106
|
-
if (convexValue
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
110
165
|
}
|
|
111
166
|
}
|
|
112
|
-
catch (
|
|
167
|
+
catch (_convexErr) { }
|
|
113
168
|
}
|
|
114
169
|
}
|
|
115
|
-
if (found4) {
|
|
116
|
-
break;
|
|
117
|
-
}
|
|
118
170
|
}
|
|
119
|
-
// BƯỚC
|
|
120
|
-
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) {
|
|
121
173
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
122
|
-
const originalCorners =
|
|
174
|
+
const originalCorners = bestPoly.map(p => ({
|
|
123
175
|
x: Math.round(p.x * actualRatio),
|
|
124
176
|
y: Math.round(p.y * actualRatio)
|
|
125
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,22 +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 (< 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)) {
|
|
151
|
+
if (area > (imgArea * MIN_AREA_RATIO)) {
|
|
109
152
|
contourMetrics.push({ index: i, area, contour });
|
|
110
153
|
}
|
|
111
154
|
}
|
|
112
155
|
|
|
113
156
|
contourMetrics.sort((a, b) => b.area - a.area);
|
|
114
157
|
|
|
115
|
-
|
|
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;
|
|
116
162
|
|
|
117
163
|
const maxChecks = Math.min(contourMetrics.length, 5);
|
|
118
164
|
for (let i = 0; i < maxChecks; i++) {
|
|
@@ -121,39 +167,42 @@ export class DocumentScanner {
|
|
|
121
167
|
|
|
122
168
|
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
123
169
|
const peri = periObj ? (periObj.value || 0) : 0;
|
|
124
|
-
|
|
125
|
-
|
|
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) {
|
|
170
|
+
|
|
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) {
|
|
130
173
|
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
131
174
|
OpenCV.invoke('approxPolyDP', contour, approx, ep * peri, true);
|
|
132
175
|
const approxJS = OpenCV.toJSValue(approx);
|
|
133
176
|
|
|
134
|
-
// Nếu nội suy thành công ra đúng 4 đỉnh
|
|
135
177
|
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
136
178
|
try {
|
|
137
179
|
const isConvex = OpenCV.invoke('isContourConvex', approx);
|
|
138
180
|
const convexValue = (typeof isConvex === 'object' && isConvex !== null) ? isConvex.value : isConvex;
|
|
139
|
-
if (convexValue
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
|
143
196
|
}
|
|
144
|
-
} catch (
|
|
197
|
+
} catch (_convexErr) {}
|
|
145
198
|
}
|
|
146
199
|
}
|
|
147
|
-
|
|
148
|
-
if (found4) {
|
|
149
|
-
break;
|
|
150
|
-
}
|
|
151
200
|
}
|
|
152
201
|
|
|
153
|
-
// BƯỚC
|
|
154
|
-
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) {
|
|
155
204
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
156
|
-
const originalCorners =
|
|
205
|
+
const originalCorners = bestPoly.map(p => ({
|
|
157
206
|
x: Math.round(p.x * actualRatio),
|
|
158
207
|
y: Math.round(p.y * actualRatio)
|
|
159
208
|
}));
|