// Read More: https://en.wikipedia.org/wiki/Canny_edge_detector const pixelSetter = require('../../util/pixelSetter.js'); // Define kernels for the sobel filter. const kernelx = [ [-1, 0, 1], [-2, 0, 2], [-1, 0, 1] ], kernely = [ [-1, -2, -1], [ 0, 0, 0], [ 1, 2, 1] ]; let pixelsToBeSupressed = []; module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, useHysteresis) { let angles = [], grads = [], strongEdgePixels = [], weakEdgePixels = []; for (var x = 0; x < pixels.shape[0]; x++) { grads.push([]); angles.push([]); for (var y = 0; y < pixels.shape[1]; y++) { var result = sobelFilter( // Convolves the sobel filter on every pixel pixels, x, y ); let pixel = result.pixel; grads.slice(-1)[0].push(pixel[3]); angles.slice(-1)[0].push(result.angle); } } nonMaxSupress(pixels, grads, angles); // Non Maximum Suppression: Filter fine edges. doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels); // Double Threshold: Categorizes edges into strong and weak edges based on two thresholds. if(useHysteresis.toLowerCase() == 'true') hysteresis(strongEdgePixels, weakEdgePixels); // Optional Hysteresis (very slow) to minimize edges generated due to noise. strongEdgePixels.forEach(pixel => preserve(pixels, pixel)); // Makes the strong edges White. weakEdgePixels.forEach(pixel => supress(pixels, pixel)); // Makes the weak edges black(bg color) after filtering. pixelsToBeSupressed.forEach(pixel => supress(pixels, pixel)); // Makes the rest of the image black. return pixels; }; /** * @method supress * @description Supresses (fills with background color) the specified (non-edge)pixel. * @param {Object} pixels ndarry of pixels * @param {Float32Array} pixel Pixel coordinates * @returns {Null} */ function supress(pixels, pixel) { pixelSetter(pixel[0], pixel[1], [0, 0, 0, 255], pixels); } /** * @method preserve * @description Preserve the specified pixel(of an edge). * @param {Object} pixels ndarray of pixels * @param {*} pixel Pixel coordinates * @returns {Null} */ function preserve(pixels, pixel) { pixelSetter(pixel[0], pixel[1], [255, 255, 255, 255], pixels); } /** * @method sobelFiler * @description Runs the sobel filter on the specified and neighbouring pixels. * @param {Object} pixels ndarray of pixels * @param {Number} x x-coordinate of the pixel * @param {Number} y y-coordinate of the pixel * @returns {Object} Object containing the gradient and angle. */ function sobelFilter(pixels, x, y) { let val = pixels.get(x, y, 0), gradX = 0.0, gradY = 0.0; for (let a = 0; a < 3; a++) { for (let b = 0; b < 3; b++) { let xn = x + a - 1, yn = y + b - 1; if (isOutOfBounds(pixels, xn, yn)) { // Fallback for coordinates which lie outside the image. gradX += pixels.get(xn + 1, yn + 1, 0) * kernelx[a][b]; // Fallback to nearest pixel gradY += pixels.get(xn + 1, yn + 1, 0) * kernely[a][b]; } else { gradX += pixels.get(xn, yn, 0) * kernelx[a][b]; gradY += pixels.get(xn, yn, 0) * kernely[a][b]; } } } const grad = Math.sqrt(Math.pow(gradX, 2) + Math.pow(gradY, 2)), angle = Math.atan2(gradY, gradX); return { pixel: [val, val, val, grad], angle: angle }; } /** * @method categorizeAngle * @description Categorizes the given angle into 4 catagories according to the Category Map given below. * @param {Number} angle Angle in degrees * @returns {Number} Category number of the given angle */ function categorizeAngle(angle){ if ((angle >= -22.5 && angle <= 22.5) || (angle < -157.5 && angle >= -180)) return 1; else if ((angle >= 22.5 && angle <= 67.5) || (angle < -112.5 && angle >= -157.5)) return 2; else if ((angle >= 67.5 && angle <= 112.5) || (angle < -67.5 && angle >= -112.5)) return 3; else if ((angle >= 112.5 && angle <= 157.5) || (angle < -22.5 && angle >= -67.5)) return 4; /* Category Map * 1 => E-W * 2 => NE-SW * 3 => N-S * 4 => NW-SE */ } /** * @method isOutOfBounds * @description Checks whether the given coordinates lie outside the bounds of the image. Used for error handling in convolution. * @param {Object} pixels ndarray of pixels * @param {*} x x-coordinate of the pixel * @param {*} y y-coordinate of the pixel * @returns {Boolean} True if the given coordinates are out of bounds. */ function isOutOfBounds(pixels, x, y){ return ((x < 0) || (y < 0) || (x >= pixels.shape[0]) || (y >= pixels.shape[1])); } const removeElem = (arr = [], elem) => { // Removes the specified element from the given array. return arr = arr.filter((arrelem) => { return arrelem !== elem; }); }; // Non Maximum Supression without interpolation. function nonMaxSupress(pixels, grads, angles) { angles = angles.map((arr) => arr.map(convertToDegrees)); for (let x = 0; x < pixels.shape[0]; x++) { for (let y = 0; y < pixels.shape[1]; y++) { let angleCategory = categorizeAngle(angles[x][y]); if (!isOutOfBounds(pixels, x - 1, y - 1) && !isOutOfBounds(pixels, x + 1, y + 1)){ switch (angleCategory){ // Non maximum suppression according to angle category case 1: if (!((grads[x][y] >= grads[x][y + 1]) && (grads[x][y] >= grads[x][y - 1]))) { pixelsToBeSupressed.push([x, y]); } break; case 2: if (!((grads[x][y] >= grads[x + 1][y + 1]) && (grads[x][y] >= grads[x - 1][y - 1]))){ pixelsToBeSupressed.push([x, y]); } break; case 3: if (!((grads[x][y] >= grads[x + 1][y]) && (grads[x][y] >= grads[x - 1][y]))) { pixelsToBeSupressed.push([x, y]); } break; case 4: if (!((grads[x][y] >= grads[x + 1][y - 1]) && (grads[x][y] >= grads[x - 1][y + 1]))) { pixelsToBeSupressed.push([x, y]); } break; } } } } } /** * @method convertToDegrees * @description Converts the given angle(in radians) to degrees. * @param {Number} radians Angle in radians * @returns {Number} Angle in degrees */ var convertToDegrees = radians => (radians * 180) / Math.PI; // Finds the max value in a 2d array like grads. var findMaxInMatrix = arr => Math.max(...arr.map(el => el.map(val => val ? val : 0)).map(el => Math.max(...el))); // Applies the double threshold to the image. function doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels) { const highThreshold = findMaxInMatrix(grads) * highThresholdRatio, // High Threshold relative to the strongest edge lowThreshold = highThreshold * lowThresholdRatio; // Low threshold relative to high threshold for (let x = 0; x < pixels.shape[0]; x++) { for (let y = 0; y < pixels.shape[1]; y++) { let pixelPos = [x, y]; if (grads[x][y] > lowThreshold){ if (grads[x][y] > highThreshold) { strongEdgePixels.push(pixelPos); } else { weakEdgePixels.push(pixelPos); } } else { pixelsToBeSupressed.push(pixelPos); } } } } /** * @method hysteresis * @description Filters weak edge pixels that are not connected to a strong edge pixel. * @param {Float32array} strongEdgePixels 2D array of strong edge pixel coordinates * @param {*} weakEdgePixels 2D array of weak edge pixel coordinated */ function hysteresis(strongEdgePixels, weakEdgePixels){ strongEdgePixels.forEach(pixel => { let x = pixel[0], y = pixel[1]; if (weakEdgePixels.includes([x + 1, y])) { removeElem(weakEdgePixels, [x + 1, y]); } else if (weakEdgePixels.includes([x - 1, y])) { removeElem(weakEdgePixels, [x - 1, y]); } else if (weakEdgePixels.includes([x, y + 1])) { removeElem(weakEdgePixels, [x, y + 1]); } else if(weakEdgePixels.includes([x, y - 1])) { removeElem(weakEdgePixels, [x, y - 1]); } }); }