rn-opencv-doc-perspective-correction 1.0.5 → 1.0.7
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 +3 -11
- package/dist/index.js +71 -33
- package/package.json +1 -1
- package/src/index.ts +76 -35
package/dist/index.d.ts
CHANGED
|
@@ -3,20 +3,12 @@ export type Point = {
|
|
|
3
3
|
y: number;
|
|
4
4
|
};
|
|
5
5
|
export declare class DocumentScanner {
|
|
6
|
-
/**
|
|
7
|
-
* Tính khoảng cách Euclidean giữa 2 điểm
|
|
8
|
-
*/
|
|
9
6
|
private static getDistance;
|
|
10
|
-
/**
|
|
11
|
-
* Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
|
|
12
|
-
*/
|
|
13
7
|
private static sortCorners;
|
|
14
|
-
/**
|
|
15
|
-
* Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
|
|
16
|
-
*/
|
|
17
8
|
static detectPageCorners(imageBase64: string, onLog?: (msg: string) => void): Point[] | undefined;
|
|
9
|
+
static applyPerspectiveCorrection(imageBase64: string, corners: Point[], onLog?: (msg: string) => void): string | undefined;
|
|
18
10
|
/**
|
|
19
|
-
*
|
|
11
|
+
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
20
12
|
*/
|
|
21
|
-
static
|
|
13
|
+
static rotateImage(imageBase64: string, angle: 90 | -90 | 180, onLog?: (msg: string) => void): string | undefined;
|
|
22
14
|
}
|
package/dist/index.js
CHANGED
|
@@ -4,15 +4,9 @@ exports.DocumentScanner = void 0;
|
|
|
4
4
|
// @ts-nocheck
|
|
5
5
|
const react_native_fast_opencv_1 = require("react-native-fast-opencv");
|
|
6
6
|
class DocumentScanner {
|
|
7
|
-
/**
|
|
8
|
-
* Tính khoảng cách Euclidean giữa 2 điểm
|
|
9
|
-
*/
|
|
10
7
|
static getDistance(p1, p2) {
|
|
11
8
|
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
12
9
|
}
|
|
13
|
-
/**
|
|
14
|
-
* Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
|
|
15
|
-
*/
|
|
16
10
|
static sortCorners(corners) {
|
|
17
11
|
if (corners.length !== 4)
|
|
18
12
|
return corners;
|
|
@@ -23,9 +17,6 @@ class DocumentScanner {
|
|
|
23
17
|
return angleA - angleB;
|
|
24
18
|
});
|
|
25
19
|
}
|
|
26
|
-
/**
|
|
27
|
-
* Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
|
|
28
|
-
*/
|
|
29
20
|
static detectPageCorners(imageBase64, onLog) {
|
|
30
21
|
let src = null;
|
|
31
22
|
let gray = null;
|
|
@@ -37,41 +28,55 @@ class DocumentScanner {
|
|
|
37
28
|
src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
38
29
|
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);
|
|
39
30
|
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);
|
|
40
|
-
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);
|
|
31
|
+
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); // This will hold the threshold output
|
|
41
32
|
react_native_fast_opencv_1.OpenCV.invoke('cvtColor', src, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
|
|
42
33
|
const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
|
|
43
34
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
44
|
-
|
|
35
|
+
// Python uses: cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
36
|
+
// THRESH_BINARY = 0
|
|
37
|
+
// THRESH_OTSU = 8
|
|
38
|
+
// THRESH_BINARY + THRESH_OTSU = 8
|
|
39
|
+
react_native_fast_opencv_1.OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
|
|
45
40
|
contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
|
|
46
41
|
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);
|
|
47
|
-
|
|
42
|
+
// Using RETR_EXTERNAL similar to the Python script for outer contours
|
|
43
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
48
44
|
const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
|
|
49
45
|
const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
|
|
50
46
|
const contoursSize = contoursArray.length;
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
47
|
+
if (contoursSize === 0) {
|
|
48
|
+
if (onLog)
|
|
49
|
+
onLog(`[OpenCV] Không tìm thấy contours.`);
|
|
50
|
+
return undefined;
|
|
51
|
+
}
|
|
52
|
+
// First pass: extract all areas to sort them and minimize JSI calls
|
|
53
|
+
let contourMetrics = [];
|
|
54
54
|
for (let i = 0; i < contoursSize; i++) {
|
|
55
55
|
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
|
|
56
56
|
const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
|
|
57
57
|
const area = areaObj ? areaObj.value : 0;
|
|
58
|
-
if (area >
|
|
59
|
-
|
|
58
|
+
if (area > 5000) { // filter very small artifacts
|
|
59
|
+
contourMetrics.push({ index: i, area, contour });
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
}
|
|
62
|
+
// Sort contours by area in descending order
|
|
63
|
+
contourMetrics.sort((a, b) => b.area - a.area);
|
|
64
|
+
let largestPoly = undefined;
|
|
65
|
+
// Second pass: only check approxPolyDP for the largest ones
|
|
66
|
+
for (let i = 0; i < contourMetrics.length; i++) {
|
|
67
|
+
const metric = contourMetrics[i];
|
|
68
|
+
const contour = metric.contour;
|
|
69
|
+
const periObj = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contour, true);
|
|
70
|
+
const peri = periObj ? periObj.value : 0;
|
|
71
|
+
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
72
|
+
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
73
|
+
const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
74
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
75
|
+
largestPoly = approxJS.array;
|
|
76
|
+
break; // Stop at the first 4-point polygon like python script
|
|
72
77
|
}
|
|
73
78
|
}
|
|
74
|
-
const logMsg = `[OpenCV]
|
|
79
|
+
const logMsg = `[OpenCV] Contours: ${contoursSize}. Metrics pass: ${contourMetrics.length}. Poly detect: ${largestPoly ? 'Thành công' : 'Thất bại'}.`;
|
|
75
80
|
console.log(logMsg);
|
|
76
81
|
if (onLog)
|
|
77
82
|
onLog(logMsg);
|
|
@@ -90,9 +95,6 @@ class DocumentScanner {
|
|
|
90
95
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
|
-
/**
|
|
94
|
-
* Bước 2: Perspective Correction
|
|
95
|
-
*/
|
|
96
98
|
static applyPerspectiveCorrection(imageBase64, corners, onLog) {
|
|
97
99
|
let src = null;
|
|
98
100
|
let dst = null;
|
|
@@ -127,7 +129,12 @@ class DocumentScanner {
|
|
|
127
129
|
const size = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, maxWidth, maxHeight);
|
|
128
130
|
const borderValue = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Scalar, 0);
|
|
129
131
|
react_native_fast_opencv_1.OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size, 1 /* INTER_LINEAR */, 0 /* BORDER_CONSTANT */, borderValue);
|
|
130
|
-
|
|
132
|
+
const dstValue = react_native_fast_opencv_1.OpenCV.toJSValue(dst);
|
|
133
|
+
// Fix "writeFile got an object" by guaranteeing string type
|
|
134
|
+
if (dstValue && dstValue.base64) {
|
|
135
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
136
|
+
}
|
|
137
|
+
return undefined;
|
|
131
138
|
}
|
|
132
139
|
catch (e) {
|
|
133
140
|
console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
|
|
@@ -139,5 +146,36 @@ class DocumentScanner {
|
|
|
139
146
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
|
140
147
|
}
|
|
141
148
|
}
|
|
149
|
+
/**
|
|
150
|
+
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
151
|
+
*/
|
|
152
|
+
static rotateImage(imageBase64, angle, onLog) {
|
|
153
|
+
let src = null;
|
|
154
|
+
let dst = null;
|
|
155
|
+
try {
|
|
156
|
+
src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
|
|
157
|
+
dst = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8U);
|
|
158
|
+
let rotateCode = 0; // ROTATE_90_CLOCKWISE
|
|
159
|
+
if (angle === -90)
|
|
160
|
+
rotateCode = 2; // ROTATE_90_COUNTERCLOCKWISE
|
|
161
|
+
else if (angle === 180)
|
|
162
|
+
rotateCode = 1; // ROTATE_180
|
|
163
|
+
react_native_fast_opencv_1.OpenCV.invoke('rotate', src, dst, rotateCode);
|
|
164
|
+
const dstValue = react_native_fast_opencv_1.OpenCV.toJSValue(dst);
|
|
165
|
+
if (dstValue && dstValue.base64) {
|
|
166
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
167
|
+
}
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
catch (e) {
|
|
171
|
+
console.error('Lỗi khi xoay ảnh (OpenCV):', e);
|
|
172
|
+
if (onLog)
|
|
173
|
+
onLog(`[OpenCV Rotate Error]: ${e.message}`);
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
finally {
|
|
177
|
+
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
|
178
|
+
}
|
|
179
|
+
}
|
|
142
180
|
}
|
|
143
181
|
exports.DocumentScanner = DocumentScanner;
|
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.7",
|
|
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
|
@@ -4,16 +4,10 @@ import { OpenCV, OpenCVMat, ObjectType, DataTypes, ColorConversionCodes } from '
|
|
|
4
4
|
export type Point = { x: number; y: number };
|
|
5
5
|
|
|
6
6
|
export class DocumentScanner {
|
|
7
|
-
/**
|
|
8
|
-
* Tính khoảng cách Euclidean giữa 2 điểm
|
|
9
|
-
*/
|
|
10
7
|
private static getDistance(p1: Point, p2: Point) {
|
|
11
8
|
return Math.sqrt(Math.pow(p1.x - p2.x, 2) + Math.pow(p1.y - p2.y, 2));
|
|
12
9
|
}
|
|
13
10
|
|
|
14
|
-
/**
|
|
15
|
-
* Sắp xếp 4 điểm thành chuỗi TL, TR, BR, BL
|
|
16
|
-
*/
|
|
17
11
|
private static sortCorners(corners: Point[]): Point[] {
|
|
18
12
|
if (corners.length !== 4) return corners;
|
|
19
13
|
|
|
@@ -29,9 +23,6 @@ export class DocumentScanner {
|
|
|
29
23
|
});
|
|
30
24
|
}
|
|
31
25
|
|
|
32
|
-
/**
|
|
33
|
-
* Bước 1: Page Corner Detection (Auto-detect góc tài liệu)
|
|
34
|
-
*/
|
|
35
26
|
public static detectPageCorners(imageBase64: string, onLog?: (msg: string) => void): Point[] | undefined {
|
|
36
27
|
let src: OpenCVMat | null = null;
|
|
37
28
|
let gray: OpenCVMat | null = null;
|
|
@@ -44,52 +35,68 @@ export class DocumentScanner {
|
|
|
44
35
|
src = OpenCV.base64ToMat(imageBase64);
|
|
45
36
|
gray = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
46
37
|
blurred = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
47
|
-
edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
38
|
+
edges = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U); // This will hold the threshold output
|
|
48
39
|
|
|
49
40
|
OpenCV.invoke('cvtColor', src, gray, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
50
41
|
|
|
51
42
|
const ksize = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
52
43
|
OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
|
|
53
44
|
|
|
54
|
-
|
|
45
|
+
// Python uses: cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
|
|
46
|
+
// THRESH_BINARY = 0
|
|
47
|
+
// THRESH_OTSU = 8
|
|
48
|
+
// THRESH_BINARY + THRESH_OTSU = 8
|
|
49
|
+
OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
|
|
55
50
|
|
|
56
51
|
contoursObj = OpenCV.createObject(ObjectType.MatVector);
|
|
57
52
|
hierarchyObj = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
58
53
|
|
|
59
|
-
|
|
54
|
+
// Using RETR_EXTERNAL similar to the Python script for outer contours
|
|
55
|
+
OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
|
|
60
56
|
|
|
61
57
|
const contoursJS = OpenCV.toJSValue(contoursObj);
|
|
62
58
|
const contoursArray = contoursJS?.array || [];
|
|
63
59
|
const contoursSize = contoursArray.length;
|
|
64
60
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
if (contoursSize === 0) {
|
|
62
|
+
if (onLog) onLog(`[OpenCV] Không tìm thấy contours.`);
|
|
63
|
+
return undefined;
|
|
64
|
+
}
|
|
68
65
|
|
|
66
|
+
// First pass: extract all areas to sort them and minimize JSI calls
|
|
67
|
+
let contourMetrics = [];
|
|
69
68
|
for (let i = 0; i < contoursSize; i++) {
|
|
70
69
|
const contour = OpenCV.copyObjectFromVector(contoursObj, i);
|
|
71
70
|
const areaObj = OpenCV.invoke('contourArea', contour);
|
|
72
71
|
const area = areaObj ? areaObj.value : 0;
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
foundContoursCount++;
|
|
72
|
+
if (area > 5000) { // filter very small artifacts
|
|
73
|
+
contourMetrics.push({ index: i, area, contour });
|
|
76
74
|
}
|
|
75
|
+
}
|
|
77
76
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
77
|
+
// Sort contours by area in descending order
|
|
78
|
+
contourMetrics.sort((a, b) => b.area - a.area);
|
|
79
|
+
|
|
80
|
+
let largestPoly: Point[] | undefined = undefined;
|
|
81
|
+
|
|
82
|
+
// Second pass: only check approxPolyDP for the largest ones
|
|
83
|
+
for (let i = 0; i < contourMetrics.length; i++) {
|
|
84
|
+
const metric = contourMetrics[i];
|
|
85
|
+
const contour = metric.contour;
|
|
86
|
+
|
|
87
|
+
const periObj = OpenCV.invoke('arcLength', contour, true);
|
|
88
|
+
const peri = periObj ? periObj.value : 0;
|
|
89
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
90
|
+
OpenCV.invoke('approxPolyDP', contour, approx, 0.02 * peri, true);
|
|
91
|
+
|
|
92
|
+
const approxJS = OpenCV.toJSValue(approx);
|
|
93
|
+
if (approxJS && approxJS.array && approxJS.array.length === 4) {
|
|
94
|
+
largestPoly = approxJS.array as Point[];
|
|
95
|
+
break; // Stop at the first 4-point polygon like python script
|
|
90
96
|
}
|
|
91
97
|
}
|
|
92
|
-
|
|
98
|
+
|
|
99
|
+
const logMsg = `[OpenCV] Contours: ${contoursSize}. Metrics pass: ${contourMetrics.length}. Poly detect: ${largestPoly ? 'Thành công' : 'Thất bại'}.`;
|
|
93
100
|
console.log(logMsg);
|
|
94
101
|
if (onLog) onLog(logMsg);
|
|
95
102
|
|
|
@@ -106,9 +113,6 @@ export class DocumentScanner {
|
|
|
106
113
|
}
|
|
107
114
|
}
|
|
108
115
|
|
|
109
|
-
/**
|
|
110
|
-
* Bước 2: Perspective Correction
|
|
111
|
-
*/
|
|
112
116
|
public static applyPerspectiveCorrection(imageBase64: string, corners: Point[], onLog?: (msg: string) => void): string | undefined {
|
|
113
117
|
let src: OpenCVMat | null = null;
|
|
114
118
|
let dst: OpenCVMat | null = null;
|
|
@@ -156,7 +160,13 @@ export class DocumentScanner {
|
|
|
156
160
|
|
|
157
161
|
OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size, 1 /* INTER_LINEAR */, 0 /* BORDER_CONSTANT */, borderValue);
|
|
158
162
|
|
|
159
|
-
|
|
163
|
+
const dstValue = OpenCV.toJSValue(dst);
|
|
164
|
+
|
|
165
|
+
// Fix "writeFile got an object" by guaranteeing string type
|
|
166
|
+
if (dstValue && dstValue.base64) {
|
|
167
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
168
|
+
}
|
|
169
|
+
return undefined;
|
|
160
170
|
} catch (e) {
|
|
161
171
|
console.error('Lỗi khi bóp phối cảnh tài liệu (OpenCV):', e);
|
|
162
172
|
if (onLog) onLog(`[OpenCV Perspective Correction Error]: ${(e as Error).message || e}`);
|
|
@@ -165,4 +175,35 @@ export class DocumentScanner {
|
|
|
165
175
|
OpenCV.clearBuffers();
|
|
166
176
|
}
|
|
167
177
|
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Xoay ảnh 90, -90 hoặc 180 độ
|
|
181
|
+
*/
|
|
182
|
+
public static rotateImage(imageBase64: string, angle: 90 | -90 | 180, onLog?: (msg: string) => void): string | undefined {
|
|
183
|
+
let src: OpenCVMat | null = null;
|
|
184
|
+
let dst: OpenCVMat | null = null;
|
|
185
|
+
try {
|
|
186
|
+
src = OpenCV.base64ToMat(imageBase64);
|
|
187
|
+
dst = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8U);
|
|
188
|
+
|
|
189
|
+
let rotateCode = 0; // ROTATE_90_CLOCKWISE
|
|
190
|
+
if (angle === -90) rotateCode = 2; // ROTATE_90_COUNTERCLOCKWISE
|
|
191
|
+
else if (angle === 180) rotateCode = 1; // ROTATE_180
|
|
192
|
+
|
|
193
|
+
OpenCV.invoke('rotate', src, dst, rotateCode);
|
|
194
|
+
|
|
195
|
+
const dstValue = OpenCV.toJSValue(dst);
|
|
196
|
+
if (dstValue && dstValue.base64) {
|
|
197
|
+
return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
|
|
198
|
+
}
|
|
199
|
+
return undefined;
|
|
200
|
+
} catch (e: any) {
|
|
201
|
+
console.error('Lỗi khi xoay ảnh (OpenCV):', e);
|
|
202
|
+
if (onLog) onLog(`[OpenCV Rotate Error]: ${e.message}`);
|
|
203
|
+
return undefined;
|
|
204
|
+
} finally {
|
|
205
|
+
OpenCV.clearBuffers();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
168
208
|
}
|
|
209
|
+
|