rn-opencv-doc-perspective-correction 1.0.9 → 1.0.11

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 CHANGED
@@ -2,6 +2,12 @@ export type Point = {
2
2
  x: number;
3
3
  y: number;
4
4
  };
5
+ /**
6
+ * Gợi ý phương thức OCR nên dùng dựa trên phân tích màu sắc.
7
+ * 'G' = ML Kit (nhiều màu, ảnh thẻ, màu sắc phong phú)
8
+ * 'S' = Tesseract (ít màu, bản scan, giấy trắng)
9
+ */
10
+ export type OcrMethodHint = 'G' | 'S';
5
11
  export declare class DocumentScanner {
6
12
  private static getDistance;
7
13
  private static sortCorners;
@@ -11,4 +17,18 @@ export declare class DocumentScanner {
11
17
  * Xoay ảnh 90, -90 hoặc 180 độ
12
18
  */
13
19
  static rotateImage(imageBase64: string, angle: 90 | -90 | 180): string | undefined;
20
+ /**
21
+ * Phân tích độ phức tạp màu sắc để gợi ý phương thức OCR phù hợp.
22
+ *
23
+ * - Nếu corners phát hiện thành công → crop vùng tài liệu và phân tích vùng đó
24
+ * - Nếu không có corners (phát hiện thất bại) → phân tích toàn bộ ảnh
25
+ * - Dùng không gian màu Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) và b* (lam↔vàng)
26
+ * - stdA + stdB > threshold → nhiều màu sắc → 'G' (ML Kit, phù hợp thẻ, ảnh màu)
27
+ * - stdA + stdB ≤ threshold → ít màu sắc → 'S' (Tesseract, phù hợp giấy trắng, bản scan)
28
+ *
29
+ * @param imageBase64 Ảnh gốc base64 JPEG
30
+ * @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
31
+ * @param colorThreshold Ngưỡng phân biệt (mặc định 18.0, điều chỉnh nếu cần tinh chỉnh)
32
+ */
33
+ static analyzeColorComplexity(imageBase64: string, corners?: Point[], colorThreshold?: number): OcrMethodHint;
14
34
  }
package/dist/index.js CHANGED
@@ -28,18 +28,14 @@ class DocumentScanner {
28
28
  src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
29
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);
30
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);
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
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);
32
32
  react_native_fast_opencv_1.OpenCV.invoke('cvtColor', src, gray, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2GRAY);
33
33
  const ksize = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
34
34
  react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', gray, blurred, ksize, 0);
35
- // Python uses: cv2.threshold(blurred, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
36
- // THRESH_BINARY = 0
37
- // THRESH_OTSU = 8
38
35
  // THRESH_BINARY + THRESH_OTSU = 8
39
36
  react_native_fast_opencv_1.OpenCV.invoke('threshold', blurred, edges, 0, 255, 8);
40
37
  contoursObj = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.MatVector);
41
38
  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);
42
- // Using RETR_EXTERNAL similar to the Python script for outer contours
43
39
  react_native_fast_opencv_1.OpenCV.invoke('findContoursWithHierarchy', edges, contoursObj, hierarchyObj, 0 /* RETR_EXTERNAL */, 2 /* CHAIN_APPROX_SIMPLE */);
44
40
  const contoursJS = react_native_fast_opencv_1.OpenCV.toJSValue(contoursObj);
45
41
  const contoursArray = (contoursJS === null || contoursJS === void 0 ? void 0 : contoursJS.array) || [];
@@ -47,20 +43,17 @@ class DocumentScanner {
47
43
  if (contoursSize === 0) {
48
44
  return undefined;
49
45
  }
50
- // First pass: extract all areas to sort them and minimize JSI calls
51
46
  let contourMetrics = [];
52
47
  for (let i = 0; i < contoursSize; i++) {
53
48
  const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contoursObj, i);
54
49
  const areaObj = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour);
55
50
  const area = areaObj ? areaObj.value : 0;
56
- if (area > 5000) { // filter very small artifacts
51
+ if (area > 5000) {
57
52
  contourMetrics.push({ index: i, area, contour });
58
53
  }
59
54
  }
60
- // Sort contours by area in descending order
61
55
  contourMetrics.sort((a, b) => b.area - a.area);
62
56
  let largestPoly = undefined;
63
- // Second pass: only check approxPolyDP for the largest ones
64
57
  for (let i = 0; i < contourMetrics.length; i++) {
65
58
  const metric = contourMetrics[i];
66
59
  const contour = metric.contour;
@@ -71,7 +64,7 @@ class DocumentScanner {
71
64
  const approxJS = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
72
65
  if (approxJS && approxJS.array && approxJS.array.length === 4) {
73
66
  largestPoly = approxJS.array;
74
- break; // Stop at the first 4-point polygon like python script
67
+ break;
75
68
  }
76
69
  }
77
70
  if (largestPoly && largestPoly.length === 4) {
@@ -122,7 +115,6 @@ class DocumentScanner {
122
115
  const borderValue = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Scalar, 0);
123
116
  react_native_fast_opencv_1.OpenCV.invoke('warpPerspective', src, dst, perspectiveMatrix, size, 1 /* INTER_LINEAR */, 0 /* BORDER_CONSTANT */, borderValue);
124
117
  const dstValue = react_native_fast_opencv_1.OpenCV.toJSValue(dst);
125
- // Fix "writeFile got an object" by guaranteeing string type
126
118
  if (dstValue && dstValue.base64) {
127
119
  return typeof dstValue.base64 === 'string' ? dstValue.base64 : String(dstValue.base64);
128
120
  }
@@ -165,5 +157,117 @@ class DocumentScanner {
165
157
  react_native_fast_opencv_1.OpenCV.clearBuffers();
166
158
  }
167
159
  }
160
+ /**
161
+ * Phân tích độ phức tạp màu sắc để gợi ý phương thức OCR phù hợp.
162
+ *
163
+ * - Nếu corners phát hiện thành công → crop vùng tài liệu và phân tích vùng đó
164
+ * - Nếu không có corners (phát hiện thất bại) → phân tích toàn bộ ảnh
165
+ * - Dùng không gian màu Lab, đo độ lệch chuẩn kênh a* (xanh↔đỏ) và b* (lam↔vàng)
166
+ * - stdA + stdB > threshold → nhiều màu sắc → 'G' (ML Kit, phù hợp thẻ, ảnh màu)
167
+ * - stdA + stdB ≤ threshold → ít màu sắc → 'S' (Tesseract, phù hợp giấy trắng, bản scan)
168
+ *
169
+ * @param imageBase64 Ảnh gốc base64 JPEG
170
+ * @param corners 4 góc tài liệu đã phát hiện (tuỳ chọn)
171
+ * @param colorThreshold Ngưỡng phân biệt (mặc định 18.0, điều chỉnh nếu cần tinh chỉnh)
172
+ */
173
+ static analyzeColorComplexity(imageBase64, corners, colorThreshold = 18.0) {
174
+ var _a, _b, _c, _d, _e, _f;
175
+ let src = null;
176
+ let roi = null;
177
+ let lab = null;
178
+ let color3Channel = null;
179
+ try {
180
+ src = react_native_fast_opencv_1.OpenCV.base64ToMat(imageBase64);
181
+ // Nếu có corners hợp lệ → crop vùng tài liệu để phân tích chính xác hơn
182
+ if (corners && corners.length === 4) {
183
+ try {
184
+ const sortedCorners = this.sortCorners([...corners]);
185
+ const [tl, tr, br, bl] = sortedCorners;
186
+ const widthA = this.getDistance(br, bl);
187
+ const widthB = this.getDistance(tr, tl);
188
+ const maxWidth = Math.max(Math.round(widthA), Math.round(widthB));
189
+ const heightA = this.getDistance(tr, br);
190
+ const heightB = this.getDistance(tl, bl);
191
+ const maxHeight = Math.max(Math.round(heightA), Math.round(heightB));
192
+ if (maxWidth > 0 && maxHeight > 0) {
193
+ const srcPts = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2fVector, [
194
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, tl.x, tl.y),
195
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, tr.x, tr.y),
196
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, br.x, br.y),
197
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, bl.x, bl.y),
198
+ ]);
199
+ const dstPts = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2fVector, [
200
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, 0, 0),
201
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, maxWidth - 1, 0),
202
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, maxWidth - 1, maxHeight - 1),
203
+ react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Point2f, 0, maxHeight - 1),
204
+ ]);
205
+ const perspM = react_native_fast_opencv_1.OpenCV.invoke('getPerspectiveTransform', srcPts, dstPts, 0);
206
+ const sz = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, maxWidth, maxHeight);
207
+ const borderVal = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Scalar, 0);
208
+ roi = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8UC3);
209
+ react_native_fast_opencv_1.OpenCV.invoke('warpPerspective', src, roi, perspM, sz, 1, 0, borderVal);
210
+ }
211
+ }
212
+ catch (_) {
213
+ roi = null; // Crop lỗi → dùng toàn ảnh
214
+ }
215
+ }
216
+ const target = roi !== null && roi !== void 0 ? roi : src; // Dùng vùng crop nếu có, ngược lại toàn ảnh
217
+ // Bắt buộc chuyển đổi về 3 kênh màu BGR để tránh lỗi khi dùng COLOR_BGR2Lab
218
+ // Trường hợp file ảnh là Grayscale (1 kênh) hoặc BGRA (4 kênh trong suốt)
219
+ color3Channel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8UC3);
220
+ const channelsObj = react_native_fast_opencv_1.OpenCV.invoke('channels', target);
221
+ if (channelsObj && channelsObj.value === 1) {
222
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', target, color3Channel, react_native_fast_opencv_1.ColorConversionCodes.COLOR_GRAY2BGR);
223
+ }
224
+ else if (channelsObj && channelsObj.value === 4) {
225
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', target, color3Channel, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGRA2BGR);
226
+ }
227
+ else {
228
+ color3Channel = target; // Đã là 3 kênh
229
+ }
230
+ // Chuyển sang Lab (L=sáng/tối, a*=màu xanh↔đỏ, b*=màu lam↔vàng)
231
+ lab = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_8UC3);
232
+ react_native_fast_opencv_1.OpenCV.invoke('cvtColor', color3Channel, lab, react_native_fast_opencv_1.ColorConversionCodes.COLOR_BGR2Lab);
233
+ // Đo mean và stddev từng kênh
234
+ const meanMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_64F);
235
+ const stdMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat, 0, 0, react_native_fast_opencv_1.DataTypes.CV_64F);
236
+ react_native_fast_opencv_1.OpenCV.invoke('meanStdDev', lab, meanMat, stdMat);
237
+ // Trích xuất stdDev một cách an toàn
238
+ const stdJS = react_native_fast_opencv_1.OpenCV.toJSValue(stdMat);
239
+ // react-native-fast-opencv thường chuyển `Mat` thành object JS có cấu trúc mảng 1 chiều trong prop `array` hoặc `data`
240
+ // hoặc trả về dưới dạng mảng lồng nhau
241
+ let stdA = 0;
242
+ let stdB = 0;
243
+ if (stdJS) {
244
+ if (Array.isArray(stdJS)) {
245
+ // Nếu nó đã là array
246
+ stdA = Array.isArray(stdJS[1]) ? stdJS[1][0] : (_a = stdJS[1]) !== null && _a !== void 0 ? _a : 0;
247
+ stdB = Array.isArray(stdJS[2]) ? stdJS[2][0] : (_b = stdJS[2]) !== null && _b !== void 0 ? _b : 0;
248
+ }
249
+ else if (stdJS.array && Array.isArray(stdJS.array)) {
250
+ stdA = (_c = stdJS.array[1]) !== null && _c !== void 0 ? _c : 0;
251
+ stdB = (_d = stdJS.array[2]) !== null && _d !== void 0 ? _d : 0;
252
+ }
253
+ else if (stdJS.data && Array.isArray(stdJS.data)) {
254
+ // Có một số version có định dạng `data`
255
+ stdA = (_e = stdJS.data[1]) !== null && _e !== void 0 ? _e : 0;
256
+ stdB = (_f = stdJS.data[2]) !== null && _f !== void 0 ? _f : 0;
257
+ }
258
+ }
259
+ const colorScore = stdA + stdB;
260
+ console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
261
+ return colorScore > colorThreshold ? 'G' : 'S';
262
+ }
263
+ catch (e) {
264
+ console.warn('[OpenCV] analyzeColorComplexity lỗi, fallback S:', e === null || e === void 0 ? void 0 : e.message);
265
+ // Ảnh ít màu thường dễ gây lỗi khi phân tích hơn nên ta Fallback về S
266
+ return 'S';
267
+ }
268
+ finally {
269
+ react_native_fast_opencv_1.OpenCV.clearBuffers();
270
+ }
271
+ }
168
272
  }
169
273
  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.9",
3
+ "version": "1.0.11",
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
@@ -218,6 +218,7 @@ export class DocumentScanner {
218
218
  let src: OpenCVMat | null = null;
219
219
  let roi: OpenCVMat | null = null;
220
220
  let lab: OpenCVMat | null = null;
221
+ let color3Channel: OpenCVMat | null = null;
221
222
 
222
223
  try {
223
224
  src = OpenCV.base64ToMat(imageBase64);
@@ -260,27 +261,57 @@ export class DocumentScanner {
260
261
 
261
262
  const target = roi ?? src; // Dùng vùng crop nếu có, ngược lại toàn ảnh
262
263
 
264
+ // Bắt buộc chuyển đổi về 3 kênh màu BGR để tránh lỗi khi dùng COLOR_BGR2Lab
265
+ // Trường hợp file ảnh là Grayscale (1 kênh) hoặc BGRA (4 kênh trong suốt)
266
+ color3Channel = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
267
+ const channelsObj = OpenCV.invoke('channels', target);
268
+ if (channelsObj && channelsObj.value === 1) {
269
+ OpenCV.invoke('cvtColor', target, color3Channel, ColorConversionCodes.COLOR_GRAY2BGR);
270
+ } else if (channelsObj && channelsObj.value === 4) {
271
+ OpenCV.invoke('cvtColor', target, color3Channel, ColorConversionCodes.COLOR_BGRA2BGR);
272
+ } else {
273
+ color3Channel = target; // Đã là 3 kênh
274
+ }
275
+
263
276
  // Chuyển sang Lab (L=sáng/tối, a*=màu xanh↔đỏ, b*=màu lam↔vàng)
264
277
  lab = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_8UC3);
265
- OpenCV.invoke('cvtColor', target, lab, ColorConversionCodes.COLOR_BGR2Lab);
278
+ OpenCV.invoke('cvtColor', color3Channel, lab, ColorConversionCodes.COLOR_BGR2Lab);
266
279
 
267
280
  // Đo mean và stddev từng kênh
268
281
  const meanMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
269
282
  const stdMat = OpenCV.createObject(ObjectType.Mat, 0, 0, DataTypes.CV_64F);
270
283
  OpenCV.invoke('meanStdDev', lab, meanMat, stdMat);
271
284
 
285
+ // Trích xuất stdDev một cách an toàn
272
286
  const stdJS = OpenCV.toJSValue(stdMat);
273
- const stdArray: number[] = stdJS?.array ?? [];
274
- const stdA = stdArray[1] ?? 0; // a*: xanh lá ↔ đỏ
275
- const stdB = stdArray[2] ?? 0; // b*: lam ↔ vàng
287
+ // react-native-fast-opencv thường chuyển `Mat` thành object JS có cấu trúc mảng 1 chiều trong prop `array` hoặc `data`
288
+ // hoặc trả về dưới dạng mảng lồng nhau
289
+ let stdA = 0;
290
+ let stdB = 0;
291
+
292
+ if (stdJS) {
293
+ if (Array.isArray(stdJS)) {
294
+ // Nếu nó đã là array
295
+ stdA = Array.isArray(stdJS[1]) ? stdJS[1][0] : stdJS[1] ?? 0;
296
+ stdB = Array.isArray(stdJS[2]) ? stdJS[2][0] : stdJS[2] ?? 0;
297
+ } else if (stdJS.array && Array.isArray(stdJS.array)) {
298
+ stdA = stdJS.array[1] ?? 0;
299
+ stdB = stdJS.array[2] ?? 0;
300
+ } else if (stdJS.data && Array.isArray(stdJS.data)) {
301
+ // Có một số version có định dạng `data`
302
+ stdA = stdJS.data[1] ?? 0;
303
+ stdB = stdJS.data[2] ?? 0;
304
+ }
305
+ }
276
306
 
277
307
  const colorScore = stdA + stdB;
278
308
  console.log(`[OCR Auto] colorScore=${colorScore.toFixed(2)} threshold=${colorThreshold} => ${colorScore > colorThreshold ? 'G (ML Kit)' : 'S (Tesseract)'}`);
279
309
 
280
310
  return colorScore > colorThreshold ? 'G' : 'S';
281
311
  } catch (e: any) {
282
- console.warn('[OpenCV] analyzeColorComplexity lỗi, fallback G:', e?.message);
283
- return 'G'; // Fallback an toàn về ML Kit
312
+ console.warn('[OpenCV] analyzeColorComplexity lỗi, fallback S:', e?.message);
313
+ // Ảnh ít màu thường dễ gây lỗi khi phân tích hơn nên ta Fallback về S
314
+ return 'S';
284
315
  } finally {
285
316
  OpenCV.clearBuffers();
286
317
  }