Decomposing an image's color information. Click an image to see it decomposed.
Pixels have been grouped by hue, saturation and lightness into discrete bins. Lightness is plotted on the x-axis, saturation on the y-axis and hue is displayed directly in the cells formed by the intersection of the other two. Each cell's size is determined by the frequency of pixels in that intersection. Treemaps are used to show the relative frequency of the various hues within each cell.
See this block for an alternative design using circle packing instead of treemaps.
Images source: National Gallery of Art's Highlights
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<title>HSL Decomposition</title>
<style>
.images {
margin-left: 75px;
margin-right: 75px;
width: 200px;
float: left;
}
.chart {
width: 500px;
float: left;
}
.axis {
font: 10px sans-serif;
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
ul {
list-style-type: none;
margin: 0;
padding: 0;
}
li {
display: inline;
}
img {
cursor: pointer;
border: 2px solid white;
}
.selected {
border: 2px solid red;
}
</style>
</head>
<body>
<div class="images">
<ul>
<li><img src="van-gogh.jpg" alt="Van Gogh" class="selected" /></li>
<li><img src="fragonard.jpg" alt="Fragonard" /></li>
<li><img src="turner.jpg" alt="Turner" /></li>
<li><img src="whistler.jpg" alt="Whistler"></li>
<li><img src="da-vinci.jpg" alt="Da Vinci"></li>
<li><img src="picasso.jpg" alt="Picasso"></li>
<li><img src="rubens.jpg" alt="Rubens"></li>
<li><img src="vermeer.jpg" alt="Vermeer"></li>
<li><img src="lichtenstein.jpg" alt="Lichtenstein"></li>
<li><img src="matisse.jpg" alt="Matisse"></li>
</ul>
</div>
<div class="chart"></div>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script>
var margin = { top: 10, left: 50, bottom: 50, right: 10 },
width = 500 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var bins = {
h: d3.scale.quantize().domain([0, 360]).range(d3.range(0, 1, 1/15)),
s: d3.scale.quantize().domain([0, 1]).range(d3.range(0, 1, 1/15)),
l: d3.scale.quantize().domain([0, 1]).range(d3.range(0, 1, 1/15)),
};
var scale = {
x: d3.scale.ordinal().domain(d3.range(0, 1, 1/15)).rangeRoundBands([0, width]),
y: d3.scale.ordinal().domain(d3.range(0, 1, 1/15)).rangeRoundBands([height, 0]),
size: d3.scale.pow().exponent(.25).range([0, 1])
};
var axis = {
x: d3.svg.axis().scale(scale.x).orient("bottom").ticks(5).tickFormat(d3.format(".2f")),
y: d3.svg.axis().scale(scale.y).orient("left").tickFormat(d3.format(".2f"))
};
var svg = d3.select(".chart").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g")
.attr("class", "x axis")
.attr("transform", "translate(0," + height + ")")
.call(axis.x)
.append("text")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Lightness");
svg.append("g")
.attr("class", "y axis")
.call(axis.y)
.append("text")
.attr("transform", "rotate(-90)")
.attr("y", 6)
.attr("dy", ".71em")
.style("text-anchor", "end")
.text("Saturation");
var images = d3.selectAll("img")
.on("click", function() {
images.classed("selected", false);
var clicked = d3.select(this).classed("selected", true),
url = clicked.attr("src");
decomposeImage(url);
});
decomposeImage("van-gogh.jpg");
function decomposeImage(url) {
getImageData(url, function(imageData) {
svg.call(render, binHsl(imageData, bins));
});
}
function render(selection, data) {
scale.size.domain([0, d3.max(data, function(d) { return d.freq; })]);
var treemap = d3.layout.treemap()
.children(function(d) { return d.values; })
.value(function(d) { return d.freq; })
.sort(function(a,b) { return a.value - b.value; });
var treemaps = selection.selectAll(".treemap").data(data);
treemaps.enter().append("g")
.attr("class", "treemap");
treemaps
.attr("transform", function(d) {
var size = scale.size(d.freq),
xShift = scale.x.rangeBand()*(1-size)/2,
yShift = scale.y.rangeBand()*(1-size)/2;
return "translate(" + (scale.x(d.l) + xShift) + "," +
(scale.y(d.s) + yShift) + ")";
});
treemaps.exit().remove();
var rects = treemaps.selectAll("rect")
.data(function(d) {
var size = scale.size(d.freq),
dx = scale.x.rangeBand(),
dy = scale.y.rangeBand();
treemap = treemap
.size([dx*size, dy*size]);
return treemap.nodes(d);
});
rects.enter().append("rect");
rects
.filter(function(d) { return d.color !== undefined; })
.attr("x", function(d) { return d.x + d.dx/2; })
.attr("y", function(d) { return d.y + d.dy/2; })
.attr("width", 0)
.attr("height", 0)
.style("fill", "white")
.transition().delay(function(d,i) { return d.s * d.l * 1000; })
.attr("x", function(d) { return d.x; })
.attr("y", function(d) { return d.y; })
.attr("width", function(d) { return d.dx; })
.attr("height", function(d) { return d.dy; })
.style("fill", function(d) { return d.color; });
rects.exit().remove();
}
function getImageData(url, callback) {
var img = new Image();
img.src = url;
var canvas = document.createElement("canvas");
var context = canvas.getContext("2d");
img.onload = function() {
canvas.width = img.width;
canvas.height = img.height;
context.drawImage(img, 0, 0);
img.style.display = "none";
var imageData = context.getImageData(0, 0, canvas.width, canvas.height);
callback(imageData);
}
}
function toMatrix(imageData) {
var data = imageData.data,
matrix = new Array(data.length);
for (var i = 0; i < data.length; i += 4) {
matrix[i/4] = data.subarray(i, i+4);
}
return matrix;
}
function binHsl(imageData, bins) {
var color = toMatrix(imageData)
.map(function(d) {
return d3.hsl("rgb(" + d[0] + "," + d[1] + "," + d[2] + ")");
})
.filter(function(d) { return d !== undefined; });
color = d3.nest()
.key(function(d) {
return "s" + bins.s(d.s) + "-l" + bins.l(d.l);
})
.key(function(d) {
return "h" + bins.h(d.h);
})
.rollup(function(d) { return d.length; })
.entries(color)
.map(function(sl) {
var s = +sl.key.split("-")[0].slice(1),
l = +sl.key.split("-")[1].slice(1),
freq = sl.values.reduce(function(a, b) {
return { values: a.values + b.values };
}).values;
return {
key: sl.key,
s: s,
l: l,
freq: freq,
values: sl.values.map(function(d) {
var h = +d.key.slice(1),
freq = d.values,
color = d3.hsl(
d3.mean(bins.h.invertExtent(h)),
d3.mean(bins.s.invertExtent(s)),
d3.mean(bins.l.invertExtent(l))
).toString();
return { h:h, s:s, l:l, freq:freq, color:color };
})
};
});
return color;
}
</script>
</body>
</html>
https://d3js.org/d3.v3.min.js