Skip to content

Instantly share code, notes, and snippets.

@duhaime
Last active February 10, 2019 15:29
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 duhaime/38d0ecec4643c04809340913b5dab1a0 to your computer and use it in GitHub Desktop.
Save duhaime/38d0ecec4643c04809340913b5dab1a0 to your computer and use it in GitHub Desktop.
Dancer Grid
<!DOCTYPE html>
<html>
<head>
<meta charset='utf-8'>
<title>title</title>
<style type='text/css'>
* {
margin: 0;
padding: 0;
}
/**
* Canvas Grid
**/
#grid-target {
margin: 30px auto;
display: block;
border: 1px solid #ddd;
width: 600px;
height: 600px;
position: relative;
}
.canvas-bar {
background: #ddd;
position: absolute;
}
.canvas-bar.x {
height: 100%;
width: 1px;
top: 0;
}
.canvas-bar.y {
width: 100%;
height: 1px;
left: 0;
}
/**
* UI Toggle
**/
#ui-box {
position: absolute;
width: 100px;
height: 100px;
background: #f9f7f7;
border: 1px solid #dedede;
}
#ui-toggle {
width: 16px;
height: 16px;
background: #fff;
border: 1px solid #909090;
border-radius: 100%;
position: relative;
display: inline-block;
box-shadow: 0px 0px 5px rgba(0, 0, 0, 0.3);
}
</style>
<script src='permutations.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/tensorflow/0.15.0/tf.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/three.js/97/three.min.js'></script>
<script src='https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.11/lodash.min.js'></script>
</head>
<body>
<div>
<code>See: <a href='https://s3.amazonaws.com/duhaime/blog/visualizations/dancer-latent-space/index.html'>https://s3.amazonaws.com/duhaime/blog/visualizations/dancer-latent-space/index.html</a></code>
</div>
<div id='grid-target'></div>
<script>
/**
* Camera Views
**/
// background color for each cell in the multiview grid
function getBackground() {
var c = 0.95;
return new THREE.Color(c, c, c);
}
// shuffle an array (so we get random camera positions)
function shuffle(a) {
var j, x, i;
for (i = a.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1));
x = a[i];
a[i] = a[j];
a[j] = x;
}
return a;
}
// set canvas size and parent node
var parentNode = document.querySelector('#grid-target'),
width = parentNode.clientWidth,
height = parentNode.clientHeight;
// get the viewport options and set grid size
var options = shuffle(permutationsWithRepetition([-55, 0, 55, -20, 20], 3)),
cols = 3,
rows = 3;
// specify the list of camera positions; one per grid cell
var views = [];
for (var x=0; x<cols; x++) {
for (var y=0; y<rows; y++) {
var idx = (y*rows) + x;
views.push({
left: x/cols,
top: y/rows,
width: 1.0/cols,
height: 1.0/rows,
background: getBackground(),
position: options[idx],
})
}
}
// add the camera for each grid cell
for (var i=0; i<views.length; i++) {
var view = views[i];
var aspectRatio = width / height;
var camScale = 15;
/*
var camera = new THREE.OrthographicCamera(
width / - camScale,
width / camScale,
height / camScale,
height / - camScale,
1,
300,
);
*/
var aspectRatio = window.innerWidth / window.innerHeight;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000);
camera.position.fromArray(view.position);
view.camera = camera;
}
/**
* Scene
**/
// generate a scene object
var scene = new THREE.Scene();
// generate a camera
var aspectRatio = width / height;
var camera = new THREE.PerspectiveCamera(75, aspectRatio, 0.1, 1000);
camera.position.set(0, 0, -150);
// generate a renderer
var renderer = new THREE.WebGLRenderer({
antialias: true,
alpha: true,
});
renderer.shadowMapEnabled = true;
renderer.shadowMapSoft = true;
renderer.setPixelRatio(window.devicePixelRatio); // <3 retina
renderer.setSize(width, height); // canvas size
parentNode.appendChild(renderer.domElement);
// generate some lights
var spotLight = new THREE.SpotLight(0xffffff);
spotLight.position.set(10, 10, -30);
spotLight.castShadow = true;
/**
* Configure UI widget for latent space sampling
**/
// draw a grid to divide up the cells in the canvas
for (var x=1; x<cols; x++) {
var elem = document.createElement('div');
elem.className = 'canvas-bar x';
elem.style.left = (x/cols)*width + 'px';
parentNode.appendChild(elem);
}
for (var y=1; y<rows; y++) {
var elem = document.createElement('div');
elem.className = 'canvas-bar y';
elem.style.top = (y/rows)*height + 'px';
parentNode.appendChild(elem);
}
// add a cell that samples from latent space
var box = document.createElement('div'),
toggle = document.createElement('div'),
initial = null, // initial coords of toggle
down = null; // object if we've mousedowned, else null
box.id = 'ui-box';
toggle.id = 'ui-toggle';
box.style.width = (width/cols)-1 + 'px';
box.style.height = (height/rows)-1 + 'px';
box.style.left = Math.floor(cols/2) * (width/cols) + 'px';
box.style.top = Math.floor(rows/2) * (height/rows) + 'px';
toggle.style.left = '0px';
toggle.style.top = '0px';
box.appendChild(toggle);
parentNode.appendChild(box);
toggle.addEventListener('mousedown', function(e) {
if (!initial) initial = {x: e.pageX, y: e.pageY};
down = {x: e.pageX, y: e.pageY};
})
window.addEventListener('mousemove', function(e) {
if (down) moveToggle(e);
})
window.addEventListener('mouseup', function(e) {
moveToggle(e);
down = null;
})
function moveToggle(e) {
if (!initial) return;
var x = e.pageX - initial.x,
y = e.pageY - initial.y;
var boxW = box.clientWidth,
toggleW = toggle.clientWidth;
// keep toggle in box
if (x < 0) x = 0;
if (y < 0) y = 0;
if (x > boxW - toggleW) x = boxW - toggleW;
if (y > boxW - toggleW) y = boxW - toggleW;
toggle.style.left = x + 'px';
toggle.style.top = y + 'px';
sample(x, y);
}
/**
* Dancer
**/
var maxLines = 140,
scalar = 100,
vertexPairs = [[26, 53], [0, 34], [4, 30], [8, 34], [0, 8], [4, 8], [0, 30], [30, 34], [29, 38], [0, 4], [45, 50], [8, 30], [18, 24], [44, 50], [3, 12], [15, 21], [19, 24], [29, 48], [14, 20], [18, 19], [16, 17], [33, 39], [41, 47], [42, 43], [4, 34], [6, 27], [3, 22], [44, 45], [40, 46], [40, 41], [7, 25], [40, 47], [14, 15], [46, 47], [20, 21], [5, 10], [14, 21], [16, 22], [35, 39], [15, 20], [38, 44], [38, 45], [33, 35], [29, 44], [2, 5], [29, 45], [11, 28], [28, 37], [29, 50], [41, 46], [38, 48], [27, 32], [38, 50], [9, 15], [7, 13], [2, 10], [9, 13], [12, 19], [3, 16], [35, 47], [6, 11], [17, 22], [12, 22], [36, 51], [16, 23], [42, 49], [31, 36], [10, 25], [12, 18], [1, 2], [1, 36], [5, 25], [1, 31], [6, 32], [26, 37], [9, 21], [32, 37], [26, 28], [3, 19], [11, 27], [2, 36], [3, 18], [35, 41], [11, 26], [37, 53], [27, 37], [42, 48], [28, 32], [2, 31], [28, 53], [11, 37], [11, 53], [6, 28], [27, 28], [6, 26], [13, 25], [3, 24], [10, 52], [6, 53], [12, 24], [2, 52], [6, 37], [1, 51], [39, 47], [27, 53], [35, 40], [43, 49], [11, 32], [26, 27], [13, 15], [33, 47], [17, 23], [23, 53], [2, 54], [39, 41], [36, 52], [29, 42], [43, 48], [3, 17], [23, 26], [26, 32], [32, 53], [31, 51], [2, 4], [9, 14], [9, 20], [44, 48], [39, 51], [13, 21], [5, 7], [26, 49], [33, 51], [49, 53], [39, 40], [5, 31], [10, 31], [7, 10], [33, 41], [45, 48], [35, 46], [1, 52], [1, 5], [12, 16], [2, 30], [29, 43], [2, 8], [31, 52], [5, 52], [31, 32], [0, 2], [32, 54], [39, 46], [13, 20], [2, 25], [48, 50], [12, 17], [52, 54], [1, 10], [32, 52], [31, 54], [2, 51], [5, 8], [13, 14], [16, 53], [33, 40], [10, 32], [2, 34], [11, 52], [2, 32], [10, 36], [36, 54], [1, 54], [1, 30], [16, 26], [7, 9]].slice(0, maxLines),
geometry = new THREE.BufferGeometry(),
verts = new Float32Array((maxLines-1)*2*3),
material = new THREE.LineBasicMaterial({
color: 0xff0000,
transparent: true,
});
for (var i=0; i<verts.length; i++) {
verts[i++] = Math.random() * 100;
}
geometry.addAttribute('position', new THREE.BufferAttribute(verts, 3));
material.opacity = 0;
material.castShadow = true;
var dancer = new THREE.LineSegments(geometry, material);
dancer.rotation.x = Math.PI*1.5;
dancer.position.y = 42;
scene.add(dancer);
/**
* Model
**/
// load a keras model
var model = tf.loadModel('./model.json');
model.then(function(model) {
window.model = model;
if (window.log === true) console.log(' * model loaded');
// create a tensor and predict on it; call .print() on .predict()
// to display the value in the console directly
sample(-1, 1);
})
// convert x, y using linear scale from box to latent coords
function boxToLatentSpace(x, y) {
var boxScale = {
x: [0, box.clientWidth],
y: [0, box.clientHeight],
};
var latentScale = {
x: [-5, 5],
y: [-5, 5],
};
// scale each var 0:1
var x = (x-boxScale.x[0])/(boxScale.x[1]-boxScale.x[0]),
y = (y-boxScale.y[0])/(boxScale.y[1]-boxScale.y[0]);
// project each var into latent space
return {
x: (x*(latentScale.x[1]-latentScale.x[0]))+latentScale.x[0],
y: (y*(latentScale.y[1]-latentScale.y[0]))+latentScale.y[0],
}
}
// sample from latent space at position x, y
function sample(x, y) {
var scaled = boxToLatentSpace(x, y);
if (window.log === true) console.log('samping', scaled);
var tensor = tf.tensor2d([[scaled.x, scaled.y]]),
prediction = model.predict(tensor);
// extract the data from the prediction
prediction.data().then(function(data) {
// data = 165 verts in 3 dims, x0,y0,z0, x1,y1,z1,...
var formatted = [],
point = {};
// format the data into 55 objs with x,y,z attrs
data.forEach(function(d, idx) {
if (idx%3==0) { point.x = d; };
if (idx%3==1) { point.y = d; };
if (idx%3==2) { point.z = d; formatted.push(point); point = {} };
});
// center the model in each dimension to allow rotation on z-axis
var sums = {x: 0, y: 0, z: 0};
formatted.forEach(function(d) {
sums.x += d.x;
sums.y += d.y;
sums.z += d.z;
})
formatted.forEach(function(d) {
d.x -= sums.x/55;
d.y -= sums.y/55;
d.z -= sums.z/55;
})
var i=0;
vertexPairs.forEach(function(pair, pairIdx) {
var a = formatted[ pair[0] ];
var b = formatted[ pair[1] ];
dancer.geometry.attributes.position.array[i++] = a.x * scalar;
dancer.geometry.attributes.position.array[i++] = a.y * scalar;
dancer.geometry.attributes.position.array[i++] = a.z * scalar;
dancer.geometry.attributes.position.array[i++] = b.x * scalar;
dancer.geometry.attributes.position.array[i++] = b.y * scalar;
dancer.geometry.attributes.position.array[i++] = b.z * scalar;
})
material.opacity = 1.0;
dancer.geometry.attributes.position.needsUpdate = true;
})
}
/**
* Main
**/
// render loop
function render() {
//dancer.rotation.z += .01;
requestAnimationFrame(render);
renderer.render(scene, camera);
for (var i=0; i<views.length; i++) {
var view = views[i];
var left = Math.floor(width * view.left);
var top = Math.floor(height * view.top);
var w = Math.floor(width * view.width);
var h = Math.floor(height * view.height);
renderer.setViewport(left, top, w, h);
renderer.setScissor(left, top, w, h);
renderer.setScissorTest(true);
renderer.setClearColor(view.background);
view.camera.lookAt(dancer.position);
view.camera.aspect = w / h;
view.camera.updateProjectionMatrix();
renderer.render(scene, view.camera);
}
};
render();
</script>
</body>
</html>
{"modelTopology": {"keras_version": "2.2.2", "backend": "tensorflow", "model_config": {"config": {"layers": [{"inbound_nodes": [], "name": "input_2", "class_name": "InputLayer", "config": {"name": "input_2", "batch_input_shape": [null, 2], "sparse": false, "dtype": "float32"}}, {"inbound_nodes": [[["input_2", 0, 0, {}]]], "name": "dense_4", "class_name": "Dense", "config": {"kernel_initializer": {"config": {"distribution": "uniform", "mode": "fan_avg", "scale": 1.0, "seed": null}, "class_name": "VarianceScaling"}, "kernel_constraint": null, "use_bias": true, "name": "dense_4", "bias_constraint": null, "trainable": true, "kernel_regularizer": null, "activity_regularizer": null, "units": 128, "bias_regularizer": null, "activation": "relu", "bias_initializer": {"config": {}, "class_name": "Zeros"}}}, {"inbound_nodes": [[["dense_4", 0, 0, {}]]], "name": "dense_5", "class_name": "Dense", "config": {"kernel_initializer": {"config": {"distribution": "uniform", "mode": "fan_avg", "scale": 1.0, "seed": null}, "class_name": "VarianceScaling"}, "kernel_constraint": null, "use_bias": true, "name": "dense_5", "bias_constraint": null, "trainable": true, "kernel_regularizer": null, "activity_regularizer": null, "units": 128, "bias_regularizer": null, "activation": "relu", "bias_initializer": {"config": {}, "class_name": "Zeros"}}}, {"inbound_nodes": [[["dense_5", 0, 0, {}]]], "name": "dense_6", "class_name": "Dense", "config": {"kernel_initializer": {"config": {"distribution": "uniform", "mode": "fan_avg", "scale": 1.0, "seed": null}, "class_name": "VarianceScaling"}, "kernel_constraint": null, "use_bias": true, "name": "dense_6", "bias_constraint": null, "trainable": true, "kernel_regularizer": null, "activity_regularizer": null, "units": 165, "bias_regularizer": null, "activation": "tanh", "bias_initializer": {"config": {}, "class_name": "Zeros"}}}, {"inbound_nodes": [[["dense_6", 0, 0, {}]]], "name": "reshape_2", "class_name": "Reshape", "config": {"name": "reshape_2", "trainable": true, "target_shape": [55, 3]}}], "name": "model_2", "output_layers": [["reshape_2", 0, 0]], "input_layers": [["input_2", 0, 0]]}, "class_name": "Model"}}, "weightsManifest": [{"weights": [{"name": "dense_4/kernel", "dtype": "float32", "shape": [2, 128]}, {"name": "dense_4/bias", "dtype": "float32", "shape": [128]}, {"name": "dense_5/kernel", "dtype": "float32", "shape": [128, 128]}, {"name": "dense_5/bias", "dtype": "float32", "shape": [128]}, {"name": "dense_6/kernel", "dtype": "float32", "shape": [128, 165]}, {"name": "dense_6/bias", "dtype": "float32", "shape": [165]}], "paths": ["group1-shard1of1"]}]}
// permutationsWithRepetition :: Int -> [a] -> [[a]]
var permutationsWithRepetition = function (as, n) {
return as.length > 0 ? (
foldl1(curry(cartesianProduct)(as), replicate(n, as))
) : [];
};
// cartesianProduct :: [a] -> [b] -> [[a, b]]
var cartesianProduct = function (xs, ys) {
return [].concat.apply([], xs.map(function (x) {
return [].concat.apply([], ys.map(function (y) {
return [
[x].concat(y)
];
}));
}));
};
// foldl1 :: (a -> a -> a) -> [a] -> a
var foldl1 = function (f, xs) {
return xs.length > 0 ? xs.slice(1)
.reduce(f, xs[0]) : [];
};
// replicate :: Int -> a -> [a]
var replicate = function (n, a) {
var v = [a],
o = [];
if (n < 1) return o;
while (n > 1) {
if (n & 1) o = o.concat(v);
n >>= 1;
v = v.concat(v);
}
return o.concat(v);
};
// curry :: ((a, b) -> c) -> a -> b -> c
var curry = function (f) {
return function (a) {
return function (b) {
return f(a, b);
};
};
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment