Skip to content

Instantly share code, notes, and snippets.

@emeeks
Created February 26, 2016 21:04
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save emeeks/2acc14409f7108826fab to your computer and use it in GitHub Desktop.
Save emeeks/2acc14409f7108826fab to your computer and use it in GitHub Desktop.

Refresh to see a new image.

Image processing with canvas. First, the RGB grid is read using getImageData then that grid is compacted so every 9x9 pixel region becomes a pixel with a value equal to the "Most dominant color" in that region. Color is defined as a hue region (with high or low luminosity defined as white and black). The resulting compacted grid is then fed into d3.geom.concaveHull to draw a concave hull around the regions of similar color, which is then filled with the hue and the average saturation and luminosity of that hue across the entire image.

colorHash = {
"white": 0,
"black": 360,
"Orange Red": 15,
"Safety Orange": 25,
"Dark Orange": 35,
"Amber": 45,
"Golden Yellow": 55,
"Chartreuse Yellow": 65,
"Electric Lime": 75,
"Spring Bud": 85,
"Bright Green": 95,
"Harlequin": 120,
"Lime": 125,
"Free Speech Green": 135,
"Spring Green": 160,
"Medium Spring Green": 180,
"Aqua": 185,
"Deep Sky Blue": 195,
"Dodger Blue": 210,
"Blue": 235,
"Han Purple": 255,
"Electric Indigo": 280,
"Psychedelic Purple": 295,
"Magenta": 305,
"Hot Magenta": 315,
"Hollywood Cerise": 325,
"Deep Pink": 335,
"Torch Red": 345,
"Red": 5
}
(function() {
d3.geom.concaveHull = function() {
var calculateDistance = stdevDistance,
padding = 0,
delaunay;
function distance(a, b) {
var dx = a[0]-b[0],
dy = a[1]-b[1];
return Math.sqrt((dx * dx) + (dy * dy));
}
function stdevDistance(delaunay) {
var sides = [];
delaunay.forEach(function (d) {
sides.push(distance(d[0],d[1]));
sides.push(distance(d[0],d[2]));
sides.push(distance(d[1],d[2]));
});
var dev = d3.deviation(sides);
var mean = d3.mean(sides);
return mean + dev;
}
function concaveHull(vertices) {
delaunay = d3.geom.delaunay(vertices);
var longEdge = calculateDistance(delaunay);
mesh = delaunay.filter(function (d) {
return distance(d[0],d[1]) < longEdge && distance(d[0],d[2]) < longEdge && distance(d[1],d[2]) < longEdge
})
var counts = {},
edges = {},
r,
result = [],
deletionList = [];
// Traverse the edges of all triangles and discard any edges that appear twice.
mesh.forEach(function(triangle) {
for (var i = 0; i < 3; i++) {
var edge = [triangle[i], triangle[(i + 1) % 3]].sort(ascendingCoords).map(String);
(edges[edge[0]] = (edges[edge[0]] || [])).push(edge[1]);
(edges[edge[1]] = (edges[edge[1]] || [])).push(edge[0]);
var k = edge.join(":");
if (counts[k]) deletionList.push(k);
else counts[k] = 1;
}
});
deletionList.forEach(function (k) {
delete counts[k];
})
while (1) {
var k = null;
// Pick an arbitrary starting point on a boundary.
for (k in counts) break;
if (k == null) break;
result.push(r = k.split(":").map(function(d) { return d.split(",").map(Number); }));
delete counts[k];
var q = r[1];
while (q[0] !== r[0][0] || q[1] !== r[0][1]) {
var p = q,
qs = edges[p.join(",")],
n = qs.length;
for (var i = 0; i < n; i++) {
q = qs[i].split(",").map(Number);
var edge = [p, q].sort(ascendingCoords).join(":");
if (counts[edge]) {
delete counts[edge];
r.push(q);
break;
}
}
}
}
if (padding !== 0) {
result = pad(result, padding);
}
return result;
}
function pad(bounds, amount) {
var result = [];
bounds.forEach(function(bound) {
var padded = [];
var area = 0;
bound.forEach(function(p, i) {
// http://forums.esri.com/Thread.asp?c=2&f=1718&t=174277
// Area = Area + (X2 - X1) * (Y2 + Y1) / 2
var im1 = i - 1;
if(i == 0) {
im1 = bound.length - 1;
}
var pm = bound[im1];
area += (p[0] - pm[0]) * (p[1] + pm[1]) / 2;
});
var handedness = 1;
if(area > 0) handedness = -1
bound.forEach(function(p, i) {
// average the tangent between
var im1 = i - 1;
if(i == 0) {
im1 = bound.length - 2;
}
//var tp = getTangent(p, bound[ip1]);
var tm = getTangent(p, bound[im1]);
//var avg = { x: (tp.x + tm.x)/2, y: (tp.y + tm.y)/2 };
//var normal = rotate2d(avg, 90);
var normal = rotate2d(tm, 90 * handedness);
padded.push([p[0] + normal.x * amount, p[1] + normal.y * amount ])
})
result.push(padded)
})
return result
}
function getTangent(a, b) {
var vector = { x: b[0] - a[0], y: b[1] - a[1] }
var magnitude = Math.sqrt(vector.x*vector.x + vector.y*vector.y);
vector.x /= magnitude;
vector.y /= magnitude;
return vector
}
function rotate2d(vector, angle) {
//rotate a vector
angle *= Math.PI/180; //convert to radians
return {
x: vector.x * Math.cos(angle) - vector.y * Math.sin(angle),
y: vector.x * Math.sin(angle) + vector.y * Math.cos(angle)
}
}
function ascendingCoords(a, b) {
return a[0] === b[0] ? b[1] - a[1] : b[0] - a[0];
}
concaveHull.padding = function (newPadding) {
if (!arguments.length) return padding;
padding = newPadding;
return concaveHull;
}
concaveHull.distance = function (newDistance) {
if (!arguments.length) return calculateDistance;
calculateDistance = newDistance;
if (typeof newDistance === "number") {
calculateDistance = function () {return newDistance};
}
return concaveHull;
}
return concaveHull;
}
})()
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Image Hulling</title>
<style>
canvas {
position: absolute;
}
svg {
position: absolute;
}
#cv-new {
top: 500px;
}
#cv {
opacity: 0;
}
img {
position: fixed;
top: 0;
left: 600px;
max-width: 500px;
z-index: 99;
}
</style>
</head>
<body>
<img></img>
<canvas id="cv" width="1000" height="500" ></canvas>
<svg width="1000" height="500" ></svg>
<script src="http://d3js.org/d3.v3.min.js" charset="utf-8" type="text/javascript"></script>
<script src="colornames.js" charset="utf-8" type="text/javascript"></script>
<script src="d3.geom.concaveHull.js" charset="utf-8" type="text/javascript"></script>
<script type="text/javascript">
var gridSize = 3;
var lessDominant = true;
var localColorization = true;
var canvas = document.getElementById('cv');
context = canvas.getContext('2d');
var img = new Image();
var rando = 'source' + (parseInt(Math.random() * 6) + 1) + '.jpg';
img.src = rando;
d3.select("img").attr("src", rando)
img.onload = function () {
d3.select("canvas").attr("width", img.width).attr("height", img.height)
d3.select("svg").attr("width", img.width).attr("height", img.height)
process(img.width, img.height);
};
function process(imageWidth, imageHeight) {
context.drawImage(img, 0, 0);
var imageData = context.getImageData(0, 0, imageWidth, imageHeight);
var hslArray = [];
var hslGrid = [[]];
var cx = 0;
var cy = 0;
for (var x = 0; x<imageData.data.length;x = x + 4) {
var r = x;
var g = x + 1;
var b = x + 2;
var hsl = d3.hsl(d3.rgb(imageData.data[r],imageData.data[g],imageData.data[b],255));
var group = "Red";
var hue = hsl.h;
if (hsl.l <= .1) {
group = "black"
hsl.h = 360;
} else if (hsl.l >= .9) {
group = "white"
hsl.h = 0;
} else if (hue < 10) {
group = "Red";
} else if (hue < 20) {
group = "Orange Red";
} else if (hue < 30) {
group = "Safety Orange";
} else if (hue < 40) {
group = "Dark Orange";
} else if (hue < 50) {
group = "Amber";
} else if (hue < 60) {
group = "Golden Yellow";
} else if (hue < 70) {
group = "Chartreuse Yellow";
} else if (hue < 80) {
group = "Electric Lime";
} else if (hue < 90) {
group = "Spring Bud";
} else if (hue < 100) {
group = "Bright Green";
} else if (hue < 110) {
group = "Harlequin";
} else if (hue < 130) {
group = "Lime";
} else if (hue < 140) {
group = "Free Speech Green";
} else if (hue < 160) {
group = "Spring Green";
} else if (hue < 170) {
group = "Medium Spring Green";
} else if (hue < 190) {
group = "Aqua";
} else if (hue < 200) {
group = "Deep Sky Blue";
} else if (hue < 220) {
group = "Dodger Blue";
} else if (hue < 250) {
group = "Blue";
} else if (hue < 260) {
group = "Han Purple";
} else if (hue < 270) {
group = "Electric Indigo";
} else if (hue < 290) {
group = "Electric Purple";
} else if (hue < 300) {
group = "Psychedelic Purple";
} else if (hue < 310) {
group = "Magenta";
} else if (hue < 320) {
group = "Hot Magenta";
} else if (hue < 330) {
group = "Hollywood Cerise";
} else if (hue < 340) {
group = "Deep Pink";
} else if (hue < 350) {
group = "Torch Red";
}
var hsl = {x: cx, y: cy, hsl: hsl, group: group};
hslArray.push(hsl);
hslGrid[cy].push(hsl);
cx = cx + 1;
if (cx === imageWidth) {
cx = 0;
cy = cy + 1;
hslGrid[cy] = [];
}
}
var data = hslArray.map(d => [d.x, d.y]);
var canvasWidth = imageWidth;
var canvasHeight = imageHeight;
var gridX = 0;
var gridY = 0;
var compactGrid = [];
while (gridY < canvasHeight) {
while (gridX < canvasWidth) {
var stepY = 0;
var stepX = 0;
var region = [];
for (x = 0; x<gridSize; x++) {
for (y = 0; y<gridSize; y++) {
if(hslGrid[y + gridY] && hslGrid[y + gridY][x + gridX]) {
region.push(hslGrid[y + gridY][x + gridX]);
}
}
}
var hue = dominantColor(region.map(d => d.group));
var color = d3.hsl(hue, d3.mean(region.map(d => d.hsl.s)), d3.mean(region.map(d => d.hsl.l)))
if (hue === undefined) {
css = "none";
}
else {
css = color.toString();
}
cArray = [];
cArray.push(color.h);
cArray.push(color.s);
cArray.push(color.l);
cArray.push(255);
var compactCell = {color: cArray, css: css, x: gridX, y: gridY};
compactGrid.push(compactCell);
gridX = gridX + gridSize;
}
gridX = 0;
gridY = gridY + gridSize;
}
hull = d3.geom.concaveHull().distance(gridSize * 3);
for (x in colorHash) {
onlyThisColor = compactGrid.filter(d => d.color[0] === colorHash[x]);
var averageSaturation = d3.mean(onlyThisColor.map(d => d.color[1]))
var averageLuminosity = d3.mean(onlyThisColor.map(d => d.color[2]))
console.log("******************")
console.log(x)
console.log(x === "black" || x === "white" ? x : d3.hsl(colorHash[x], averageSaturation, averageLuminosity).toString())
console.log(onlyThisColor)
if (onlyThisColor.length > 3) {
d3.select("svg").append("g")
.attr("transform", "translate(0,0)")
.selectAll("path")
.data(hull(onlyThisColor.map(d => [d.x + (Math.random()), d.y + (Math.random())])))
.enter().append("path")
.attr("d", function(d) { return "M" + d.join("L") + "Z"; })
.style("fill-opacity", .9)
.style("fill", x === "black" || x === "white" ? x : d3.hsl(colorHash[x], averageSaturation, averageLuminosity).toString())
}
}
function dominantColor(colorArray) {
var colorCensus = {};
var colorLength = colorArray.length;
colorArray.forEach(color => {
colorCensus[color] ? colorCensus[color]++ : colorCensus[color] = 1;
});
for (x in colorCensus) {
if (colorCensus[x] * 2 >= colorLength) {
return colorHash[x];
}
}
if (lessDominant) {
var max = 0;
var value = "none";
for (x in colorCensus) {
if (colorCensus[x] > max) {
value = colorHash[x];
}
}
}
return value;
}
}
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment