| | <!DOCTYPE html> |
| | <html> |
| | <head> |
| | <meta charset="utf-8"> |
| | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
| | <title>MNIST with GGML</title> |
| | <script src="mnist.js"></script> |
| | </head> |
| | <body> |
| | <h2>MNIST digit recognizer with <a href="https://github.com/ggerganov/ggml">GGML</a></h2> |
| | <p id="msg">Loading model and data set, please wait ...</p> |
| | <canvas id="ggCanvas" width="364" height="364" style="border:2px solid #d3d3d3;"> |
| | Your browser does not support the HTML canvas tag. |
| | </canvas> |
| | <div> |
| | <button id="clear" onclick="onClear()">Clear</button> |
| | <button id="random" onclick="onRandom()" disabled>Random</button> |
| | <button id="download" onclick="onDownload()">Download</button> |
| | </div> |
| | <div> |
| | <p id="prediction"></p> |
| | </div> |
| | <script> |
| | "use strict"; |
| | const DIGIT_SIZE = 28; |
| | var canvas = document.getElementById("ggCanvas"); |
| | var ctx = canvas.getContext("2d", { alpha: false, willReadFrequently: true }); |
| | ctx.fillStyle = "white"; |
| | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| | var dragging = false; |
| | var lastX, lastY; |
| | |
| | function onClear(event) { |
| | ctx.fillStyle = "white"; |
| | ctx.fillRect(0, 0, canvas.width, canvas.height); |
| | document.getElementById("prediction").innerHTML = ""; |
| | } |
| | |
| | function predict(digit) { |
| | let buf = Module._malloc(digit.length); |
| | if (buf == 0) { |
| | console.log("failed to allocate memory"); |
| | return; |
| | } |
| | Module.HEAPU8.set(digit, buf); |
| | let prediction = Module.ccall('wasm_eval', null, ['number'], [buf]); |
| | Module._free(buf); |
| | if (prediction >= 0) { |
| | document.getElementById("prediction").innerHTML = "Predicted digit is <b>" + prediction + "</b>"; |
| | } |
| | } |
| | |
| | function onRandom(event) { |
| | onClear(); |
| | const bufLength = DIGIT_SIZE*DIGIT_SIZE; |
| | var buf = Module._malloc(bufLength); |
| | if (buf == 0) { |
| | console.log("failed to allocate memory"); |
| | return; |
| | } |
| | let ret = Module.ccall('wasm_random_digit', null, ['number'], [buf]); |
| | let digit = new Uint8Array(Module.HEAPU8.buffer, buf, bufLength); |
| | for (let i = 0; i < digit.length; i++) { |
| | let x = i % DIGIT_SIZE; |
| | let y = Math.floor(i / DIGIT_SIZE); |
| | setPixel(x, y, digit[i]); |
| | } |
| | Module._free(buf); |
| | predict(digit); |
| | } |
| | |
| | function onDownload(event) { |
| | let digit = scaleCanvas(); |
| | let digitBlob = new Blob([new Uint8Array(digit)], {type: "application/octet-stream"}); |
| | let url = URL.createObjectURL(digitBlob); |
| | let link = document.createElement('a'); |
| | link.href = url; |
| | link.download = "image.raw"; |
| | document.body.appendChild(link); |
| | link.click(); |
| | document.body.removeChild(link); |
| | } |
| | |
| | |
| | function getMousePos(event) { |
| | if (event.touches !== undefined && event.touches.length > 0) { |
| | event = event.touches[0]; |
| | } |
| | var rect = canvas.getBoundingClientRect(); |
| | return [Math.floor(event.clientX) - rect.left, Math.floor(event.clientY) - rect.top]; |
| | } |
| | |
| | function setPixel(x, y, val) { |
| | let canvasX = x * 13; |
| | let canvasY = y * 13; |
| | let color = 255 - val; |
| | ctx.fillStyle = "#" + color.toString(16) + color.toString(16) + color.toString(16); |
| | ctx.fillRect(canvasX, canvasY, 13, 13); |
| | } |
| | |
| | function onMouseDown(e) { |
| | dragging = true; |
| | [lastX, lastY] = getMousePos(e); |
| | } |
| | |
| | |
| | function scaleCanvas() { |
| | let imgData = ctx.getImageData(0, 0, canvas.width, canvas.height); |
| | let tempCanvas = document.createElement('canvas'); |
| | tempCanvas.width = DIGIT_SIZE; |
| | tempCanvas.height = DIGIT_SIZE; |
| | let tempCtx = tempCanvas.getContext("2d"); |
| | tempCtx.drawImage(canvas, 0, 0, DIGIT_SIZE, DIGIT_SIZE); |
| | let tempImgData = tempCtx.getImageData(0, 0, DIGIT_SIZE, DIGIT_SIZE); |
| | let tempData = tempImgData.data; |
| | let digit = new Array(DIGIT_SIZE*DIGIT_SIZE).fill(0); |
| | for (let i = 0; i < tempData.length; i += 4) { |
| | let val = 255 - tempData[i]; |
| | digit[i / 4] = val; |
| | } |
| | return digit; |
| | } |
| | |
| | function onMouseUp(e) { |
| | dragging = false; |
| | let digit = scaleCanvas(); |
| | predict(digit); |
| | } |
| | |
| | function onMouseMove(e) { |
| | if (dragging) { |
| | let [mouseX, mouseY] = getMousePos(e); |
| | ctx.beginPath(); |
| | ctx.moveTo(lastX, lastY); |
| | ctx.lineTo(mouseX, mouseY); |
| | ctx.lineWidth = 20; |
| | ctx.lineJoin = ctx.lineCap = 'round'; |
| | ctx.strokeStyle = "#000000"; |
| | ctx.stroke(); |
| | ctx.closePath(); |
| | lastX = mouseX; |
| | lastY = mouseY; |
| | } |
| | } |
| | |
| | |
| | document.body.addEventListener("touchstart", function (e) { |
| | if (e.target == canvas) { |
| | e.preventDefault(); |
| | } |
| | }, {passive: false}); |
| | document.body.addEventListener("touchend", function (e) { |
| | if (e.target == canvas) { |
| | e.preventDefault(); |
| | } |
| | }, {passive: false}); |
| | document.body.addEventListener("touchmove", function (e) { |
| | if (e.target == canvas) { |
| | e.preventDefault(); |
| | } |
| | }, {passive: false}); |
| | |
| | function onRuntimeInitialized() { |
| | |
| | canvas.onmousedown = onMouseDown; |
| | canvas.onmouseup = onMouseUp; |
| | canvas.onmousemove = onMouseMove; |
| | canvas.ontouchstart = onMouseDown; |
| | canvas.ontouchend = onMouseUp; |
| | canvas.ontouchmove = onMouseMove; |
| | document.getElementById("msg").innerHTML = "Draw a single digit on the canvas below:" |
| | document.getElementById("random").disabled = false; |
| | } |
| | |
| | Module['onRuntimeInitialized'] = onRuntimeInitialized; |
| | </script> |
| | </body> |
| | </html> |
| |
|