Skip to content

Instantly share code, notes, and snippets.

@Sphinxxxx
Created January 30, 2021 16:33
Show Gist options
  • Save Sphinxxxx/d4cb89d27fdfeab91c137963066b7994 to your computer and use it in GitHub Desktop.
Save Sphinxxxx/d4cb89d27fdfeab91c137963066b7994 to your computer and use it in GitHub Desktop.
Canny edge detection with jsfeat
<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>
(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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';
})();
})();
.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