rn-opencv-doc-perspective-correction 1.0.18 → 1.0.20
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 +5 -0
- package/dist/index.js +199 -126
- package/package.json +1 -1
- package/src/index.ts +234 -145
package/dist/index.d.ts
CHANGED
|
@@ -22,6 +22,11 @@ export declare class DocumentScanner {
|
|
|
22
22
|
private static getAngleRange;
|
|
23
23
|
/** Fallback: 4 góc của toàn bộ ảnh (như Python gốc khi không tìm được contour) */
|
|
24
24
|
private static buildFullImageCorners;
|
|
25
|
+
/**
|
|
26
|
+
* Tìm tứ giác tốt nhất từ một MatVector contours.
|
|
27
|
+
* Trả về { poly, area, angleRange } hoặc null nếu không tìm được.
|
|
28
|
+
*/
|
|
29
|
+
private static findBestQuad;
|
|
25
30
|
static detectPageCorners(imageBase64: string): Point[] | undefined;
|
|
26
31
|
static applyPerspectiveCorrection(imageBase64: string, corners: Point[]): string | undefined;
|
|
27
32
|
/**
|
package/dist/index.js
CHANGED
|
@@ -55,163 +55,236 @@ class DocumentScanner {
|
|
|
55
55
|
{ x: 0, y: h }, // bottom-left
|
|
56
56
|
];
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Tìm tứ giác tốt nhất từ một MatVector contours.
|
|
60
|
+
* Trả về { poly, area, angleRange } hoặc null nếu không tìm được.
|
|
61
|
+
*/
|
|
62
|
+
static findBestQuad(contoursObj, contoursSize, imgArea, minAreaRatio, pipelineName) {
|
|
63
|
+
var _a, _b, _c;
|
|
64
|
+
const allMetrics = [];
|
|
65
|
+
let maxRawArea = 0;
|
|
66
|
+
for (let i = 0; i < contoursSize; i++) {
|
|
67
|
+
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
68
|
+
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
69
|
+
const area = areaObj ? areaObj.value : 0;
|
|
70
|
+
if (area > maxRawArea)
|
|
71
|
+
maxRawArea = area;
|
|
72
|
+
allMetrics.push({ area, contour });
|
|
73
|
+
}
|
|
74
|
+
allMetrics.sort((a, b) => b.area - a.area);
|
|
75
|
+
const top = allMetrics.slice(0, 5);
|
|
76
|
+
let bestPoly;
|
|
77
|
+
let bestArea = 0;
|
|
78
|
+
let bestAngle = 999;
|
|
79
|
+
const debug = [];
|
|
80
|
+
for (let i = 0; i < top.length; i++) {
|
|
81
|
+
const m = top[i];
|
|
82
|
+
// Thử nhiều epsilon (10→120px) để approxPolyDP ra đúng 4 đỉnh
|
|
83
|
+
let fPts = null;
|
|
84
|
+
let fVerts = 0;
|
|
85
|
+
let fAngle = 999;
|
|
86
|
+
let fApproxArea = 0;
|
|
87
|
+
for (let ep = 10; ep <= 120; ep += 5) {
|
|
88
|
+
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
89
|
+
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', m.contour, approx, ep, true);
|
|
90
|
+
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
91
|
+
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;
|
|
92
|
+
if (verts === 4) {
|
|
93
|
+
const pts = approxJS.array;
|
|
94
|
+
const sorted = this.sortCorners([...pts]);
|
|
95
|
+
const angleRange = this.getAngleRange(sorted);
|
|
96
|
+
// Lấy diện tích polygon 4-điểm (đúng như Python: contourArea(approx))
|
|
97
|
+
const aObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', approx);
|
|
98
|
+
const approxArea = aObj ? ((_c = aObj.value) !== null && _c !== void 0 ? _c : 0) : 0;
|
|
99
|
+
if (angleRange < fAngle) {
|
|
100
|
+
fAngle = angleRange;
|
|
101
|
+
fPts = pts;
|
|
102
|
+
fVerts = verts;
|
|
103
|
+
fApproxArea = approxArea;
|
|
104
|
+
}
|
|
105
|
+
if (approxArea > imgArea * minAreaRatio && angleRange < 40) {
|
|
106
|
+
if (approxArea > bestArea || (approxArea === bestArea && angleRange < bestAngle)) {
|
|
107
|
+
bestPoly = pts;
|
|
108
|
+
bestArea = approxArea;
|
|
109
|
+
bestAngle = angleRange;
|
|
110
|
+
}
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
debug.push(` [${pipelineName}:C${i}] raw=${m.area.toFixed(0)}(${(m.area / imgArea * 100).toFixed(1)}%) ` +
|
|
116
|
+
`approx=${fApproxArea > 0 ? fApproxArea.toFixed(0) + '(' + ((fApproxArea / imgArea * 100).toFixed(1)) + '%)' : '-'} ` +
|
|
117
|
+
`v=${fVerts} a=${fAngle === 999 ? '-' : fAngle.toFixed(1)}° ` +
|
|
118
|
+
`ok=${fApproxArea > imgArea * minAreaRatio && fAngle < 40}`);
|
|
119
|
+
}
|
|
120
|
+
if (bestPoly && bestPoly.length === 4) {
|
|
121
|
+
return { poly: bestPoly, area: bestArea, angleRange: bestAngle, debug };
|
|
122
|
+
}
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
58
125
|
static detectPageCorners(imageBase64) {
|
|
59
|
-
var _a, _b;
|
|
60
|
-
let src = null;
|
|
61
|
-
let resized = null;
|
|
62
|
-
let gray = null;
|
|
63
|
-
let blurred = null;
|
|
64
|
-
let closed = null;
|
|
65
|
-
let edges = null;
|
|
66
|
-
let dilated = null;
|
|
67
|
-
let contoursObj = null;
|
|
68
|
-
let hierarchyObj = null;
|
|
126
|
+
var _a, _b, _c, _d, _e, _f;
|
|
69
127
|
try {
|
|
70
|
-
src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
128
|
+
const src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
71
129
|
const jsSrc = react_native_fast_opencv_1.OpenCV.toJSValue(src);
|
|
72
130
|
const origCols = jsSrc.cols || 1000;
|
|
73
131
|
const origRows = jsSrc.rows || 1000;
|
|
74
132
|
const targetHeight = 500.0;
|
|
75
133
|
const ratio = origRows / targetHeight;
|
|
76
134
|
const targetWidth = Math.max(1, Math.round(origCols / ratio));
|
|
77
|
-
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);
|
|
135
|
+
const 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);
|
|
78
136
|
const dstSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, targetWidth, targetHeight);
|
|
79
137
|
try {
|
|
80
138
|
react_native_fast_opencv_1.OpenCV.invoke('resize', src, resized, dstSize, 0, 0, 1 /* INTER_LINEAR */);
|
|
81
139
|
}
|
|
82
|
-
catch (
|
|
83
|
-
|
|
140
|
+
catch (_) {
|
|
141
|
+
// nếu resize fail, dùng src gốc — ratio=1
|
|
84
142
|
}
|
|
85
|
-
//
|
|
86
|
-
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);
|
|
143
|
+
// Grayscale + Blur
|
|
144
|
+
const 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);
|
|
87
145
|
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', resized, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
|
|
88
|
-
|
|
89
|
-
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);
|
|
146
|
+
const 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);
|
|
90
147
|
const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 7, 7);
|
|
91
148
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
149
|
+
const imgArea = targetWidth * targetHeight;
|
|
150
|
+
const MIN_AREA = 0.15; // 15% — khoan dung hơn Python gốc (25%)
|
|
151
|
+
const allDebug = [];
|
|
152
|
+
let winner = null;
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════
|
|
154
|
+
// PIPELINE 1: Adaptive Threshold → contour KÍN (method chính)
|
|
155
|
+
//
|
|
156
|
+
// Adaptive threshold tạo ảnh binary phân biệt foreground/background.
|
|
157
|
+
// findContours trên binary image cho contour KÍN → area = diện tích thật.
|
|
158
|
+
// Đây thay thế LSD line detection (không có trong react-native-fast-opencv).
|
|
159
|
+
// ═══════════════════════════════════════════════════════════════
|
|
99
160
|
try {
|
|
100
|
-
react_native_fast_opencv_1.OpenCV.
|
|
161
|
+
const adaptive = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
162
|
+
// ADAPTIVE_THRESH_GAUSSIAN_C = 1, THRESH_BINARY_INV = 1
|
|
163
|
+
// blockSize=15 (must be odd), C=10
|
|
164
|
+
react_native_fast_opencv_1.OpenCV.invoke('adaptiveThreshold', blurred, adaptive, 255, 1 /* GAUSSIAN_C */, 1 /* BINARY_INV */, 15, 10);
|
|
165
|
+
// MORPH_CLOSE để nối vùng tài liệu lại — kernel lớn 15x15
|
|
166
|
+
const closeKSz = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 15, 15);
|
|
167
|
+
const closeK = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* RECT */, closeKSz);
|
|
168
|
+
const adaptClosed = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
169
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', adaptive, adaptClosed, 3 /* MORPH_CLOSE */, closeK);
|
|
170
|
+
const cnt1 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
171
|
+
const hier1 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
172
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', adaptClosed, cnt1, hier1, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
173
|
+
const js1 = react_native_fast_opencv_1.OpenCV.toJSValue(cnt1);
|
|
174
|
+
const sz1 = (_b = (_a = js1 === null || js1 === void 0 ? void 0 : js1.array) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
|
|
175
|
+
console.log(`[DocDetect] P1(AdaptThresh): ${sz1} contours`);
|
|
176
|
+
if (sz1 > 0) {
|
|
177
|
+
const r1 = this.findBestQuad(cnt1, sz1, imgArea, MIN_AREA, 'P1');
|
|
178
|
+
if (r1) {
|
|
179
|
+
allDebug.push(...r1.debug);
|
|
180
|
+
if (!winner || r1.area > winner.area) {
|
|
181
|
+
winner = { poly: r1.poly, area: r1.area, angleRange: r1.angleRange };
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
else if (r1 === null) {
|
|
185
|
+
// Ghi debug dù không tìm được
|
|
186
|
+
}
|
|
187
|
+
}
|
|
101
188
|
}
|
|
102
|
-
catch (
|
|
103
|
-
|
|
104
|
-
morphCloseUsed = false;
|
|
189
|
+
catch (p1Err) {
|
|
190
|
+
console.log('[DocDetect] P1 failed:', p1Err);
|
|
105
191
|
}
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
//
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
const dilKernelSize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 3, 3);
|
|
113
|
-
const dilKernel = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
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;
|
|
192
|
+
// ═══════════════════════════════════════════════════════════════
|
|
193
|
+
// PIPELINE 2: Canny (giống Python gốc andrewdcampbell)
|
|
194
|
+
//
|
|
195
|
+
// MORPH_CLOSE trên grayscale → Canny → dilate mạnh → contours.
|
|
196
|
+
// Dilate 7x7 x2 iterations để NỐI edge fragments thành contour kín.
|
|
197
|
+
// ═══════════════════════════════════════════════════════════════
|
|
116
198
|
try {
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
199
|
+
// MORPH_CLOSE 9x9 trên grayscale (đúng Python)
|
|
200
|
+
const morphSz = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 9, 9);
|
|
201
|
+
const morphK = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* RECT */, morphSz);
|
|
202
|
+
const morphed = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
203
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', blurred, morphed, 3 /* MORPH_CLOSE */, morphK);
|
|
204
|
+
// Canny (0, 84)
|
|
205
|
+
const cannyOut = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
206
|
+
react_native_fast_opencv_1.OpenCV.invoke('Canny', morphed, cannyOut, 0, 84);
|
|
207
|
+
// Dilate MẠNH: kernel 7x7 — nối edge fragments lại thành contour kín
|
|
208
|
+
const dilSz = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 7, 7);
|
|
209
|
+
const dilK = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* RECT */, dilSz);
|
|
210
|
+
const dilOut = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
211
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', cannyOut, dilOut, 1 /* MORPH_DILATE */, dilK);
|
|
212
|
+
// MORPH_CLOSE lần nữa trên edge image để đóng gap
|
|
213
|
+
const closeSz2 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 11, 11);
|
|
214
|
+
const closeK2 = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* RECT */, closeSz2);
|
|
215
|
+
const closedEdge = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
216
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', dilOut, closedEdge, 3 /* MORPH_CLOSE */, closeK2);
|
|
217
|
+
const cnt2 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
218
|
+
const hier2 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
219
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', closedEdge, cnt2, hier2, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
220
|
+
const js2 = react_native_fast_opencv_1.OpenCV.toJSValue(cnt2);
|
|
221
|
+
const sz2 = (_d = (_c = js2 === null || js2 === void 0 ? void 0 : js2.array) === null || _c === void 0 ? void 0 : _c.length) !== null && _d !== void 0 ? _d : 0;
|
|
222
|
+
console.log(`[DocDetect] P2(CannyStrong): ${sz2} contours`);
|
|
223
|
+
if (sz2 > 0) {
|
|
224
|
+
const r2 = this.findBestQuad(cnt2, sz2, imgArea, MIN_AREA, 'P2');
|
|
225
|
+
if (r2) {
|
|
226
|
+
allDebug.push(...r2.debug);
|
|
227
|
+
if (!winner || r2.area > winner.area) {
|
|
228
|
+
winner = { poly: r2.poly, area: r2.area, angleRange: r2.angleRange };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
136
232
|
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
let maxContourArea = 0;
|
|
140
|
-
for (let i = 0; i < contoursSize; i++) {
|
|
141
|
-
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
142
|
-
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
143
|
-
const area = areaObj ? areaObj.value : 0;
|
|
144
|
-
if (area > maxContourArea)
|
|
145
|
-
maxContourArea = area;
|
|
146
|
-
allContourMetrics.push({ index: i, area, contour });
|
|
233
|
+
catch (p2Err) {
|
|
234
|
+
console.log('[DocDetect] P2 failed:', p2Err);
|
|
147
235
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
//
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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;
|
|
186
|
-
}
|
|
187
|
-
break;
|
|
236
|
+
// ═══════════════════════════════════════════════════════════════
|
|
237
|
+
// PIPELINE 3: OTSU threshold (binary toàn cục)
|
|
238
|
+
//
|
|
239
|
+
// Cho ảnh có tương phản rõ giữa tài liệu và nền.
|
|
240
|
+
// ═══════════════════════════════════════════════════════════════
|
|
241
|
+
try {
|
|
242
|
+
const otsu = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
243
|
+
// threshold type = THRESH_BINARY_INV + THRESH_OTSU = 1 + 8 = 9
|
|
244
|
+
react_native_fast_opencv_1.OpenCV.invoke('threshold', blurred, otsu, 0, 255, 9);
|
|
245
|
+
// MORPH_CLOSE lớn để nối vùng
|
|
246
|
+
const oCloseSz = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 15, 15);
|
|
247
|
+
const oCloseK = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', 0 /* RECT */, oCloseSz);
|
|
248
|
+
const otsuClosed = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
249
|
+
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', otsu, otsuClosed, 3 /* MORPH_CLOSE */, oCloseK);
|
|
250
|
+
const cnt3 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
251
|
+
const hier3 = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
252
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', otsuClosed, cnt3, hier3, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
253
|
+
const js3 = react_native_fast_opencv_1.OpenCV.toJSValue(cnt3);
|
|
254
|
+
const sz3 = (_f = (_e = js3 === null || js3 === void 0 ? void 0 : js3.array) === null || _e === void 0 ? void 0 : _e.length) !== null && _f !== void 0 ? _f : 0;
|
|
255
|
+
console.log(`[DocDetect] P3(OTSU): ${sz3} contours`);
|
|
256
|
+
if (sz3 > 0) {
|
|
257
|
+
const r3 = this.findBestQuad(cnt3, sz3, imgArea, MIN_AREA, 'P3');
|
|
258
|
+
if (r3) {
|
|
259
|
+
allDebug.push(...r3.debug);
|
|
260
|
+
if (!winner || r3.area > winner.area) {
|
|
261
|
+
winner = { poly: r3.poly, area: r3.area, angleRange: r3.angleRange };
|
|
188
262
|
}
|
|
189
263
|
}
|
|
190
264
|
}
|
|
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}`);
|
|
194
265
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
266
|
+
catch (p3Err) {
|
|
267
|
+
console.log('[DocDetect] P3 failed:', p3Err);
|
|
268
|
+
}
|
|
269
|
+
// ═══════════════════════════════════════════════════════════════
|
|
270
|
+
// Trả kết quả
|
|
271
|
+
// ═══════════════════════════════════════════════════════════════
|
|
272
|
+
console.log(`[DocDetect] All debug:\n${allDebug.join('\n')}`);
|
|
273
|
+
if (winner) {
|
|
274
|
+
console.log(`[DocDetect] ✓ Winner: area=${winner.area.toFixed(0)}(${(winner.area / imgArea * 100).toFixed(1)}%) angle=${winner.angleRange.toFixed(1)}°`);
|
|
275
|
+
const originalCorners = winner.poly.map(p => ({
|
|
276
|
+
x: Math.round(p.x * ratio),
|
|
277
|
+
y: Math.round(p.y * ratio)
|
|
203
278
|
}));
|
|
204
279
|
return this.sortCorners(originalCorners);
|
|
205
280
|
}
|
|
206
|
-
//
|
|
207
|
-
|
|
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.`);
|
|
281
|
+
// Không pipeline nào thành công
|
|
282
|
+
console.log('[DocDetect] ✗ All pipelines failed');
|
|
209
283
|
throw new Error(`Không phát hiện được viền tài liệu.\n` +
|
|
210
284
|
`Ảnh: ${origCols}x${origRows}px | Resize: ${targetWidth}x${targetHeight}px\n` +
|
|
211
|
-
`
|
|
212
|
-
`
|
|
213
|
-
`
|
|
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.`);
|
|
285
|
+
`MinArea: ${(MIN_AREA * 100).toFixed(0)}%\n` +
|
|
286
|
+
`Chi tiết:\n${allDebug.join('\n')}\n` +
|
|
287
|
+
`Gợi ý: Hãy đảm bảo tài liệu chiếm ≥15% khung hình, viền rõ ràng, nền tương phản.`);
|
|
215
288
|
}
|
|
216
289
|
catch (e) {
|
|
217
290
|
// Trích message từ mọi dạng exception (Error object, string, native, undefined...)
|
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.20",
|
|
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
|
@@ -69,20 +69,94 @@ export class DocumentScanner {
|
|
|
69
69
|
];
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
72
|
+
/**
|
|
73
|
+
* Tìm tứ giác tốt nhất từ một MatVector contours.
|
|
74
|
+
* Trả về { poly, area, angleRange } hoặc null nếu không tìm được.
|
|
75
|
+
*/
|
|
76
|
+
private static findBestQuad(
|
|
77
|
+
contoursObj: any,
|
|
78
|
+
contoursSize: number,
|
|
79
|
+
imgArea: number,
|
|
80
|
+
minAreaRatio: number,
|
|
81
|
+
pipelineName: string,
|
|
82
|
+
): { poly: Point[]; area: number; angleRange: number; debug: string[] } | null {
|
|
83
|
+
const allMetrics: { area: number; contour: any }[] = [];
|
|
84
|
+
let maxRawArea = 0;
|
|
85
|
+
|
|
86
|
+
for (let i = 0; i < contoursSize; i++) {
|
|
87
|
+
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
88
|
+
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
89
|
+
const area = areaObj ? areaObj.value : 0;
|
|
90
|
+
if (area > maxRawArea) maxRawArea = area;
|
|
91
|
+
allMetrics.push({ area, contour });
|
|
92
|
+
}
|
|
93
|
+
allMetrics.sort((a, b) => b.area - a.area);
|
|
94
|
+
const top = allMetrics.slice(0, 5);
|
|
95
|
+
|
|
96
|
+
let bestPoly: Point[] | undefined;
|
|
97
|
+
let bestArea = 0;
|
|
98
|
+
let bestAngle = 999;
|
|
99
|
+
const debug: string[] = [];
|
|
100
|
+
|
|
101
|
+
for (let i = 0; i < top.length; i++) {
|
|
102
|
+
const m = top[i];
|
|
103
|
+
|
|
104
|
+
// Thử nhiều epsilon (10→120px) để approxPolyDP ra đúng 4 đỉnh
|
|
105
|
+
let fPts: Point[] | null = null;
|
|
106
|
+
let fVerts = 0;
|
|
107
|
+
let fAngle = 999;
|
|
108
|
+
let fApproxArea = 0;
|
|
109
|
+
|
|
110
|
+
for (let ep = 10; ep <= 120; ep += 5) {
|
|
111
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
112
|
+
OpenCV.invoke('approxPolyDP', m.contour, approx, ep, true);
|
|
113
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
114
|
+
const verts = approxJS?.array?.length ?? 0;
|
|
115
|
+
|
|
116
|
+
if (verts === 4) {
|
|
117
|
+
const pts = approxJS.array as Point[];
|
|
118
|
+
const sorted = this.sortCorners([...pts]);
|
|
119
|
+
const angleRange = this.getAngleRange(sorted);
|
|
120
|
+
|
|
121
|
+
// Lấy diện tích polygon 4-điểm (đúng như Python: contourArea(approx))
|
|
122
|
+
const aObj = OpenCV.invoke('contourArea', approx);
|
|
123
|
+
const approxArea = aObj ? (aObj.value ?? 0) : 0;
|
|
124
|
+
|
|
125
|
+
if (angleRange < fAngle) {
|
|
126
|
+
fAngle = angleRange;
|
|
127
|
+
fPts = pts;
|
|
128
|
+
fVerts = verts;
|
|
129
|
+
fApproxArea = approxArea;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (approxArea > imgArea * minAreaRatio && angleRange < 40) {
|
|
133
|
+
if (approxArea > bestArea || (approxArea === bestArea && angleRange < bestAngle)) {
|
|
134
|
+
bestPoly = pts;
|
|
135
|
+
bestArea = approxArea;
|
|
136
|
+
bestAngle = angleRange;
|
|
137
|
+
}
|
|
138
|
+
break;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
debug.push(
|
|
144
|
+
` [${pipelineName}:C${i}] raw=${m.area.toFixed(0)}(${(m.area/imgArea*100).toFixed(1)}%) ` +
|
|
145
|
+
`approx=${fApproxArea > 0 ? fApproxArea.toFixed(0)+'('+((fApproxArea/imgArea*100).toFixed(1))+'%)' : '-'} ` +
|
|
146
|
+
`v=${fVerts} a=${fAngle === 999 ? '-' : fAngle.toFixed(1)}° ` +
|
|
147
|
+
`ok=${fApproxArea > imgArea * minAreaRatio && fAngle < 40}`
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (bestPoly && bestPoly.length === 4) {
|
|
152
|
+
return { poly: bestPoly, area: bestArea, angleRange: bestAngle, debug };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
82
156
|
|
|
157
|
+
public static detectPageCorners(imageBase64: string): Point[] | undefined {
|
|
83
158
|
try {
|
|
84
|
-
src = OpenCV.base64ToMat(imageBase64);
|
|
85
|
-
|
|
159
|
+
const src = OpenCV.base64ToMat(imageBase64);
|
|
86
160
|
const jsSrc = OpenCV.toJSValue(src);
|
|
87
161
|
const origCols = jsSrc.cols || 1000;
|
|
88
162
|
const origRows = jsSrc.rows || 1000;
|
|
@@ -91,168 +165,183 @@ export class DocumentScanner {
|
|
|
91
165
|
const ratio = origRows / targetHeight;
|
|
92
166
|
const targetWidth = Math.max(1, Math.round(origCols / ratio));
|
|
93
167
|
|
|
94
|
-
resized = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
168
|
+
const resized = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
95
169
|
const dstSize = OpenCV.createObject(ObjectType.Size, targetWidth, targetHeight);
|
|
96
|
-
|
|
97
170
|
try {
|
|
98
171
|
OpenCV.invoke('resize', src, resized, dstSize, 0, 0, 1 /* INTER_LINEAR */);
|
|
99
|
-
} catch (
|
|
100
|
-
|
|
172
|
+
} catch (_) {
|
|
173
|
+
// nếu resize fail, dùng src gốc — ratio=1
|
|
101
174
|
}
|
|
102
175
|
|
|
103
|
-
//
|
|
104
|
-
gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
176
|
+
// Grayscale + Blur
|
|
177
|
+
const gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
105
178
|
OpenCV.invoke('cvtColor', resized, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
106
179
|
|
|
107
|
-
|
|
108
|
-
blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
180
|
+
const blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
109
181
|
const ksize = OpenCV.createObject(ObjectType.Size, 7, 7);
|
|
110
182
|
OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
111
183
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
184
|
+
const imgArea = targetWidth * targetHeight;
|
|
185
|
+
const MIN_AREA = 0.15; // 15% — khoan dung hơn Python gốc (25%)
|
|
186
|
+
|
|
187
|
+
const allDebug: string[] = [];
|
|
188
|
+
let winner: { poly: Point[]; area: number; angleRange: number } | null = null;
|
|
189
|
+
|
|
190
|
+
// ═══════════════════════════════════════════════════════════════
|
|
191
|
+
// PIPELINE 1: Adaptive Threshold → contour KÍN (method chính)
|
|
192
|
+
//
|
|
193
|
+
// Adaptive threshold tạo ảnh binary phân biệt foreground/background.
|
|
194
|
+
// findContours trên binary image cho contour KÍN → area = diện tích thật.
|
|
195
|
+
// Đây thay thế LSD line detection (không có trong react-native-fast-opencv).
|
|
196
|
+
// ═══════════════════════════════════════════════════════════════
|
|
119
197
|
try {
|
|
120
|
-
OpenCV.
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
198
|
+
const adaptive = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
199
|
+
// ADAPTIVE_THRESH_GAUSSIAN_C = 1, THRESH_BINARY_INV = 1
|
|
200
|
+
// blockSize=15 (must be odd), C=10
|
|
201
|
+
OpenCV.invoke('adaptiveThreshold', blurred, adaptive,
|
|
202
|
+
255, 1 /* GAUSSIAN_C */, 1 /* BINARY_INV */, 15, 10);
|
|
203
|
+
|
|
204
|
+
// MORPH_CLOSE để nối vùng tài liệu lại — kernel lớn 15x15
|
|
205
|
+
const closeKSz = OpenCV.createObject(ObjectType.Size, 15, 15);
|
|
206
|
+
const closeK = OpenCV.invoke('getStructuringElement', 0 /* RECT */, closeKSz);
|
|
207
|
+
const adaptClosed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
208
|
+
OpenCV.invoke('morphologyEx', adaptive, adaptClosed, 3 /* MORPH_CLOSE */, closeK);
|
|
209
|
+
|
|
210
|
+
const cnt1 = OpenCV.createObject(ObjectType.MatVector);
|
|
211
|
+
const hier1 = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
212
|
+
OpenCV.invoke('findContoursWithHierarchy', adaptClosed, cnt1, hier1,
|
|
213
|
+
0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
214
|
+
|
|
215
|
+
const js1 = OpenCV.toJSValue(cnt1);
|
|
216
|
+
const sz1 = js1?.array?.length ?? 0;
|
|
217
|
+
console.log(`[DocDetect] P1(AdaptThresh): ${sz1} contours`);
|
|
218
|
+
|
|
219
|
+
if (sz1 > 0) {
|
|
220
|
+
const r1 = this.findBestQuad(cnt1, sz1, imgArea, MIN_AREA, 'P1');
|
|
221
|
+
if (r1) {
|
|
222
|
+
allDebug.push(...r1.debug);
|
|
223
|
+
if (!winner || r1.area > winner.area) {
|
|
224
|
+
winner = { poly: r1.poly, area: r1.area, angleRange: r1.angleRange };
|
|
225
|
+
}
|
|
226
|
+
} else if (r1 === null) {
|
|
227
|
+
// Ghi debug dù không tìm được
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
} catch (p1Err) {
|
|
231
|
+
console.log('[DocDetect] P1 failed:', p1Err);
|
|
124
232
|
}
|
|
125
233
|
|
|
126
|
-
//
|
|
127
|
-
//
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
// BƯỚC 5: Dilate 3x3 – nối các pixel viền lân cận bị đứt
|
|
133
|
-
const dilKernelSize = OpenCV.createObject(ObjectType.Size, 3, 3);
|
|
134
|
-
const dilKernel = OpenCV.invoke('getStructuringElement', 0 /* MORPH_RECT */, dilKernelSize);
|
|
135
|
-
dilated = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
136
|
-
let dilateUsed = true;
|
|
234
|
+
// ═══════════════════════════════════════════════════════════════
|
|
235
|
+
// PIPELINE 2: Canny (giống Python gốc andrewdcampbell)
|
|
236
|
+
//
|
|
237
|
+
// MORPH_CLOSE trên grayscale → Canny → dilate mạnh → contours.
|
|
238
|
+
// Dilate 7x7 x2 iterations để NỐI edge fragments thành contour kín.
|
|
239
|
+
// ═══════════════════════════════════════════════════════════════
|
|
137
240
|
try {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
console.log(`[DocDetect] top5 areas: ${top5.map(m => m.area.toFixed(0)).join(', ')} | imgArea=${imgArea.toFixed(0)}`);
|
|
177
|
-
|
|
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;
|
|
180
|
-
|
|
181
|
-
let bestPoly: Point[] | undefined = undefined;
|
|
182
|
-
let bestArea = 0;
|
|
183
|
-
let bestAngleRange = 999;
|
|
184
|
-
const debugCandidates: string[] = [];
|
|
185
|
-
|
|
186
|
-
for (let i = 0; i < top5.length; i++) {
|
|
187
|
-
const metric = top5[i];
|
|
188
|
-
const contour = metric.contour;
|
|
189
|
-
|
|
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;
|
|
196
|
-
|
|
197
|
-
for (let epPx = 10; epPx <= 120; epPx += 5) {
|
|
198
|
-
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
199
|
-
OpenCV.invoke('approxPolyDP', contour, approx, epPx, true);
|
|
200
|
-
const approxJS = OpenCV.toJSValue(approx);
|
|
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;
|
|
241
|
+
// MORPH_CLOSE 9x9 trên grayscale (đúng Python)
|
|
242
|
+
const morphSz = OpenCV.createObject(ObjectType.Size, 9, 9);
|
|
243
|
+
const morphK = OpenCV.invoke('getStructuringElement', 0 /* RECT */, morphSz);
|
|
244
|
+
const morphed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
245
|
+
OpenCV.invoke('morphologyEx', blurred, morphed, 3 /* MORPH_CLOSE */, morphK);
|
|
246
|
+
|
|
247
|
+
// Canny (0, 84)
|
|
248
|
+
const cannyOut = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
249
|
+
OpenCV.invoke('Canny', morphed, cannyOut, 0, 84);
|
|
250
|
+
|
|
251
|
+
// Dilate MẠNH: kernel 7x7 — nối edge fragments lại thành contour kín
|
|
252
|
+
const dilSz = OpenCV.createObject(ObjectType.Size, 7, 7);
|
|
253
|
+
const dilK = OpenCV.invoke('getStructuringElement', 0 /* RECT */, dilSz);
|
|
254
|
+
const dilOut = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
255
|
+
OpenCV.invoke('morphologyEx', cannyOut, dilOut, 1 /* MORPH_DILATE */, dilK);
|
|
256
|
+
|
|
257
|
+
// MORPH_CLOSE lần nữa trên edge image để đóng gap
|
|
258
|
+
const closeSz2 = OpenCV.createObject(ObjectType.Size, 11, 11);
|
|
259
|
+
const closeK2 = OpenCV.invoke('getStructuringElement', 0 /* RECT */, closeSz2);
|
|
260
|
+
const closedEdge = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
261
|
+
OpenCV.invoke('morphologyEx', dilOut, closedEdge, 3 /* MORPH_CLOSE */, closeK2);
|
|
262
|
+
|
|
263
|
+
const cnt2 = OpenCV.createObject(ObjectType.MatVector);
|
|
264
|
+
const hier2 = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
265
|
+
OpenCV.invoke('findContoursWithHierarchy', closedEdge, cnt2, hier2,
|
|
266
|
+
0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
267
|
+
|
|
268
|
+
const js2 = OpenCV.toJSValue(cnt2);
|
|
269
|
+
const sz2 = js2?.array?.length ?? 0;
|
|
270
|
+
console.log(`[DocDetect] P2(CannyStrong): ${sz2} contours`);
|
|
271
|
+
|
|
272
|
+
if (sz2 > 0) {
|
|
273
|
+
const r2 = this.findBestQuad(cnt2, sz2, imgArea, MIN_AREA, 'P2');
|
|
274
|
+
if (r2) {
|
|
275
|
+
allDebug.push(...r2.debug);
|
|
276
|
+
if (!winner || r2.area > winner.area) {
|
|
277
|
+
winner = { poly: r2.poly, area: r2.area, angleRange: r2.angleRange };
|
|
212
278
|
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
} catch (p2Err) {
|
|
282
|
+
console.log('[DocDetect] P2 failed:', p2Err);
|
|
283
|
+
}
|
|
213
284
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
285
|
+
// ═══════════════════════════════════════════════════════════════
|
|
286
|
+
// PIPELINE 3: OTSU threshold (binary toàn cục)
|
|
287
|
+
//
|
|
288
|
+
// Cho ảnh có tương phản rõ giữa tài liệu và nền.
|
|
289
|
+
// ═══════════════════════════════════════════════════════════════
|
|
290
|
+
try {
|
|
291
|
+
const otsu = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
292
|
+
// threshold type = THRESH_BINARY_INV + THRESH_OTSU = 1 + 8 = 9
|
|
293
|
+
OpenCV.invoke('threshold', blurred, otsu, 0, 255, 9);
|
|
294
|
+
|
|
295
|
+
// MORPH_CLOSE lớn để nối vùng
|
|
296
|
+
const oCloseSz = OpenCV.createObject(ObjectType.Size, 15, 15);
|
|
297
|
+
const oCloseK = OpenCV.invoke('getStructuringElement', 0 /* RECT */, oCloseSz);
|
|
298
|
+
const otsuClosed = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
299
|
+
OpenCV.invoke('morphologyEx', otsu, otsuClosed, 3 /* MORPH_CLOSE */, oCloseK);
|
|
300
|
+
|
|
301
|
+
const cnt3 = OpenCV.createObject(ObjectType.MatVector);
|
|
302
|
+
const hier3 = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
303
|
+
OpenCV.invoke('findContoursWithHierarchy', otsuClosed, cnt3, hier3,
|
|
304
|
+
0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
305
|
+
|
|
306
|
+
const js3 = OpenCV.toJSValue(cnt3);
|
|
307
|
+
const sz3 = js3?.array?.length ?? 0;
|
|
308
|
+
console.log(`[DocDetect] P3(OTSU): ${sz3} contours`);
|
|
309
|
+
|
|
310
|
+
if (sz3 > 0) {
|
|
311
|
+
const r3 = this.findBestQuad(cnt3, sz3, imgArea, MIN_AREA, 'P3');
|
|
312
|
+
if (r3) {
|
|
313
|
+
allDebug.push(...r3.debug);
|
|
314
|
+
if (!winner || r3.area > winner.area) {
|
|
315
|
+
winner = { poly: r3.poly, area: r3.area, angleRange: r3.angleRange };
|
|
222
316
|
}
|
|
223
317
|
}
|
|
224
318
|
}
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
);
|
|
319
|
+
} catch (p3Err) {
|
|
320
|
+
console.log('[DocDetect] P3 failed:', p3Err);
|
|
231
321
|
}
|
|
232
322
|
|
|
233
|
-
|
|
234
|
-
|
|
323
|
+
// ═══════════════════════════════════════════════════════════════
|
|
324
|
+
// Trả kết quả
|
|
325
|
+
// ═══════════════════════════════════════════════════════════════
|
|
326
|
+
console.log(`[DocDetect] All debug:\n${allDebug.join('\n')}`);
|
|
235
327
|
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
const
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
y: Math.round(p.y * actualRatio)
|
|
328
|
+
if (winner) {
|
|
329
|
+
console.log(`[DocDetect] ✓ Winner: area=${winner.area.toFixed(0)}(${(winner.area/imgArea*100).toFixed(1)}%) angle=${winner.angleRange.toFixed(1)}°`);
|
|
330
|
+
const originalCorners = winner.poly.map(p => ({
|
|
331
|
+
x: Math.round(p.x * ratio),
|
|
332
|
+
y: Math.round(p.y * ratio)
|
|
242
333
|
}));
|
|
243
334
|
return this.sortCorners(originalCorners);
|
|
244
335
|
}
|
|
245
336
|
|
|
246
|
-
//
|
|
247
|
-
|
|
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.`);
|
|
337
|
+
// Không pipeline nào thành công
|
|
338
|
+
console.log('[DocDetect] ✗ All pipelines failed');
|
|
249
339
|
throw new Error(
|
|
250
340
|
`Không phát hiện được viền tài liệu.\n` +
|
|
251
341
|
`Ảnh: ${origCols}x${origRows}px | Resize: ${targetWidth}x${targetHeight}px\n` +
|
|
252
|
-
`
|
|
253
|
-
`
|
|
254
|
-
`
|
|
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.`
|
|
342
|
+
`MinArea: ${(MIN_AREA*100).toFixed(0)}%\n` +
|
|
343
|
+
`Chi tiết:\n${allDebug.join('\n')}\n` +
|
|
344
|
+
`Gợi ý: Hãy đảm bảo tài liệu chiếm ≥15% khung hình, viền rõ ràng, nền tương phản.`
|
|
256
345
|
);
|
|
257
346
|
} catch (e: any) {
|
|
258
347
|
// Trích message từ mọi dạng exception (Error object, string, native, undefined...)
|