diff --git a/src/jscanify.js b/src/jscanify.js new file mode 100644 index 0000000..a2a4c6d --- /dev/null +++ b/src/jscanify.js @@ -0,0 +1,274 @@ +/*! jscanify v1.0.0 | (c) ColonelParrot and other contributors | MIT License */ + +(function (global, factory) { + typeof exports === "object" && typeof module !== "undefined" + ? (module.exports = factory()) + : typeof define === "function" && define.amd + ? define(factory) + : (global.jscanify = factory()); +})(this, function () { + "use strict"; + + /** + * 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() { } + + /** + * 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, + }; + } + + /** + * Finds the contour of the paper within the image + * @param {*} img image to process + * @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 = document.createElement("canvas"); + const ctx = canvas.getContext("2d"); + const img = cv.imread(image); + + const maxContour = this.findPaperContour(img); + if (maxContour) { + const { + topLeftCorner, + topRightCorner, + bottomLeftCorner, + bottomRightCorner, + } = this.getCornerPoints(maxContour, img); + + cv.imshow(canvas, 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 = document.createElement("canvas"); + + 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 newImg = document.createElement("img"); + newImg.src = canvas.toDataURL(); + newImg.onload = function () { + // flip unwarped image + + let ctx = canvas.getContext("2d"); + ctx.clearRect(0, 0, canvas.width, canvas.height); + canvas.width = resultWidth; + canvas.height = resultHeight; + ctx.setTransform(1, 0, 0, -1, 0, canvas.height); + + ctx.drawImage(newImg, 0, 0); + + ctx.setTransform(1, 0, 0, 1, 0, 0); + + img.delete(); + warpedDst.delete(); + onComplete(canvas); + }; + } + } + + if (typeof module !== "undefined") { + module.exports = { jscanify }; + } + return jscanify; +});