mirror of
https://github.com/publiclab/image-sequencer.git
synced 2025-12-11 10:49:59 +01:00
* fix: edge detect horizontal angle fixed * test: fix edge detect tests Co-authored-by: Jeffrey Warren <jeff@unterbahn.com>
238 lines
7.7 KiB
JavaScript
238 lines
7.7 KiB
JavaScript
// 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]
|
|
];
|
|
|
|
module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, useHysteresis) {
|
|
let angles = [], grads = [], strongEdgePixels = [], weakEdgePixels = [], pixelsToBeSupressed = [];
|
|
|
|
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, pixelsToBeSupressed); // Non Maximum Suppression: Filter fine edges.
|
|
doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels, pixelsToBeSupressed); // 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){
|
|
const pi = Math.PI;
|
|
angle = angle > 0 ? angle : pi - Math.abs(angle); // Diagonally flip the angle if it is negative (since edge remains the same)
|
|
|
|
if (angle <= pi / 8 || angle > 7 * pi / 8) return 1;
|
|
else if (angle > pi / 8 && angle <= 3 * pi / 8) return 2;
|
|
else if (angle > 3 * pi / 8 && angle <= 5 * pi / 8) return 3;
|
|
else if (angle > 5 * pi / 8 && angle <= 7 * pi / 8) 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, pixelsToBeSupressed) {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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, pixelsToBeSupressed) {
|
|
|
|
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]);
|
|
}
|
|
});
|
|
}
|