mirror of
https://github.com/publiclab/image-sequencer.git
synced 2025-12-15 21:00:02 +01:00
Complete Edge Detect Module (#771)
* update dist Signed-off-by: tech4GT <varun.gupta1798@gmail.com> dist update Revert "dist update" This reverts commit 9ee2a987e8f978961656ae8f71f6e6702bbbd30d. * fix insert step button * add icon * add dist * use normal functions * use normal functions * add new function * add dist * remove console logs * changes * refactor * add dist * Bump looks-same from 5.0.2 to 6.0.0 (#729) Bumps [looks-same](https://github.com/gemini-testing/looks-same) from 5.0.2 to 6.0.0. - [Release notes](https://github.com/gemini-testing/looks-same/releases) - [Changelog](https://github.com/gemini-testing/looks-same/blob/master/CHANGELOG.md) - [Commits](https://github.com/gemini-testing/looks-same/compare/v5.0.2...v6.0.0) Signed-off-by: dependabot[bot] <support@dependabot.com> * Setup UI testing (#720) * default sequencer ui test * default step ui test suite * intermediate step ui test * preview ui test suite * url methods test suite * add set url params method test suite * argument call tests * test directory refactor * travis fix * CLI code refactor (#665) * CLI refactor * es6 rollback * Travis fix * syntax fix * clustered require statements * travis debug * travis debug * Added line that selects the "More modules..." after adding a step. (#713) * Added line making module selection correct after adding a step * Added line with appropriate module selection * Add: Module tests (#748) * WIP * module testing harness * adjustments * Fix choose file option and insert step functionality (#712) * Fix choose file option * changes * Parse info.json to set module defaults (#650) * changes * changes * changes * changes * changes * changes * Update package-lock.json * Bump jsqr from 1.1.1 to 1.2.0 (#749) Bumps [jsqr](https://github.com/cozmo/jsQR) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/cozmo/jsQR/releases) - [Commits](https://github.com/cozmo/jsQR/commits) Signed-off-by: dependabot[bot] <support@dependabot.com> * Bump jsqr from 1.1.1 to 1.2.0 (#749) Bumps [jsqr](https://github.com/cozmo/jsQR) from 1.1.1 to 1.2.0. - [Release notes](https://github.com/cozmo/jsQR/releases) - [Commits](https://github.com/cozmo/jsQR/commits) Signed-off-by: dependabot[bot] <support@dependabot.com> * dist * changes * working * final touches * revert mapTypes * proper revert * Fix arctangent * working hysteresis * hysteresis optional
This commit is contained in:
committed by
Jeffrey Warren
parent
e1a113cde1
commit
c6457323cc
@@ -1,174 +1,201 @@
|
||||
const _ = require('lodash')
|
||||
// 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]
|
||||
];
|
||||
|
||||
//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, inBrowser) {
|
||||
let angles = [], mags = [], strongEdgePixels = [], weakEdgePixels = [], notInUI = !inBrowser;
|
||||
for (var x = 0; x < pixels.shape[0]; x++) {
|
||||
angles.push([]);
|
||||
mags.push([]);
|
||||
for (var y = 0; y < pixels.shape[1]; y++) {
|
||||
var result = changePixel(
|
||||
pixels,
|
||||
pixels.get(x, y, 0),
|
||||
pixels.get(x, y, 3),
|
||||
x,
|
||||
y
|
||||
);
|
||||
let pixel = result.pixel;
|
||||
module.exports = function(pixels, highThresholdRatio, lowThresholdRatio, hysteresis) {
|
||||
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(
|
||||
pixels,
|
||||
x,
|
||||
y
|
||||
);
|
||||
let pixel = result.pixel;
|
||||
|
||||
pixels.set(x, y, 0, pixel[0]);
|
||||
pixels.set(x, y, 1, pixel[1]);
|
||||
pixels.set(x, y, 2, pixel[2]);
|
||||
pixels.set(x, y, 3, pixel[3]);
|
||||
|
||||
mags.slice(-1)[0].push(pixel[3]);
|
||||
angles.slice(-1)[0].push(result.angle);
|
||||
}
|
||||
grads.slice(-1)[0].push(pixel[3]);
|
||||
angles.slice(-1)[0].push(result.angle);
|
||||
}
|
||||
nonMaxSupress(pixels, mags, angles);
|
||||
doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, mags, strongEdgePixels, weakEdgePixels);
|
||||
return pixels;
|
||||
}
|
||||
nonMaxSupress(pixels, grads, angles);
|
||||
doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels);
|
||||
if(hysteresis.toLowerCase() == 'true') hysteresis(strongEdgePixels, weakEdgePixels);
|
||||
|
||||
strongEdgePixels.forEach(pixel => preserve(pixels, pixel));
|
||||
weakEdgePixels.forEach(pixel => supress(pixels, pixel));
|
||||
pixelsToBeSupressed.forEach(pixel => supress(pixels, pixel));
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
//changepixel function that convolutes every pixel (sobel filter)
|
||||
function changePixel(pixels, val, a, x, y) {
|
||||
let magX = 0.0;
|
||||
for (let a = 0; a < 3; a++) {
|
||||
for (let b = 0; b < 3; b++) {
|
||||
|
||||
let xn = x + a - 1;
|
||||
let yn = y + b - 1;
|
||||
|
||||
magX += pixels.get(xn, yn, 0) * kernelx[a][b];
|
||||
}
|
||||
}
|
||||
let magY = 0.0;
|
||||
for (let a = 0; a < 3; a++) {
|
||||
for (let b = 0; b < 3; b++) {
|
||||
|
||||
let xn = x + a - 1;
|
||||
let yn = y + b - 1;
|
||||
|
||||
magY += pixels.get(xn, yn, 0) * kernely[a][b];
|
||||
}
|
||||
}
|
||||
let mag = Math.sqrt(Math.pow(magX, 2) + Math.pow(magY, 2));
|
||||
let angle = Math.atan2(magY, magX);
|
||||
return {
|
||||
pixel:
|
||||
[val, val, val, mag],
|
||||
angle: angle
|
||||
};
|
||||
function supress(pixels, pixel) {
|
||||
pixels.set(pixel[0], pixel[1], 0, 0);
|
||||
pixels.set(pixel[0], pixel[1], 1, 0);
|
||||
pixels.set(pixel[0], pixel[1], 2, 0);
|
||||
pixels.set(pixel[0], pixel[1], 3, 255);
|
||||
}
|
||||
|
||||
//Non Maximum Supression without interpolation
|
||||
function nonMaxSupress(pixels, mags, angles) {
|
||||
|
||||
angles = angles.map((arr) => arr.map(convertToDegrees));
|
||||
|
||||
for (let i = 1; i < pixels.shape[0] - 1; i++) {
|
||||
for (let j = 1; j < pixels.shape[1] - 1; j++) {
|
||||
|
||||
let angle = angles[i][j];
|
||||
let pixel = pixels.get(i, j);
|
||||
|
||||
if ((angle >= -22.5 && angle <= 22.5) ||
|
||||
(angle < -157.5 && angle >= -180))
|
||||
|
||||
if ((mags[i][j] >= mags[i][j + 1]) &&
|
||||
(mags[i][j] >= mags[i][j - 1]))
|
||||
pixels.set(i, j, 3, mags[i][j]);
|
||||
else
|
||||
pixels.set(i, j, 3, 0);
|
||||
|
||||
else if ((angle >= 22.5 && angle <= 67.5) ||
|
||||
(angle < -112.5 && angle >= -157.5))
|
||||
|
||||
if ((mags[i][j] >= mags[i + 1][j + 1]) &&
|
||||
(mags[i][j] >= mags[i - 1][j - 1]))
|
||||
pixels.set(i, j, 3, mags[i][j]);
|
||||
else
|
||||
pixels.set(i, j, 3, 0);
|
||||
|
||||
else if ((angle >= 67.5 && angle <= 112.5) ||
|
||||
(angle < -67.5 && angle >= -112.5))
|
||||
|
||||
if ((mags[i][i] >= mags[i + 1][j]) &&
|
||||
(mags[i][j] >= mags[i][j]))
|
||||
pixels.set(i, j, 3, mags[i][j]);
|
||||
else
|
||||
pixels.set(i, j, 3, 0);
|
||||
|
||||
else if ((angle >= 112.5 && angle <= 157.5) ||
|
||||
(angle < -22.5 && angle >= -67.5))
|
||||
|
||||
if ((mags[i][j] >= mags[i + 1][j - 1]) &&
|
||||
(mags[i][j] >= mags[i - 1][j + 1]))
|
||||
pixels.set(i, j, 3, mags[i][j]);
|
||||
else
|
||||
pixels.set(i, j, 3, 0);
|
||||
|
||||
}
|
||||
}
|
||||
function preserve(pixels, pixel) {
|
||||
pixels.set(pixel[0], pixel[1], 0, 255);
|
||||
pixels.set(pixel[0], pixel[1], 1, 255);
|
||||
pixels.set(pixel[0], pixel[1], 2, 255);
|
||||
pixels.set(pixel[0], pixel[1], 3, 255);
|
||||
}
|
||||
//Converts radians to degrees
|
||||
|
||||
// sobelFilter function that convolves sobel kernel over every pixel
|
||||
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)) {
|
||||
gradX += pixels.get(xn+1, yn+1, 0) * kernelx[a][b];
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
}
|
||||
|
||||
function isOutOfBounds(pixels, x, y){
|
||||
return ((x < 0) || (y < 0) || (x >= pixels.shape[0]) || (y >= pixels.shape[1]));
|
||||
}
|
||||
|
||||
const removeElem = (arr = [], elem) => {
|
||||
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){
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Converts radians to degrees
|
||||
var convertToDegrees = radians => (radians * 180) / Math.PI;
|
||||
|
||||
//Finds the max value in a 2d array like mags
|
||||
// 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, mags, strongEdgePixels, weakEdgePixels) {
|
||||
// Applies the double threshold to the image
|
||||
function doubleThreshold(pixels, highThresholdRatio, lowThresholdRatio, grads, strongEdgePixels, weakEdgePixels) {
|
||||
|
||||
const highThreshold = findMaxInMatrix(mags) * highThresholdRatio;
|
||||
const lowThreshold = highThreshold * lowThresholdRatio;
|
||||
const highThreshold = findMaxInMatrix(grads) * highThresholdRatio,
|
||||
lowThreshold = highThreshold * lowThresholdRatio;
|
||||
|
||||
for (let i = 0; i < pixels.shape[0]; i++) {
|
||||
for (let j = 0; j < pixels.shape[1]; j++) {
|
||||
let pixelPos = [i, j];
|
||||
for (let x = 0; x < pixels.shape[0]; x++) {
|
||||
for (let y = 0; y < pixels.shape[1]; y++) {
|
||||
let pixelPos = [x, y];
|
||||
|
||||
mags[i][j] > lowThreshold
|
||||
? mags[i][j] > highThreshold
|
||||
? strongEdgePixels.push(pixelPos)
|
||||
: weakEdgePixels.push(pixelPos)
|
||||
: pixels.set(i, j, 3, 0);
|
||||
if (grads[x][y] > lowThreshold){
|
||||
if (grads[x][y] > highThreshold) {
|
||||
strongEdgePixels.push(pixelPos);
|
||||
}
|
||||
else {
|
||||
weakEdgePixels.push(pixelPos);
|
||||
}
|
||||
}
|
||||
else {
|
||||
pixelsToBeSupressed.push(pixelPos);
|
||||
}
|
||||
}
|
||||
|
||||
strongEdgePixels.forEach(pix => pixels.set(pix[0], pix[1], 3, 255));
|
||||
}
|
||||
}
|
||||
|
||||
// hysteresis edge tracking algorithm -- not working as of now
|
||||
/* function hysteresis(pixels) {
|
||||
function getNeighbouringPixelPositions(pixelPosition) {
|
||||
let x = pixelPosition[0], y = pixelPosition[1]
|
||||
return [[x + 1, y + 1],
|
||||
[x + 1, y],
|
||||
[x + 1, y - 1],
|
||||
[x, y + 1],
|
||||
[x, y - 1],
|
||||
[x - 1, y + 1],
|
||||
[x - 1, y],
|
||||
[x - 1, y - 1]]
|
||||
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]);
|
||||
}
|
||||
|
||||
//This can potentially be improved see https://en.wikipedia.org/wiki/Connected-component_labeling
|
||||
for (weakPixel in weakEdgePixels) {
|
||||
let neighbourPixels = getNeighbouringPixelPositions(weakEdgePixels[weakPixel])
|
||||
for (pixel in neighbourPixels) {
|
||||
if (strongEdgePixels.find(el => _.isEqual(el, neighbourPixels[pixel]))) {
|
||||
pixels.set(weakPixel[0], weakPixel[1], 3, 255)
|
||||
weakEdgePixels.splice(weakPixel, weakPixel)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
weakEdgePixels.forEach(pix => pixels.set(pix[0], pix[1], 3, 0))
|
||||
return pixels
|
||||
} */
|
||||
|
||||
|
||||
|
||||
else if (weakEdgePixels.includes([x, y+1])) {
|
||||
removeElem(weakEdgePixels, [x, y+1]);
|
||||
}
|
||||
else if(weakEdgePixels.includes([x, y-1])) {
|
||||
removeElem(weakEdgePixels, [x, y-1]);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user