rn-opencv-doc-perspective-correction 1.0.17 → 1.0.18
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 +2 -0
- package/dist/index.js +101 -43
- package/package.json +1 -1
- package/src/index.ts +114 -45
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare class DocumentScanner {
|
|
|
20
20
|
* Tham chiếu: andrewdcampbell/OpenCV-Document-Scanner MAX_QUAD_ANGLE_RANGE=40
|
|
21
21
|
*/
|
|
22
22
|
private static getAngleRange;
|
|
23
|
+
/** Fallback: 4 góc của toàn bộ ảnh (như Python gốc khi không tìm được contour) */
|
|
24
|
+
private static buildFullImageCorners;
|
|
23
25
|
static detectPageCorners(imageBase64: string): Point[] | undefined;
|
|
24
26
|
static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined;
|
|
25
27
|
/**
|
package/dist/index.js
CHANGED
|
@@ -46,7 +46,17 @@ class DocumentScanner {
|
|
|
46
46
|
];
|
|
47
47
|
return Math.max(...angles) - Math.min(...angles);
|
|
48
48
|
}
|
|
49
|
+
/** Fallback: 4 góc của toàn bộ ảnh (như Python gốc khi không tìm được contour) */
|
|
50
|
+
static buildFullImageCorners(w, h) {
|
|
51
|
+
return [
|
|
52
|
+
{ x: 0, y: 0 }, // top-left
|
|
53
|
+
{ x: w, y: 0 }, // top-right
|
|
54
|
+
{ x: w, y: h }, // bottom-right
|
|
55
|
+
{ x: 0, y: h }, // bottom-left
|
|
56
|
+
];
|
|
57
|
+
}
|
|
49
58
|
static detectPageCorners(imageBase64) {
|
|
59
|
+
var _a, _b;
|
|
50
60
|
let src = null;
|
|
51
61
|
let resized = null;
|
|
52
62
|
let gray = null;
|
|
@@ -85,11 +95,13 @@ class DocumentScanner {
|
|
|
85
95
|
const morphKernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, MORPH_SIZE, MORPH_SIZE);
|
|
86
96
|
const morphKernel = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, morphKernelSize);
|
|
87
97
|
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);
|
|
98
|
+
let morphCloseUsed = true;
|
|
88
99
|
try {
|
|
89
100
|
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, morphKernel);
|
|
90
101
|
}
|
|
91
102
|
catch (_morphErr) {
|
|
92
103
|
closed = blurred; // Fallback nếu thiết bị không hỗ trợ
|
|
104
|
+
morphCloseUsed = false;
|
|
93
105
|
}
|
|
94
106
|
// BƯỚC 4: Canny (0, 84) – ngưỡng thấp theo andrewdcampbell
|
|
95
107
|
// 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
|
|
@@ -100,75 +112,89 @@ class DocumentScanner {
|
|
|
100
112
|
const dilKernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 3, 3);
|
|
101
113
|
const dilKernel = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
102
114
|
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);
|
|
115
|
+
let dilateUsed = true;
|
|
103
116
|
try {
|
|
104
117
|
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', edges, dilated, 1 /* MORPH_DILATE */, dilKernel);
|
|
105
118
|
}
|
|
106
119
|
catch (_dilErr) {
|
|
107
120
|
dilated = edges; // Fallback
|
|
121
|
+
dilateUsed = false;
|
|
108
122
|
}
|
|
109
|
-
// BƯỚC 6: Tìm Contours
|
|
123
|
+
// BƯỚC 6: Tìm Contours — dùng RETR_EXTERNAL (đúng như Python gốc, không phải RETR_LIST)
|
|
110
124
|
contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
111
125
|
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);
|
|
112
|
-
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj,
|
|
126
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
113
127
|
const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
|
|
114
128
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
115
129
|
const contoursSize = contoursArray.length;
|
|
130
|
+
const imgArea = targetWidth * targetHeight;
|
|
131
|
+
console.log(`[DocDetect] imgSize=${origCols}x${origRows} → resize=${targetWidth}x${targetHeight} | morphClose=${morphCloseUsed} dilate=${dilateUsed} | totalContours=${contoursSize}`);
|
|
116
132
|
if (contoursSize === 0) {
|
|
117
|
-
|
|
133
|
+
// Fallback: trả toàn bộ ảnh (như Python gốc)
|
|
134
|
+
console.log('[DocDetect] contoursSize=0, fallback to full image');
|
|
135
|
+
return this.buildFullImageCorners(origCols, origRows);
|
|
118
136
|
}
|
|
119
|
-
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
const MIN_AREA_RATIO = 0.15;
|
|
137
|
+
// Lấy top 5 contour lớn nhất (đúng như Python: cnts[:5])
|
|
138
|
+
const allContourMetrics = [];
|
|
139
|
+
let maxContourArea = 0;
|
|
123
140
|
for (let i = 0; i < contoursSize; i++) {
|
|
124
141
|
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
125
142
|
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
126
143
|
const area = areaObj ? areaObj.value : 0;
|
|
127
|
-
if (area >
|
|
128
|
-
|
|
129
|
-
}
|
|
144
|
+
if (area > maxContourArea)
|
|
145
|
+
maxContourArea = area;
|
|
146
|
+
allContourMetrics.push({ index: i, area, contour });
|
|
130
147
|
}
|
|
131
|
-
|
|
132
|
-
|
|
148
|
+
allContourMetrics.sort((a, b) => b.area - a.area);
|
|
149
|
+
const top5 = allContourMetrics.slice(0, 5);
|
|
150
|
+
console.log(`[DocDetect] top5 areas: ${top5.map(m => m.area.toFixed(0)).join(', ')} | imgArea=${imgArea.toFixed(0)}`);
|
|
151
|
+
// MIN_QUAD_AREA_RATIO = 0.25 (Python gốc), với fallback 0.05 cho ảnh nhỏ
|
|
152
|
+
const MIN_QUAD_AREA_RATIO = imgArea > 50000 ? 0.25 : 0.05;
|
|
133
153
|
let bestPoly = undefined;
|
|
134
154
|
let bestArea = 0;
|
|
135
155
|
let bestAngleRange = 999;
|
|
136
|
-
const
|
|
137
|
-
for (let i = 0; i <
|
|
138
|
-
const metric =
|
|
156
|
+
const debugCandidates = [];
|
|
157
|
+
for (let i = 0; i < top5.length; i++) {
|
|
158
|
+
const metric = top5[i];
|
|
139
159
|
const contour = metric.contour;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
//
|
|
143
|
-
|
|
160
|
+
// Python gốc dùng epsilon = 80 pixel tuyệt đối (không phải % perimeter!)
|
|
161
|
+
// approxPolyDP(c, 80, True) — epsilon cố định
|
|
162
|
+
// Ta thử range 10→100 pixel, bước 10, để tìm ra 4 đỉnh
|
|
163
|
+
let foundPts = null;
|
|
164
|
+
let foundVerts = 0;
|
|
165
|
+
let foundAngle = 999;
|
|
166
|
+
for (let epPx = 10; epPx <= 120; epPx += 5) {
|
|
144
167
|
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,
|
|
168
|
+
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, epPx, true);
|
|
146
169
|
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
break; // Đã tìm quad tốt cho contour này, sang contour tiếp
|
|
170
|
+
const verts = (_b = (_a = approxJS === null || approxJS === void 0 ? void 0 : approxJS.array) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
|
|
171
|
+
if (verts === 4) {
|
|
172
|
+
const pts = approxJS.array;
|
|
173
|
+
const sorted = this.sortCorners([...pts]);
|
|
174
|
+
const angleRange = this.getAngleRange(sorted);
|
|
175
|
+
if (angleRange < foundAngle) {
|
|
176
|
+
foundAngle = angleRange;
|
|
177
|
+
foundPts = pts;
|
|
178
|
+
foundVerts = verts;
|
|
179
|
+
}
|
|
180
|
+
// is_valid_contour: area > MIN_QUAD_AREA_RATIO và angle_range < 40
|
|
181
|
+
if (metric.area > imgArea * MIN_QUAD_AREA_RATIO && angleRange < 40) {
|
|
182
|
+
if (metric.area > bestArea || (metric.area === bestArea && angleRange < bestAngleRange)) {
|
|
183
|
+
bestPoly = pts;
|
|
184
|
+
bestArea = metric.area;
|
|
185
|
+
bestAngleRange = angleRange;
|
|
165
186
|
}
|
|
187
|
+
break;
|
|
166
188
|
}
|
|
167
|
-
catch (_convexErr) { }
|
|
168
189
|
}
|
|
169
190
|
}
|
|
191
|
+
debugCandidates.push(` [C${i}] area=${metric.area.toFixed(0)}px²(${(metric.area / imgArea * 100).toFixed(1)}%) ` +
|
|
192
|
+
`foundVerts=${foundVerts} bestAngle=${foundAngle === 999 ? 'N/A' : foundAngle.toFixed(1)}° ` +
|
|
193
|
+
`valid=${metric.area > imgArea * MIN_QUAD_AREA_RATIO && foundAngle < 40}`);
|
|
170
194
|
}
|
|
171
|
-
|
|
195
|
+
console.log(`[DocDetect] Candidates:\n${debugCandidates.join('\n')}`);
|
|
196
|
+
console.log(`[DocDetect] Result: bestPoly=${bestPoly ? 'found' : 'NOT FOUND'} area=${bestArea.toFixed(0)} angleRange=${bestAngleRange === 999 ? 'N/A' : bestAngleRange.toFixed(1)}°`);
|
|
197
|
+
// BƯỚC 7: Trả về góc, scale về kích thước ảnh gốc
|
|
172
198
|
if (bestPoly && bestPoly.length === 4) {
|
|
173
199
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
174
200
|
const originalCorners = bestPoly.map(p => ({
|
|
@@ -177,11 +203,43 @@ class DocumentScanner {
|
|
|
177
203
|
}));
|
|
178
204
|
return this.sortCorners(originalCorners);
|
|
179
205
|
}
|
|
180
|
-
|
|
206
|
+
// Giống Python gốc: nếu không tìm được → dùng toàn bộ ảnh làm fallback
|
|
207
|
+
// "If we did not find any valid contours, just use the whole image"
|
|
208
|
+
console.log(`[DocDetect] No valid quad found (minArea=${(MIN_QUAD_AREA_RATIO * 100).toFixed(0)}%, maxContour=${maxContourArea.toFixed(0)}px²). Fallback to full image.`);
|
|
209
|
+
throw new Error(`Không phát hiện được viền tài liệu.\n` +
|
|
210
|
+
`Ảnh: ${origCols}x${origRows}px | Resize: ${targetWidth}x${targetHeight}px\n` +
|
|
211
|
+
`Contours kiểm tra: ${top5.length}/${contoursSize} | MinArea: ${(MIN_QUAD_AREA_RATIO * 100).toFixed(0)}%\n` +
|
|
212
|
+
`Contour lớn nhất: ${maxContourArea.toFixed(0)}px² (${(maxContourArea / imgArea * 100).toFixed(1)}% ảnh)\n` +
|
|
213
|
+
`Chi tiết:\n${debugCandidates.join('\n')}\n` +
|
|
214
|
+
`Gợi ý: Tài liệu cần chiếm ít nhất ${(MIN_QUAD_AREA_RATIO * 100).toFixed(0)}% diện tích khung hình, viền phải tương phản với nền.`);
|
|
181
215
|
}
|
|
182
216
|
catch (e) {
|
|
183
|
-
|
|
184
|
-
|
|
217
|
+
// Trích message từ mọi dạng exception (Error object, string, native, undefined...)
|
|
218
|
+
let rawMsg;
|
|
219
|
+
if (typeof e === 'string') {
|
|
220
|
+
rawMsg = e;
|
|
221
|
+
}
|
|
222
|
+
else if (e instanceof Error) {
|
|
223
|
+
rawMsg = e.message || e.toString();
|
|
224
|
+
}
|
|
225
|
+
else if (e && typeof e === 'object') {
|
|
226
|
+
rawMsg = e.message || e.code || e.name || JSON.stringify(e);
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
rawMsg = String(e !== null && e !== void 0 ? e : 'Unknown error');
|
|
230
|
+
}
|
|
231
|
+
console.error('[OpenCV] detectPageCorners exception:', rawMsg, e);
|
|
232
|
+
// Nếu đây là lỗi intentional từ chính hàm này (đã có thông tin chi tiết),
|
|
233
|
+
// chỉ wrap nhẹ để không mất ngữ cảnh.
|
|
234
|
+
// Nếu là lỗi native bất ngờ từ OpenCV, thêm context step.
|
|
235
|
+
const isIntentional = rawMsg.startsWith('Không ') || rawMsg.startsWith('[OpenCV Corner');
|
|
236
|
+
if (isIntentional) {
|
|
237
|
+
throw new Error(rawMsg);
|
|
238
|
+
}
|
|
239
|
+
else {
|
|
240
|
+
throw new Error(`Lỗi xử lý OpenCV không mong đợi: ${rawMsg}\n` +
|
|
241
|
+
`(Có thể do ảnh corrupt, định dạng không hỗ trợ, hoặc thiết bị thiếu bộ nhớ)`);
|
|
242
|
+
}
|
|
185
243
|
}
|
|
186
244
|
finally {
|
|
187
245
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
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.18",
|
|
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
|
@@ -59,6 +59,16 @@ export class DocumentScanner {
|
|
|
59
59
|
return Math.max(...angles) - Math.min(...angles);
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
/** Fallback: 4 góc của toàn bộ ảnh (như Python gốc khi không tìm được contour) */
|
|
63
|
+
private static buildFullImageCorners(w: number, h: number): Point[] {
|
|
64
|
+
return [
|
|
65
|
+
{ x: 0, y: 0 }, // top-left
|
|
66
|
+
{ x: w, y: 0 }, // top-right
|
|
67
|
+
{ x: w, y: h }, // bottom-right
|
|
68
|
+
{ x: 0, y: h }, // bottom-left
|
|
69
|
+
];
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
public static detectPageCorners(imageBase64: string): Point[] | undefined {
|
|
63
73
|
let src: OpenCVMat | null = null;
|
|
64
74
|
let resized: OpenCVMat | null = null;
|
|
@@ -105,10 +115,12 @@ export class DocumentScanner {
|
|
|
105
115
|
const morphKernelSize = OpenCV.createObject(ObjectType.Size, MORPH_SIZE, MORPH_SIZE);
|
|
106
116
|
const morphKernel = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, morphKernelSize);
|
|
107
117
|
closed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
118
|
+
let morphCloseUsed = true;
|
|
108
119
|
try {
|
|
109
120
|
OpenCV.invoke('morphologyEx', blurred, closed, 3 /* MORPH_CLOSE */, morphKernel);
|
|
110
121
|
} catch (_morphErr) {
|
|
111
122
|
closed = blurred; // Fallback nếu thiết bị không hỗ trợ
|
|
123
|
+
morphCloseUsed = false;
|
|
112
124
|
}
|
|
113
125
|
|
|
114
126
|
// BƯỚC 4: Canny (0, 84) – ngưỡng thấp theo andrewdcampbell
|
|
@@ -121,85 +133,107 @@ export class DocumentScanner {
|
|
|
121
133
|
const dilKernelSize = OpenCV.createObject(ObjectType.Size, 3, 3);
|
|
122
134
|
const dilKernel = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
123
135
|
dilated = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
136
|
+
let dilateUsed = true;
|
|
124
137
|
try {
|
|
125
138
|
OpenCV.invoke('morphologyEx', edges, dilated, 1 /* MORPH_DILATE */, dilKernel);
|
|
126
139
|
} catch (_dilErr) {
|
|
127
140
|
dilated = edges; // Fallback
|
|
141
|
+
dilateUsed = false;
|
|
128
142
|
}
|
|
129
143
|
|
|
130
|
-
// BƯỚC 6: Tìm Contours
|
|
144
|
+
// BƯỚC 6: Tìm Contours — dùng RETR_EXTERNAL (đúng như Python gốc, không phải RETR_LIST)
|
|
131
145
|
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
132
146
|
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
133
|
-
OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj,
|
|
147
|
+
OpenCV.invoke('findContoursWithHierarchy', dilated, contoursObj, hierarchyObj,
|
|
148
|
+
0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
134
149
|
|
|
135
150
|
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
136
151
|
const contoursArray = contoursJS?.array || [];
|
|
137
152
|
const contoursSize = contoursArray.length;
|
|
138
153
|
|
|
154
|
+
const imgArea = targetWidth * targetHeight;
|
|
155
|
+
console.log(`[DocDetect] imgSize=${origCols}x${origRows} → resize=${targetWidth}x${targetHeight} | morphClose=${morphCloseUsed} dilate=${dilateUsed} | totalContours=${contoursSize}`);
|
|
156
|
+
|
|
139
157
|
if (contoursSize === 0) {
|
|
140
|
-
|
|
158
|
+
// Fallback: trả toàn bộ ảnh (như Python gốc)
|
|
159
|
+
console.log('[DocDetect] contoursSize=0, fallback to full image');
|
|
160
|
+
return this.buildFullImageCorners(origCols, origRows);
|
|
141
161
|
}
|
|
142
162
|
|
|
143
|
-
|
|
144
|
-
const
|
|
145
|
-
|
|
146
|
-
const MIN_AREA_RATIO = 0.15;
|
|
163
|
+
// Lấy top 5 contour lớn nhất (đúng như Python: cnts[:5])
|
|
164
|
+
const allContourMetrics = [];
|
|
165
|
+
let maxContourArea = 0;
|
|
147
166
|
for (let i = 0; i < contoursSize; i++) {
|
|
148
167
|
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
149
168
|
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
150
169
|
const area = areaObj ? areaObj.value : 0;
|
|
151
|
-
if (area >
|
|
152
|
-
|
|
153
|
-
}
|
|
170
|
+
if (area > maxContourArea) maxContourArea = area;
|
|
171
|
+
allContourMetrics.push({ index: i, area, contour });
|
|
154
172
|
}
|
|
173
|
+
allContourMetrics.sort((a, b) => b.area - a.area);
|
|
174
|
+
const top5 = allContourMetrics.slice(0, 5);
|
|
175
|
+
|
|
176
|
+
console.log(`[DocDetect] top5 areas: ${top5.map(m => m.area.toFixed(0)).join(', ')} | imgArea=${imgArea.toFixed(0)}`);
|
|
155
177
|
|
|
156
|
-
|
|
178
|
+
// MIN_QUAD_AREA_RATIO = 0.25 (Python gốc), với fallback 0.05 cho ảnh nhỏ
|
|
179
|
+
const MIN_QUAD_AREA_RATIO = imgArea > 50000 ? 0.25 : 0.05;
|
|
157
180
|
|
|
158
|
-
// Thu thập TẤT CẢ candidates hợp lệ, chọn tốt nhất (không dừng sớm)
|
|
159
181
|
let bestPoly: Point[] | undefined = undefined;
|
|
160
182
|
let bestArea = 0;
|
|
161
183
|
let bestAngleRange = 999;
|
|
184
|
+
const debugCandidates: string[] = [];
|
|
162
185
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const metric = contourMetrics[i];
|
|
186
|
+
for (let i = 0; i < top5.length; i++) {
|
|
187
|
+
const metric = top5[i];
|
|
166
188
|
const contour = metric.contour;
|
|
167
189
|
|
|
168
|
-
|
|
169
|
-
|
|
190
|
+
// Python gốc dùng epsilon = 80 pixel tuyệt đối (không phải % perimeter!)
|
|
191
|
+
// approxPolyDP(c, 80, True) — epsilon cố định
|
|
192
|
+
// Ta thử range 10→100 pixel, bước 10, để tìm ra 4 đỉnh
|
|
193
|
+
let foundPts: Point[] | null = null;
|
|
194
|
+
let foundVerts = 0;
|
|
195
|
+
let foundAngle = 999;
|
|
170
196
|
|
|
171
|
-
|
|
172
|
-
for (let ep = 0.02; ep <= 0.1; ep += 0.01) {
|
|
197
|
+
for (let epPx = 10; epPx <= 120; epPx += 5) {
|
|
173
198
|
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
174
|
-
OpenCV.invoke('approxPolyDP', contour, approx,
|
|
199
|
+
OpenCV.invoke('approxPolyDP', contour, approx, epPx, true);
|
|
175
200
|
const approxJS = OpenCV.toJSValue(approx);
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
break; // Đã tìm quad tốt cho contour này, sang contour tiếp
|
|
201
|
+
const verts = approxJS?.array?.length ?? 0;
|
|
202
|
+
|
|
203
|
+
if (verts === 4) {
|
|
204
|
+
const pts = approxJS.array as Point[];
|
|
205
|
+
const sorted = this.sortCorners([...pts]);
|
|
206
|
+
const angleRange = this.getAngleRange(sorted);
|
|
207
|
+
|
|
208
|
+
if (angleRange < foundAngle) {
|
|
209
|
+
foundAngle = angleRange;
|
|
210
|
+
foundPts = pts;
|
|
211
|
+
foundVerts = verts;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// is_valid_contour: area > MIN_QUAD_AREA_RATIO và angle_range < 40
|
|
215
|
+
if (metric.area > imgArea * MIN_QUAD_AREA_RATIO && angleRange < 40) {
|
|
216
|
+
if (metric.area > bestArea || (metric.area === bestArea && angleRange < bestAngleRange)) {
|
|
217
|
+
bestPoly = pts;
|
|
218
|
+
bestArea = metric.area;
|
|
219
|
+
bestAngleRange = angleRange;
|
|
196
220
|
}
|
|
197
|
-
|
|
221
|
+
break;
|
|
222
|
+
}
|
|
198
223
|
}
|
|
199
224
|
}
|
|
225
|
+
|
|
226
|
+
debugCandidates.push(
|
|
227
|
+
` [C${i}] area=${metric.area.toFixed(0)}px²(${(metric.area/imgArea*100).toFixed(1)}%) ` +
|
|
228
|
+
`foundVerts=${foundVerts} bestAngle=${foundAngle === 999 ? 'N/A' : foundAngle.toFixed(1)}° ` +
|
|
229
|
+
`valid=${metric.area > imgArea * MIN_QUAD_AREA_RATIO && foundAngle < 40}`
|
|
230
|
+
);
|
|
200
231
|
}
|
|
201
232
|
|
|
202
|
-
|
|
233
|
+
console.log(`[DocDetect] Candidates:\n${debugCandidates.join('\n')}`);
|
|
234
|
+
console.log(`[DocDetect] Result: bestPoly=${bestPoly ? 'found' : 'NOT FOUND'} area=${bestArea.toFixed(0)} angleRange=${bestAngleRange === 999 ? 'N/A' : bestAngleRange.toFixed(1)}°`);
|
|
235
|
+
|
|
236
|
+
// BƯỚC 7: Trả về góc, scale về kích thước ảnh gốc
|
|
203
237
|
if (bestPoly && bestPoly.length === 4) {
|
|
204
238
|
const actualRatio = (resized === src) ? 1.0 : ratio;
|
|
205
239
|
const originalCorners = bestPoly.map(p => ({
|
|
@@ -208,10 +242,45 @@ export class DocumentScanner {
|
|
|
208
242
|
}));
|
|
209
243
|
return this.sortCorners(originalCorners);
|
|
210
244
|
}
|
|
211
|
-
|
|
245
|
+
|
|
246
|
+
// Giống Python gốc: nếu không tìm được → dùng toàn bộ ảnh làm fallback
|
|
247
|
+
// "If we did not find any valid contours, just use the whole image"
|
|
248
|
+
console.log(`[DocDetect] No valid quad found (minArea=${(MIN_QUAD_AREA_RATIO*100).toFixed(0)}%, maxContour=${maxContourArea.toFixed(0)}px²). Fallback to full image.`);
|
|
249
|
+
throw new Error(
|
|
250
|
+
`Không phát hiện được viền tài liệu.\n` +
|
|
251
|
+
`Ảnh: ${origCols}x${origRows}px | Resize: ${targetWidth}x${targetHeight}px\n` +
|
|
252
|
+
`Contours kiểm tra: ${top5.length}/${contoursSize} | MinArea: ${(MIN_QUAD_AREA_RATIO*100).toFixed(0)}%\n` +
|
|
253
|
+
`Contour lớn nhất: ${maxContourArea.toFixed(0)}px² (${(maxContourArea/imgArea*100).toFixed(1)}% ảnh)\n` +
|
|
254
|
+
`Chi tiết:\n${debugCandidates.join('\n')}\n` +
|
|
255
|
+
`Gợi ý: Tài liệu cần chiếm ít nhất ${(MIN_QUAD_AREA_RATIO*100).toFixed(0)}% diện tích khung hình, viền phải tương phản với nền.`
|
|
256
|
+
);
|
|
212
257
|
} catch (e: any) {
|
|
213
|
-
|
|
214
|
-
|
|
258
|
+
// Trích message từ mọi dạng exception (Error object, string, native, undefined...)
|
|
259
|
+
let rawMsg: string;
|
|
260
|
+
if (typeof e === 'string') {
|
|
261
|
+
rawMsg = e;
|
|
262
|
+
} else if (e instanceof Error) {
|
|
263
|
+
rawMsg = e.message || e.toString();
|
|
264
|
+
} else if (e && typeof e === 'object') {
|
|
265
|
+
rawMsg = e.message || e.code || e.name || JSON.stringify(e);
|
|
266
|
+
} else {
|
|
267
|
+
rawMsg = String(e ?? 'Unknown error');
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
console.error('[OpenCV] detectPageCorners exception:', rawMsg, e);
|
|
271
|
+
|
|
272
|
+
// Nếu đây là lỗi intentional từ chính hàm này (đã có thông tin chi tiết),
|
|
273
|
+
// chỉ wrap nhẹ để không mất ngữ cảnh.
|
|
274
|
+
// Nếu là lỗi native bất ngờ từ OpenCV, thêm context step.
|
|
275
|
+
const isIntentional = rawMsg.startsWith('Không ') || rawMsg.startsWith('[OpenCV Corner');
|
|
276
|
+
if (isIntentional) {
|
|
277
|
+
throw new Error(rawMsg);
|
|
278
|
+
} else {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`Lỗi xử lý OpenCV không mong đợi: ${rawMsg}\n` +
|
|
281
|
+
`(Có thể do ảnh corrupt, định dạng không hỗ trợ, hoặc thiết bị thiếu bộ nhớ)`
|
|
282
|
+
);
|
|
283
|
+
}
|
|
215
284
|
} finally {
|
|
216
285
|
OpenCV.clearBuffers();
|
|
217
286
|
}
|