jscanify 1.0.0 → 1.1.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/README.md +11 -1
- package/docs/images/test/test-sized.png +0 -0
- package/docs/images/test/test2-sized.png +0 -0
- package/docs/images/test/test2.png +0 -0
- package/docs/index.css +141 -141
- package/docs/index.html +124 -105
- package/docs/script.js +42 -41
- package/install.md +5 -0
- package/package.json +9 -4
- package/src/jscanify-node.js +261 -0
- package/src/jscanify.js +271 -275
- package/src/opencv.js +48 -0
- package/test/tests.js +74 -0
- package/docs/images/test/test2.avif +0 -0
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "jscanify",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Open-source Javascript mobile document scanner.",
|
|
5
|
-
"main": "src/jscanify.js",
|
|
5
|
+
"main": "src/jscanify-node.js",
|
|
6
6
|
"directories": {
|
|
7
7
|
"doc": "docs"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"test": "
|
|
10
|
+
"test": "mocha"
|
|
11
11
|
},
|
|
12
12
|
"repository": {
|
|
13
13
|
"type": "git",
|
|
@@ -23,5 +23,10 @@
|
|
|
23
23
|
"bugs": {
|
|
24
24
|
"url": "https://github.com/ColonelParrot/jscanify/issues"
|
|
25
25
|
},
|
|
26
|
-
"homepage": "https://colonelparrot.github.io/jscanify/"
|
|
26
|
+
"homepage": "https://colonelparrot.github.io/jscanify/",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"canvas": "^2.11.2",
|
|
29
|
+
"jsdom": "^22.0.0",
|
|
30
|
+
"mocha": "^10.2.0"
|
|
31
|
+
}
|
|
27
32
|
}
|
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
/*! jscanify v1.1.0 | (c) ColonelParrot and other contributors | MIT License */
|
|
2
|
+
|
|
3
|
+
const { Canvas, createCanvas, Image, ImageData } = require("canvas");
|
|
4
|
+
const { JSDOM } = require("jsdom");
|
|
5
|
+
|
|
6
|
+
function installDOM() {
|
|
7
|
+
const dom = new JSDOM();
|
|
8
|
+
|
|
9
|
+
global.document = dom.window.document;
|
|
10
|
+
global.Image = Image;
|
|
11
|
+
global.HTMLCanvasElement = Canvas;
|
|
12
|
+
global.ImageData = ImageData;
|
|
13
|
+
global.HTMLImageElement = Image;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
let cv;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Calculates distance between two points. Each point must have `x` and `y` property
|
|
20
|
+
* @param {*} p1 point 1
|
|
21
|
+
* @param {*} p2 point 2
|
|
22
|
+
* @returns distance between two points
|
|
23
|
+
*/
|
|
24
|
+
function distance(p1, p2) {
|
|
25
|
+
return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
class jscanify {
|
|
29
|
+
constructor() {
|
|
30
|
+
installDOM();
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
loadOpenCV(callback) {
|
|
34
|
+
cv = require("./opencv");
|
|
35
|
+
cv["onRuntimeInitialized"] = () => {
|
|
36
|
+
callback(cv);
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Finds the contour of the paper within the image
|
|
42
|
+
* @param {*} img image to process (cv.Mat)
|
|
43
|
+
* @returns the biggest contour inside the image
|
|
44
|
+
*/
|
|
45
|
+
findPaperContour(img) {
|
|
46
|
+
const imgGray = new cv.Mat();
|
|
47
|
+
cv.cvtColor(img, imgGray, cv.COLOR_RGBA2GRAY);
|
|
48
|
+
|
|
49
|
+
const imgBlur = new cv.Mat();
|
|
50
|
+
cv.GaussianBlur(
|
|
51
|
+
imgGray,
|
|
52
|
+
imgBlur,
|
|
53
|
+
new cv.Size(5, 5),
|
|
54
|
+
0,
|
|
55
|
+
0,
|
|
56
|
+
cv.BORDER_DEFAULT
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
const imgThresh = new cv.Mat();
|
|
60
|
+
cv.threshold(imgBlur, imgThresh, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU);
|
|
61
|
+
|
|
62
|
+
let contours = new cv.MatVector();
|
|
63
|
+
let hierarchy = new cv.Mat();
|
|
64
|
+
|
|
65
|
+
cv.findContours(
|
|
66
|
+
imgThresh,
|
|
67
|
+
contours,
|
|
68
|
+
hierarchy,
|
|
69
|
+
cv.RETR_CCOMP,
|
|
70
|
+
cv.CHAIN_APPROX_SIMPLE
|
|
71
|
+
);
|
|
72
|
+
let maxArea = 0;
|
|
73
|
+
let maxContourIndex = -1;
|
|
74
|
+
for (let i = 0; i < contours.size(); ++i) {
|
|
75
|
+
let contourArea = cv.contourArea(contours.get(i));
|
|
76
|
+
if (contourArea > maxArea) {
|
|
77
|
+
maxArea = contourArea;
|
|
78
|
+
maxContourIndex = i;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const maxContour = contours.get(maxContourIndex);
|
|
83
|
+
|
|
84
|
+
imgGray.delete();
|
|
85
|
+
imgBlur.delete();
|
|
86
|
+
imgThresh.delete();
|
|
87
|
+
contours.delete();
|
|
88
|
+
hierarchy.delete();
|
|
89
|
+
return maxContour;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Highlights the paper detected inside the image.
|
|
94
|
+
* @param {*} image image to process
|
|
95
|
+
* @param {*} options options for highlighting. Accepts `color` and `thickness` parameter
|
|
96
|
+
* @returns `HTMLCanvasElement` with original image and paper highlighted
|
|
97
|
+
*/
|
|
98
|
+
highlightPaper(image, options) {
|
|
99
|
+
options = options || {};
|
|
100
|
+
options.color = options.color || "orange";
|
|
101
|
+
options.thickness = options.thickness || 10;
|
|
102
|
+
const canvas = createCanvas();
|
|
103
|
+
const ctx = canvas.getContext("2d");
|
|
104
|
+
const img = cv.imread(image);
|
|
105
|
+
|
|
106
|
+
const maxContour = this.findPaperContour(img);
|
|
107
|
+
cv.imshow(canvas, img);
|
|
108
|
+
if (maxContour) {
|
|
109
|
+
const {
|
|
110
|
+
topLeftCorner,
|
|
111
|
+
topRightCorner,
|
|
112
|
+
bottomLeftCorner,
|
|
113
|
+
bottomRightCorner,
|
|
114
|
+
} = this.getCornerPoints(maxContour, img);
|
|
115
|
+
|
|
116
|
+
if (
|
|
117
|
+
topLeftCorner &&
|
|
118
|
+
topRightCorner &&
|
|
119
|
+
bottomLeftCorner &&
|
|
120
|
+
bottomRightCorner
|
|
121
|
+
) {
|
|
122
|
+
ctx.strokeStyle = options.color;
|
|
123
|
+
ctx.lineWidth = options.thickness;
|
|
124
|
+
ctx.beginPath();
|
|
125
|
+
ctx.moveTo(...Object.values(topLeftCorner));
|
|
126
|
+
ctx.lineTo(...Object.values(topRightCorner));
|
|
127
|
+
ctx.lineTo(...Object.values(bottomRightCorner));
|
|
128
|
+
ctx.lineTo(...Object.values(bottomLeftCorner));
|
|
129
|
+
ctx.lineTo(...Object.values(topLeftCorner));
|
|
130
|
+
ctx.stroke();
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
img.delete();
|
|
135
|
+
return canvas;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Extracts and undistorts the image detected within the frame.
|
|
140
|
+
* @param {*} image image to process
|
|
141
|
+
* @param {*} resultWidth desired result paper width
|
|
142
|
+
* @param {*} resultHeight desired result paper height
|
|
143
|
+
* @param {*} onComplete callback with `HTMLCanvasElement` passed - the unwarped paper
|
|
144
|
+
* @param {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect
|
|
145
|
+
*/
|
|
146
|
+
extractPaper(image, resultWidth, resultHeight, onComplete, cornerPoints) {
|
|
147
|
+
const canvas = createCanvas();
|
|
148
|
+
const img = cv.imread(image);
|
|
149
|
+
const maxContour = this.findPaperContour(img);
|
|
150
|
+
const {
|
|
151
|
+
topLeftCorner,
|
|
152
|
+
topRightCorner,
|
|
153
|
+
bottomLeftCorner,
|
|
154
|
+
bottomRightCorner,
|
|
155
|
+
} = cornerPoints || this.getCornerPoints(maxContour, img);
|
|
156
|
+
let warpedDst = new cv.Mat();
|
|
157
|
+
let dsize = new cv.Size(resultWidth, resultHeight);
|
|
158
|
+
let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
159
|
+
topLeftCorner.x,
|
|
160
|
+
topLeftCorner.y,
|
|
161
|
+
topRightCorner.x,
|
|
162
|
+
topRightCorner.y,
|
|
163
|
+
bottomLeftCorner.x,
|
|
164
|
+
bottomLeftCorner.y,
|
|
165
|
+
bottomRightCorner.x,
|
|
166
|
+
bottomRightCorner.y,
|
|
167
|
+
]);
|
|
168
|
+
let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [
|
|
169
|
+
0,
|
|
170
|
+
0,
|
|
171
|
+
resultWidth,
|
|
172
|
+
0,
|
|
173
|
+
0,
|
|
174
|
+
resultHeight,
|
|
175
|
+
resultWidth,
|
|
176
|
+
resultHeight,
|
|
177
|
+
]);
|
|
178
|
+
let M = cv.getPerspectiveTransform(srcTri, dstTri);
|
|
179
|
+
cv.warpPerspective(
|
|
180
|
+
img,
|
|
181
|
+
warpedDst,
|
|
182
|
+
M,
|
|
183
|
+
dsize,
|
|
184
|
+
cv.INTER_LINEAR,
|
|
185
|
+
cv.BORDER_CONSTANT,
|
|
186
|
+
new cv.Scalar()
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
cv.imshow(canvas, warpedDst);
|
|
190
|
+
const canvasContext = canvas.getContext("2d");
|
|
191
|
+
canvasContext.save();
|
|
192
|
+
canvasContext.scale(1, -1);
|
|
193
|
+
canvasContext.drawImage(canvas, 0, -resultHeight);
|
|
194
|
+
canvasContext.restore();
|
|
195
|
+
|
|
196
|
+
img.delete();
|
|
197
|
+
warpedDst.delete();
|
|
198
|
+
onComplete(canvas);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Calculates the corner points of a contour.
|
|
203
|
+
* @param {*} contour contour from {@link findPaperContour}
|
|
204
|
+
* @returns object with properties `topLeftCorner`, `topRightCorner`, `bottomLeftCorner`, `bottomRightCorner`, each with `x` and `y` property
|
|
205
|
+
*/
|
|
206
|
+
getCornerPoints(contour) {
|
|
207
|
+
let rect = cv.minAreaRect(contour);
|
|
208
|
+
const center = rect.center;
|
|
209
|
+
|
|
210
|
+
let topLeftCorner;
|
|
211
|
+
let topLeftCornerDist = 0;
|
|
212
|
+
|
|
213
|
+
let topRightCorner;
|
|
214
|
+
let topRightCornerDist = 0;
|
|
215
|
+
|
|
216
|
+
let bottomLeftCorner;
|
|
217
|
+
let bottomLeftCornerDist = 0;
|
|
218
|
+
|
|
219
|
+
let bottomRightCorner;
|
|
220
|
+
let bottomRightCornerDist = 0;
|
|
221
|
+
|
|
222
|
+
for (let i = 0; i < contour.data32S.length; i += 2) {
|
|
223
|
+
const point = { x: contour.data32S[i], y: contour.data32S[i + 1] };
|
|
224
|
+
const dist = distance(point, center);
|
|
225
|
+
if (point.x < center.x && point.y > center.y) {
|
|
226
|
+
// top left
|
|
227
|
+
if (dist > topLeftCornerDist) {
|
|
228
|
+
topLeftCorner = point;
|
|
229
|
+
topLeftCornerDist = dist;
|
|
230
|
+
}
|
|
231
|
+
} else if (point.x > center.x && point.y > center.y) {
|
|
232
|
+
// top right
|
|
233
|
+
if (dist > topRightCornerDist) {
|
|
234
|
+
topRightCorner = point;
|
|
235
|
+
topRightCornerDist = dist;
|
|
236
|
+
}
|
|
237
|
+
} else if (point.x < center.x && point.y < center.y) {
|
|
238
|
+
// bottom left
|
|
239
|
+
if (dist > bottomLeftCornerDist) {
|
|
240
|
+
bottomLeftCorner = point;
|
|
241
|
+
bottomLeftCornerDist = dist;
|
|
242
|
+
}
|
|
243
|
+
} else if (point.x > center.x && point.y < center.y) {
|
|
244
|
+
// bottom right
|
|
245
|
+
if (dist > bottomRightCornerDist) {
|
|
246
|
+
bottomRightCorner = point;
|
|
247
|
+
bottomRightCornerDist = dist;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
topLeftCorner,
|
|
254
|
+
topRightCorner,
|
|
255
|
+
bottomLeftCorner,
|
|
256
|
+
bottomRightCorner,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
module.exports = jscanify;
|