Last active
February 10, 2019 15:29
-
-
Save duhaime/38d0ecec4643c04809340913b5dab1a0 to your computer and use it in GitHub Desktop.
Dancer Grid
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{"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"]}]} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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