Published
Edited
Feb 27, 2020
2 forks
Importers
24 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
{
if (!segmentation) return "No segments computed yet";
let { curves, imgData } = segmentation;
let canvas = texturePack(curves, imgData, showTextureBoxes).texture
return canvas
}
Insert cell
Insert cell
mutable bgPixAddress = ({x:-1,y:-1})
Insert cell
mutable backgroundColor = [0,0,0,0]
Insert cell
viewof distThreshold = new View(10)
Insert cell
viewof operation = new View("floodByColor")
Insert cell
viewof subsampleTol = new View (0)
Insert cell
viewof minArea = new View (1)
Insert cell
viewof drawSettings = new View(['maskedImage', 'polygons'])
Insert cell
viewof showTextureBoxes = new View(false)
Insert cell
viewof inpaintAlgorithm = new View('none')
Insert cell
viewof animateSegments = new View(true)
Insert cell
comment('The segmented image, mask and polygons')
Insert cell
mutable segmentation = null
Insert cell
Insert cell
comment(`Creates a mask where pixels similar to the background color are made transparent`)
Insert cell
bgColorMask = function (pixelData, bgPixel, distThreshold = 10) {
let [R,G,B,A] = bgPixel;
let src = pixelData.data;
let maskData = new ImageData(pixelData.width, pixelData.height)
let dst = maskData.data;
let count = 0;
let threshold2 = distThreshold*distThreshold
for (let i = 0; i < src.length; i += 4) {
let [dr,dg,db,da] = [src[i]-R,src[i+1]-G,src[i+2]-B,src[i+3]-A];
let dist = dr*dr+dg*dg+db*db+da*da;
if (dist < threshold2) {
[dst[i],dst[i+1],dst[i+2],dst[i+3]] = [0,0,0,0];
} else {
[dst[i],dst[i+1],dst[i+2],dst[i+3]] = [0,0,0,255];
count++;
}
};
return maskData
}
Insert cell
comment('Creates a mask by flood-filling by color similarity starting with a seed background pixel at `x,y`')
Insert cell
floodFillMask = function (pixelData, x, y, distThreshold = 10) {
let [w,h] = [pixelData.width, pixelData.height];
let src = pixelData.data;
let maskData = new ImageData(w,h);
let dst = maskData.data;
for (let i = 0; i < dst.length; i+=4) dst [i+3] = 255;
let visited = new Uint8Array(src.length);
let count = 0;
let stack = [[x,y]];
let threshold2 = distThreshold*distThreshold
while (stack.length) {
let [x,y] = stack.pop();
let i = (w*y+x)*4;
visited[i] = true;
count++;
let [R,G,B,A] = src.slice(i,i+4);
let dist = (j) => {
let [dr,dg,db,da] = [src[j]-R,src[j+1]-G,src[j+2]-B,src[j+3]-A];
return dr*dr+dg*dg+db*db+da*da;
}
dst[i+3] = 0;
if (x > 0 && !visited[i-4] && dist(i-4) < threshold2) stack.push([x-1,y]);
if (x+1 < w && !visited[i+4] && dist(i+4) < threshold2) stack.push([x+1,y]);
if (y > 0 && !visited[i-w*4] && dist(i-w*4) < threshold2) stack.push([x,y-1]);
if (y+1 < h && !visited[i+w*4] && dist(i+w*4) < threshold2) stack.push([x,y+1]);
}
return maskData
}
Insert cell
Insert cell
imageEdges = function (pixelData, distThreshold = 10) {
let [w,h] = [pixelData.width, pixelData.height];
let src = pixelData.data;
let maskData = new ImageData(w,h);
let dst = maskData.data;
let threshold2 = distThreshold*distThreshold
let dist = function(i,j) {
let [dr,dg,db,da] = [src[j]-src[i],src[j+1]-src[i+1],src[j+2]-src[i+2],src[j+3]-src[i+3]];
return dr*dr+dg*dg+db*db+da*da;
}
for (let x = 1; x < w-1; x++) {
for (let y = 1; y < h-1; y++) {
let i = (w*y+x)*4;
let maxdist = Math.max(dist(i,i-w*4),dist(i,i+w*4),dist(i,i+4),dist(i,i-4));
if (maxdist>=threshold2) dst[i+3] = 255;
}
}
return maskData
}
Insert cell
Insert cell
floodFillEdgeMask = function (pixelData, x,y, distThreshold = 10) {
let [w,h] = [pixelData.width, pixelData.height];
let maskData = new ImageData(w,h);
let src = pixelData.data;
let dst = maskData.data;
let edge = erode(imageEdges(pixelData, distThreshold)).data;
for (let i = 0; i < dst.length; i+=4) dst [i+3] = 255;
let visited = new Uint8Array(src.length);
let count = 0;
let stack = [[x,y]];
while (stack.length) {
let [x,y] = stack.pop();
let i = (w*y+x)*4;
visited[i] = true;
count++;
dst[i+3] = 0;
if (x > 0 && !visited[i-4] && edge[i-4+3]==0) stack.push([x-1,y]);
if (x+1 < w && !visited[i+4] && edge[i+4+3]==0) stack.push([x+1,y]);
if (y > 0 && !visited[i-w*4] && edge[i-w*4+3]==0) stack.push([x,y-1]);
if (y+1 < h && !visited[i+w*4] && edge[i+w*4+3]==0) stack.push([x,y+1]);
}
return maskData
}
Insert cell
Insert cell
Insert cell
blurInpaint = function (pixelData, maskData, blurSteps = 3) {
let [w,h] = [pixelData.width, pixelData.height];
console.assert(w == maskData.width && h == maskData.height);
let src = new Uint32Array (pixelData.data.buffer);
let result = new ImageData(w,h);
let dst = new Uint32Array (result.data.buffer);
let mask = new Uint32Array (maskData.data.buffer);
let size = w*h;
let queue = []
let visited = new Uint8Array(size);
// Set up queue and visited
for (let x = 1; x+1 < w; x++) {
for (let y = 1; y+1 < h; y++) {
let i = y*w+x;
dst[i] = src[i];
if (mask[i]) dst[i] = 0;
else dst[i] = src[i];
if (mask[i] &&
!(mask[i-w-1] && mask[i-w] && mask[i-w+1] &&
mask[i-1] && mask[i+1] &&
mask[i+w-1] && mask[i+w] && mask[i+w+1])) {
queue.push (i);
}
}
}
// A gaussian blur function
let gauss = pixels => {
let newValues = new Uint32Array(pixels.length);
let rgbaWord = new Uint32Array(1);
let rgba = new Uint8Array(rgbaWord.buffer);
let n = 0;
for (let i of pixels) {
let [r,g,b] = [0,0,0];
let sum = 0;
for (let [j,s] of [[i-w-1,0.073235],[i-w, 0.176765],[i-w+1,0.073235],
[i-1, 0.176765],[i+1, 0.176765],
[i+w-1, 0.073235],[i+w, 0.176765],[i+w+1,0.073235]]) {
// for (let [j,s] of [[i-w-1,1],[i-w, 1],[i-w+1,1],
// [i-1, 1],[i+1, 1],
// [i+w-1, 1],[i+w, 1],[i+w+1,1]]) {
if (!mask[j] || (visited[j] > 0 && visited[j] < 255)) {
let k = j * 4;
r += result.data[k++]*s;
g += result.data[k++]*s;
b += result.data[k++]*s;
sum += s;
}
}
if (sum > 0) {
let k = i*4;
rgba.set([Math.round(r/sum),Math.round(g/sum),Math.round(b/sum),255]);
//rgba.set([r/sum,g/sum,b/sum,255]);
newValues[n] = rgbaWord[0];
}
n++;
}
n = 0;
for (let i of pixels) {
dst[i] = newValues[n++];
visited[i] = 1;
}
}

// Inpaint pixels in queue and find neighbor masked pixels
let first = 0;
for (let run = 1; run <= width && queue.length - first > 0; run++) {
let n = queue.length;
gauss(queue.slice(first,n));
for (let q = first; q < n; q++) {
let i = queue[q];
for (let j of [i-w-1, i-w, i-w+1,
i-1, i+1,
i+w-1, i+w, i+w+1]) {
if (visited[j] == 0 && mask[j]) {
queue.push (j);
visited[j] = 255;
}
}
}
first = n;
}
while (blurSteps--) gauss(queue);
return result
}
Insert cell
Insert cell
quadtreeInpaint = function (pixelData, maskData) {
let [w,h] = [pixelData.width, pixelData.height];
console.assert(w == maskData.width && h == maskData.height);
let k = Math.ceil(Math.log2(Math.max(w,h)));
let n = 1<<k;
let base = new ImageData(n,n);
let src = new Uint32Array (pixelData.data.buffer);
let dst = new Uint32Array (base.data.buffer);
let mask = new Uint32Array (maskData.data.buffer);
// Set up base layer
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y*w+x;
let j = y*n+x;
if (!mask[i]) dst[j] = src[i];
}
}
// Function to average 2x2 groups of pixels into a single pixel
let halve = (original,n) => {
let m = n/2;
let half = new ImageData(m,m);
let src = new Uint32Array (original.data.buffer);
let dst = new Uint32Array (half.data.buffer);
let rgbaWord = new Uint32Array(1);
let rgba = new Uint8Array(rgbaWord.buffer);
let average = (words) => {
let [R,G,B,A] = [0,0,0,0];
let sum = 0;
for (let w of words) {
rgbaWord [0] = w;
if (rgba[3]) {
sum++;
R += rgba[0];
G += rgba[1];
B += rgba[2];
//break;
}
}
if (sum>0) {
rgba.set([Math.round(R/sum),Math.round(G/sum),Math.round(B/sum),255])
}
else {
rgbaWord [0] = 0
}
return rgbaWord [0];
}
for (let x = 0; x < m; x++) {
for (let y = 0; y < m; y++) {
let words = [src[2*y*n+2*x],src[2*y*n+2*x+1],
src[2*y*n+n+2*x],src[2*y*n+n+2*x+1]]
if (x > 0 && y > 0) {
words.push (src[2*y*n+2*x-1],src[2*y*n+n+2*x-1],
src[2*y*n-n+2*x],src[2*y*n-n+2*x+1],src[2*y*n-n+2*x-1])
}
dst[y*m+x] = average(words)
}
}
return half
}
// Replace original pixels with pixels of halved image if masked
let double = (original, half, n) => {
let m = n/2;
let src = new Uint32Array (original.data.buffer);
let dst = new Uint32Array (half.data.buffer);
let rgbaWord = new Uint32Array(1);
let rgba = new Uint8Array(rgbaWord.buffer);
let weightedAverage = (words_weights) => {
let [R,G,B,A] = [0,0,0,0];
let sum = 0;
for (let [w,p] of words_weights) {
rgbaWord [0] = w;
if (rgba[3]) {
sum+=p;
R += rgba[0]*p;
G += rgba[1]*p;
B += rgba[2]*p;
//break;
}
}
if (sum>0) {
rgba.set([Math.round(R/sum),Math.round(G/sum),Math.round(B/sum),255])
}
else {
rgbaWord [0] = 0
}
return rgbaWord [0];
}
for (let x = 0; x < m; x++) {
for (let y = 0; y < m; y++) {
let fallback = dst[y*m+x];
if (x+1 < m && y+1 < m) {
fallback = weightedAverage ([[fallback,9], [dst[y*m+x+1],4], [dst[y*m+m+x],4], [dst[y*m+m+x+1],1]])
}
for (let i of [2*y*n+2*x,2*y*n+2*x+1,2*y*n+n+2*x,2*y*n+n+2*x+1]) {
rgbaWord[0] = src[i];
if (rgba[3] == 0) src[i] = fallback;
}
}
}
}
// The algorithm, proper
let pyramid = [base];
let N = n;
for (let i = 0; i < k; i++) {
let top = pyramid[i];
pyramid[i+1] = halve(top,N);
N = N/2;
}
for (let i = k; i > 0; i--) {
let bottom = pyramid[i];
N = N*2;
double (pyramid[i-1],pyramid[i],N)
}
// Build return image
let result = new ImageData(w,h);
let res = new Uint32Array (result.data.buffer);
for (let x = 0; x < w; x++) {
for (let y = 0; y < h; y++) {
let i = y*w+x;
let j = y*n+x;
res[i] = dst[j]
}
}
return result
}
Insert cell
md`## Topological filters`
Insert cell
Insert cell
erode = function (maskData, reference = 0) {
let [w,h] = [maskData.width, maskData.height];
let data = maskData.data;
let newMask = new ImageData(w,h)
let newData = newMask.data;
let i = 3;
let w4 = w*4;
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
if (data[i] != reference &&
( (x <= 0 || data[i-4] != reference) &&
(x+1 >= w || data[i+4] != reference) &&
(y <= 0 || data[i-w4] != reference) &&
(y+1 >= h || data[i+w4] != reference))) {
newData[i] = 255-reference;
}
else {
newData[i] = reference;
}
i += 4;
}
}
return newMask
}
Insert cell
Insert cell
dilate = (maskData)=> erode(maskData,255)
Insert cell
Insert cell
Insert cell
function texturePack (curves, imgData, showBoxes = false) {
let tmpCanvas = DOM.canvas (imgData.width, imgData.height, 1);
let tmpCtx = tmpCanvas.getContext("2d");
tmpCtx.putImageData(imgData,0,0);
let pattern = tmpCtx.createPattern(tmpCanvas,"repeat");

let div = html`<div></div>`;
let boxes = curves.map(c => {
let mbr = c.mbr();
let size = mbr.size();
return { sx: mbr.min.x, sy: mbr.min.y, w: size.x, h: size.y, c:c }
})
let rect = potpack(boxes)
let canvas = DOM.canvas (rect.w, rect.h, 1)
let ctx = canvas.getContext("2d")
ctx.imageSmoothingEnabled = false;
ctx.fillStyle = pattern;
for (let box of boxes) {
ctx.setTransform(1, 0, 0, 1, 0, 0); // Reset transform
if (showBoxes) {
ctx.strokeStyle = 'black';
ctx.beginPath();
ctx.rect(box.x,box.y,box.w,box.h);
ctx.stroke();
}
ctx.translate(box.x-box.sx, box.y-box.sy);
ctx.strokeStyle = pattern;
ctx.beginPath()
let {x,y} = box.c[0];
ctx.moveTo(x,y);
for (let {x,y} of box.c) {
ctx.lineTo(x,y)
}
ctx.fill()
ctx.stroke()
}
return { texture: canvas, pieces: boxes }
}
Insert cell
Insert cell
Insert cell
import { bindSliderNumber, bindRadioGroup, bindSelect, bindCheckGroup, bindCheckbox, bindFile, View } from "@esperanc/inline-inputs"
Insert cell
import { comment } from "@esperanc/notebook-css-utilities"
Insert cell
import { Vec, Curve } from "@esperanc/2d-geometry-utils"
Insert cell
Insert cell
import { getCirculations } from "@esperanc/bitmap-tracing"
Insert cell
Insert cell
potpack = require('potpack@1.0.1/index.js')
Insert cell
Insert cell
images = ({ 'basket' : FileAttachment("000R-222.jpg"),
'machine' : FileAttachment("000R-138.jpg"),
'geometry' : FileAttachment("000R-268.jpg")
})
Insert cell
viewof imageName = new View('basket')
Insert cell
mutable img = images[imageName].image()
Insert cell
viewof imgfile = new View(null)
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more