A Pen by Andreas Borgen on CodePen.
Created
January 30, 2021 16:33
-
-
Save Sphinxxxx/d4cb89d27fdfeab91c137963066b7994 to your computer and use it in GitHub Desktop.
Canny edge detection with jsfeat
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script>console.clear();</script> | |
<script type="text/javascript" src="https://unpkg.com/[email protected]/build/dat.gui.js"></script> | |
<script type="text/javascript" src="https://cdn.jsdelivr.net/gh/inspirit/jsfeat@master/build/jsfeat.js"></script> | |
<script> | |
/* | |
(c) 2017, Vladimir Agafonkin | |
Simplify.js, a high-performance JS polyline simplification library | |
mourner.github.io/simplify-js | |
*/ | |
window.simplify = (function(){"use strict";function n(n,r){var t=n[0]-r[0],u=n[1]-r[1];return t*t+u*u}function r(n,r,t){var u=r[0],i=r[1],f=t[0]-u,o=t[1]-i;if(0!==f||0!==o){var e=((n[0]-u)*f+(n[1]-i)*o)/(f*f+o*o);e>1?(u=t[0],i=t[1]):e>0&&(u+=f*e,i+=o*e)}return f=n[0]-u,o=n[1]-i,f*f+o*o}function t(r,t){for(var u,i=r[0],f=[i],o=1,e=r.length;e>o;o++)u=r[o],n(u,i)>t&&(f.push(u),i=u);return i!==u&&f.push(u),f}function u(n,t,i,f,o){for(var e,v=f,a=t+1;i>a;a++){var c=r(n[a],n[t],n[i]);c>v&&(e=a,v=c)}v>f&&(e-t>1&&u(n,t,e,f,o),o.push(n[e]),i-e>1&&u(n,e,i,f,o))}function i(n,r){var t=n.length-1,i=[n[0]];return u(n,0,t,r,i),i.push(n[t]),i}function f(n,r,u){if(n.length<=2)return n;var f=void 0!==r?r*r:1;return n=u?n:t(n,f),n=i(n,f)}return f})(); | |
</script> | |
<script> | |
/* | |
tuple by Ry-♦ | |
https://stackoverflow.com/a/21839292/1869660 | |
*/ | |
window.tuple = (function(){let map=new Map();function tuple(){let current=map;let args=Object.freeze(Array.from(arguments));for(let item of args){if(current.has(item)){current=current.get(item);}else{let next=new Map();current.set(item,next);current=next;}}if(!current._myVal){current._myVal=args;}return current._myVal;}return tuple;})(); | |
</script> | |
<input type="file" id="source" accept="image/*" /> | |
<div id="result"> | |
<canvas id="canv"></canvas> | |
<svg id="lines"></svg> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function() { | |
"use strict"; | |
function log() { | |
const now = new Date(), | |
time = now.toLocaleTimeString() + '.' + now.getMilliseconds().toString().padEnd(3, '0'); | |
console.log.apply(console, [time].concat(Array.from(arguments))); | |
} | |
function debugColor(i) { | |
//https://coolors.co/ffbe0b-fb5607-ff006e-8338ec-3a86ff-79ff4d | |
//https://learnui.design/tools/data-color-picker.html#divergent | |
const colors = [ | |
[0xff, 0x00, 0x00], | |
[0xff, 0xff, 0x00], | |
[0x00, 0xff, 0x00], | |
[0x00, 0xff, 0xff], | |
[0x00, 0x00, 0xff], | |
[0xff, 0x00, 0xff], | |
]; | |
const color = colors[i % colors.length]; | |
return color; | |
} | |
const _canvas = document.querySelector('#canv'), | |
_svg = document.querySelector('#lines'), | |
_options = { | |
zoom: 1, | |
canny: { | |
blur_radius: 5, | |
low_threshold: 1, | |
high_threshold: 70, | |
}, | |
tracing: { | |
minLength: 10, | |
lineTolerance: 8, | |
} | |
}; | |
let _img, _w, _h, _ctx, _bitmap; | |
function init(img) { | |
_img = img; | |
_w = img.width; | |
_h = img.height; | |
const measure = Math.max(_w, _h); | |
if(measure > 600) { | |
const shrink = 600/measure; | |
_w = Math.round(_w * shrink); | |
_h = Math.round(_h * shrink); | |
} | |
_canvas.width = _w; | |
_canvas.height = _h; | |
_ctx = _canvas.getContext("2d"); | |
_bitmap = new jsfeat.matrix_t(_w, _h, jsfeat.U8C1_t); | |
} | |
function render() { | |
const ctx = _ctx, | |
canny = _options.canny; | |
ctx.drawImage(_img, 0, 0, _w, _h); | |
_svg.innerHTML = ''; | |
var imageData = ctx.getImageData(0, 0, _w, _h); | |
log("grayscale"); | |
jsfeat.imgproc.grayscale(imageData.data, _w, _h, _bitmap); | |
var r = canny.blur_radius | 0.0; | |
var kernel_size = (r + 1) << 1; | |
log("gauss blur", r, kernel_size); | |
jsfeat.imgproc.gaussian_blur(_bitmap, _bitmap, kernel_size, 0); | |
log("canny edge"); | |
jsfeat.imgproc.canny( | |
_bitmap, | |
_bitmap, | |
canny.low_threshold, | |
canny.high_threshold | |
); | |
// render result back to canvas | |
log("render", _bitmap); | |
const data_u32 = new Uint32Array(imageData.data.buffer); | |
function setPixel(i, r, g, b) { | |
const alpha = 0xff << 24; | |
data_u32[i] = alpha | (b << 16)| (g << 8) | r; | |
} | |
let i = _bitmap.cols * _bitmap.rows, | |
pix = 0; | |
while (--i >= 0) { | |
pix = _bitmap.data[i] ? 160 : 0; | |
setPixel(i, pix, pix, pix); | |
} | |
ctx.putImageData(imageData, 0, 0); | |
log('trace lines'); | |
const grid = []; | |
i = 0; | |
for(let y = 0; y < _h; y++) { | |
const row = _bitmap.data.slice(i, i + _w); | |
grid.push(row); | |
i += _w; | |
} | |
//console.log(grid); | |
const traces = []; | |
for(let y = 0; y < _h; y++) { | |
for(let x = 0; x < _w; x++) { | |
const px = grid[y][x]; | |
if(px) { | |
traces.push.apply(traces, traceFrom(x, y)); | |
} | |
} | |
} | |
log('traced lines', traces.length); //.reduce((sum, x) => sum + x.length, 0)); | |
const tracing = _options.tracing; | |
let drawn = 0; | |
traces.forEach((trace, i) => { | |
if(trace.length < tracing.minLength) { return; } | |
const [r, g, b] = debugColor(i); | |
drawn++; | |
//Mark pixels: | |
trace.forEach(([x, y]) => setPixel(y * _w + x, r, g, b)); | |
//Draw SVG: | |
trace = simplify(trace, tracing.lineTolerance, true); | |
const line = document.createElementNS(_svg.namespaceURI, 'polyline'); | |
line.setAttribute('points', trace.toString()); | |
line.setAttribute('stroke', `rgba(${[r, g, b]})`); | |
_svg.appendChild(line); | |
//const markers = document.createElementNS(_svg.namespaceURI, 'g'); | |
//_svg.appendChild(markers); | |
//trace.forEach(point => { | |
// const marker = document.createElementNS(_svg.namespaceURI, 'circle'); | |
// marker.setAttribute('cx', point[0]); | |
// marker.setAttribute('cy', point[1]); | |
// marker.setAttribute('stroke', `rgba(${[r, g, b]})`); | |
// markers.appendChild(marker); | |
//}); | |
}); | |
ctx.putImageData(imageData, 0, 0); | |
log('drawn lines', drawn); | |
function traceFrom(x_1, y_1) { | |
//const lines = []; | |
const branches = []; | |
function checkPixel(x, y) { | |
return grid[y] && grid[y][x]; | |
} | |
function findNext(x, y, findAll) { | |
const options = []; | |
//Check up/down/left/right first, and only diagonals if we don't find anything. | |
//This will let us trace "staircases" as one line, and not a series or intersections. | |
if(checkPixel(x, y - 1)) { options.push(tuple(x, y - 1)); } | |
if(checkPixel(x, y + 1)) { options.push(tuple(x, y + 1)); } | |
if(checkPixel(x - 1, y)) { options.push(tuple(x - 1, y)); } | |
if(checkPixel(x + 1, y)) { options.push(tuple(x + 1, y)); } | |
if(options.length && !findAll) { return options; } | |
if(checkPixel(x - 1, y - 1)) { options.push(tuple(x - 1, y - 1)); } | |
if(checkPixel(x + 1, y - 1)) { options.push(tuple(x + 1, y - 1)); } | |
if(checkPixel(x - 1, y + 1)) { options.push(tuple(x - 1, y + 1)); } | |
if(checkPixel(x + 1, y + 1)) { options.push(tuple(x + 1, y + 1)); } | |
return options; | |
} | |
function collect(a, b, branch) { | |
branch.push(tuple(a, b)); | |
grid[b][a] = 0; | |
} | |
function addBranch(startX, startY) { | |
const branch = []; | |
collect(startX, startY, branch); | |
branches.push(branch); | |
return branch; | |
} | |
//Start by initing our main branch | |
addBranch(x_1, y_1); | |
for(let i = 0; i < branches.length; i++) { | |
const branch = branches[i]; | |
let [x0, y0] = branch[0], | |
[x, y] = branch[branch.length - 1]; | |
let nexts, pass = 1, endOfLine = false; | |
while(true) { | |
nexts = findNext(x, y); | |
//If we reach an intersection: | |
if((nexts.length > 1) && (branch.length > 1)) { | |
//This may just be a 1px branch/noise, or a small section where the edge is 2px wide. | |
//To test that, we look at all the pixels we can reach from the first step of a new branch. | |
//If there are no more pixels than we can reach from our current position, | |
//it's just noise and not an actual branch. | |
const currentReach = findNext(x, y, true); | |
let maxReach = 0, maxBranch; | |
nexts = nexts.filter(([xx, yy]) => { | |
const branchReach = findNext(xx, yy, true), | |
branchHasNewPixels = branchReach.some(px => !currentReach.includes(px)); | |
if(branchReach.length > maxReach) { | |
maxBranch = tuple(xx, yy); | |
} | |
//If we decide this is just noise, clear the pixel so it's not picked up by a later traceFrom(): | |
if(!branchHasNewPixels) { grid[yy][xx] = 0; } | |
return branchHasNewPixels; | |
}); | |
//If we filtered away all the branches, keep the one with more pixels. | |
//This lets us go around noisy corners or finish noisy line ends: | |
if(nexts.length === 0) { nexts = [maxBranch]; } | |
//This is a real intersection of branches: | |
if(nexts.length > 1) { | |
nexts.forEach(coord => { | |
const otherBranch = addBranch(x, y); | |
collect(coord[0], coord[1], otherBranch); | |
}); | |
endOfLine = true; | |
} | |
} | |
//Keep following the current branch: | |
if(nexts.length && !endOfLine) { | |
[x, y] = nexts[0]; | |
collect(x, y, branch); | |
} | |
else { | |
endOfLine = true; | |
} | |
if(endOfLine) { | |
if(pass === 1) { | |
//Reverse and trace in the other direction from our starting point; | |
branch.reverse(); | |
x = x0; | |
y = y0; | |
pass = 2; | |
endOfLine = false; | |
} | |
else { | |
break; | |
} | |
} | |
} | |
} | |
return branches; | |
} | |
} | |
(function load() { | |
(function controls() { | |
const gui = new dat.GUI({ | |
autoPlace: false, | |
}); | |
gui.add(_options, "zoom", 1, 6).step(1).onChange(x => document.querySelector('#result').style.zoom = x); | |
const canny = gui.addFolder('Canny'); | |
canny.open(); | |
canny.add(_options.canny, "blur_radius", 0, 10).step(1).onChange(render); | |
canny.add(_options.canny, "low_threshold", 1, 200).step(1).onChange(render); | |
canny.add(_options.canny, "high_threshold", 1, 200).step(1).onChange(render); | |
const tracing = gui.addFolder('Tracing'); | |
tracing.open(); | |
tracing.add(_options.tracing, "minLength", 0, 50).step(1).onChange(render); | |
tracing.add(_options.tracing, "lineTolerance", 0, 10).step(1).onChange(render); | |
document.querySelector('#result').insertAdjacentElement('beforebegin', gui.domElement); | |
})(); | |
const img = new Image(); | |
img.onload = e => { | |
init(img); | |
render(); | |
} | |
document.querySelector('#source').onchange = function(e) { | |
var url = URL.createObjectURL(this.files[0]); | |
img.src = url; | |
}; | |
img.src = 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAIVcZHVkU4V1bHWWjoWeyP/ZyLe3yP////L/////////////////////////////////////////////////////2wBDAY6WlsivyP/Z2f//////////////////////////////////////////////////////////////////////////wgARCAHSAlgDAREAAhEBAxEB/8QAFwABAQEBAAAAAAAAAAAAAAAAAAECA//EABYBAQEBAAAAAAAAAAAAAAAAAAABAv/aAAwDAQACEAMQAAABtgAAAAAAAAAAAAAAAAAAAAAAAAAAAAgAABQACAAFAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABQCAAAAAAAAAAAAAAAAAAAAAAAAAAoIUAAAAAAAAAkKAAAAAAAAAAAAAAAAAAAAAoAAAAAAAAAhQAAARkhF3YAAAAAAAAAAAAAAAAAAABQAAAAAACRRSIaURAFSAMrAAbsAAAAAAAAAAAAAAAAAAAoAAAAAJEIRQNIIu4pzN0QYUQAFEUuoAAAAAAAAAAAAAAAAAAKAAACEgoRKgLAlAU6QMkJUAAALGgNSggAAAAAAAAAAAAAAAAgqygAEjJF0CEKCRKgAKUAAAGopDJTRRqAQAAAAAAAAAAAAAAAFhLAXUAkZIoAoBkgBYlAAUAoig0CA0UE1ABAAAAAAAUEAAAAAABRCWAupIyRQBQQyAWKVRmyAoKIAFNGgZIbAJqACAoABAACgAEAAAAAKAIsuQZoQoBCEBYq0AAzYBYAoKAaKDINAE1AAAAAAAAAAABAAAUAAQlySoCkIQFirQUGoGKEQUFAABTQIZKUoJqAAAAAAAAAAAAAAAAIhlQBCEBYq0AGixmtQM1AlAKAAAaBCFBQTUAAAAAAAAAAAAAAAkFyQAhAWKtABTUShZM20sktyCpQAA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'; | |
})(); | |
})(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.dg.main { | |
margin: .5em 0 2em 0; | |
} | |
#result { | |
position: relative; | |
display: inline-block; | |
image-rendering: pixelated; | |
svg { | |
position: absolute; | |
top:0; left:0; | |
width: 100%; | |
height: 100%; | |
fill: none; | |
stroke-width: 4; | |
stroke-opacity: .6; | |
polyline { | |
stroke-linecap: square; | |
} | |
circle { | |
r: 4; | |
stroke-width: 1; | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment