From 0f206c62ea1c3ba0fa492ed904676cf2c6f05249 Mon Sep 17 00:00:00 2001 From: ColonelParrot <65585002+ColonelParrot@users.noreply.github.com> Date: Mon, 8 May 2023 17:17:36 -0400 Subject: [PATCH] jscanify-node --- src/jscanify-node.js | 261 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 src/jscanify-node.js diff --git a/src/jscanify-node.js b/src/jscanify-node.js new file mode 100644 index 0000000..2351ee8 --- /dev/null +++ b/src/jscanify-node.js @@ -0,0 +1,261 @@ +/*! jscanify v1.1.0 | (c) ColonelParrot and other contributors | MIT License */ + +const { Canvas, createCanvas, Image, ImageData } = require("canvas"); +const { JSDOM } = require("jsdom"); + +function installDOM() { + const dom = new JSDOM(); + + global.document = dom.window.document; + global.Image = Image; + global.HTMLCanvasElement = Canvas; + global.ImageData = ImageData; + global.HTMLImageElement = Image; +} + +let cv; + +/** + * Calculates distance between two points. Each point must have `x` and `y` property + * @param {*} p1 point 1 + * @param {*} p2 point 2 + * @returns distance between two points + */ +function distance(p1, p2) { + return Math.sqrt((p1.x - p2.x) ** 2 + (p1.y - p2.y) ** 2); +} + +class jscanify { + constructor() { + installDOM(); + } + + loadOpenCV(callback) { + cv = require("./opencv"); + cv["onRuntimeInitialized"] = () => { + callback(cv); + }; + } + + /** + * Finds the contour of the paper within the image + * @param {*} img image to process (cv.Mat) + * @returns the biggest contour inside the image + */ + findPaperContour(img) { + const imgGray = new cv.Mat(); + cv.cvtColor(img, imgGray, cv.COLOR_RGBA2GRAY); + + const imgBlur = new cv.Mat(); + cv.GaussianBlur( + imgGray, + imgBlur, + new cv.Size(5, 5), + 0, + 0, + cv.BORDER_DEFAULT + ); + + const imgThresh = new cv.Mat(); + cv.threshold(imgBlur, imgThresh, 0, 255, cv.THRESH_BINARY + cv.THRESH_OTSU); + + let contours = new cv.MatVector(); + let hierarchy = new cv.Mat(); + + cv.findContours( + imgThresh, + contours, + hierarchy, + cv.RETR_CCOMP, + cv.CHAIN_APPROX_SIMPLE + ); + let maxArea = 0; + let maxContourIndex = -1; + for (let i = 0; i < contours.size(); ++i) { + let contourArea = cv.contourArea(contours.get(i)); + if (contourArea > maxArea) { + maxArea = contourArea; + maxContourIndex = i; + } + } + + const maxContour = contours.get(maxContourIndex); + + imgGray.delete(); + imgBlur.delete(); + imgThresh.delete(); + contours.delete(); + hierarchy.delete(); + return maxContour; + } + + /** + * Highlights the paper detected inside the image. + * @param {*} image image to process + * @param {*} options options for highlighting. Accepts `color` and `thickness` parameter + * @returns `HTMLCanvasElement` with original image and paper highlighted + */ + highlightPaper(image, options) { + options = options || {}; + options.color = options.color || "orange"; + options.thickness = options.thickness || 10; + const canvas = createCanvas(); + const ctx = canvas.getContext("2d"); + const img = cv.imread(image); + + const maxContour = this.findPaperContour(img); + cv.imshow(canvas, img); + if (maxContour) { + const { + topLeftCorner, + topRightCorner, + bottomLeftCorner, + bottomRightCorner, + } = this.getCornerPoints(maxContour, img); + + if ( + topLeftCorner && + topRightCorner && + bottomLeftCorner && + bottomRightCorner + ) { + ctx.strokeStyle = options.color; + ctx.lineWidth = options.thickness; + ctx.beginPath(); + ctx.moveTo(...Object.values(topLeftCorner)); + ctx.lineTo(...Object.values(topRightCorner)); + ctx.lineTo(...Object.values(bottomRightCorner)); + ctx.lineTo(...Object.values(bottomLeftCorner)); + ctx.lineTo(...Object.values(topLeftCorner)); + ctx.stroke(); + } + } + + img.delete(); + return canvas; + } + + /** + * Extracts and undistorts the image detected within the frame. + * @param {*} image image to process + * @param {*} resultWidth desired result paper width + * @param {*} resultHeight desired result paper height + * @param {*} onComplete callback with `HTMLCanvasElement` passed - the unwarped paper + * @param {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect + */ + extractPaper(image, resultWidth, resultHeight, onComplete, cornerPoints) { + const canvas = createCanvas(); + const img = cv.imread(image); + const maxContour = this.findPaperContour(img); + const { + topLeftCorner, + topRightCorner, + bottomLeftCorner, + bottomRightCorner, + } = cornerPoints || this.getCornerPoints(maxContour, img); + let warpedDst = new cv.Mat(); + let dsize = new cv.Size(resultWidth, resultHeight); + let srcTri = cv.matFromArray(4, 1, cv.CV_32FC2, [ + topLeftCorner.x, + topLeftCorner.y, + topRightCorner.x, + topRightCorner.y, + bottomLeftCorner.x, + bottomLeftCorner.y, + bottomRightCorner.x, + bottomRightCorner.y, + ]); + let dstTri = cv.matFromArray(4, 1, cv.CV_32FC2, [ + 0, + 0, + resultWidth, + 0, + 0, + resultHeight, + resultWidth, + resultHeight, + ]); + let M = cv.getPerspectiveTransform(srcTri, dstTri); + cv.warpPerspective( + img, + warpedDst, + M, + dsize, + cv.INTER_LINEAR, + cv.BORDER_CONSTANT, + new cv.Scalar() + ); + + cv.imshow(canvas, warpedDst); + const canvasContext = canvas.getContext("2d"); + canvasContext.save(); + canvasContext.scale(1, -1); + canvasContext.drawImage(canvas, 0, -resultHeight); + canvasContext.restore(); + + img.delete(); + warpedDst.delete(); + onComplete(canvas); + } + + /** + * Calculates the corner points of a contour. + * @param {*} contour contour from {@link findPaperContour} + * @returns object with properties `topLeftCorner`, `topRightCorner`, `bottomLeftCorner`, `bottomRightCorner`, each with `x` and `y` property + */ + getCornerPoints(contour) { + let rect = cv.minAreaRect(contour); + const center = rect.center; + + let topLeftCorner; + let topLeftCornerDist = 0; + + let topRightCorner; + let topRightCornerDist = 0; + + let bottomLeftCorner; + let bottomLeftCornerDist = 0; + + let bottomRightCorner; + let bottomRightCornerDist = 0; + + for (let i = 0; i < contour.data32S.length; i += 2) { + const point = { x: contour.data32S[i], y: contour.data32S[i + 1] }; + const dist = distance(point, center); + if (point.x < center.x && point.y > center.y) { + // top left + if (dist > topLeftCornerDist) { + topLeftCorner = point; + topLeftCornerDist = dist; + } + } else if (point.x > center.x && point.y > center.y) { + // top right + if (dist > topRightCornerDist) { + topRightCorner = point; + topRightCornerDist = dist; + } + } else if (point.x < center.x && point.y < center.y) { + // bottom left + if (dist > bottomLeftCornerDist) { + bottomLeftCorner = point; + bottomLeftCornerDist = dist; + } + } else if (point.x > center.x && point.y < center.y) { + // bottom right + if (dist > bottomRightCornerDist) { + bottomRightCorner = point; + bottomRightCornerDist = dist; + } + } + } + + return { + topLeftCorner, + topRightCorner, + bottomLeftCorner, + bottomRightCorner, + }; + } +} + +module.exports = jscanify;