// vim:ts=4:sw=4:expandtab 'use strict'; function plot(elementId, seriesList) { let plots = []; for (let i = 0; i < seriesList.length; i++) { let y = seriesList[i]; let x = []; for (let i = 0; i < y.length; i++) { x.push(i); } plots.push({x: x, y: y}); } Plotly.plot(document.getElementById(elementId), plots); } function removeColours(src, dst, threshold) { let from = src.data32S; let to = dst.data32S; let pixels = src.rows * src.cols; for (let i = 0; i < pixels; i++) { let pixel = from[i]; let r = pixel & 0xFF; let g = (pixel >> 8) & 0xFF; let b = (pixel >> 16) & 0xFF; to[i] = from[i] | (Math.max(r, g, b) - Math.min(r, g, b) > threshold ? 0xFFFFFF : 0); } } function preprocessImage(src, dst, gaussianBlurSize, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment, numDilations) { cv.GaussianBlur(src, dst, new cv.Size(gaussianBlurSize, gaussianBlurSize), 0); cv.adaptiveThreshold(dst, dst, 255, cv.ADAPTIVE_THRESH_GAUSSIAN_C, cv.THRESH_BINARY_INV, adaptiveThresholdBlockSize, adaptiveThresholdMeanAdjustment); let kernel = cv.getStructuringElement(cv.MORPH_CROSS, new cv.Size(3, 3)); try { cv.dilate(dst, dst, kernel, new cv.Point(-1, -1), numDilations); } finally { kernel.delete(); } } function morphOpenImage(src, dst, kernelSize, iterations) { let kernel = cv.getStructuringElement(cv.MORPH_RECT, kernelSize); try { cv.morphologyEx(src, dst, cv.MORPH_OPEN, kernel, new cv.Point(-1, -1), iterations); } finally { kernel.delete(); } } function findBiggestContour(img) { let contours = new cv.MatVector(); try { let hierarchy = new cv.Mat(); try { cv.findContours(img, contours, hierarchy, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE); let biggest = null; let maxArea = 0; for (let i = 0; i < contours.size(); i++) { let contour = contours.get(i); let area = cv.contourArea(contour); if (area > maxArea) { maxArea = area; if (biggest !== null) { biggest.delete(); } biggest = contour; } else { contour.delete(); } } return biggest; } finally { hierarchy.delete(); } } finally { contours.delete(); } } function erodeContour(imageSize, contour, kernelSize, iterations) { let contourImg = cv.Mat.zeros(imageSize.height, imageSize.width, cv.CV_8U); try { let contours = new cv.MatVector(); try { contours.push_back(contour); cv.drawContours(contourImg, contours, 0, new cv.Scalar(255), -1); } finally { contours.delete(); } morphOpenImage(contourImg, contourImg, new cv.Size(kernelSize, kernelSize), iterations); return findBiggestContour(contourImg); } finally { contourImg.delete(); } } function getContourCorners(imageSize, contour) { let topLeft = new cv.Point(imageSize.width, imageSize.height); let topRight = new cv.Point(-1, imageSize.height); let bottomLeft = new cv.Point(imageSize.width, -1); let bottomRight = new cv.Point(-1, -1); for (let i = 0; i < contour.rows; i++) { let vertex = new cv.Point(contour.data32S[i * 2], contour.data32S[i * 2 + 1]); let sum = vertex.x + vertex.y; let diff = vertex.x - vertex.y; if (sum < topLeft.x + topLeft.y) { topLeft = vertex; } if (sum > bottomRight.x + bottomRight.y) { bottomRight = vertex; } if (diff < bottomLeft.x - bottomLeft.y) { bottomLeft = vertex; } if (diff > topRight.x - topRight.y) { topRight = vertex; } } return [topLeft, topRight, bottomRight, bottomLeft]; } function segmentLength(p1, p2) { let dx = p1.x - p2.x; let dy = p1.y - p2.y; return Math.sqrt(dx ** 2 + dy ** 2); } function getLongestSide(corners) { let previous = corners[corners.length - 1]; let max = 0; for (let i = 0; i < corners.length; i++) { let current = corners[i]; let length = segmentLength(previous, current); if (length > max) { max = length; } previous = current; } return max; } function extractSquare(img, corners) { let longest = getLongestSide(corners); let end = longest - 1; let sourceRect = cv.matFromArray(4, 1, cv.CV_32FC2, [corners[0].x, corners[0].y, corners[1].x, corners[1].y, corners[2].x, corners[2].y, corners[3].x, corners[3].y]); try { let destRect = cv.matFromArray(4, 1, cv.CV_32FC2, [0, 0, end, 0, end, end, 0, end]); try { let m = cv.getPerspectiveTransform(sourceRect, destRect); try { let destImg = new cv.Mat(); try { cv.warpPerspective(img, destImg, m, new cv.Size(longest, longest)); return destImg; } catch (err) { destImg.delete(); throw err; } } finally { m.delete(); } } finally { destRect.delete(); } } finally { sourceRect.delete(); } } function indexOfMax(arr) { return arr.reduce((iMax, x, i, arr) => x > arr[iMax] ? i : iMax, 0); } function getFundamentalFrequency(mag) { mag = mag.slice(0, Math.ceil(mag.length / 2)); mag[0] = 0; return indexOfMax(mag); } function createMatVector(length) { let vec = new cv.MatVector(); try { let mat = new cv.Mat(); try { for (let i = 0; i < length; i++) { vec.push_back(mat); } } finally { mat.delete(); } return vec; } catch (err) { vec.delete(); throw err; } } function getLineFFT(img, lineDetectorElementSize, axis) { let lines = new cv.Mat(); try { morphOpenImage(img, lines, axis === 1 ? new cv.Size(lineDetectorElementSize, 1) : new cv.Size(1, lineDetectorElementSize), 1); let sums = new cv.Mat(); try { cv.reduce(lines, sums, axis, cv.REDUCE_SUM, cv.CV_32FC1); let fft = new cv.Mat(); try { cv.dft(sums, fft, cv.DFT_COMPLEX_OUTPUT, 0); return fft; } catch (err) { fft.delete(); throw err; } } finally { sums.delete(); } } finally { lines.delete(); } } function getFFTMagnitude(fft) { let planes = createMatVector(2); try { cv.split(fft, planes); let real = planes.get(0); try { let imag = planes.get(1); try { let ret = []; let length = Math.max(real.cols, real.rows); for (let i = 0; i < length; i++) { ret.push(Math.sqrt(real.data32F[i] ** 2 + imag.data32F[i] ** 2)) } return ret; } finally { imag.delete(); } } finally { real.delete(); } } finally { planes.delete(); } } function getLineFrequency(img, lineDetectorElementSize, axis) { let fft = getLineFFT(img, lineDetectorElementSize, axis); try { return getFundamentalFrequency(getFFTMagnitude(fft)); } finally { fft.delete(); } } function extractGridColours(img, numRows, numCols, samplingBlockSizeRatio) { let imageSize = img.size(); let rowOffset = Math.floor(imageSize.height * samplingBlockSizeRatio / numRows / 2); let rowHeight = 2 * rowOffset + 1; let colOffset = Math.floor(imageSize.width * samplingBlockSizeRatio / numCols / 2); let colWidth = 2 * colOffset + 1; let gridColours = []; for (let row = 0; row < numRows; row++) { let line = []; let y = Math.floor((row + 0.5) / numRows * imageSize.height) - rowOffset; for (let col = 0; col < numCols; col++) { let x = Math.floor((col + 0.5) / numCols * imageSize.width) - colOffset; let roi = img.roi(new cv.Rect(x, y, colWidth, rowHeight)); try { line.push(cv.mean(roi)[0]); } finally { roi.delete(); } } gridColours.push(line); } return gridColours; } function getGridColourThreshold(gridColours) { let colours = gridColours.reduce(function(acc, x) { return acc.concat(x); }, []).sort(function (a, b) { return a - b; }); let deltaMax = 0; let iMax; for (let i = 0; i < colours.length; i++) { let delta = colours[i] - colours[i - 1]; if (delta > deltaMax) { deltaMax = delta; iMax = i; } } return (colours[iMax] + colours[iMax - 1]) / 2; } function gridColoursToBlocks(gridColours, numRows, numCols, samplingThreshold) { let blocks = JSON.parse(JSON.stringify(gridColours)); let warning = false; let midpoint = Math.floor(numRows / 2) + (numRows % 2 > 0 ? 1 : 0); for (let row = 0; row < midpoint; row++) { for (let col = 0; col < numCols; col++) { // If there is an odd number of rows then row and row2 will point to // the same row when we reach the middle. Doesn't seem worth adding a // special case. let row2 = numRows - row - 1; let col2 = numCols - col - 1; let delta1 = gridColours[row][col] - samplingThreshold; let delta2 = gridColours[row2][col2] - samplingThreshold; let filled; if ((delta1 > 0) && (delta2 > 0)) { filled = false; } else if ((delta1 < 0) && (delta2 < 0)) { filled = true; } else { warning = true; if (Math.abs(delta1) > Math.abs(delta2)) { filled = delta1 < 0; } else { filled = delta2 < 0; } } blocks[row][col] = {filled: filled} blocks[row2][col2] = {filled: filled} } } let number = 1 for (let row = 0; row < numRows; row++) { for (let col = 0; col < numCols; col++) { if (! blocks[row][col].filled && ( (((col == 0) || blocks[row][col - 1].filled) && (col < numCols - 1) && ! blocks[row][col + 1].filled) || (((row == 0) || blocks[row - 1][col].filled) && (row < numRows - 1) && ! blocks[row + 1][col].filled) )) { blocks[row][col].number = number number += 1 } } } return {warning: warning, blocks: blocks}; } function drawGrid(canvas, blocks, numRows, numCols, gridLineThickness, gridSquareSize, gridBorderSize) { let step = gridSquareSize + gridLineThickness; let gridHeight = numRows * step + gridLineThickness; let gridWidth = numCols * step + gridLineThickness; canvas.width = 2 * gridBorderSize + gridWidth; canvas.height = 2 * gridBorderSize + gridHeight; let context = canvas.getContext('2d'); context.fillStyle = 'black'; context.fillRect(gridBorderSize, gridBorderSize, gridWidth, gridHeight); context.fillStyle = 'white'; for (let row = 0; row < numRows; row++) { let y = row * step + gridLineThickness + gridBorderSize; for (let col = 0; col < numCols; col++) { if (! blocks[row][col].filled) { let x = col * step + gridLineThickness + gridBorderSize; context.fillRect(x, y, gridSquareSize, gridSquareSize); } } } }