自己动手做一个识别手写数字的web应用04
接着往期的3篇继续,一步步动手做:
如果你练习里前面三篇,相信你已经熟悉了Docker和Keras,以及Flask了,接下来我们实现一个提供给用户输入手写字的前端web页面。
前端画板我们可以自己用最基本的canvas写,也可以选择封装好的开源库:
下面介绍2个比较好的模拟手写效果的画板库:
1 signature_pad
https://github.com/szimek/signature_pad/
2 drawingboard.js
https://github.com/Leimi/drawingboard.js
这边我选择的是signature_pad。
HTML代码:
<!doctype html>
<html lang="zh"><head>
<meta charset="utf-8">
<title>mnist demo</title>
<meta name="viewport" content="width=device-width,initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<link rel="stylesheet" href="./static/css/main.css"></head>
<body onselectstart="return false">
<div id="mnist-pad">
<div class="mnist-pad-body"> <canvas></canvas>
</div>
<div class="mnist-pad-footer"> <div class="mnist-pad-result"> <h5>识别结果:</h5> <h5 id="mnist-pad-result"></h5> </div>
<div class="mnist-pad-actions"> <button type="button" id="mnist-pad-clear">清除</button> <button type="button" id="mnist-pad-save">识别</button>
</div>
</div></div>
<script src="./static/js/signature_pad.js"></script>
<script src="./static/js/mnist.js"></script>
<script src="./static/js/app.js"></script>
</body>
</html>
移动端注意要写这句标签,把屏幕缩放设为no,比例设为1:
<meta name="viewport" content="width=device-width,initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
CSS代码:
body { display: flex; justify-content: center; align-items: center; height: 100vh; width: 100%; user-select: none; margin: 0; padding: 0; }
h5 { margin: 0; padding: 0
}
#mnist-pad { position: relative; display: flex; flex-direction: column; font-size: 1em; width: 100%; height: 100%; background-color: #fff; box-shadow: 0 1px 5px rgba(0, 0, 0, 0.27), 0 0 40px rgba(0, 0, 0, 0.08) inset; padding: 16px; }
.mnist-pad-body { position: relative; flex: 1; border: 1px solid #f4f4f4; }
.mnist-pad-body canvas { position: absolute; left: 0; top: 0; width: 100%; height: 100%; border-radius: 4px; box-shadow: 0 0 5px rgba(0, 0, 0, 0.02) inset; }
.mnist-pad-footer { color: #C3C3C3; font-size: 1.2em; margin-top: 8px; margin-bottom: 8px; }
.mnist-pad-result { display: flex; justify-content: center; align-items: center; margin-bottom: 8px; }
.mnist-pad-actions { display: flex; justify-content: space-between; margin-bottom: 8px; }
#mnist-pad-clear { height: 44px; background-color: #eeeeee; width: 98px; border: none; font-size: 16px; color: #4a4a4a; }
#mnist-pad-save { height: 44px; background-color: #3b3b3b; width: 98px; border: none; font-size: 16px; color: #ffffff; }
CSS样式都是一些常用的,有兴趣可以自己实现个简单的UI。
JS代码,有3个文件:
signature_pad.js 这是引用的开源库;
mnist.js 这是我们给开源库写的一些扩展,下文会介绍;
app.js主要是一些初始化,事件绑定,请求后端接口的处理。
先来看看app.js:
1 初始化画板,绑定按钮事件;
var clearBtn = document.getElementById("mnist-pad-clear");
var saveBtn = document.getElementById("mnist-pad-save");
var canvas = document.querySelector("canvas");
var mnistPad = new SignaturePad(canvas, {
backgroundColor: 'transparent',
minWidth: 6,
maxWidth: 8
});
clearBtn.addEventListener("click", function (event) { mnistPad.clear(); });
saveBtn.addEventListener("click", function (event) {
if (mnistPad.isEmpty()) { alert("请书写一个数字"); } else { mnistPad.getMNISTGridBySize(true,28,img2text); } });
注意minWidth及MaxWidth的设置,我试验下来,比较好的数值是6跟8,识别效果较好,也可以自行试验修改。
ministPad的方法,getMNISTGridBySize将把截取画板上的手写数字,并缩放成28x28的尺寸,然后调用img2text函数。
img2text主要是把28x28的图片传给后端,获取识别结果,这边由于canvas的数据是base64,需要用到转化为blob的函数,dataURItoBlob(github上有写好的),转化后通过构造一个表单,注意文件名predictImg一定要与后端flask接受函数里的写的一致。调用XMLHttpRequest请求后端接口即可。
2 这一步“如何把canvas生成的图片上传至后端”是个很典型的问题。
function img2text(b64img){
var formData = new FormData();
var blob = dataURItoBlob(b64img);
formData.append("predictImg", blob);
var request = new XMLHttpRequest();
request.onreadystatechange = function () {
if (request.readyState == 4) {
if ((request.status >= 200 && request.status < 300) || request.status == 304) {
console.log(request.response)
document.querySelector('#mnist-pad-result').innerHTML=request.response; }; } }; request.open("POST", "./predict"); request.send(formData); };
3 还有一个比较重要的函数:
画板根据屏幕尺寸自适应的代码(尤其是PC端,记得加):
function resizeCanvas() {
var ratio = Math.max(window.devicePixelRatio || 1, 1); canvas.width = canvas.offsetWidth; canvas.height = canvas.offsetHeight;
// canvas.getContext("2d").scale(ratio, ratio); mnistPad.clear(); };
window.onresize = resizeCanvas; resizeCanvas();
到这一步可以试一下前端的输入效果先:
接下来完成mnist.js
4 signature_pad有个方法是toData,可以获取所有手写输入的坐标点。
var ps=mnistPad.toData()[0];
mnistPad._ctx.strokeStyle='red';
ps.forEach((p,i)=>{ mnistPad._ctx.beginPath(); mnistPad._ctx.arc(p.x, p.y, 4, 0, 2 * Math.PI); mnistPad._ctx.stroke(); })
我们可以在chrome的控制台直接试验。
红色的圈圈就是所有的坐标点,只要求出如下图所示的紫色框,第一步也就完成了。
5 给signature_pad扩展个getArea方法:
SignaturePad.prototype.getArea = function() {
var xs = [], ys = [];
var orign = this.toData();
for (var i = 0; i < orign.length; i++) {
var orignChild = orign[i];
for (var j = 0; j < orignChild.length; j++) { xs.push(orignChild[j].x); ys.push(orignChild[j].y); } };
var paddingNum = 30;
var min_x = Math.min.apply(null, xs) - paddingNum;
var min_y = Math.min.apply(null, ys) - paddingNum;
var max_x = Math.max.apply(null, xs) + paddingNum;
var max_y = Math.max.apply(null, ys) + paddingNum;
var width = max_x - min_x, height = max_y - min_y;
var grid = { x: min_x, y: min_y, w: width, h: height };
return grid;
};
测试下:
注意paddingNum,我设置了个30的值,把边框稍微放大了下,原因见mnist手写字训练集的图片就知道啦。
到这一步,我们的手写字数据集是下图这样的:
6 我们还需要把边框变成方形。
再写个转换函数:
SignaturePad.prototype.change2grid = function(area) {
var w = area.w, h = area.h, x = area.x, y = area.y; var xc = x, yc = y, wc = w, hc = h; if (h >= w) { xc = x - (h - w) * 0.5; wc = h; } else { yc = y - (w - h) * 0.5; hc = w; }; return { x: xc, y: yc, w: wc, h: hc } }
原理如下图,判断下长边是哪个,然后计算出x,y,width,height即可。
写好代码后,试一下:
红框是最后要提交的范围。
这个时候,还要处理下,把图片变成黑底白字的图片,因为MNIST数据集是这样的。
7 主要代码如下:
ctx.fillStyle = "white"; ctx.fillRect(0, 0, grid.w, grid.h); ctx.drawImage(img, grid.x, grid.y, grid.w, grid.h, 0, 0, size, size);
var imgData = ctx.getImageData(0, 0, size, size);
for (var i = 0; i < imgData.data.length; i += 4) { imgData.data[i] = 255 - imgData.data[i]; imgData.data[i + 1] = 255 - imgData.data[i + 1]; imgData.data[i + 2] = 255 - imgData.data[i + 2]; imgData.data[i + 3] = 255; }
ctx.putImageData(imgData, 0, 0);
画上背景,遍历像素,把颜色反色下就ok啦。
最后都测试下:
ps:
今天我用上了Markdown Here美化了代码块的展示,推荐下:
使用 Markdown Here 浏览器插件,能够直接在微信公众平台的图文编辑器中把 Markdown 转换成带样式的文本,从而避免拷贝引起的样式丢失,再对「代码块」的缩进、换行、字号、行间距进行微调即可。
https://markdown-here.com/get.html
最后,注意下MNIST数据集里的数据,对应的是灰度图,28x28的尺寸,黑底白字,并且数字是像素的重心居中处理的。本文没有介绍如何把web前端的手写字根据重心居中处理这一内容,将会挑选合适时机介绍,用上了可以提高识别率哦!
相关源代码,可以留言获取。
这个系列也基本上完成了,如果你有疑问可以留言。
我要不要考虑开个面授课啊(如果有10个人以上在下方留言区留言,我就考虑开设了),大家抱着电脑,我们一起动手花个半天,亲手实现一个识别手写数字的web应用。
技术栈:
Docker+Keras+Flask+JS+HTML+CSS。
涉及到的内容都可以讲解。
地点限于:上海。
近期热文:
码字不易,开启新的打赏方式:
本公众号定期更新关于
设计师、程序员发挥创意
互相融合的指南、作品。
主要技术栈:
nodejs、react native、electron
Elasticsearch
Solidity
Keras