Skip to content

Commit

Permalink
Pretty cluster action
Browse files Browse the repository at this point in the history
  • Loading branch information
Dylan Fogarty-MacDonald committed Nov 5, 2011
1 parent cf9a801 commit 6287e12
Show file tree
Hide file tree
Showing 9 changed files with 273 additions and 19 deletions.
2 changes: 2 additions & 0 deletions index.html
Expand Up @@ -18,6 +18,8 @@ <h1>Ladderfck</h1>

<img src="images/testa.jpeg" />

<div></div>


<script src="javascripts/jquery-1.7.min.js"></script>
<script src="javascripts/app.js"></script>
Expand Down
37 changes: 31 additions & 6 deletions javascripts/app.coffee
@@ -1,8 +1,10 @@
$ ->
results = $ 'div'

# Make a canvas
c = $ '<canvas width="500" height="333"></canvas>'
c = $ '<canvas width="500" height="333"/>'
# Add it to the DOM
$('body').append c
c.insertBefore results
ctx = c.get(0).getContext '2d'

sanitise = (data) ->
Expand All @@ -14,11 +16,12 @@ $ ->
i = new Image()
i.onload = ->
ctx.drawImage i, 0, 0
d = ctx.getImageData(0, 0, 500, 333)
#d = ctx.getImageData(0, 0, 500, 333)
d = ctx.getImageData(100, 100, 30, 30)
# Send the data to the worker to convert to colours
worker = new Worker 'javascripts/dataToColours.js'
worker.onmessage = (e) ->
colours = e.data
$(document).trigger 'newColours.LF', [e.data]
worker.terminate()
worker.postMessage sanitise(d.data)
i.src = url
Expand All @@ -27,5 +30,27 @@ $ ->
img = $ 'img'
newImage img.attr('src')

# Feed it into clusterfck
# Show the results
clustersToColours = (clusters) ->
clusters.map (cl) -> cl.canonical

showColours = (colours) ->
results.empty()
sq = $ '<span/>'
colours.forEach (rgb) ->
[r,g,b] = rgb
results.append sq.clone().css('background-color', "rgb(#{r},#{g},#{b})")

# On new colours
$(document).on 'newColours.LF', (e, colours) ->
# Feed it into clusterfck
worker = new Worker 'javascripts/clusterColours.js'
worker.onmessage = (e) ->
$(document).trigger 'newClusters.LF', [e.data]
worker.postMessage colours

# On new clusters
$(document).on 'newClusters.LF', (e, clusters) ->
# Clusters to colours
colours = clustersToColours clusters
# Show the colours
showColours colours
42 changes: 35 additions & 7 deletions javascripts/app.js
@@ -1,8 +1,9 @@
(function() {
$(function() {
var c, ctx, img, newImage, sanitise;
c = $('<canvas width="500" height="333"></canvas>');
$('body').append(c);
var c, clustersToColours, ctx, img, newImage, results, sanitise, showColours;
results = $('div');
c = $('<canvas width="500" height="333"/>');
c.insertBefore(results);
ctx = c.get(0).getContext('2d');
sanitise = function(data) {
var clean, i, pix, _len;
Expand All @@ -19,18 +20,45 @@
i.onload = function() {
var d, worker;
ctx.drawImage(i, 0, 0);
d = ctx.getImageData(0, 0, 500, 333);
d = ctx.getImageData(100, 100, 30, 30);
worker = new Worker('javascripts/dataToColours.js');
worker.onmessage = function(e) {
var colours;
colours = e.data;
$(document).trigger('newColours.LF', [e.data]);
return worker.terminate();
};
return worker.postMessage(sanitise(d.data));
};
return i.src = url;
};
img = $('img');
return newImage(img.attr('src'));
newImage(img.attr('src'));
clustersToColours = function(clusters) {
return clusters.map(function(cl) {
return cl.canonical;
});
};
showColours = function(colours) {
var sq;
results.empty();
sq = $('<span/>');
return colours.forEach(function(rgb) {
var b, g, r;
r = rgb[0], g = rgb[1], b = rgb[2];
return results.append(sq.clone().css('background-color', "rgb(" + r + "," + g + "," + b + ")"));
});
};
$(document).on('newColours.LF', function(e, colours) {
var worker;
worker = new Worker('javascripts/clusterColours.js');
worker.onmessage = function(e) {
return $(document).trigger('newClusters.LF', [e.data]);
};
return worker.postMessage(colours);
});
return $(document).on('newClusters.LF', function(e, clusters) {
var colours;
colours = clustersToColours(clusters);
return showColours(colours);
});
});
}).call(this);
5 changes: 5 additions & 0 deletions javascripts/clusterColours.coffee
@@ -0,0 +1,5 @@
importScripts 'clusterfck-0.1.js'

self.onmessage = (e) ->
clusters = clusterfck.hcluster e.data, clusterfck.EUCLIDEAN_DISTANCE, clusterfck.AVERAGE_LINKAGE, 10
self.postMessage clusters
8 changes: 8 additions & 0 deletions javascripts/clusterColours.js
@@ -0,0 +1,8 @@
(function() {
importScripts('clusterfck-0.1.js');
self.onmessage = function(e) {
var clusters;
clusters = clusterfck.hcluster(e.data, clusterfck.EUCLIDEAN_DISTANCE, clusterfck.AVERAGE_LINKAGE, 10);
return self.postMessage(clusters);
};
}).call(this);
180 changes: 180 additions & 0 deletions javascripts/clusterfck-0.1.js
@@ -0,0 +1,180 @@
var clusterfck = (function() {
var module = { exports: {}};
var exports = module.exports;
module.exports = (function() {
var module = { exports: {}};
var exports = module.exports;

var HierarchicalClustering = function(distance, merge, threshold) {
this.distance = distance || clusterfck.EUCLIDEAN_DISTANCE;
this.merge = merge || clusterfck.AVERAGE_LINKAGE;
this.threshold = threshold == undefined ? Infinity : threshold;
}

HierarchicalClustering.prototype = {
cluster : function(items, snapshot, snapshotCallback) {
var clusters = [];
var dists = []; // distances between each pair of clusters
var mins = []; // closest cluster for each cluster
var index = []; // keep a hash of all clusters by key
for(var i = 0; i < items.length; i++) {
var cluster = { canonical: items[i], key: i, index: i, size: 1};
clusters[i] = cluster;
index[i] = cluster;
dists[i] = [];
mins[i] = 0;
}

for(var i = 0; i < clusters.length; i++) {
for(var j = 0; j <= i; j++) {
var dist = (i == j) ? Infinity :
this.distance(clusters[i].canonical, clusters[j].canonical);
dists[i][j] = dist;
dists[j][i] = dist;

if(dist < dists[i][mins[i]])
mins[i] = j;
}
}

var merged = this.mergeClosest(clusters, dists, mins, index);
var i = 0;
while(merged) {
if(snapshotCallback && (i % snapshot) == 0)
snapshotCallback(clusters);

merged = this.mergeClosest(clusters, dists, mins, index);
i++;
}

clusters.forEach(function(cluster) {
// clean up metadata used for clustering
delete cluster.key;
delete cluster.index;
});

return clusters;
},

mergeClosest: function(clusters, dists, mins, index) {
// find two closest clusters from cached mins
var minKey = 0, min = Infinity;
for(var i = 0; i < clusters.length; i++) {
var key = clusters[i].key,
dist = dists[key][mins[key]];
if(dist < min) {
minKey = key;
min = dist;
}
}
if(min >= this.threshold)
return false;

var c1 = index[minKey],
c2 = index[mins[minKey]];

// merge two closest clusters
var merged = { canonical: this.merge(c1.canonical, c2.canonical),
left: c1,
right: c2,
key: c1.key,
size: c1.size + c2.size };

clusters[c1.index] = merged;
clusters.splice(c2.index, 1);
index[c1.key] = merged;


// update distances with new merged cluster
for(var i = 0; i < clusters.length; i++) {
var ci = clusters[i];
var dist;
if(c1.key == ci.key)
dist = Infinity;
else if(this.merge == clusterfck.SINGLE_LINKAGE) {
dist = dists[c1.key][ci.key];
if(dists[c1.key][ci.key] > dists[c2.key][ci.key])
dist = dists[c2.key][ci.key];
}
else if(this.merge == clusterfck.COMPLETE_LINKAGE) {
dist = dists[c1.key][ci.key];
if(dists[c1.key][ci.key] < dists[c2.key][ci.key])
dist = dists[c2.key][ci.key];
}
else if(this.merge == clusterfck.AVERAGE_LINKAGE) {
dist = (dists[c1.key][ci.key] * c1.size
+ dists[c2.key][ci.key] * c2.size) / (c1.size + c2.size);
}
else
dist = this.distance(ci.canonical, c1.canonical);

dists[c1.key][ci.key] = dists[ci.key][c1.key] = dist;
}


// update cached mins
for(var i = 0; i < clusters.length; i++) {
var key1 = clusters[i].key;
if(mins[key1] == c1.key || mins[key1] == c2.key) {
var min = key1;
for(var j = 0; j < clusters.length; j++) {
var key2 = clusters[j].key;
if(dists[key1][key2] < dists[key1][min])
min = key2;
}
mins[key1] = min;
}
clusters[i].index = i;
}

// clean up metadata used for clustering
delete c1.key; delete c2.key;
delete c1.index; delete c2.index;

return true;
}
}

var SINGLE_LINKAGE = function(c1, c2) { return c1; };
var COMPLETE_LINKAGE = function(c1, c2) { return c1; };
var AVERAGE_LINKAGE = function(c1, c2) { return c1; };

var EUCLIDEAN_DISTANCE = function(v1, v2) {
var total = 0;
for(var i = 0; i < v1.length; i++)
total += Math.pow(v2[i] - v1[i], 2)
return Math.sqrt(total);
}

var MANHATTAN_DISTANCE = function(v1, v2) {
var total = 0;
for(var i = 0; i < v1.length ; i++)
total += Math.abs(v2[i] - v1[i])
return total;
}

var MAX_DISTANCE = function(v1, v2) {
var max = 0;
for(var i = 0; i < v1.length; i++)
max = Math.max(max , Math.abs(v2[i] - v1[i]));
return max;
}

var hcluster = function(items, distance, merge, threshold, snapshot, snapshotCallback) {
return (new HierarchicalClustering(distance, merge, threshold))
.cluster(items, snapshot, snapshotCallback);
}

clusterfck = {
hcluster: hcluster,
SINGLE_LINKAGE: SINGLE_LINKAGE,
COMPLETE_LINKAGE: COMPLETE_LINKAGE,
AVERAGE_LINKAGE: AVERAGE_LINKAGE,
EUCLIDEAN_DISTANCE: EUCLIDEAN_DISTANCE,
MANHATTAN_DISTANCE: MANHATTAN_DISTANCE,
MAX_DISTANCE: MAX_DISTANCE
};

module.exports = clusterfck;
return module.exports; })();
return module.exports; })()
8 changes: 5 additions & 3 deletions javascripts/dataToColours.coffee
@@ -1,8 +1,10 @@
self.onmessage = (e) ->
colours = []
colours = [[]]

for i, pix of e.data
colours.push([]) if ((+i)%4) is 1
colours[colours.length-1].push pix
if (+i)%4 is 0
colours.push([])
else
colours[colours.length-1].push pix

self.postMessage colours
7 changes: 4 additions & 3 deletions javascripts/dataToColours.js
@@ -1,14 +1,15 @@
(function() {
self.onmessage = function(e) {
var colours, i, pix, _ref;
colours = [];
colours = [[]];
_ref = e.data;
for (i in _ref) {
pix = _ref[i];
if (((+i) % 4) === 1) {
if ((+i) % 4 === 0) {
colours.push([]);
} else {
colours[colours.length - 1].push(pix);
}
colours[colours.length - 1].push(pix);
}
return self.postMessage(colours);
};
Expand Down
3 changes: 3 additions & 0 deletions stylesheets/app.css
Expand Up @@ -80,3 +80,6 @@ html {
b, strong {font-weight:bold}

img {display:none}

/*Results*/
div span {display:inline-block;width:10px;height:10px;margin:0 3px 3px 0}

0 comments on commit 6287e12

Please sign in to comment.