/*! jscanify v1.2.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.hypot(p1.x - p2.x, p1.y - p2.y); } class jscanify { constructor() {} /** * 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(canvas, ctx, options) { options = options || {}; options.color = options.color || "#009fe3"; options.thickness = options.thickness || 10; const img = cv.imread(canvas); const maxContour = this.findPaperContour(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(); } /** * 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 {*} cornerPoints optional custom corner points, in case automatic corner points are incorrect * @returns `HTMLCanvasElement` containing undistorted image */ extractPaper(image) { const A4_WIDTH = 2480; const A4_HEIGHT = 3508; const ID_CARD_WIDTH = 540; const ID_CARD_HEIGHT = 856; const canvas = document.createElement("canvas"); const img = cv.imread(image); const maxContour = this.findPaperContour(img); const { topLeftCorner, topRightCorner, bottomLeftCorner, bottomRightCorner } = this.getCornerPoints(maxContour); const width = distance(topLeftCorner, topRightCorner); const height = distance(topLeftCorner, bottomLeftCorner); let resultWidth, resultHeight; if (height > width) { if (Math.abs(height - ID_CARD_HEIGHT) < Math.abs(height - A4_HEIGHT)) { resultWidth = ID_CARD_WIDTH; resultHeight = ID_CARD_HEIGHT; } else { resultWidth = A4_WIDTH; resultHeight = A4_HEIGHT; } } else { if (Math.abs(width - ID_CARD_WIDTH) < Math.abs(width - A4_WIDTH)) { resultWidth = ID_CARD_HEIGHT; resultHeight = ID_CARD_WIDTH; } else { resultWidth = A4_HEIGHT; resultHeight = A4_WIDTH; } } 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); img.delete(); warpedDst.delete(); return 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, }; } } return jscanify; });