diff --git a/demos/src/_variables.css b/demos/src/_variables.css index 4360544..29f3208 100644 --- a/demos/src/_variables.css +++ b/demos/src/_variables.css @@ -1,6 +1,7 @@ $color-1: #1BBC9B; $color-2: #69707a; $color-3: #393E46; +$color-4: #E87E04; $font-1: 'Fira Sans', sans-serif; $font-2: 'Inconsolata', sans-serif; diff --git a/demos/src/mnist-vae.css b/demos/src/mnist-vae.css index d92db3d..1d3b015 100644 --- a/demos/src/mnist-vae.css +++ b/demos/src/mnist-vae.css @@ -11,8 +11,7 @@ justify-content: flex-end; .input-container { - display: inline-flex; - flex-direction: column; + text-align: right; margin: 20px; position: relative; @@ -32,20 +31,50 @@ } .canvas-container { + position: relative; display: inline-flex; justify-content: flex-end; margin: 10px 0; + border: 15px solid rgba(27, 188, 155, 0.3); + transition: border-color 0.2s ease-in; + + &:hover { + border-color: rgba(27, 188, 155, 0.6); + } canvas { background: white; - border: 15px solid rgba(27, 188, 155, 0.3); - transition: border-color 0.2s ease-in; &:hover { - border-color: rgba(27, 188, 155, 0.6); cursor: crosshair; } } + + .axis { + position: absolute; + cursor: default; + user-select: none; + display: flex; + align-items: center; + justify-content: space-between; + font-family: $font-2; + font-size: 14px; + color: $color-1; + } + + .axis.x-axis { + right: 0; + bottom: -45px; + width: 200px; + flex-direction: row; + } + + .axis.y-axis { + top: 0; + left: -55px; + height: 200px; + flex-direction: column; + } } } } @@ -66,6 +95,65 @@ } .column.output-column { - justify-content: center; + justify-content: flex-start; + + .output { + border-radius: 10px; + overflow: hidden; + + canvas { + background: white; + } + } + } + + .layer-results-container { + position: relative; + + .bg-line { + position: absolute; + z-index: 0; + top: 0; + left: 50%; + background: white; + width: 15px; + height: 100%; + } + + .layer-result { + position: relative; + z-index: 1; + margin: 30px 20px; + background: white; + border-radius: 10px; + padding: 20px; + overflow-x: auto; + + .layer-result-heading { + font-size: 1rem; + color: #999999; + margin-bottom: 10px; + display: flex; + flex-direction: column; + font-size: 12px; + + span.layer-class { + color: $color-1; + font-size: 14px; + font-weight: bold; + } + } + + .layer-result-canvas-container { + display: inline-flex; + flex-wrap: wrap; + background: white; + + canvas { + border: 1px solid lightgray; + margin: 1px; + } + } + } } } diff --git a/demos/src/mnist-vae.js b/demos/src/mnist-vae.js index 061e0ba..4843b85 100644 --- a/demos/src/mnist-vae.js +++ b/demos/src/mnist-vae.js @@ -2,7 +2,6 @@ import './mnist-vae.css' -import debounce from 'lodash/debounce' import * as utils from './utils' const MODEL_CONFIG = { @@ -26,13 +25,17 @@ if (process.env.NODE_ENV === 'production') { const LAYER_DISPLAY_CONFIG = { 'dense_10': { - heading: 'ReLU activation, output dimensions = 128', + heading: 'input dimensions = 2, output dimensions = 128, ReLU activation', scalingFactor: 2 }, 'dense_11': { heading: 'ReLU activation, output dimensions = 25088 (64 x 14 x 14)', scalingFactor: 2 }, + 'reshape_4': { + heading: '', + scalingFactor: 2 + }, 'deconvolution2d_10': { heading: '64 3x3 filters, border mode same, 1x1 strides, ReLU activation', scalingFactor: 2 @@ -66,21 +69,41 @@ export const MnistVae = Vue.extend({
-
Move around the latent space
+
Click around the latent space
+
+ -1.5 + x + 1.5 +
+
+ -1.5 + y + 1.5 +
-
+
Use GPU +
+
x: {{ inputCoordinates[0] < 0 ? inputCoordinates[0].toFixed(2) : inputCoordinates[0].toFixed(3) }}
+
y: {{ inputCoordinates[1] < 0 ? inputCoordinates[1].toFixed(2) : inputCoordinates[1].toFixed(3) }}
+
+
+
+
+ + +
@@ -115,10 +138,10 @@ export const MnistVae = Vue.extend({ return { model: new KerasJS.Model(MODEL_CONFIG), modelLoading: true, - input: new Float32Array(2), output: new Float32Array(27 * 27), crosshairsActivated: false, - coordinates: [0, 0], + inputCoordinates: [0, 0], + position: [100, 100], layerResultImages: [], layerDisplayConfig: LAYER_DISPLAY_CONFIG, useGpu: MODEL_CONFIG.gpu @@ -140,35 +163,56 @@ export const MnistVae = Vue.extend({ }) }, + ready: function () { + this.drawPosition() + }, + methods: { + toggleGpu: function () { + this.model.gpu = !this.useGpu + }, + activateCrosshairs: function (e) { this.crosshairsActivated = true }, deactivateCrosshairs: function (e) { this.crosshairsActivated = false + this.draw(e) }, - drawCrosshairs: function (e) { - if (!this.crosshairsActivated) return - + draw: function (e) { const [x, y] = this.getEventCanvasCoordinates(e) const ctx = document.getElementById('input-canvas').getContext('2d') ctx.clearRect(0, 0, 200, 200) - ctx.strokeStyle = '#1BBC9B' + + this.drawPosition() + + if (this.crosshairsActivated) { + ctx.strokeStyle = '#1BBC9B' + ctx.beginPath() + ctx.moveTo(x, 0) + ctx.lineTo(x, 200) + ctx.stroke() + ctx.beginPath() + ctx.moveTo(0, y) + ctx.lineTo(200, y) + ctx.stroke() + } + }, + + drawPosition: function () { + const ctx = document.getElementById('input-canvas').getContext('2d') + ctx.clearRect(0, 0, 200, 200) + ctx.fillStyle = '#E87E04' ctx.beginPath() - ctx.moveTo(x, 0) - ctx.lineTo(x, 200) - ctx.stroke() - ctx.beginPath() - ctx.moveTo(0, y) - ctx.lineTo(200, y) - ctx.stroke() + ctx.arc(...this.position, 5, 0, Math.PI * 2, true) + ctx.closePath() + ctx.fill() }, getEventCanvasCoordinates: function (e) { - const borderSize = 15 let { clientX, clientY } = e // for touch event if (e.touches && e.touches.length) { @@ -178,20 +222,45 @@ export const MnistVae = Vue.extend({ const canvas = document.getElementById('input-canvas') const { left, top } = canvas.getBoundingClientRect() - const [x, y] = [clientX - left - borderSize, clientY - top - borderSize] + const [x, y] = [clientX - left, clientY - top] return [x, y] }, - selectCoordinate: function (e) { + selectCoordinates: function (e) { + const [x, y] = this.getEventCanvasCoordinates(e) + if (!this.model.isRunning) { + this.position = [x, y] + this.inputCoordinates = [x * 3 / 200 - 1.5, y * 3 / 200 - 1.5] + this.draw(e) + const inputData = { + 'input_4': new Float32Array(this.inputCoordinates) + } + const outputData = this.model.predict(inputData) + this.output = outputData['convolution2d_8'] + this.drawOutput() + this.getIntermediateResults() + } + }, + drawOutput: function () { + const ctx = document.getElementById('output-canvas').getContext('2d') + ctx.putImageData(utils.image2Darray(this.output, 27, 27), 0, 0) + + // scaled up + // const ctxScaled = document.getElementById('output-canvas-scaled').getContext('2d') + // ctxScaled.save() + // ctxScaled.scale(28 / ctxCenterCrop.canvas.width, 28 / ctxCenterCrop.canvas.height) + // ctxScaled.clearRect(0, 0, ctxCenterCrop.canvas.width, ctxCenterCrop.canvas.height) + // ctxScaled.drawImage(document.getElementById('output-canvas-centercrop'), 0, 0) + // const imageDataScaled = ctxScaled.getImageData(0, 0, ctxScaled.canvas.width, ctxScaled.canvas.height) + // ctxScaled.restore() }, getIntermediateResults: function () { let results = [] for (let [name, layer] of this.model.modelLayersMap.entries()) { - if (name === 'input') continue - const layerClass = layer.layerClass || '' + if (layerClass === 'InputLayer') continue let images = [] if (layer.result && layer.result.tensor.shape.length === 3) { @@ -240,10 +309,6 @@ export const MnistVae = Vue.extend({ ctxScaled.restore() }) }) - }, - - toggleGpu: function () { - this.model.gpu = !this.useGpu } } })