react-native-rectangle-doc-scanner 0.60.0 → 0.64.0
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/DocScanner.js +115 -149
- package/dist/index.d.ts +0 -1
- package/dist/index.js +1 -3
- package/package.json +1 -1
- package/src/DocScanner.tsx +120 -163
- package/src/index.ts +0 -1
- package/src/utils/documentDetection.ts +0 -278
package/dist/DocScanner.js
CHANGED
|
@@ -278,191 +278,157 @@ const DocScanner = ({ onCapture, overlayColor = '#e7a649', autoCapture = true, m
|
|
|
278
278
|
const element = react_native_fast_opencv_1.OpenCV.invoke('getStructuringElement', react_native_fast_opencv_1.MorphShapes.MORPH_RECT, morphologyKernel);
|
|
279
279
|
step = 'morphologyEx';
|
|
280
280
|
reportStage(step);
|
|
281
|
+
// MORPH_CLOSE to fill small holes in edges
|
|
281
282
|
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
|
|
283
|
+
// MORPH_OPEN to remove small noise
|
|
282
284
|
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', mat, mat, react_native_fast_opencv_1.MorphTypes.MORPH_OPEN, element);
|
|
283
|
-
const ADAPTIVE_THRESH_GAUSSIAN_C = 1;
|
|
284
|
-
const THRESH_BINARY = 0;
|
|
285
|
-
const THRESH_OTSU = 8;
|
|
286
285
|
// Bilateral filter for edge-preserving smoothing (better quality than Gaussian)
|
|
287
286
|
step = 'bilateralFilter';
|
|
288
287
|
reportStage(step);
|
|
289
|
-
let processed = mat;
|
|
290
288
|
try {
|
|
291
289
|
const tempMat = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Mat);
|
|
292
290
|
react_native_fast_opencv_1.OpenCV.invoke('bilateralFilter', mat, tempMat, 9, 75, 75);
|
|
293
|
-
|
|
291
|
+
mat = tempMat;
|
|
294
292
|
}
|
|
295
293
|
catch (error) {
|
|
296
294
|
if (__DEV__) {
|
|
297
295
|
console.warn('[DocScanner] bilateralFilter unavailable, falling back to GaussianBlur', error);
|
|
298
296
|
}
|
|
297
|
+
step = 'gaussianBlurFallback';
|
|
298
|
+
reportStage(step);
|
|
299
299
|
const blurKernel = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.Size, 5, 5);
|
|
300
300
|
react_native_fast_opencv_1.OpenCV.invoke('GaussianBlur', mat, mat, blurKernel, 0);
|
|
301
|
-
processed = mat;
|
|
302
301
|
}
|
|
303
|
-
|
|
304
|
-
step
|
|
302
|
+
step = 'Canny';
|
|
303
|
+
reportStage(step);
|
|
304
|
+
// Configurable Canny parameters for adaptive edge detection
|
|
305
|
+
react_native_fast_opencv_1.OpenCV.invoke('Canny', mat, mat, CANNY_LOW, CANNY_HIGH);
|
|
306
|
+
step = 'createContours';
|
|
305
307
|
reportStage(step);
|
|
306
|
-
const
|
|
307
|
-
react_native_fast_opencv_1.OpenCV.invoke('
|
|
308
|
-
|
|
309
|
-
|
|
308
|
+
const contours = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVectorOfVectors);
|
|
309
|
+
react_native_fast_opencv_1.OpenCV.invoke('findContours', mat, contours, react_native_fast_opencv_1.RetrievalModes.RETR_EXTERNAL, react_native_fast_opencv_1.ContourApproximationModes.CHAIN_APPROX_SIMPLE);
|
|
310
|
+
let best = null;
|
|
311
|
+
let maxArea = 0;
|
|
310
312
|
const frameArea = width * height;
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
const
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
'
|
|
323
|
-
|
|
324
|
-
|
|
313
|
+
step = 'toJSValue';
|
|
314
|
+
reportStage(step);
|
|
315
|
+
const contourVector = react_native_fast_opencv_1.OpenCV.toJSValue(contours);
|
|
316
|
+
const contourArray = Array.isArray(contourVector?.array) ? contourVector.array : [];
|
|
317
|
+
for (let i = 0; i < contourArray.length; i += 1) {
|
|
318
|
+
step = `contour_${i}_copy`;
|
|
319
|
+
reportStage(step);
|
|
320
|
+
const contour = react_native_fast_opencv_1.OpenCV.copyObjectFromVector(contours, i);
|
|
321
|
+
// Compute absolute area first
|
|
322
|
+
step = `contour_${i}_area_abs`;
|
|
323
|
+
reportStage(step);
|
|
324
|
+
const { value: area } = react_native_fast_opencv_1.OpenCV.invoke('contourArea', contour, false);
|
|
325
|
+
// Skip extremely small contours, but keep threshold very low to allow distant documents
|
|
326
|
+
if (typeof area !== 'number' || !isFinite(area)) {
|
|
327
|
+
continue;
|
|
325
328
|
}
|
|
326
|
-
if (
|
|
327
|
-
|
|
328
|
-
bestArea = candidate.area;
|
|
329
|
+
if (area < 50) {
|
|
330
|
+
continue;
|
|
329
331
|
}
|
|
330
|
-
|
|
331
|
-
const evaluateContours = (inputMat, attemptLabel) => {
|
|
332
|
-
'worklet';
|
|
333
|
-
step = `findContours_${attemptLabel}`;
|
|
332
|
+
step = `contour_${i}_area`; // ratio stage
|
|
334
333
|
reportStage(step);
|
|
335
|
-
const
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
334
|
+
const areaRatio = area / frameArea;
|
|
335
|
+
if (__DEV__) {
|
|
336
|
+
console.log('[DocScanner] area', area, 'ratio', areaRatio);
|
|
337
|
+
}
|
|
338
|
+
// Skip if area ratio is too small or too large
|
|
339
|
+
if (areaRatio < 0.0002 || areaRatio > 0.99) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// Try to use convex hull for better corner detection
|
|
343
|
+
let contourToUse = contour;
|
|
344
|
+
try {
|
|
345
|
+
step = `contour_${i}_convexHull`;
|
|
345
346
|
reportStage(step);
|
|
346
|
-
const
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
347
|
+
const hull = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
348
|
+
react_native_fast_opencv_1.OpenCV.invoke('convexHull', contour, hull, false, true);
|
|
349
|
+
contourToUse = hull;
|
|
350
|
+
}
|
|
351
|
+
catch (err) {
|
|
352
|
+
// If convexHull fails, use original contour
|
|
353
|
+
if (__DEV__) {
|
|
354
|
+
console.warn('[DocScanner] convexHull failed, using original contour');
|
|
353
355
|
}
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
356
|
+
}
|
|
357
|
+
step = `contour_${i}_arcLength`;
|
|
358
|
+
reportStage(step);
|
|
359
|
+
const { value: perimeter } = react_native_fast_opencv_1.OpenCV.invoke('arcLength', contourToUse, true);
|
|
360
|
+
const approx = react_native_fast_opencv_1.OpenCV.createObject(react_native_fast_opencv_1.ObjectType.PointVector);
|
|
361
|
+
let approxArray = [];
|
|
362
|
+
// Try more epsilon values from 0.1% to 10% for difficult shapes
|
|
363
|
+
const epsilonValues = [
|
|
364
|
+
0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009,
|
|
365
|
+
0.01, 0.012, 0.015, 0.018, 0.02, 0.025, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1
|
|
366
|
+
];
|
|
367
|
+
for (let attempt = 0; attempt < epsilonValues.length; attempt += 1) {
|
|
368
|
+
const epsilon = epsilonValues[attempt] * perimeter;
|
|
369
|
+
step = `contour_${i}_approxPolyDP_attempt_${attempt}`;
|
|
370
|
+
reportStage(step);
|
|
371
|
+
react_native_fast_opencv_1.OpenCV.invoke('approxPolyDP', contourToUse, approx, epsilon, true);
|
|
372
|
+
step = `contour_${i}_toJS_attempt_${attempt}`;
|
|
373
|
+
reportStage(step);
|
|
374
|
+
const approxValue = react_native_fast_opencv_1.OpenCV.toJSValue(approx);
|
|
375
|
+
const candidate = Array.isArray(approxValue?.array) ? approxValue.array : [];
|
|
376
|
+
if (__DEV__) {
|
|
377
|
+
console.log('[DocScanner] approx length', candidate.length, 'epsilon', epsilon);
|
|
359
378
|
}
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
convexHullWarned = true;
|
|
364
|
-
}
|
|
379
|
+
if (candidate.length === 4) {
|
|
380
|
+
approxArray = candidate;
|
|
381
|
+
break;
|
|
365
382
|
}
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
383
|
+
}
|
|
384
|
+
// Only proceed if we found exactly 4 corners
|
|
385
|
+
if (approxArray.length !== 4) {
|
|
386
|
+
continue;
|
|
387
|
+
}
|
|
388
|
+
step = `contour_${i}_convex`;
|
|
389
|
+
reportStage(step);
|
|
390
|
+
// Validate points before processing
|
|
391
|
+
const isValidPoint = (pt) => {
|
|
392
|
+
return typeof pt.x === 'number' && typeof pt.y === 'number' &&
|
|
393
|
+
!isNaN(pt.x) && !isNaN(pt.y) &&
|
|
394
|
+
isFinite(pt.x) && isFinite(pt.y);
|
|
395
|
+
};
|
|
396
|
+
if (!approxArray.every(isValidPoint)) {
|
|
397
|
+
if (__DEV__) {
|
|
398
|
+
console.warn('[DocScanner] invalid points in approxArray', approxArray);
|
|
369
399
|
}
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
if (
|
|
380
|
-
|
|
381
|
-
break;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
const points = approxArray.map((pt) => ({
|
|
403
|
+
x: pt.x / ratio,
|
|
404
|
+
y: pt.y / ratio,
|
|
405
|
+
}));
|
|
406
|
+
// Verify the quadrilateral is convex (valid document shape)
|
|
407
|
+
try {
|
|
408
|
+
if (!isConvexQuadrilateral(points)) {
|
|
409
|
+
if (__DEV__) {
|
|
410
|
+
console.log('[DocScanner] not convex, skipping:', points);
|
|
382
411
|
}
|
|
383
|
-
}
|
|
384
|
-
if (approxArray.length !== 4) {
|
|
385
|
-
continue;
|
|
386
|
-
}
|
|
387
|
-
const isValidPoint = (pt) => typeof pt.x === 'number' && typeof pt.y === 'number' && isFinite(pt.x) && isFinite(pt.y);
|
|
388
|
-
if (!approxArray.every(isValidPoint)) {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
const normalizedPoints = approxArray.map((pt) => ({
|
|
392
|
-
x: pt.x / ratio,
|
|
393
|
-
y: pt.y / ratio,
|
|
394
|
-
}));
|
|
395
|
-
if (!isConvexQuadrilateral(normalizedPoints)) {
|
|
396
|
-
continue;
|
|
397
|
-
}
|
|
398
|
-
const sanitized = (0, quad_1.sanitizeQuad)((0, quad_1.orderQuadPoints)(normalizedPoints));
|
|
399
|
-
if (!(0, quad_1.isValidQuad)(sanitized)) {
|
|
400
|
-
continue;
|
|
401
|
-
}
|
|
402
|
-
const edges = (0, quad_1.quadEdgeLengths)(sanitized);
|
|
403
|
-
const minEdge = Math.min(...edges);
|
|
404
|
-
const maxEdge = Math.max(...edges);
|
|
405
|
-
if (!Number.isFinite(minEdge) || minEdge < minEdgeThreshold) {
|
|
406
412
|
continue;
|
|
407
413
|
}
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
const quadAreaValue = (0, quad_1.quadArea)(sanitized);
|
|
413
|
-
const originalRatio = originalArea > 0 ? quadAreaValue / originalArea : 0;
|
|
414
|
-
if (originalRatio < 0.00012 || originalRatio > 0.92) {
|
|
415
|
-
continue;
|
|
416
|
-
}
|
|
417
|
-
const candidate = {
|
|
418
|
-
quad: sanitized,
|
|
419
|
-
area: quadAreaValue,
|
|
420
|
-
};
|
|
421
|
-
if (!bestLocal || candidate.area > bestLocal.area) {
|
|
422
|
-
bestLocal = candidate;
|
|
414
|
+
}
|
|
415
|
+
catch (err) {
|
|
416
|
+
if (__DEV__) {
|
|
417
|
+
console.warn('[DocScanner] convex check error:', err, 'points:', points);
|
|
423
418
|
}
|
|
419
|
+
continue;
|
|
424
420
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
step = `${label}_canny`;
|
|
431
|
-
reportStage(step);
|
|
432
|
-
react_native_fast_opencv_1.OpenCV.invoke('Canny', working, working, low, high);
|
|
433
|
-
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
|
|
434
|
-
considerCandidate(evaluateContours(working, label));
|
|
435
|
-
};
|
|
436
|
-
const runAdaptive = (label, blockSize, c) => {
|
|
437
|
-
'worklet';
|
|
438
|
-
const working = react_native_fast_opencv_1.OpenCV.invoke('clone', baseMat);
|
|
439
|
-
step = `${label}_adaptive`;
|
|
440
|
-
reportStage(step);
|
|
441
|
-
react_native_fast_opencv_1.OpenCV.invoke('adaptiveThreshold', working, working, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, blockSize, c);
|
|
442
|
-
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
|
|
443
|
-
considerCandidate(evaluateContours(working, label));
|
|
444
|
-
};
|
|
445
|
-
const runOtsu = () => {
|
|
446
|
-
'worklet';
|
|
447
|
-
const working = react_native_fast_opencv_1.OpenCV.invoke('clone', baseMat);
|
|
448
|
-
step = 'otsu_threshold';
|
|
449
|
-
reportStage(step);
|
|
450
|
-
react_native_fast_opencv_1.OpenCV.invoke('threshold', working, working, 0, 255, THRESH_BINARY | THRESH_OTSU);
|
|
451
|
-
react_native_fast_opencv_1.OpenCV.invoke('morphologyEx', working, working, react_native_fast_opencv_1.MorphTypes.MORPH_CLOSE, element);
|
|
452
|
-
considerCandidate(evaluateContours(working, 'otsu'));
|
|
453
|
-
};
|
|
454
|
-
runCanny('canny_primary', CANNY_LOW, CANNY_HIGH);
|
|
455
|
-
runCanny('canny_soft', Math.max(6, CANNY_LOW * 0.6), Math.max(CANNY_LOW * 1.2, CANNY_HIGH * 0.75));
|
|
456
|
-
runCanny('canny_hard', Math.max(12, CANNY_LOW * 1.1), CANNY_HIGH * 1.25);
|
|
457
|
-
runAdaptive('adaptive_19', 19, 7);
|
|
458
|
-
runAdaptive('adaptive_23', 23, 5);
|
|
459
|
-
runOtsu();
|
|
421
|
+
if (area > maxArea) {
|
|
422
|
+
best = points;
|
|
423
|
+
maxArea = area;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
460
426
|
step = 'clearBuffers';
|
|
461
427
|
reportStage(step);
|
|
462
428
|
react_native_fast_opencv_1.OpenCV.clearBuffers();
|
|
463
429
|
step = 'updateQuad';
|
|
464
430
|
reportStage(step);
|
|
465
|
-
updateQuad(
|
|
431
|
+
updateQuad(best);
|
|
466
432
|
}
|
|
467
433
|
catch (error) {
|
|
468
434
|
reportError(step, error);
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,3 @@ export type { FullDocScannerResult, FullDocScannerProps, FullDocScannerStrings,
|
|
|
5
5
|
export type { Point, Quad, Rectangle, CapturedDocument } from './types';
|
|
6
6
|
export type { DetectionConfig } from './DocScanner';
|
|
7
7
|
export { quadToRectangle, rectangleToQuad, scaleCoordinates, scaleRectangle, } from './utils/coordinate';
|
|
8
|
-
export { DocumentDetector } from './utils/documentDetection';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.scaleRectangle = exports.scaleCoordinates = exports.rectangleToQuad = exports.quadToRectangle = exports.FullDocScanner = exports.CropEditor = exports.DocScanner = void 0;
|
|
4
4
|
// Main components
|
|
5
5
|
var DocScanner_1 = require("./DocScanner");
|
|
6
6
|
Object.defineProperty(exports, "DocScanner", { enumerable: true, get: function () { return DocScanner_1.DocScanner; } });
|
|
@@ -14,5 +14,3 @@ Object.defineProperty(exports, "quadToRectangle", { enumerable: true, get: funct
|
|
|
14
14
|
Object.defineProperty(exports, "rectangleToQuad", { enumerable: true, get: function () { return coordinate_1.rectangleToQuad; } });
|
|
15
15
|
Object.defineProperty(exports, "scaleCoordinates", { enumerable: true, get: function () { return coordinate_1.scaleCoordinates; } });
|
|
16
16
|
Object.defineProperty(exports, "scaleRectangle", { enumerable: true, get: function () { return coordinate_1.scaleRectangle; } });
|
|
17
|
-
var documentDetection_1 = require("./utils/documentDetection");
|
|
18
|
-
Object.defineProperty(exports, "DocumentDetector", { enumerable: true, get: function () { return documentDetection_1.DocumentDetector; } });
|
package/package.json
CHANGED
package/src/DocScanner.tsx
CHANGED
|
@@ -344,225 +344,182 @@ export const DocScanner: React.FC<Props> = ({
|
|
|
344
344
|
const element = OpenCV.invoke('getStructuringElement', MorphShapes.MORPH_RECT, morphologyKernel);
|
|
345
345
|
step = 'morphologyEx';
|
|
346
346
|
reportStage(step);
|
|
347
|
+
// MORPH_CLOSE to fill small holes in edges
|
|
347
348
|
OpenCV.invoke('morphologyEx', mat, mat, MorphTypes.MORPH_CLOSE, element);
|
|
349
|
+
// MORPH_OPEN to remove small noise
|
|
348
350
|
OpenCV.invoke('morphologyEx', mat, mat, MorphTypes.MORPH_OPEN, element);
|
|
349
351
|
|
|
350
|
-
const ADAPTIVE_THRESH_GAUSSIAN_C = 1;
|
|
351
|
-
const THRESH_BINARY = 0;
|
|
352
|
-
const THRESH_OTSU = 8;
|
|
353
|
-
|
|
354
352
|
// Bilateral filter for edge-preserving smoothing (better quality than Gaussian)
|
|
355
353
|
step = 'bilateralFilter';
|
|
356
354
|
reportStage(step);
|
|
357
|
-
let processed = mat;
|
|
358
355
|
try {
|
|
359
356
|
const tempMat = OpenCV.createObject(ObjectType.Mat);
|
|
360
357
|
OpenCV.invoke('bilateralFilter', mat, tempMat, 9, 75, 75);
|
|
361
|
-
|
|
358
|
+
mat = tempMat;
|
|
362
359
|
} catch (error) {
|
|
363
360
|
if (__DEV__) {
|
|
364
361
|
console.warn('[DocScanner] bilateralFilter unavailable, falling back to GaussianBlur', error);
|
|
365
362
|
}
|
|
363
|
+
step = 'gaussianBlurFallback';
|
|
364
|
+
reportStage(step);
|
|
366
365
|
const blurKernel = OpenCV.createObject(ObjectType.Size, 5, 5);
|
|
367
366
|
OpenCV.invoke('GaussianBlur', mat, mat, blurKernel, 0);
|
|
368
|
-
processed = mat;
|
|
369
367
|
}
|
|
370
368
|
|
|
371
|
-
|
|
372
|
-
step
|
|
369
|
+
step = 'Canny';
|
|
370
|
+
reportStage(step);
|
|
371
|
+
// Configurable Canny parameters for adaptive edge detection
|
|
372
|
+
OpenCV.invoke('Canny', mat, mat, CANNY_LOW, CANNY_HIGH);
|
|
373
|
+
|
|
374
|
+
step = 'createContours';
|
|
373
375
|
reportStage(step);
|
|
374
|
-
const
|
|
375
|
-
OpenCV.invoke('
|
|
376
|
-
OpenCV.invoke('morphologyEx', processed, processed, MorphTypes.MORPH_CLOSE, element);
|
|
376
|
+
const contours = OpenCV.createObject(ObjectType.PointVectorOfVectors);
|
|
377
|
+
OpenCV.invoke('findContours', mat, contours, RetrievalModes.RETR_EXTERNAL, ContourApproximationModes.CHAIN_APPROX_SIMPLE);
|
|
377
378
|
|
|
378
|
-
|
|
379
|
+
let best: Point[] | null = null;
|
|
380
|
+
let maxArea = 0;
|
|
379
381
|
const frameArea = width * height;
|
|
380
|
-
const originalArea = frame.width * frame.height;
|
|
381
|
-
const minEdgeThreshold = Math.max(14, Math.min(frame.width, frame.height) * MIN_EDGE_RATIO);
|
|
382
|
-
const epsilonValues = [
|
|
383
|
-
0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009,
|
|
384
|
-
0.01, 0.012, 0.015, 0.018, 0.02, 0.025, 0.03, 0.035, 0.04, 0.05,
|
|
385
|
-
0.06, 0.07, 0.08, 0.09, 0.1, 0.12,
|
|
386
|
-
];
|
|
387
|
-
|
|
388
|
-
let bestQuad: Point[] | null = null;
|
|
389
|
-
let bestArea = 0;
|
|
390
|
-
let convexHullWarned = false;
|
|
391
|
-
|
|
392
|
-
const considerCandidate = (candidate: { quad: Point[]; area: number } | null) => {
|
|
393
|
-
'worklet';
|
|
394
|
-
if (!candidate) {
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
if (!bestQuad || candidate.area > bestArea) {
|
|
398
|
-
bestQuad = candidate.quad;
|
|
399
|
-
bestArea = candidate.area;
|
|
400
|
-
}
|
|
401
|
-
};
|
|
402
382
|
|
|
403
|
-
|
|
404
|
-
|
|
383
|
+
step = 'toJSValue';
|
|
384
|
+
reportStage(step);
|
|
385
|
+
const contourVector = OpenCV.toJSValue(contours);
|
|
386
|
+
const contourArray = Array.isArray(contourVector?.array) ? contourVector.array : [];
|
|
405
387
|
|
|
406
|
-
|
|
388
|
+
for (let i = 0; i < contourArray.length; i += 1) {
|
|
389
|
+
step = `contour_${i}_copy`;
|
|
407
390
|
reportStage(step);
|
|
408
|
-
const
|
|
409
|
-
OpenCV.invoke('findContours', inputMat, contours, RetrievalModes.RETR_EXTERNAL, ContourApproximationModes.CHAIN_APPROX_SIMPLE);
|
|
391
|
+
const contour = OpenCV.copyObjectFromVector(contours, i);
|
|
410
392
|
|
|
411
|
-
|
|
412
|
-
|
|
393
|
+
// Compute absolute area first
|
|
394
|
+
step = `contour_${i}_area_abs`;
|
|
395
|
+
reportStage(step);
|
|
396
|
+
const { value: area } = OpenCV.invoke('contourArea', contour, false);
|
|
413
397
|
|
|
414
|
-
|
|
398
|
+
// Skip extremely small contours, but keep threshold very low to allow distant documents
|
|
399
|
+
if (typeof area !== 'number' || !isFinite(area)) {
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
415
402
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
const contour = OpenCV.copyObjectFromVector(contours, i);
|
|
403
|
+
if (area < 50) {
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
420
406
|
|
|
421
|
-
|
|
407
|
+
step = `contour_${i}_area`; // ratio stage
|
|
408
|
+
reportStage(step);
|
|
409
|
+
const areaRatio = area / frameArea;
|
|
410
|
+
|
|
411
|
+
if (__DEV__) {
|
|
412
|
+
console.log('[DocScanner] area', area, 'ratio', areaRatio);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Skip if area ratio is too small or too large
|
|
416
|
+
if (areaRatio < 0.0002 || areaRatio > 0.99) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Try to use convex hull for better corner detection
|
|
421
|
+
let contourToUse = contour;
|
|
422
|
+
try {
|
|
423
|
+
step = `contour_${i}_convexHull`;
|
|
422
424
|
reportStage(step);
|
|
423
|
-
const
|
|
424
|
-
|
|
425
|
-
|
|
425
|
+
const hull = OpenCV.createObject(ObjectType.PointVector);
|
|
426
|
+
OpenCV.invoke('convexHull', contour, hull, false, true);
|
|
427
|
+
contourToUse = hull;
|
|
428
|
+
} catch (err) {
|
|
429
|
+
// If convexHull fails, use original contour
|
|
430
|
+
if (__DEV__) {
|
|
431
|
+
console.warn('[DocScanner] convexHull failed, using original contour');
|
|
426
432
|
}
|
|
433
|
+
}
|
|
427
434
|
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
435
|
+
step = `contour_${i}_arcLength`;
|
|
436
|
+
reportStage(step);
|
|
437
|
+
const { value: perimeter } = OpenCV.invoke('arcLength', contourToUse, true);
|
|
438
|
+
const approx = OpenCV.createObject(ObjectType.PointVector);
|
|
432
439
|
|
|
433
|
-
|
|
434
|
-
try {
|
|
435
|
-
const hull = OpenCV.createObject(ObjectType.PointVector);
|
|
436
|
-
OpenCV.invoke('convexHull', contour, hull, false, true);
|
|
437
|
-
contourToUse = hull;
|
|
438
|
-
} catch (err) {
|
|
439
|
-
if (__DEV__ && !convexHullWarned) {
|
|
440
|
-
console.warn('[DocScanner] convexHull failed, using original contour');
|
|
441
|
-
convexHullWarned = true;
|
|
442
|
-
}
|
|
443
|
-
}
|
|
440
|
+
let approxArray: Array<{ x: number; y: number }> = [];
|
|
444
441
|
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
442
|
+
// Try more epsilon values from 0.1% to 10% for difficult shapes
|
|
443
|
+
const epsilonValues = [
|
|
444
|
+
0.001, 0.002, 0.003, 0.004, 0.005, 0.006, 0.007, 0.008, 0.009,
|
|
445
|
+
0.01, 0.012, 0.015, 0.018, 0.02, 0.025, 0.03, 0.04, 0.05, 0.06, 0.07, 0.08, 0.09, 0.1
|
|
446
|
+
];
|
|
449
447
|
|
|
450
|
-
|
|
451
|
-
|
|
448
|
+
for (let attempt = 0; attempt < epsilonValues.length; attempt += 1) {
|
|
449
|
+
const epsilon = epsilonValues[attempt] * perimeter;
|
|
450
|
+
step = `contour_${i}_approxPolyDP_attempt_${attempt}`;
|
|
451
|
+
reportStage(step);
|
|
452
|
+
OpenCV.invoke('approxPolyDP', contourToUse, approx, epsilon, true);
|
|
452
453
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
OpenCV.invoke('approxPolyDP', contourToUse, approx, epsilon, true);
|
|
454
|
+
step = `contour_${i}_toJS_attempt_${attempt}`;
|
|
455
|
+
reportStage(step);
|
|
456
|
+
const approxValue = OpenCV.toJSValue(approx);
|
|
457
|
+
const candidate = Array.isArray(approxValue?.array) ? approxValue.array : [];
|
|
458
458
|
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
if (candidate.length === 4) {
|
|
462
|
-
approxArray = candidate as Array<{ x: number; y: number }>;
|
|
463
|
-
break;
|
|
464
|
-
}
|
|
459
|
+
if (__DEV__) {
|
|
460
|
+
console.log('[DocScanner] approx length', candidate.length, 'epsilon', epsilon);
|
|
465
461
|
}
|
|
466
462
|
|
|
467
|
-
if (
|
|
468
|
-
|
|
463
|
+
if (candidate.length === 4) {
|
|
464
|
+
approxArray = candidate as Array<{ x: number; y: number }>;
|
|
465
|
+
break;
|
|
469
466
|
}
|
|
467
|
+
}
|
|
470
468
|
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
continue;
|
|
476
|
-
}
|
|
469
|
+
// Only proceed if we found exactly 4 corners
|
|
470
|
+
if (approxArray.length !== 4) {
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
477
473
|
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
y: pt.y / ratio,
|
|
481
|
-
}));
|
|
474
|
+
step = `contour_${i}_convex`;
|
|
475
|
+
reportStage(step);
|
|
482
476
|
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
477
|
+
// Validate points before processing
|
|
478
|
+
const isValidPoint = (pt: { x: number; y: number }) => {
|
|
479
|
+
return typeof pt.x === 'number' && typeof pt.y === 'number' &&
|
|
480
|
+
!isNaN(pt.x) && !isNaN(pt.y) &&
|
|
481
|
+
isFinite(pt.x) && isFinite(pt.y);
|
|
482
|
+
};
|
|
486
483
|
|
|
487
|
-
|
|
488
|
-
if (
|
|
489
|
-
|
|
484
|
+
if (!approxArray.every(isValidPoint)) {
|
|
485
|
+
if (__DEV__) {
|
|
486
|
+
console.warn('[DocScanner] invalid points in approxArray', approxArray);
|
|
490
487
|
}
|
|
488
|
+
continue;
|
|
489
|
+
}
|
|
491
490
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
continue;
|
|
497
|
-
}
|
|
498
|
-
const aspectRatio = maxEdge / Math.max(minEdge, 1);
|
|
499
|
-
if (!Number.isFinite(aspectRatio) || aspectRatio > 8.5) {
|
|
500
|
-
continue;
|
|
501
|
-
}
|
|
491
|
+
const points: Point[] = approxArray.map((pt: { x: number; y: number }) => ({
|
|
492
|
+
x: pt.x / ratio,
|
|
493
|
+
y: pt.y / ratio,
|
|
494
|
+
}));
|
|
502
495
|
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (
|
|
496
|
+
// Verify the quadrilateral is convex (valid document shape)
|
|
497
|
+
try {
|
|
498
|
+
if (!isConvexQuadrilateral(points)) {
|
|
499
|
+
if (__DEV__) {
|
|
500
|
+
console.log('[DocScanner] not convex, skipping:', points);
|
|
501
|
+
}
|
|
506
502
|
continue;
|
|
507
503
|
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
area: quadAreaValue,
|
|
512
|
-
};
|
|
513
|
-
|
|
514
|
-
if (!bestLocal || candidate.area > bestLocal.area) {
|
|
515
|
-
bestLocal = candidate;
|
|
504
|
+
} catch (err) {
|
|
505
|
+
if (__DEV__) {
|
|
506
|
+
console.warn('[DocScanner] convex check error:', err, 'points:', points);
|
|
516
507
|
}
|
|
508
|
+
continue;
|
|
517
509
|
}
|
|
518
510
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
const working = OpenCV.invoke('clone', baseMat);
|
|
525
|
-
step = `${label}_canny`;
|
|
526
|
-
reportStage(step);
|
|
527
|
-
OpenCV.invoke('Canny', working, working, low, high);
|
|
528
|
-
OpenCV.invoke('morphologyEx', working, working, MorphTypes.MORPH_CLOSE, element);
|
|
529
|
-
considerCandidate(evaluateContours(working, label));
|
|
530
|
-
};
|
|
531
|
-
|
|
532
|
-
const runAdaptive = (label: string, blockSize: number, c: number) => {
|
|
533
|
-
'worklet';
|
|
534
|
-
const working = OpenCV.invoke('clone', baseMat);
|
|
535
|
-
step = `${label}_adaptive`;
|
|
536
|
-
reportStage(step);
|
|
537
|
-
OpenCV.invoke('adaptiveThreshold', working, working, 255, ADAPTIVE_THRESH_GAUSSIAN_C, THRESH_BINARY, blockSize, c);
|
|
538
|
-
OpenCV.invoke('morphologyEx', working, working, MorphTypes.MORPH_CLOSE, element);
|
|
539
|
-
considerCandidate(evaluateContours(working, label));
|
|
540
|
-
};
|
|
541
|
-
|
|
542
|
-
const runOtsu = () => {
|
|
543
|
-
'worklet';
|
|
544
|
-
const working = OpenCV.invoke('clone', baseMat);
|
|
545
|
-
step = 'otsu_threshold';
|
|
546
|
-
reportStage(step);
|
|
547
|
-
OpenCV.invoke('threshold', working, working, 0, 255, THRESH_BINARY | THRESH_OTSU);
|
|
548
|
-
OpenCV.invoke('morphologyEx', working, working, MorphTypes.MORPH_CLOSE, element);
|
|
549
|
-
considerCandidate(evaluateContours(working, 'otsu'));
|
|
550
|
-
};
|
|
551
|
-
|
|
552
|
-
runCanny('canny_primary', CANNY_LOW, CANNY_HIGH);
|
|
553
|
-
runCanny('canny_soft', Math.max(6, CANNY_LOW * 0.6), Math.max(CANNY_LOW * 1.2, CANNY_HIGH * 0.75));
|
|
554
|
-
runCanny('canny_hard', Math.max(12, CANNY_LOW * 1.1), CANNY_HIGH * 1.25);
|
|
555
|
-
|
|
556
|
-
runAdaptive('adaptive_19', 19, 7);
|
|
557
|
-
runAdaptive('adaptive_23', 23, 5);
|
|
558
|
-
runOtsu();
|
|
511
|
+
if (area > maxArea) {
|
|
512
|
+
best = points;
|
|
513
|
+
maxArea = area;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
559
516
|
|
|
560
517
|
step = 'clearBuffers';
|
|
561
518
|
reportStage(step);
|
|
562
519
|
OpenCV.clearBuffers();
|
|
563
520
|
step = 'updateQuad';
|
|
564
521
|
reportStage(step);
|
|
565
|
-
updateQuad(
|
|
522
|
+
updateQuad(best);
|
|
566
523
|
} catch (error) {
|
|
567
524
|
reportError(step, error);
|
|
568
525
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
OpenCV,
|
|
3
|
-
ColorConversionCodes,
|
|
4
|
-
MorphShapes,
|
|
5
|
-
MorphTypes,
|
|
6
|
-
RetrievalModes,
|
|
7
|
-
ContourApproximationModes,
|
|
8
|
-
} from 'react-native-fast-opencv';
|
|
9
|
-
import type { Point } from '../types';
|
|
10
|
-
|
|
11
|
-
type MatLike = { release?: () => void } | null | undefined;
|
|
12
|
-
|
|
13
|
-
type Size = { width: number; height: number };
|
|
14
|
-
|
|
15
|
-
type Quad = [Point, Point, Point, Point];
|
|
16
|
-
|
|
17
|
-
const OUTPUT_SIZE: Size = { width: 800, height: 600 };
|
|
18
|
-
const MIN_AREA = 1000;
|
|
19
|
-
const GAUSSIAN_KERNEL: Size = { width: 5, height: 5 };
|
|
20
|
-
const MORPH_KERNEL: Size = { width: 3, height: 3 };
|
|
21
|
-
const ADAPTIVE_THRESH_GAUSSIAN_C = 1;
|
|
22
|
-
const THRESH_BINARY = 0;
|
|
23
|
-
|
|
24
|
-
const safeRelease = (mat: MatLike) => {
|
|
25
|
-
if (mat && typeof mat.release === 'function') {
|
|
26
|
-
mat.release();
|
|
27
|
-
}
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const normalizePoint = (value: unknown): Point | null => {
|
|
31
|
-
if (!value) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (Array.isArray(value) && value.length >= 2) {
|
|
36
|
-
const [x, y] = value;
|
|
37
|
-
const px = Number(x);
|
|
38
|
-
const py = Number(y);
|
|
39
|
-
return Number.isFinite(px) && Number.isFinite(py) ? { x: px, y: py } : null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (typeof value === 'object') {
|
|
43
|
-
const maybePoint = value as { x?: unknown; y?: unknown };
|
|
44
|
-
const px = Number(maybePoint.x);
|
|
45
|
-
const py = Number(maybePoint.y);
|
|
46
|
-
return Number.isFinite(px) && Number.isFinite(py) ? { x: px, y: py } : null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
return null;
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
const toPointArray = (value: unknown): Point[] | null => {
|
|
53
|
-
if (!value) {
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (Array.isArray(value)) {
|
|
58
|
-
const points = value.map(normalizePoint).filter((point): point is Point => point !== null);
|
|
59
|
-
return points.length === value.length ? points : null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
if (typeof value === 'object') {
|
|
63
|
-
const mat = value as { data32F?: number[]; data64F?: number[]; data32S?: number[] };
|
|
64
|
-
const data = mat.data32F ?? mat.data64F ?? mat.data32S;
|
|
65
|
-
if (!data || data.length < 8) {
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const points: Point[] = [];
|
|
70
|
-
for (let i = 0; i + 1 < data.length; i += 2) {
|
|
71
|
-
const x = data[i];
|
|
72
|
-
const y = data[i + 1];
|
|
73
|
-
if (Number.isFinite(x) && Number.isFinite(y)) {
|
|
74
|
-
points.push({ x, y });
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return points.length >= 4 ? points.slice(0, 4) : null;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
return null;
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
const ensureQuad = (points: Point[] | null): Quad | null => {
|
|
85
|
-
if (!points || points.length < 4) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const quad: Quad = [points[0], points[1], points[2], points[3]];
|
|
90
|
-
for (const point of quad) {
|
|
91
|
-
if (typeof point.x !== 'number' || typeof point.y !== 'number') {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return quad;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Provides document detection utilities using react-native-fast-opencv.
|
|
101
|
-
*/
|
|
102
|
-
export class DocumentDetector {
|
|
103
|
-
private static initialized = false;
|
|
104
|
-
|
|
105
|
-
/** Initialize OpenCV runtime once */
|
|
106
|
-
static async initialize(): Promise<void> {
|
|
107
|
-
if (!DocumentDetector.initialized) {
|
|
108
|
-
await OpenCV.initialize();
|
|
109
|
-
DocumentDetector.initialized = true;
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/** Find document contours and return the largest quadrilateral */
|
|
114
|
-
static async findDocumentContours(imagePath: string): Promise<Quad | null> {
|
|
115
|
-
await DocumentDetector.initialize();
|
|
116
|
-
|
|
117
|
-
let image: MatLike;
|
|
118
|
-
let gray: MatLike;
|
|
119
|
-
let blurred: MatLike;
|
|
120
|
-
let thresh: MatLike;
|
|
121
|
-
let morphed: MatLike;
|
|
122
|
-
let kernel: MatLike;
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
image = OpenCV.imread(imagePath);
|
|
126
|
-
gray = OpenCV.cvtColor(image, ColorConversionCodes.COLOR_BGR2GRAY);
|
|
127
|
-
blurred = OpenCV.GaussianBlur(gray, GAUSSIAN_KERNEL, 0);
|
|
128
|
-
|
|
129
|
-
thresh = OpenCV.adaptiveThreshold(
|
|
130
|
-
blurred,
|
|
131
|
-
255,
|
|
132
|
-
ADAPTIVE_THRESH_GAUSSIAN_C,
|
|
133
|
-
THRESH_BINARY,
|
|
134
|
-
11,
|
|
135
|
-
2,
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
kernel = OpenCV.getStructuringElement(MorphShapes.MORPH_RECT, MORPH_KERNEL);
|
|
139
|
-
morphed = OpenCV.morphologyEx(thresh, MorphTypes.MORPH_CLOSE, kernel);
|
|
140
|
-
|
|
141
|
-
const contours = OpenCV.findContours(
|
|
142
|
-
morphed,
|
|
143
|
-
RetrievalModes.RETR_EXTERNAL,
|
|
144
|
-
ContourApproximationModes.CHAIN_APPROX_SIMPLE,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
let largestQuad: Quad | null = null;
|
|
148
|
-
let maxArea = 0;
|
|
149
|
-
|
|
150
|
-
for (const contour of contours) {
|
|
151
|
-
const area = OpenCV.contourArea(contour);
|
|
152
|
-
if (area <= maxArea || area <= MIN_AREA) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
const perimeter = OpenCV.arcLength(contour, true);
|
|
157
|
-
const approx = OpenCV.approxPolyDP(contour, 0.02 * perimeter, true);
|
|
158
|
-
const points = ensureQuad(toPointArray(approx));
|
|
159
|
-
|
|
160
|
-
safeRelease(approx as MatLike);
|
|
161
|
-
|
|
162
|
-
if (!points) {
|
|
163
|
-
continue;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
maxArea = area;
|
|
167
|
-
largestQuad = points;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
return largestQuad;
|
|
171
|
-
} catch (error) {
|
|
172
|
-
if (__DEV__) {
|
|
173
|
-
console.error('[DocumentDetector] findDocumentContours error', error);
|
|
174
|
-
}
|
|
175
|
-
return null;
|
|
176
|
-
} finally {
|
|
177
|
-
safeRelease(kernel);
|
|
178
|
-
safeRelease(morphed);
|
|
179
|
-
safeRelease(thresh);
|
|
180
|
-
safeRelease(blurred);
|
|
181
|
-
safeRelease(gray);
|
|
182
|
-
safeRelease(image);
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Apply a perspective transform using detected corners */
|
|
187
|
-
static async perspectiveTransform(
|
|
188
|
-
imagePath: string,
|
|
189
|
-
corners: Quad,
|
|
190
|
-
outputSize: Size = OUTPUT_SIZE,
|
|
191
|
-
): Promise<string | null> {
|
|
192
|
-
await DocumentDetector.initialize();
|
|
193
|
-
|
|
194
|
-
let image: MatLike;
|
|
195
|
-
let srcPoints: MatLike;
|
|
196
|
-
let dstPoints: MatLike;
|
|
197
|
-
let transformMatrix: MatLike;
|
|
198
|
-
let warped: MatLike;
|
|
199
|
-
|
|
200
|
-
try {
|
|
201
|
-
image = OpenCV.imread(imagePath);
|
|
202
|
-
|
|
203
|
-
srcPoints = OpenCV.matFromArray(4, 1, OpenCV.CV_32FC2, [
|
|
204
|
-
corners[0].x,
|
|
205
|
-
corners[0].y,
|
|
206
|
-
corners[1].x,
|
|
207
|
-
corners[1].y,
|
|
208
|
-
corners[2].x,
|
|
209
|
-
corners[2].y,
|
|
210
|
-
corners[3].x,
|
|
211
|
-
corners[3].y,
|
|
212
|
-
]);
|
|
213
|
-
|
|
214
|
-
dstPoints = OpenCV.matFromArray(4, 1, OpenCV.CV_32FC2, [
|
|
215
|
-
0,
|
|
216
|
-
0,
|
|
217
|
-
outputSize.width,
|
|
218
|
-
0,
|
|
219
|
-
outputSize.width,
|
|
220
|
-
outputSize.height,
|
|
221
|
-
0,
|
|
222
|
-
outputSize.height,
|
|
223
|
-
]);
|
|
224
|
-
|
|
225
|
-
transformMatrix = OpenCV.getPerspectiveTransform(srcPoints, dstPoints);
|
|
226
|
-
|
|
227
|
-
warped = OpenCV.warpPerspective(image, transformMatrix, outputSize);
|
|
228
|
-
|
|
229
|
-
const outputPath = imagePath.replace(/\.jpg$/i, '_normalized.jpg');
|
|
230
|
-
OpenCV.imwrite(outputPath, warped);
|
|
231
|
-
|
|
232
|
-
return outputPath;
|
|
233
|
-
} catch (error) {
|
|
234
|
-
if (__DEV__) {
|
|
235
|
-
console.error('[DocumentDetector] perspectiveTransform error', error);
|
|
236
|
-
}
|
|
237
|
-
return null;
|
|
238
|
-
} finally {
|
|
239
|
-
safeRelease(warped);
|
|
240
|
-
safeRelease(transformMatrix);
|
|
241
|
-
safeRelease(dstPoints);
|
|
242
|
-
safeRelease(srcPoints);
|
|
243
|
-
safeRelease(image);
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
/** Detect document and apply normalization */
|
|
248
|
-
static async detectAndNormalize(imagePath: string, outputSize?: Size): Promise<string | null> {
|
|
249
|
-
try {
|
|
250
|
-
const corners = await DocumentDetector.findDocumentContours(imagePath);
|
|
251
|
-
if (!corners) {
|
|
252
|
-
if (__DEV__) {
|
|
253
|
-
console.log('[DocumentDetector] No document detected');
|
|
254
|
-
}
|
|
255
|
-
return null;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
return DocumentDetector.perspectiveTransform(imagePath, corners, outputSize ?? OUTPUT_SIZE);
|
|
259
|
-
} catch (error) {
|
|
260
|
-
if (__DEV__) {
|
|
261
|
-
console.error('[DocumentDetector] detectAndNormalize error', error);
|
|
262
|
-
}
|
|
263
|
-
return null;
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
/** Only detect document corners without transforming */
|
|
268
|
-
static async getDocumentBounds(imagePath: string): Promise<Quad | null> {
|
|
269
|
-
try {
|
|
270
|
-
return DocumentDetector.findDocumentContours(imagePath);
|
|
271
|
-
} catch (error) {
|
|
272
|
-
if (__DEV__) {
|
|
273
|
-
console.error('[DocumentDetector] getDocumentBounds error', error);
|
|
274
|
-
}
|
|
275
|
-
return null;
|
|
276
|
-
}
|
|
277
|
-
}
|
|
278
|
-
}
|