├── .gitignore ├── README.md ├── dist ├── index.js ├── main.js └── worker.js ├── index.html ├── package.json ├── src ├── index.ts ├── main.ts └── worker.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # js-canny-edge-detector 2 | 3 | > Example of Canny edge detection algorithm in javascript 4 | 5 | This is an implementation Canny Edge Detection algorithm in JavaScript. It's really just for fun. The story behind it is - I found an old faculty project written in C#, and decided to rewrite it in JS. I did it one evening, and it works! :D 6 | 7 | P.S. You can see the original C# implementation we did here - [https://github.com/petarjs/cs-canny-edge-detector](https://github.com/petarjs/cs-canny-edge-detector)! 8 | 9 | ## Demo 10 | 11 | See it in action at 12 | [https://petarjs.github.io/js-canny-edge-detector/](https://petarjs.github.io/js-canny-edge-detector/) 13 | 14 | ## Usage 15 | 16 | First, you load the worker: 17 | 18 | ```js 19 | let worker = new Worker('./dist/worker.js') 20 | ``` 21 | 22 | Then, to set up the worker, you need to send the command `appData`. 23 | 24 | ```js 25 | worker.postMessage({ 26 | cmd: 'appData', 27 | data: { 28 | width: window.appData.width, 29 | height: window.appData.height, 30 | ut: window.appData.ut, 31 | lt: window.appData.lt 32 | } 33 | }) 34 | ``` 35 | 36 | The command requires the following settings in the `data` object: 37 | 38 | - `width` - Width of the image we're going to work on 39 | - `height` - Height of the image we're going to work on 40 | - `ut` - Upper treshold for edge detection 41 | - `lt` - Lower treshold for edge detection 42 | 43 | Setting `ut` and `lt` allows you to make them configurable from the UI. If `ut` and `lt` are not provided, tresholds will be automatically determined. 44 | 45 | Finally, you need to provide the image to work on to the worker: 46 | 47 | ```js 48 | worker.postMessage({ 49 | cmd: 'imgData', 50 | data: pixels 51 | }) 52 | ``` 53 | 54 | `pixels` must be of type `ImageData`. You can get it from the canvas into which the image is loaded: 55 | 56 | ```js 57 | const imgd = canvasFrom 58 | .getContext('2d') 59 | .getImageData(0, 0, width, height) 60 | 61 | const imageData = imgd.data 62 | ``` 63 | 64 | ## Install 65 | 66 | Not sure why you'd use this in a project, but if you really really want to... 67 | With [npm](https://npmjs.org/) installed, run 68 | 69 | ``` 70 | $ npm install petarjs/js-canny-edge-detector 71 | ``` 72 | 73 | And refer to the `index.html` in the repo to find the example of usage. 74 | 75 | ## See Also 76 | 77 | - [Canny Edge Detector on Wikipedia](https://en.wikipedia.org/wiki/Canny_edge_detector) 78 | - [Implementation with similar results as this one](https://github.com/yuta1984/CannyJS) 79 | - [Another, more awesome implementation](https://github.com/cmisenas/canny-edge-detection) 80 | 81 | ## License 82 | 83 | MIT 84 | 85 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | window.appData = {}; 2 | const canvasFrom = document.querySelector('.image--from'); 3 | const canvasGrayscale = document.querySelector('.image--grayscale'); 4 | const canvasResult = document.querySelector('.image--result'); 5 | const canvasBlurred = document.querySelector('.image--blurred'); 6 | const canvasXDerived = document.querySelector('.image--x-derived'); 7 | const canvasYDerived = document.querySelector('.image--y-derived'); 8 | const $file = document.querySelector('.js_image'); 9 | const $submit = document.querySelector('.js_submit'); 10 | const $controls = document.querySelector('.js_controls'); 11 | const $imageNav = document.querySelector('.js_image-nav'); 12 | const $status = document.querySelector('.js_status'); 13 | const $ut = document.querySelector('.js_ut'); 14 | const $lt = document.querySelector('.js_lt'); 15 | const $cancel = document.querySelector('.js_cancel'); 16 | const $result = document.querySelector('.result'); 17 | let worker; 18 | function initWorker() { 19 | worker = new Worker('./dist/worker.js'); 20 | worker.addEventListener('message', onWorkerMessage, false); 21 | } 22 | $file.addEventListener('change', event => { 23 | const file = $file.files[0]; 24 | readFileAsDataURL(file) 25 | .then(loadImage) 26 | .then(setCanvasSizeFromImage(canvasFrom)) 27 | .then(drawImageOnCanvas(canvasFrom)) 28 | .then(setCanvasSizeFromImage(canvasGrayscale)) 29 | .then(setCanvasSizeFromImage(canvasResult)) 30 | .then(setCanvasSizeFromImage(canvasBlurred)) 31 | .then(setCanvasSizeFromImage(canvasYDerived)) 32 | .then(setCanvasSizeFromImage(canvasXDerived)) 33 | .then((img) => { 34 | window.appData = { 35 | img, 36 | width: img.naturalWidth, 37 | height: img.naturalHeight 38 | }; 39 | }) 40 | .then(() => { 41 | [...document.querySelectorAll('canvas')].forEach(canvas => { 42 | let newWidth = Math.min($result.clientWidth, canvas.width); 43 | canvas.style.width = newWidth + 'px'; 44 | canvas.style.height = newWidth * (canvas.height / canvas.width) + 'px'; 45 | }); 46 | }) 47 | .then(showControls); 48 | }); 49 | $submit.addEventListener('click', event => { 50 | window.appData.ut = parseFloat($ut.value); 51 | window.appData.lt = parseFloat($lt.value); 52 | initWorker(); 53 | worker.postMessage({ 54 | cmd: 'appData', 55 | data: { 56 | width: window.appData.width, 57 | height: window.appData.height, 58 | ut: window.appData.ut, 59 | lt: window.appData.lt 60 | } 61 | }); 62 | const imgd = canvasFrom 63 | .getContext('2d') 64 | .getImageData(0, 0, window.appData.width, window.appData.height); 65 | const pixels = imgd.data; 66 | setProcessingStatus('1/7 Loaded image data'); 67 | blockControls(); 68 | resetImageNav(); 69 | worker.postMessage({ cmd: 'imgData', data: pixels }); 70 | }); 71 | $cancel.addEventListener('click', event => { 72 | worker.terminate(); 73 | worker.removeEventListener('message', onWorkerMessage, false); 74 | setProcessingStatus(''); 75 | unblockControls(); 76 | initWorker(); 77 | resetImageNav(); 78 | }); 79 | function showControls() { 80 | $status.style.display = 'inline-block'; 81 | $controls.style.display = 'inline-block'; 82 | $imageNav.classList.add('image-nav--active'); 83 | $result.style.height = canvasFrom.style.height; 84 | resetImageNav(); 85 | setProcessingStatus('Waiting for start.'); 86 | } 87 | function blockControls() { 88 | $controls.classList.add('controls--blocked'); 89 | $cancel.style.display = 'inline-block'; 90 | } 91 | function unblockControls() { 92 | $controls.classList.remove('controls--blocked'); 93 | $cancel.style.display = ''; 94 | } 95 | function resetImageNav() { 96 | [...document.querySelectorAll('.image-nav__item--active')].forEach(el => el.classList.remove('image-nav__item--active')); 97 | document.querySelector(`[data-target="js_image--from"]`).classList.add('image-nav__item--active'); 98 | } 99 | function setProcessingStatus(status) { 100 | window.appData.status = status; 101 | $status.innerText = status; 102 | } 103 | function activateImage(className) { 104 | let imageNavItem = document.querySelector(`[data-target="${className}"]`); 105 | if (imageNavItem) { 106 | imageNavItem.classList.add('image-nav__item--active'); 107 | } 108 | document.querySelector(`.${className}`).classList.add(`image--active`); 109 | } 110 | function showThresholds(thresh) { 111 | $ut.value = thresh.ut; 112 | $lt.value = thresh.lt; 113 | } 114 | function onWorkerMessage(e) { 115 | const drawBytesOnCanvasForImg = drawBytesOnCanvas(window.appData.width, window.appData.height); 116 | if (e.data.type === 'grayscale') { 117 | setProcessingStatus('2/7 Converted to Grayscale'); 118 | drawBytesOnCanvasForImg(canvasGrayscale, e.data.data); 119 | activateImage('js_image--grayscale'); 120 | } 121 | else if (e.data.type === 'normalized') { 122 | setProcessingStatus('3/7 Normalized pixel values'); 123 | } 124 | else if (e.data.type === 'blurred') { 125 | setProcessingStatus('4/7 Blurred image'); 126 | drawBytesOnCanvasForImg(canvasBlurred, e.data.data); 127 | activateImage('js_image--blurred'); 128 | } 129 | else if (e.data.type === 'xAxis') { 130 | setProcessingStatus('5/7 Created X axis derivation'); 131 | drawBytesOnCanvasForImg(canvasXDerived, e.data.data); 132 | activateImage('js_image--x-derived'); 133 | } 134 | else if (e.data.type === 'yAxis') { 135 | setProcessingStatus('6/7 Created Y axis derivation'); 136 | drawBytesOnCanvasForImg(canvasYDerived, e.data.data); 137 | activateImage('js_image--y-derived'); 138 | } 139 | else if (e.data.type === 'gradientMagnitude') { 140 | setProcessingStatus('7/7 Calculated Gradient magnitude'); 141 | drawBytesOnCanvasForImg(canvasResult, e.data.data); 142 | showThresholds(e.data.threshold); 143 | activateImage('js_image--result'); 144 | setProcessingStatus('Done!'); 145 | unblockControls(); 146 | } 147 | } 148 | document.querySelector('.js_image-nav').addEventListener('click', e => { 149 | let eventTarget = e.target; 150 | if (eventTarget.classList.contains('image-nav__item--active')) { 151 | let target = eventTarget.dataset.target; 152 | Array.from(document.querySelectorAll('.image--active')).forEach(el => el.classList.remove('image--active')); 153 | document.querySelector(`.${target}`).classList.add(`image--active`); 154 | } 155 | }); 156 | -------------------------------------------------------------------------------- /dist/main.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const MAX_PRECISION = false; 3 | const precision = 2; 4 | const gaussMatrix = [ 5 | [0.0121, 0.0261, 0.0337, 0.0261, 0.0121], 6 | [0.0261, 0.0561, 0.0724, 0.0561, 0.0261], 7 | [0.0337, 0.0724, 0.0935, 0.0724, 0.0337], 8 | [0.0261, 0.0561, 0.0724, 0.0561, 0.0261], 9 | [0.0121, 0.0261, 0.0337, 0.0261, 0.0121] 10 | ]; 11 | const xMatrix = [[1, 0, -1], [2, 0, -2], [1, 0, -1]]; 12 | const yMatrix = [[-1, -2, -1], [0, 0, 0], [1, 2, 1]]; 13 | const MAX_IMAGE_HEIGHT = 300; 14 | function curry(f, n) { 15 | var args = Array.prototype.slice.call(arguments, 0); 16 | if (typeof n === 'undefined') 17 | args[1] = f.length; 18 | if (n === args.length - 2) 19 | return f.apply(undefined, args.slice(2)); 20 | return function () { 21 | return curry.apply(undefined, args.concat(Array.prototype.slice.call(arguments, 0))); 22 | }; 23 | } 24 | function loadImage(imageUrl) { 25 | return new Promise((resolve, reject) => { 26 | console.time('loadImage'); 27 | const img = new Image(); 28 | img.src = imageUrl; 29 | img.crossOrigin = 'Anonymous'; 30 | img.onload = function () { 31 | console.timeEnd('loadImage'); 32 | resolve(img); 33 | }; 34 | }); 35 | } 36 | function readFileAsDataURL(file) { 37 | return new Promise((resolve, reject) => { 38 | var reader = new FileReader(); 39 | reader.onloadend = function () { 40 | resolve(reader.result); 41 | }; 42 | if (file) { 43 | reader.readAsDataURL(file); 44 | } 45 | else { 46 | reject(''); 47 | } 48 | }); 49 | } 50 | function _drawImageOnCanvas(canvas, image) { 51 | canvas.getContext('2d').drawImage(image, 0, 0); 52 | return image; 53 | } 54 | var drawImageOnCanvas = curry(_drawImageOnCanvas); 55 | function _setCanvasSizeFromImage(canvas, image) { 56 | const ratio = image.naturalWidth / image.naturalHeight; 57 | canvas.style.width = ''; 58 | canvas.getContext('2d').clearRect(0, 0, image.width, image.height); 59 | canvas.height = image.height; 60 | canvas.width = image.width; 61 | return image; 62 | } 63 | var setCanvasSizeFromImage = curry(_setCanvasSizeFromImage); 64 | function _drawBytesOnCanvas(width, height, canvas, bytes) { 65 | canvas 66 | .getContext('2d') 67 | .putImageData(new ImageData(new Uint8ClampedArray(bytes), width, height), 0, 0); 68 | } 69 | var drawBytesOnCanvas = curry(_drawBytesOnCanvas); 70 | function toGrayscale(bytes, width, height) { 71 | console.time('toGrayscale'); 72 | const grayscale = []; 73 | for (let i = 0; i < bytes.length; i += 4) { 74 | var gray = .299 * bytes[i + 2] + .587 * bytes[i + 1] + .114 * bytes[i]; 75 | grayscale.push(gray); 76 | } 77 | console.timeEnd('toGrayscale'); 78 | return grayscale; 79 | } 80 | function _toConvolution(width, height, kernel, radius, bytes) { 81 | console.time('toConvolution'); 82 | const convolution = []; 83 | let newValue, idxX, idxY, kernx, kerny; 84 | for (let i = 0; i < width; i++) { 85 | for (let j = 0; j < height; j++) { 86 | newValue = 0; 87 | for (let innerI = i - radius; innerI < i + radius + 1; innerI++) { 88 | for (let innerJ = j - radius; innerJ < j + radius + 1; innerJ++) { 89 | idxX = (innerI + width) % width; 90 | idxY = (innerJ + height) % height; 91 | kernx = innerI - (i - radius); 92 | kerny = innerJ - (j - radius); 93 | newValue += bytes[idxY * width + idxX] * kernel[kernx][kerny]; 94 | } 95 | } 96 | convolution[j * width + i] = newValue; 97 | } 98 | } 99 | console.time('toConvolution'); 100 | return convolution; 101 | } 102 | const toConvolution = curry(_toConvolution); 103 | /** 104 | * From image bytes (0 - 255) to values between 0 and 1 105 | * @param {Array} bytes 106 | * @return {Array} normalized values 107 | */ 108 | function toNormalized(bytes) { 109 | console.time('toNormalized'); 110 | const normalized = []; 111 | for (let i = 0; i < bytes.length; i += 4) { 112 | normalized.push(bytes[i] / 255); 113 | } 114 | console.timeEnd('toNormalized'); 115 | return normalized; 116 | } 117 | /** 118 | * From normalized array that has values from 0 to 1 119 | * to image data with values between 0 and 255 120 | * @param {Array} normalized 121 | * @return {Array} denormlized 122 | */ 123 | function toDenormalized(normalized) { 124 | console.time('toDenormalized'); 125 | const denormalized = normalized.map(value => value * 255); 126 | console.timeEnd('toDenormalized'); 127 | return denormalized; 128 | } 129 | function toGradientMagnitude(xDerived, yDerived, width, height, lt = 0, ut = 0) { 130 | console.time('toGradientMagnitude'); 131 | const gradientMagnitude = []; 132 | const gradientDirection = []; 133 | let index; 134 | let pom; 135 | for (let y = 0; y < height; y++) { 136 | for (let x = 0; x < width; x++) { 137 | index = y * width + x; 138 | gradientMagnitude[index] = Math.sqrt(xDerived[index] * xDerived[index] + yDerived[index] * yDerived[index]); 139 | pom = Math.atan2(xDerived[index], yDerived[index]); 140 | if ((pom >= -Math.PI / 8 && pom < Math.PI / 8) || (pom <= -7 * Math.PI / 8 && pom > 7 * Math.PI / 8)) { 141 | gradientDirection[index] = 0; 142 | } 143 | else if ((pom >= Math.PI / 8 && pom < 3 * Math.PI / 8) || (pom <= -5 * Math.PI / 8 && pom > -7 * Math.PI / 8)) { 144 | gradientDirection[index] = Math.PI / 4; 145 | } 146 | else if ((pom >= 3 * Math.PI / 8 && pom <= 5 * Math.PI / 8) || (-3 * Math.PI / 8 >= pom && pom > -5 * Math.PI / 8)) { 147 | gradientDirection[index] = Math.PI / 2; 148 | } 149 | else if ((pom < -Math.PI / 8 && pom >= -3 * Math.PI / 8) || (pom > 5 * Math.PI / 8 && pom <= 7 * Math.PI / 8)) { 150 | gradientDirection[index] = -Math.PI / 4; 151 | } 152 | } 153 | } 154 | const max = getMax(gradientMagnitude); 155 | const gradientMagnitudeCapped = gradientMagnitude.map(x => x / max); 156 | if (!ut && !lt) { 157 | let res = getTresholds(gradientMagnitudeCapped); 158 | ut = res.ut; 159 | lt = res.lt; 160 | } 161 | const gradientMagnitudeLt = gradientMagnitudeCapped.map(value => value < lt ? 0 : value); 162 | for (var y = 1; y < height - 1; y++) { 163 | for (var x = 1; x < width - 1; x++) { 164 | index = y * width + x; 165 | if (gradientDirection[index] == 0 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[y * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[y * width + x + 1])) 166 | gradientMagnitudeLt[index] = 0; 167 | else if (gradientDirection[index] == Math.PI / 2 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x] || gradientMagnitudeLt[(y + 1) * width + x] >= gradientMagnitudeLt[index])) 168 | gradientMagnitudeLt[index] = 0; 169 | else if (gradientDirection[index] == Math.PI / 4 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y + 1) * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x + 1])) 170 | gradientMagnitudeLt[index] = 0; 171 | else if (gradientDirection[index] == -Math.PI / 4 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y + 1) * width + x + 1])) 172 | gradientMagnitudeLt[index] = 0; 173 | } 174 | } 175 | for (let y = 2; y < height - 2; y++) { 176 | for (let x = 2; x < width - 2; x++) { 177 | if (gradientDirection[y * width + x] == 0) 178 | if (gradientMagnitudeLt[y * width + x - 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[y * width + x + 2] > gradientMagnitudeLt[y * width + x]) 179 | gradientMagnitudeLt[y * width + x] = 0; 180 | if (gradientDirection[y * width + x] == Math.PI / 2) 181 | if (gradientMagnitudeLt[(y - 2) * width + x] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y + 2) * width + x] > gradientMagnitudeLt[y * width + x]) 182 | gradientMagnitudeLt[y * width + x] = 0; 183 | if (gradientDirection[y * width + x] == Math.PI / 4) 184 | if (gradientMagnitudeLt[(y + 2) * width + x - 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y - 2) * width + x + 2] > gradientMagnitudeLt[y * width + x]) 185 | gradientMagnitudeLt[y * width + x] = 0; 186 | if (gradientDirection[y * width + x] == -Math.PI / 4) 187 | if (gradientMagnitudeLt[(y + 2) * width + x + 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y - 2) * width + x - 2] > gradientMagnitudeLt[y * width + x]) 188 | gradientMagnitudeLt[y * width + x] = 0; 189 | } 190 | } 191 | const gradientMagnitudeUt = gradientMagnitudeLt.map(value => value > ut ? 1 : value); 192 | // histeresis start 193 | let pomH = 0; 194 | let pomOld = -1; 195 | let pass = 0; 196 | let nastavi = true; 197 | let gradientMagnitudeCappedBottom = []; 198 | while (nastavi) { 199 | pass = pass + 1; 200 | pomOld = pomH; 201 | for (let y = 1; y < height - 1; y++) { 202 | for (let x = 1; x < width - 1; x++) { 203 | if (gradientMagnitudeUt[y * width + x] <= ut && gradientMagnitudeUt[y * width + x] >= lt) { 204 | let pom1 = gradientMagnitudeUt[(y - 1) * width + x - 1]; 205 | let pom2 = gradientMagnitudeUt[(y - 1) * width + x]; 206 | let pom3 = gradientMagnitudeUt[(y - 1) * width + x + 1]; 207 | let pom4 = gradientMagnitudeUt[y * width + x - 1]; 208 | let pom5 = gradientMagnitudeUt[y * width + x + 1]; 209 | let pom6 = gradientMagnitudeUt[(y + 1) * width + x - 1]; 210 | let pom7 = gradientMagnitudeUt[(y + 1) * width + x]; 211 | let pom8 = gradientMagnitudeUt[(y + 1) * width + x + 1]; 212 | if (pom1 === 1 || pom2 === 1 || pom3 === 1 || pom4 === 1 || pom5 === 1 || pom6 === 1 || pom7 === 1 || pom8 === 1) { 213 | gradientMagnitudeUt[y * width + x] = 1; 214 | pomH = pomH + 1; 215 | } 216 | } 217 | } 218 | } 219 | if (MAX_PRECISION) { 220 | nastavi = pomH != pomOld; 221 | } 222 | else { 223 | nastavi = pass <= precision; 224 | } 225 | gradientMagnitudeCappedBottom = gradientMagnitudeUt.map(x => x <= ut ? 0 : x); 226 | } 227 | console.timeEnd('toGradientMagnitude'); 228 | return { 229 | data: gradientMagnitudeCappedBottom, 230 | threshold: { 231 | ut: ut, 232 | lt: lt 233 | } 234 | }; 235 | } 236 | function getMax(values) { 237 | return values.reduce((prev, now) => now > prev ? now : prev, -1); 238 | } 239 | function getTresholds(gradientMagnitude) { 240 | let sum = 0; 241 | let count = 0; 242 | sum = gradientMagnitude.reduce((memo, x) => x + memo, 0); 243 | count = gradientMagnitude.filter(x => x !== 0).length; 244 | const ut = sum / count; 245 | const lt = 0.4 * ut; 246 | return { ut, lt }; 247 | } 248 | /** 249 | * Takes an array of values (0-255) and returns 250 | * an expaneded array [x, x, x, 255] for each value. 251 | * 252 | * @param {Array} values 253 | * @return {Array} expanded values 254 | */ 255 | function toPixels(values) { 256 | console.time('toPixels'); 257 | const expanded = []; 258 | values.forEach(x => { 259 | expanded.push(x); 260 | expanded.push(x); 261 | expanded.push(x); 262 | expanded.push(255); 263 | }); 264 | console.timeEnd('toPixels'); 265 | return expanded; 266 | } 267 | -------------------------------------------------------------------------------- /dist/worker.js: -------------------------------------------------------------------------------- 1 | importScripts('./main.js'); 2 | self.appData = {}; 3 | self.addEventListener('message', function (e) { 4 | const data = e.data; 5 | switch (data.cmd) { 6 | case 'appData': 7 | self.appData = data.data; 8 | break; 9 | case 'imgData': 10 | self.postMessage('WORKER STARTED'); 11 | self.imgData = data.data; 12 | start(); 13 | break; 14 | } 15 | }); 16 | function start() { 17 | const toConvolutionForImg = toConvolution(self.appData.width, self.appData.height); 18 | const grayscale = toPixels(toGrayscale(self.imgData, self.appData.width, self.appData.height)); 19 | self.postMessage({ type: 'grayscale', data: grayscale }); 20 | const normalized = toNormalized(grayscale); 21 | self.postMessage({ type: 'normalized' }); 22 | const blurred = toConvolutionForImg(gaussMatrix, 2, normalized); 23 | self.postMessage({ type: 'blurred', data: toPixels(toDenormalized(blurred)) }); 24 | const xDerived = toConvolutionForImg(xMatrix, 1, blurred); 25 | self.postMessage({ type: 'xAxis', data: toPixels(toDenormalized(xDerived)) }); 26 | const yDerived = toConvolutionForImg(yMatrix, 1, blurred); 27 | self.postMessage({ type: 'yAxis', data: toPixels(toDenormalized(yDerived)) }); 28 | const gradientMagnitude = toGradientMagnitude(xDerived, yDerived, self.appData.width, self.appData.height, self.appData.lt, self.appData.ut); 29 | self.postMessage({ 30 | type: 'gradientMagnitude', 31 | data: toPixels(toDenormalized(gradientMagnitude.data)), 32 | threshold: gradientMagnitude.threshold 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 132 | 133 | 134 |
135 |
136 |
137 | Back to GitHub 138 |

Canny Edge Detector

139 |
140 |
141 | 142 |
143 |
144 |
145 | 146 | 147 |
148 |
149 |
150 | 151 |
152 |
153 |
154 |
155 |
156 | 157 | 158 |
159 |
160 | 161 |
162 |
163 | 164 | 165 |
166 |
167 |
168 |
169 |
170 | 171 |
172 |
173 |
174 |
175 |
176 | 177 |
178 |
179 |
180 |
    181 |
  • 182 |
  • 183 |
  • 184 |
  • 185 |
  • 186 |
  • 187 |
188 |
189 |
190 |
191 | 192 |
193 |
194 |
195 | 196 |
197 | 198 |
199 | 200 |
201 | 202 |
203 | 204 |
205 | 206 |
207 | 208 |
209 | 210 |
211 | 212 |
213 | 214 |
215 | 216 |
217 |
218 |
219 |
220 | 221 | 222 | 223 | 224 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "js-canny-edge-detector", 3 | "version": "1.0.0", 4 | "description": "Example of Canny edge detection algorithm in javascript", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "build": "tsc", 9 | "dev": "tsc --watch" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/petarjs/js-canny-egde-detector.git" 14 | }, 15 | "author": "Petar Slovic", 16 | "license": "MIT", 17 | "bugs": { 18 | "url": "https://github.com/petarjs/js-canny-egde-detector/issues" 19 | }, 20 | "homepage": "https://github.com/petarjs/js-canny-egde-detector#readme" 21 | } 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | window.appData = {} 2 | 3 | const canvasFrom = document.querySelector('.image--from') as HTMLCanvasElement 4 | const canvasGrayscale = document.querySelector('.image--grayscale') as HTMLCanvasElement 5 | const canvasResult = document.querySelector('.image--result') as HTMLCanvasElement 6 | const canvasBlurred = document.querySelector('.image--blurred') as HTMLCanvasElement 7 | const canvasXDerived = document.querySelector('.image--x-derived') as HTMLCanvasElement 8 | const canvasYDerived = document.querySelector('.image--y-derived') as HTMLCanvasElement 9 | 10 | const $file = document.querySelector('.js_image') as HTMLInputElement 11 | const $submit = document.querySelector('.js_submit') as HTMLElement 12 | const $controls = document.querySelector('.js_controls') as HTMLElement 13 | const $imageNav = document.querySelector('.js_image-nav') as HTMLElement 14 | const $status = document.querySelector('.js_status') as HTMLElement 15 | const $ut = document.querySelector('.js_ut') as HTMLInputElement 16 | const $lt = document.querySelector('.js_lt') as HTMLInputElement 17 | const $cancel = document.querySelector('.js_cancel') as HTMLElement 18 | const $result = document.querySelector('.result') as HTMLElement 19 | 20 | let worker 21 | 22 | function initWorker () { 23 | worker = new Worker('./dist/worker.js') 24 | worker.addEventListener('message', onWorkerMessage, false) 25 | } 26 | 27 | $file.addEventListener('change', event => { 28 | const file = $file.files[0] 29 | readFileAsDataURL(file) 30 | .then(loadImage) 31 | .then(setCanvasSizeFromImage(canvasFrom)) 32 | .then(drawImageOnCanvas(canvasFrom)) 33 | .then(setCanvasSizeFromImage(canvasGrayscale)) 34 | .then(setCanvasSizeFromImage(canvasResult)) 35 | .then(setCanvasSizeFromImage(canvasBlurred)) 36 | .then(setCanvasSizeFromImage(canvasYDerived)) 37 | .then(setCanvasSizeFromImage(canvasXDerived)) 38 | .then((img: HTMLImageElement) => { 39 | window.appData = { 40 | img, 41 | width: img.naturalWidth, 42 | height: img.naturalHeight 43 | } 44 | }) 45 | .then(() => { 46 | [...document.querySelectorAll('canvas')].forEach(canvas => { 47 | let newWidth = Math.min($result.clientWidth, canvas.width) 48 | canvas.style.width = newWidth + 'px' 49 | canvas.style.height = newWidth * (canvas.height / canvas.width) + 'px' 50 | }) 51 | }) 52 | .then(showControls) 53 | }) 54 | 55 | $submit.addEventListener('click', event => { 56 | 57 | window.appData.ut = parseFloat($ut.value) 58 | window.appData.lt = parseFloat($lt.value) 59 | 60 | initWorker() 61 | 62 | worker.postMessage({ 63 | cmd: 'appData', 64 | data: { 65 | width: window.appData.width, 66 | height: window.appData.height, 67 | ut: window.appData.ut, 68 | lt: window.appData.lt 69 | } 70 | }) 71 | 72 | const imgd = canvasFrom 73 | .getContext('2d') 74 | .getImageData(0, 0, window.appData.width, window.appData.height) 75 | 76 | const pixels = imgd.data 77 | setProcessingStatus('1/7 Loaded image data') 78 | blockControls() 79 | resetImageNav() 80 | 81 | worker.postMessage({ cmd: 'imgData', data: pixels }) 82 | }) 83 | 84 | $cancel.addEventListener('click', event => { 85 | worker.terminate() 86 | worker.removeEventListener('message', onWorkerMessage, false) 87 | setProcessingStatus('') 88 | unblockControls() 89 | initWorker() 90 | resetImageNav() 91 | }) 92 | 93 | function showControls () { 94 | $status.style.display = 'inline-block' 95 | $controls.style.display = 'inline-block' 96 | $imageNav.classList.add('image-nav--active') 97 | $result.style.height = canvasFrom.style.height 98 | resetImageNav() 99 | setProcessingStatus('Waiting for start.') 100 | } 101 | 102 | function blockControls () { 103 | $controls.classList.add('controls--blocked') 104 | $cancel.style.display = 'inline-block' 105 | } 106 | 107 | function unblockControls () { 108 | $controls.classList.remove('controls--blocked') 109 | $cancel.style.display = '' 110 | } 111 | 112 | function resetImageNav () { 113 | [...document.querySelectorAll('.image-nav__item--active')].forEach(el => el.classList.remove('image-nav__item--active')) 114 | document.querySelector(`[data-target="js_image--from"]`).classList.add('image-nav__item--active') 115 | } 116 | 117 | function setProcessingStatus (status: string) { 118 | window.appData.status = status 119 | $status.innerText = status 120 | } 121 | 122 | function activateImage (className) { 123 | let imageNavItem = document.querySelector(`[data-target="${className}"]`) 124 | if (imageNavItem) { 125 | imageNavItem.classList.add('image-nav__item--active') 126 | } 127 | document.querySelector(`.${className}`).classList.add(`image--active`) 128 | } 129 | 130 | function showThresholds(thresh){ 131 | $ut.value = thresh.ut; 132 | $lt.value = thresh.lt; 133 | 134 | } 135 | 136 | function onWorkerMessage (e: ServiceWorkerMessageEvent) { 137 | const drawBytesOnCanvasForImg = drawBytesOnCanvas(window.appData.width, window.appData.height) 138 | 139 | if (e.data.type === 'grayscale') { 140 | setProcessingStatus('2/7 Converted to Grayscale') 141 | drawBytesOnCanvasForImg(canvasGrayscale, e.data.data) 142 | activateImage('js_image--grayscale') 143 | } else if (e.data.type === 'normalized') { 144 | setProcessingStatus('3/7 Normalized pixel values') 145 | } else if (e.data.type === 'blurred') { 146 | setProcessingStatus('4/7 Blurred image') 147 | drawBytesOnCanvasForImg(canvasBlurred, e.data.data) 148 | activateImage('js_image--blurred') 149 | } else if (e.data.type === 'xAxis') { 150 | setProcessingStatus('5/7 Created X axis derivation') 151 | drawBytesOnCanvasForImg(canvasXDerived, e.data.data) 152 | activateImage('js_image--x-derived') 153 | } else if (e.data.type === 'yAxis') { 154 | setProcessingStatus('6/7 Created Y axis derivation') 155 | drawBytesOnCanvasForImg(canvasYDerived, e.data.data) 156 | activateImage('js_image--y-derived') 157 | } else if (e.data.type === 'gradientMagnitude') { 158 | setProcessingStatus('7/7 Calculated Gradient magnitude') 159 | drawBytesOnCanvasForImg(canvasResult, e.data.data) 160 | showThresholds(e.data.threshold) 161 | activateImage('js_image--result') 162 | setProcessingStatus('Done!') 163 | unblockControls() 164 | } 165 | } 166 | 167 | document.querySelector('.js_image-nav').addEventListener('click', e => { 168 | let eventTarget = (e.target as HTMLElement) 169 | if (eventTarget.classList.contains('image-nav__item--active')) { 170 | let target = eventTarget.dataset.target 171 | Array.from(document.querySelectorAll('.image--active')).forEach(el => el.classList.remove('image--active')) 172 | document.querySelector(`.${target}`).classList.add(`image--active`) 173 | } 174 | }) -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const MAX_PRECISION = false 4 | const precision = 2 5 | 6 | const gaussMatrix = [ 7 | [0.0121, 0.0261, 0.0337, 0.0261, 0.0121], 8 | [0.0261, 0.0561, 0.0724, 0.0561, 0.0261], 9 | [0.0337, 0.0724, 0.0935, 0.0724, 0.0337], 10 | [0.0261, 0.0561, 0.0724, 0.0561, 0.0261], 11 | [0.0121, 0.0261, 0.0337, 0.0261, 0.0121] 12 | ] 13 | 14 | const xMatrix = [ [ 1, 0, -1 ], [ 2, 0, -2 ], [ 1, 0, -1 ] ] 15 | const yMatrix = [ [ -1, -2, -1 ], [ 0, 0, 0 ], [ 1, 2, 1 ] ] 16 | 17 | const MAX_IMAGE_HEIGHT = 300 18 | 19 | function curry(f: Function, n?: number): Function { 20 | var args = Array.prototype.slice.call(arguments, 0); 21 | if (typeof n === 'undefined') 22 | args[1] = f.length; 23 | if (n === args.length - 2) 24 | return f.apply(undefined, args.slice(2)); 25 | return function() { 26 | return curry.apply(undefined, args.concat(Array.prototype.slice.call(arguments, 0))); 27 | }; 28 | } 29 | 30 | function loadImage(imageUrl: string) { 31 | return new Promise ((resolve, reject) => { 32 | console.time('loadImage') 33 | const img = new Image() 34 | img.src = imageUrl 35 | img.crossOrigin = 'Anonymous' 36 | img.onload = function () { 37 | console.timeEnd('loadImage') 38 | resolve(img) 39 | } 40 | }) 41 | } 42 | 43 | function readFileAsDataURL(file: Blob) { 44 | return new Promise((resolve, reject) => { 45 | var reader = new FileReader(); 46 | 47 | reader.onloadend = function () { 48 | resolve(reader.result) 49 | } 50 | 51 | if (file) { 52 | reader.readAsDataURL(file); 53 | } else { 54 | reject('') 55 | } 56 | }) 57 | } 58 | 59 | function _drawImageOnCanvas(canvas, image) { 60 | canvas.getContext('2d').drawImage(image, 0, 0) 61 | return image 62 | } 63 | 64 | var drawImageOnCanvas = curry(_drawImageOnCanvas) 65 | 66 | function _setCanvasSizeFromImage(canvas: HTMLCanvasElement, image: HTMLImageElement) { 67 | const ratio = image.naturalWidth / image.naturalHeight 68 | canvas.style.width = '' 69 | canvas.getContext('2d').clearRect(0, 0, image.width, image.height); 70 | canvas.height = image.height 71 | canvas.width = image.width 72 | return image 73 | } 74 | 75 | var setCanvasSizeFromImage = curry(_setCanvasSizeFromImage) 76 | 77 | function _drawBytesOnCanvas(width: number, height: number, canvas: HTMLCanvasElement, bytes: Uint8Array) { 78 | canvas 79 | .getContext('2d') 80 | .putImageData( 81 | new ImageData(new Uint8ClampedArray( 82 | bytes 83 | ), width, height), 84 | 0, 0 85 | ) 86 | } 87 | 88 | var drawBytesOnCanvas = curry(_drawBytesOnCanvas) 89 | 90 | function toGrayscale(bytes: Uint8Array, width: number, height: number): Array { 91 | console.time('toGrayscale') 92 | const grayscale = [] 93 | for (let i = 0; i < bytes.length; i += 4) { 94 | var gray = .299 * bytes[i + 2] + .587 * bytes[i + 1] + .114 * bytes[i] 95 | grayscale.push(gray) 96 | } 97 | console.timeEnd('toGrayscale') 98 | 99 | return grayscale 100 | } 101 | 102 | function _toConvolution (width: number, height: number, kernel: number[][], radius: number, bytes: Uint8Array): number[] { 103 | console.time('toConvolution') 104 | const convolution = [] 105 | let newValue, idxX, idxY, kernx, kerny 106 | for (let i = 0; i < width; i++) { 107 | for (let j = 0; j < height; j++) { 108 | newValue = 0 109 | for (let innerI = i - radius; innerI < i + radius + 1; innerI++) { 110 | for (let innerJ = j - radius; innerJ < j + radius + 1; innerJ++) { 111 | idxX = (innerI + width) % width 112 | idxY = (innerJ + height) % height 113 | 114 | kernx = innerI - (i - radius) 115 | kerny = innerJ - (j - radius) 116 | newValue += bytes[idxY * width + idxX] * kernel[kernx][kerny] 117 | 118 | } 119 | } 120 | convolution[j * width + i] = newValue 121 | } 122 | } 123 | console.time('toConvolution') 124 | 125 | return convolution 126 | } 127 | 128 | const toConvolution = curry(_toConvolution) 129 | 130 | /** 131 | * From image bytes (0 - 255) to values between 0 and 1 132 | * @param {Array} bytes 133 | * @return {Array} normalized values 134 | */ 135 | function toNormalized(bytes: Array): Array { 136 | console.time('toNormalized') 137 | 138 | const normalized = [] 139 | for (let i = 0; i < bytes.length; i += 4) { 140 | normalized.push(bytes[i] / 255) 141 | } 142 | console.timeEnd('toNormalized') 143 | 144 | return normalized 145 | } 146 | 147 | /** 148 | * From normalized array that has values from 0 to 1 149 | * to image data with values between 0 and 255 150 | * @param {Array} normalized 151 | * @return {Array} denormlized 152 | */ 153 | function toDenormalized(normalized: Array): Array { 154 | console.time('toDenormalized') 155 | const denormalized = normalized.map(value => value * 255) 156 | console.timeEnd('toDenormalized') 157 | return denormalized 158 | } 159 | 160 | function toGradientMagnitude( 161 | xDerived: Array, 162 | yDerived: Array, 163 | width: number, 164 | height: number, 165 | lt = 0, 166 | ut = 0): any { 167 | 168 | console.time('toGradientMagnitude') 169 | const gradientMagnitude = [] 170 | const gradientDirection = [] 171 | 172 | let index 173 | let pom 174 | 175 | for (let y = 0; y < height; y++) { 176 | for (let x = 0; x < width; x++) { 177 | index = y * width + x 178 | gradientMagnitude[index] = Math.sqrt(xDerived[index] * xDerived[index] + yDerived[index] * yDerived[index]) 179 | pom = Math.atan2(xDerived[index], yDerived[index]); 180 | if ((pom >= -Math.PI / 8 && pom < Math.PI / 8) || (pom <= -7 * Math.PI / 8 && pom > 7 * Math.PI / 8)) { 181 | gradientDirection[index] = 0; 182 | } else if ((pom >= Math.PI / 8 && pom < 3 * Math.PI / 8) || (pom <= -5 * Math.PI / 8 && pom > -7 * Math.PI / 8)) { 183 | gradientDirection[index] = Math.PI / 4; 184 | } else if ((pom >= 3 * Math.PI / 8 && pom <= 5 * Math.PI / 8) || (-3 * Math.PI / 8 >= pom && pom > -5 * Math.PI / 8)) { 185 | gradientDirection[index] = Math.PI / 2; 186 | } else if ((pom < -Math.PI / 8 && pom >= -3 * Math.PI / 8) || (pom > 5 * Math.PI / 8 && pom <= 7 * Math.PI / 8)) { 187 | gradientDirection[index] = -Math.PI / 4; 188 | } 189 | } 190 | } 191 | 192 | const max = getMax(gradientMagnitude) 193 | const gradientMagnitudeCapped = gradientMagnitude.map(x => x / max) 194 | 195 | if (!ut && !lt) { 196 | let res = getTresholds(gradientMagnitudeCapped) 197 | ut = res.ut 198 | lt = res.lt 199 | } 200 | 201 | const gradientMagnitudeLt = gradientMagnitudeCapped.map(value => value < lt ? 0 : value) 202 | 203 | for (var y = 1; y < height - 1; y++) { 204 | for (var x = 1; x < width - 1; x++) { 205 | index = y * width + x 206 | 207 | if (gradientDirection[index] == 0 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[y * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[y * width + x + 1])) 208 | gradientMagnitudeLt[index] = 0; 209 | else if (gradientDirection[index] == Math.PI / 2 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x] || gradientMagnitudeLt[(y + 1) * width + x] >= gradientMagnitudeLt[index])) 210 | gradientMagnitudeLt[index] = 0; 211 | else if (gradientDirection[index] == Math.PI / 4 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y + 1) * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x + 1])) 212 | gradientMagnitudeLt[index] = 0; 213 | else if (gradientDirection[index] == -Math.PI / 4 && (gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y - 1) * width + x - 1] || gradientMagnitudeLt[index] <= gradientMagnitudeLt[(y + 1) * width + x + 1])) 214 | gradientMagnitudeLt[index] = 0; 215 | } 216 | } 217 | 218 | for (let y = 2; y < height - 2; y++) { 219 | for (let x = 2; x < width - 2; x++) { 220 | if (gradientDirection[y * width + x] == 0) 221 | if (gradientMagnitudeLt[y * width + x - 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[y * width + x + 2] > gradientMagnitudeLt[y * width + x]) 222 | gradientMagnitudeLt[y * width + x] = 0; 223 | if (gradientDirection[y * width + x] == Math.PI / 2) 224 | if (gradientMagnitudeLt[(y - 2) * width + x] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y + 2) * width + x] > gradientMagnitudeLt[y * width + x]) 225 | gradientMagnitudeLt[y * width + x] = 0; 226 | if (gradientDirection[y * width + x] == Math.PI / 4) 227 | if (gradientMagnitudeLt[(y + 2) * width + x - 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y - 2) * width + x + 2] > gradientMagnitudeLt[y * width + x]) 228 | gradientMagnitudeLt[y * width + x] = 0; 229 | if (gradientDirection[y * width + x] == -Math.PI / 4) 230 | if (gradientMagnitudeLt[(y + 2) * width + x + 2] > gradientMagnitudeLt[y * width + x] || gradientMagnitudeLt[(y - 2) * width + x - 2] > gradientMagnitudeLt[y * width + x]) 231 | gradientMagnitudeLt[y * width + x] = 0; 232 | } 233 | } 234 | 235 | const gradientMagnitudeUt = gradientMagnitudeLt.map(value => value > ut ? 1 : value) 236 | 237 | // histeresis start 238 | let pomH = 0 239 | let pomOld = -1 240 | let pass = 0 241 | 242 | let nastavi = true 243 | let gradientMagnitudeCappedBottom = [] 244 | while ( nastavi ) { 245 | pass = pass + 1; 246 | pomOld = pomH; 247 | for (let y = 1; y < height - 1; y++) { 248 | for (let x = 1; x < width - 1; x++) { 249 | if (gradientMagnitudeUt[y * width + x] <= ut && gradientMagnitudeUt[y * width + x] >= lt) { 250 | let pom1 = gradientMagnitudeUt[(y - 1) * width + x - 1]; 251 | let pom2 = gradientMagnitudeUt[(y - 1) * width + x]; 252 | let pom3 = gradientMagnitudeUt[(y - 1) * width + x + 1]; 253 | let pom4 = gradientMagnitudeUt[y * width + x - 1]; 254 | let pom5 = gradientMagnitudeUt[y * width + x + 1]; 255 | let pom6 = gradientMagnitudeUt[(y + 1) * width + x - 1]; 256 | let pom7 = gradientMagnitudeUt[(y + 1) * width + x]; 257 | let pom8 = gradientMagnitudeUt[(y + 1) * width + x + 1]; 258 | 259 | if (pom1 === 1 || pom2 === 1 || pom3 === 1 || pom4 === 1 || pom5 === 1 || pom6 === 1 || pom7 === 1 || pom8 === 1) { 260 | gradientMagnitudeUt[y * width + x] = 1; 261 | pomH = pomH + 1; 262 | } 263 | } 264 | } 265 | } 266 | 267 | if (MAX_PRECISION) { 268 | nastavi = pomH != pomOld; 269 | } else { 270 | nastavi = pass <= precision; 271 | } 272 | 273 | gradientMagnitudeCappedBottom = gradientMagnitudeUt.map(x => x <= ut ? 0 : x) 274 | } 275 | 276 | console.timeEnd('toGradientMagnitude') 277 | return { 278 | data: gradientMagnitudeCappedBottom, 279 | threshold: { 280 | ut: ut, 281 | lt: lt 282 | } 283 | } 284 | 285 | } 286 | 287 | function getMax(values: Array): number { 288 | return values.reduce((prev, now) => now > prev ? now : prev, -1) 289 | } 290 | 291 | function getTresholds(gradientMagnitude: Array): { ut: number, lt: number } { 292 | let sum = 0; 293 | let count = 0; 294 | 295 | sum = gradientMagnitude.reduce((memo, x) => x + memo, 0) 296 | count = gradientMagnitude.filter(x => x !== 0).length 297 | 298 | const ut = sum / count 299 | const lt = 0.4 * ut 300 | return { ut, lt } 301 | } 302 | 303 | /** 304 | * Takes an array of values (0-255) and returns 305 | * an expaneded array [x, x, x, 255] for each value. 306 | * 307 | * @param {Array} values 308 | * @return {Array} expanded values 309 | */ 310 | function toPixels(values: Array): Array { 311 | console.time('toPixels') 312 | const expanded = [] 313 | values.forEach(x => { 314 | expanded.push(x) 315 | expanded.push(x) 316 | expanded.push(x) 317 | expanded.push(255) 318 | }) 319 | 320 | console.timeEnd('toPixels') 321 | return expanded 322 | } -------------------------------------------------------------------------------- /src/worker.ts: -------------------------------------------------------------------------------- 1 | importScripts('./main.js') 2 | 3 | interface Window { appData: any, imgData: any } 4 | 5 | self.appData = {} 6 | 7 | self.addEventListener('message', function (e) { 8 | const data = e.data 9 | 10 | switch (data.cmd) { 11 | case 'appData': 12 | self.appData = data.data 13 | break 14 | case 'imgData': 15 | self.postMessage('WORKER STARTED') 16 | self.imgData = data.data 17 | start() 18 | break 19 | } 20 | }) 21 | 22 | function start () { 23 | const toConvolutionForImg = toConvolution(self.appData.width, self.appData.height) 24 | 25 | const grayscale = toPixels(toGrayscale(self.imgData, self.appData.width, self.appData.height)) 26 | self.postMessage({ type: 'grayscale', data: grayscale }) 27 | 28 | const normalized = toNormalized(grayscale) 29 | self.postMessage({ type: 'normalized' }) 30 | 31 | const blurred = toConvolutionForImg(gaussMatrix, 2, normalized) 32 | self.postMessage({ type: 'blurred', data: toPixels(toDenormalized(blurred)) }) 33 | 34 | const xDerived = toConvolutionForImg(xMatrix, 1, blurred) 35 | self.postMessage({ type: 'xAxis', data: toPixels(toDenormalized(xDerived)) }) 36 | 37 | const yDerived = toConvolutionForImg(yMatrix, 1, blurred) 38 | self.postMessage({ type: 'yAxis', data: toPixels(toDenormalized(yDerived)) }) 39 | 40 | const gradientMagnitude = toGradientMagnitude( 41 | xDerived, 42 | yDerived, 43 | self.appData.width, 44 | self.appData.height, 45 | self.appData.lt, 46 | self.appData.ut 47 | ) 48 | self.postMessage({ 49 | type: 'gradientMagnitude', 50 | data: toPixels(toDenormalized(gradientMagnitude.data)), 51 | threshold: gradientMagnitude.threshold 52 | }) 53 | } 54 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": true, 3 | "compilerOptions": { 4 | "outDir": "./dist/", 5 | "sourceMap": false, 6 | "noImplicitAny": false, 7 | "module": "commonjs", 8 | "target": "es6" 9 | }, 10 | "include": [ 11 | "src/**/*" 12 | ], 13 | "exclude": [ 14 | "node_modules", 15 | "**/*.spec.ts" 16 | ] 17 | } --------------------------------------------------------------------------------