From 741a5887a1455a685052fa4bc2a6c6ec601df06c Mon Sep 17 00:00:00 2001 From: Leon Chen Date: Mon, 19 Sep 2016 15:57:17 -0400 Subject: [PATCH] update mnist convnet demo --- .gitignore | 4 +- demos/index.html | 2 +- demos/src/mnist-cnn.js | 83 ++++++++++++++----------------- demos/src/utils/index.js | 96 ++++++++++++++++++++++++++++++++++++ demos/webpack.dev.config.js | 2 +- demos/webpack.prod.config.js | 2 +- 6 files changed, 137 insertions(+), 52 deletions(-) create mode 100644 demos/src/utils/index.js diff --git a/.gitignore b/.gitignore index d148090..3a0e6d3 100644 --- a/.gitignore +++ b/.gitignore @@ -8,5 +8,5 @@ npm-debug.log* # jupyter notebooks/**/.ipynb_checkpoints/ -# data files -*.hdf5 +# data files for demos +demos/data/ diff --git a/demos/index.html b/demos/index.html index 346a073..7a10b29 100644 --- a/demos/index.html +++ b/demos/index.html @@ -24,6 +24,6 @@ - + diff --git a/demos/src/mnist-cnn.js b/demos/src/mnist-cnn.js index 478825c..1eba47f 100644 --- a/demos/src/mnist-cnn.js +++ b/demos/src/mnist-cnn.js @@ -4,33 +4,14 @@ import './mnist-cnn.css' import debounce from 'lodash/debounce' import range from 'lodash/range' +import * as utils from './utils' -/** - * Find mindpoint of two points - */ -const getMidpoint = (p1, p2) => { - const [x1, y1] = p1 - const [x2, y2] = p2 - return [ - x1 + (x2 - x1) / 2, - y1 + (y2 - y1) / 2 - ] -} - -/** - * Gets the (x, y) coordinates of an UI event relative to its target, - * e.g., canvas. Accounts for touch events as well as mouse events. - */ -const getCoordinates = e => { - let { clientX, clientY } = e - // for touch event - if (e.touches && e.touches.length) { - clientX = e.touches[0].clientX - clientY = e.touches[0].clientY +const MODEL_CONFIG = { + filepaths: { + model: '/demos/data/mnist_cnn/mnist_cnn.json', + weights: '/demos/data/mnist_cnn/mnist_cnn_weights.buf', + metadata: '/demos/data/mnist_cnn/mnist_cnn_metadata.json' } - const { left, top } = e.target.getBoundingClientRect() - const [x, y] = [clientX - left, clientY - top] - return [x, y] } /** @@ -60,7 +41,8 @@ export const MnistCnn = Vue.extend({ @touchend="deactivateDrawAndPredict" @touchmove="draw" > - + +
clearCLEAR @@ -81,16 +63,15 @@ export const MnistCnn = Vue.extend({
+
+ +
`, data: function () { return { - model: new KerasJS.Model({ - model: '/demos/mnist_cnn/mnist_cnn.json', - weights: '/demos/mnist_cnn/mnist_cnn_weights.buf', - metadata: '/demos/mnist_cnn/mnist_cnn_metadata.json' - }), + model: new KerasJS.Model(MODEL_CONFIG), modelLoading: true, input: new Float32Array(784), output: new Float32Array(10), @@ -120,19 +101,15 @@ export const MnistCnn = Vue.extend({ }) }, - ready: function () { - // initialize scaling helper canvas - const ctxScaled = document.getElementById('input-canvas-scaled').getContext('2d') - ctxScaled.scale(28 / 240, 28 / 240) - }, - methods: { clear: function (e) { const ctx = document.getElementById('input-canvas').getContext('2d') ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + const ctxCenterCrop = document.getElementById('input-canvas-centercrop').getContext('2d') + ctxCenterCrop.clearRect(0, 0, ctxCenterCrop.canvas.width, ctxCenterCrop.canvas.height) const ctxScaled = document.getElementById('input-canvas-scaled').getContext('2d') - ctxScaled.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) + ctxScaled.clearRect(0, 0, ctxScaled.canvas.width, ctxScaled.canvas.height) this.output = new Float32Array(10) this.drawing = false this.strokes = [] @@ -142,7 +119,7 @@ export const MnistCnn = Vue.extend({ this.drawing = true this.strokes.push([]) let points = this.strokes[this.strokes.length - 1] - points.push(getCoordinates(e)) + points.push(utils.getCoordinates(e)) }, draw: function (e) { @@ -150,14 +127,14 @@ export const MnistCnn = Vue.extend({ const ctx = document.getElementById('input-canvas').getContext('2d') - ctx.lineWidth = 15 + ctx.lineWidth = 20 ctx.lineJoin = ctx.lineCap = 'round' ctx.strokeStyle = '#393E46' ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) let points = this.strokes[this.strokes.length - 1] - points.push(getCoordinates(e)) + points.push(utils.getCoordinates(e)) // draw individual strokes for (let s = 0, slen = this.strokes.length; s < slen; s++) { @@ -171,7 +148,7 @@ export const MnistCnn = Vue.extend({ // draw points in stroke // quadratic bezier curve for (let i = 1, len = points.length; i < len; i++) { - ctx.quadraticCurveTo(...p1, ...getMidpoint(p1, p2)) + ctx.quadraticCurveTo(...p1, ...utils.getMidpoint(p1, p2)) p1 = points[i] p2 = points[i + 1] } @@ -182,15 +159,27 @@ export const MnistCnn = Vue.extend({ deactivateDrawAndPredict: debounce(function () { if (!this.drawing) return - this.drawing = false const ctx = document.getElementById('input-canvas').getContext('2d') - const ctxScaled = document.getElementById('input-canvas-scaled').getContext('2d') - ctxScaled.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height) - ctxScaled.drawImage(document.getElementById('input-canvas'), 0, 0) - const imageDataScaled = ctxScaled.getImageData(0, 0, ctxScaled.canvas.width, ctxScaled.canvas.height) + // center crop + const imageDataCenterCrop = utils.centerCrop(ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height)) + const ctxCenterCrop = document.getElementById('input-canvas-centercrop').getContext('2d') + ctxCenterCrop.canvas.width = imageDataCenterCrop.width + ctxCenterCrop.canvas.height = imageDataCenterCrop.height + ctxCenterCrop.putImageData(imageDataCenterCrop, 0, 0) + + // scaled to 28 x 28 + const ctxScaled = document.getElementById('input-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('input-canvas-centercrop'), 0, 0) + const imageDataScaled = ctxScaled.getImageData(0, 0, ctxScaled.canvas.width, ctxScaled.canvas.height) + ctxScaled.restore() + + // process image data for model input const { data } = imageDataScaled this.input = new Float32Array(784) for (let i = 0, len = data.length; i < len; i += 4) { diff --git a/demos/src/utils/index.js b/demos/src/utils/index.js new file mode 100644 index 0000000..063cb83 --- /dev/null +++ b/demos/src/utils/index.js @@ -0,0 +1,96 @@ +/* global ImageData */ + +/** + * Find mindpoint of two points + */ +export function getMidpoint (p1, p2) { + const [x1, y1] = p1 + const [x2, y2] = p2 + return [ + x1 + (x2 - x1) / 2, + y1 + (y2 - y1) / 2 + ] +} + +/** + * Gets the (x, y) coordinates of an UI event relative to its target, + * e.g., canvas. Accounts for touch events as well as mouse events. + */ +export function getCoordinates (e) { + let { clientX, clientY } = e + // for touch event + if (e.touches && e.touches.length) { + clientX = e.touches[0].clientX + clientY = e.touches[0].clientY + } + const { left, top } = e.target.getBoundingClientRect() + const [x, y] = [clientX - left, clientY - top] + return [x, y] +} + +/** + * Centers and crops canvas ImageData based on alpha channel. + * @param {ImageData} imageData + * @returns {ImageData} + */ +export function centerCrop (imageData) { + const { data, width, height } = imageData + let [xmin, ymin] = [width, height] + let [xmax, ymax] = [-1, -1] + for (let i = 0; i < width; i++) { + for (let j = 0; j < height; j++) { + const idx = (i + j * width) + if (data[4 * idx + 3] > 0) { + if (i < xmin) xmin = i + if (i > xmax) xmax = i + if (j < ymin) ymin = j + if (j > ymax) ymax = j + } + } + } + + // add a little padding + xmin -= 20 + xmax += 20 + ymin -= 20 + ymax += 20 + + // make bounding box square + let [widthNew, heightNew] = [(xmax - xmin + 1), (ymax - ymin + 1)] + if (widthNew < heightNew) { + // new width < new height + const halfBefore = Math.floor((heightNew - widthNew) / 2) + const halfAfter = heightNew - widthNew - halfBefore + xmax += halfAfter + xmin -= halfBefore + } else if (widthNew > heightNew) { + // new width > new height + const halfBefore = Math.floor((widthNew - heightNew) / 2) + const halfAfter = widthNew - heightNew - halfBefore + ymax += halfAfter + ymin -= halfBefore + } + + widthNew = xmax - xmin + 1 + heightNew = ymax - ymin + 1 + let dataNew = new Uint8ClampedArray(widthNew * heightNew * 4) + for (let i = xmin; i <= xmax; i++) { + for (let j = ymin; j <= ymax; j++) { + if (i >= 0 && i < width && j >= 0 && j < height) { + const idx = (i + j * width) + const idxNew = ((i - xmin) + (j - ymin) * widthNew) + dataNew[4 * idxNew + 3] = data[4 * idx + 3] + } + } + } + + return new ImageData(dataNew, widthNew, heightNew) +} + +/** + * Takes in a ndarray of shape [x, y, z] + * and creates ImageData layed out as [x*z, y] + */ +export function flatten3DTensor (imageData) { + +} diff --git a/demos/webpack.dev.config.js b/demos/webpack.dev.config.js index 09633c6..1cd210f 100644 --- a/demos/webpack.dev.config.js +++ b/demos/webpack.dev.config.js @@ -7,7 +7,7 @@ module.exports = { path.join(__dirname, 'src/index') ], output: { - path: __dirname, + path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, devtool: 'cheap-module-eval-source-map', diff --git a/demos/webpack.prod.config.js b/demos/webpack.prod.config.js index 808aed2..56e5fc9 100644 --- a/demos/webpack.prod.config.js +++ b/demos/webpack.prod.config.js @@ -7,7 +7,7 @@ module.exports = { path.join(__dirname, 'src/index') ], output: { - path: __dirname, + path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, module: {