AI应用开发实战 - 手写识别应用入门
AI应用开发实战 - 手写识别应用入门
手写体识别的应用已经非常流行了,如输入法,图片中的文字识别等。但对于大多数开发人员来说,如何实现这样的一个应用,还是会感觉无从下手。本文从简单的MNIST训练出来的模型开始,和大家一起入门手写体识别。
在本教程结束后,会得到一个能用的AI应用,也许是你的第一个AI应用。虽然离实际使用还有较大的距离(具体差距在文章后面会分析),但会让你对AI应用有一个初步的认识,有能力逐步搭建出能够实际应用的模型。
建议和反馈,请发送到
https://github.com/Microsoft/vs-tools-for-ai/issues
联系我们
OpenmindChina@microsoft.com
准备工作
- 使用win10 64位操作系统的计算机
- 参考上一篇博客AI应用开发实战 - 从零开始配置环境。在电脑上训练并导出MNIST模型。
一、 思路
通过上一篇文章搭建环境的介绍后,就能得到一个能识别单个手写数字的模型了,并且识别的准确度会在98%,甚至99%以上了。那么我们要怎么使用这个模型来搭建应用呢?
大致的步骤如下:
- 实现简单的界面,将用户用鼠标或者触屏的
输入变成图片
。 - 将生成的模型
包装
起来,成为有公开数据接口的类。 - 将输入的图片进行
规范化
,成为数据接口能够使用的格式。 - 最后通过模型来推理(inference)出图片应该是哪个数字,并显示出来。
是不是很简单?
二、动手
步骤一:获取手写的数字
提问:那我们要怎么获取手写的数字呢?
回答:我们可以写一个简单的WinForm画图程序,让我们可以用鼠标手写数字,然后把图片保存下来。
首先,我们打开Visual Studio,选择文件->新建->项目
。
在弹出的窗口里选择Visual C#->Windows窗体应用
,项目名称不妨叫做DrawDigit
,解决方案名称不妨叫做MnistForm
,点击确定。
此时,Visual Studio也自动弹出了一个窗口的设计图。
在DrawDigit项目上点击右键,选择属性,在生成一栏将平台目标从Any CPU
改为x64
。
否则,DrawDigit(首选32位)与它引用的MnistForm(64位)的编译平台不一致会引发System.BadImageFormatException
的异常。
然后我们对这个窗口做一些简单的修改:
首先我们打开VS窗口左侧的工具箱,这个窗口程序需要以下三种组件:
- PictureBox:用来手写数字,并且把数字保存成图片
- Label:用来显示模型的识别结果
- Button:用来清理PictureBox的手写结果
那经过一些简单的选择与拖动还有调整大小
,这个窗口现在是这样的:
一些注意事项
- 这些组件都可以通过
右键->查看属性
,在属性里修改它们的设置 - 为了方便把PictureBox里的图片转化成Mnist能识别的格式,PictureBox的需要是正方形
- 可以给这些控件起上有意义的名称。
- 可以调整一下label控件大小、字体等,让它更美观。
经过一些简单的调整,这个窗口现在是这样的:
现在来让我们愉快地给这些组件添加事件!
还是在属性窗口,我们选择某个组件,右键->查看属性,点击闪电符号,给组件绑定对应的事件。每次绑定后,会跳到代码部分,生成一个空函数。点回设计视图继续操作即可。
组件类型 | 事件 |
---|---|
pictureBox1 | 在Mouse 下双击MouseDown 、MouseUp 、MouseMove 来生成对应的响应事件函数。 |
button1 | 如上,在Action 下双击Click 。 |
Form1 | 如上,在Behavior 下双击Load 。 |
然后我们开始补全对应的函数体内容。
注意,如果在上面改变了控件的名称,下面的代码需要做对应的更改。
废话少说上代码!
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Drawing.Drawing2D;//用于优化绘制的结果
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;
using MnistModel;
namespace DrawDigit
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Bitmap digitImage;//用来保存手写数字
private Point startPoint;//用于绘制线段,作为线段的初始端点坐标
private Mnist model;//用于识别手写数字
private const int MnistImageSize = 28;//Mnist模型所需的输入图片大小
private void Form1_Load(object sender, EventArgs e)
{
//当窗口加载时,绘制一个白色方框
model = new Mnist();
digitImage = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(digitImage);
g.Clear(Color.White);
pictureBox1.Image = digitImage;
}
private void clean_click(object sender, EventArgs e)
{
//当点击清除时,重新绘制一个白色方框,同时清除label1显示的文本
digitImage = new Bitmap(pictureBox1.Width, pictureBox1.Height);
Graphics g = Graphics.FromImage(digitImage);
g.Clear(Color.White);
pictureBox1.Image = digitImage;
label1.Text = "";
}
private void pictureBox1_MouseDown(object sender, MouseEventArgs e)
{
//当鼠标左键被按下时,设置isPainting为true,并记录下需要绘制的线段的起始坐标
startPoint = (e.Button == MouseButtons.Left) ? e.Location : startPoint;
}
private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
{
//当鼠标在移动,且当前处于绘制状态时,根据鼠标的实时位置与记录的起始坐标绘制线段,同时更新需要绘制的线段的起始坐标
if (e.Button == MouseButtons.Left)
{
Graphics g = Graphics.FromImage(digitImage);
Pen myPen = new Pen(Color.Black, 40);
myPen.StartCap = LineCap.Round;
myPen.EndCap = LineCap.Round;
g.DrawLine(myPen,startPoint, e.Location);
pictureBox1.Image = digitImage;
g.Dispose();
startPoint = e.Location;
}
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//当鼠标左键释放时
//同时开始处理图片进行推理
//暂时不处理这里的代码
}
}
}
步骤二:把模型包装成一个类
将模型包装成一个C#是整个过程中比较麻烦的一步。所幸的是,Tools for AI对此提供了很好的支持。进一步了解,可以看这里。
首先,我们在解决方案MnistForm下点击鼠标右键,选择添加->新建项目
,在弹出的窗口里选择AI Tools->Inference->模型推理类库
,名称不妨叫做MnistModel
,点击确定,于是我们又多了一个项目,
然后自己配置好这个项目的名称、位置,点击确定
。
然后弹出一个模型推理类库创建向导,这个时候就需要我们选择自己之前训练好的模型了~
首先在模型路径里选择保存的模型文件的路径。这里我们使用在AI应用开发实战 - 从零开始配置环境博客中训练并导出的模型
note:模型可在
/samples-for-ai/examples/tensorflow/MNIST
目录下找到,其中output
文件夹保存了检查点文件,export
文件夹保存了模型文件。
对于TensorFlow,我们可以选择检查点的.meta
文件,或者是保存的模型的.pb
文件
这里我们选择在AI应用开发实战 - 从零开始配置环境这篇博客最后生成的export
目录下的检查点的SavedModel.pb
文件,这时程序将自动配置好配置推理接口,见下图:
类名可以自己定义,因为我们用的是MNIST,那么类名就叫Mnist
好了,然后点击确定。
这样,在解决方案资源管理器
里,在解决方案MnistForm
下,就多了一个MnistModel
:
双击Mnist.cs
,我们可以看到项目自动把模型进行了封装,生成了一个公开的infer
函数。
然后我们在MnistModel
上右击,再选择生成
,等待一会,这个项目就可以使用了~
步骤三:连接两个部分
这一步差不多就是这么个感觉:
I have an apple , I have a pen. AH~ , Applepen
首先,我们来给DrawDigit添加引用,让它能使用MnistModel。在DrawDigit项目的引用上点击鼠标右键,点击添加引用
,在弹出的窗口中选择MnistModel
,点击确定。
然后,由于MNIST的模型的输入是一个28×28的白字黑底的灰度图,因此我们首先要对图片进行一些处理。
首先将图片转为28×28的大小。
然后将RGB图片转化为灰阶图,将灰阶标准化到[-0.5,0.5]区间内,转换为黑底白字。
最后将图片用mnist模型要求的格式包装起来,并传送给它进行推理。
于是,我们在pictureBox1_MouseUp
中添加上这些代码,并且在文件最初添加上using MnistModel;
:
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//当鼠标左键释放时
//同时开始处理图片进行推理
if (e.Button == MouseButtons.Left)
{
// 复制pictureBox中的图片并缩放到28*28成为新的图片(tmpBmp)
Bitmap tmpBmp = new Bitmap(digitImage, MinstImageSize, MinstImageSize);
//将图片转为灰阶图,并将图片的像素信息保存在list中
var imageData = new List<float>(MnistImageSize * MnistImageSize);
for (var y = 0; y < MnistImageSize; y++)
{
for (var x = 0; x < MnistImageSize; x++)
{
var color = tmpBmp.GetPixel(x, y);
var pixel = (float)(0.5 - (color.R + color.G + color.B) / (3.0 * 255));
imageData.Add(pixel);
}
}
//将图片信息包装为mnist模型规定的输入格式
var batchData = new List<IEnumerable<float>>();
batchData.Add(imageData);
//将图片传送给mnist模型进行推理
var result = model.Infer(batchData);
//将推理结果输出
label1.Text = result.First().First().ToString();
}
}
最后让我们尝试一下运行~
三、效果展示
现在我们就有了一个简单的小程序,可以识别手写的数字了。
赶紧试试效果怎么样~
注意
- 路径中不能有中文字符,否则可能找不到模型。
扩展
尝试识别多个数字
我们已经支持了单个手写数字的识别,那能不能支持多个手写数字的识别呢?同时写下多个数字,正是现实中更为常见的情形。相比之下,如果只能一次识别一个手写数字,应用就会有比较大的局限性。
首先,我们可以尝试在现有的应用里一次写下两个数字,看看识别效果(为了更好的展示效果,将笔画的宽度由40调整为20。这一改动对单个数字的识别并无大的影响):
识别效果不尽人意。
右上角展示的结果准确地反应了模型对我们手写输入的推理结果(即result.First().First().ToString()
),然而这一结果并不像我们期望的那样是“42”。
了解MNIST数据集的读者们可能已经意识到了,这是“理所当然”的。归根结底,这一问题的症结在于:作为我们AI应用核心的AI模型,本身并不具备识别多个数字的能力——当前案例中我们使用的AI模型是基于MNIST数据集训练的(训练过程请回顾我们之前的博客AI应用开发实战 - 从零开始配置环境),而MNIST数据集只覆盖了单个的手写数字;并且,我们并未对笔迹图形作额外的处理。
结果是在写下多个数字的情况下,我们实际上在“强行”让AI模型做超出其适应性范围的判断。这属于AI模型的误用。其结果自然难以令人满意。
那么,为了增强应用的可用性,我们能不能改善它、让它能识别多个数字呢?我们很自然地想到,既然MNIST模型已经能很好地识别单个数字,那我们只需要把多个数字分开,一个一个地让MNIST模型进行识别就好了。这样,我们就引入了一个新的子问题,即是“多个手写数字的分割”。
子问题:分割多个手写数字
我们注意到本文介绍的应用有一个特点,那就是最终用作输入的图形,是用户当场写下的,而非通过图片文件导入的静态图片,即我们拥有笔画产生过程中的全部动态信息,比如笔画的先后顺序,笔画的重叠关系等等。考虑到这些信息,我们可以设计一种基本的分割规则:在水平面上的投影相重叠的笔画,我们就认为它们同属于一个数字。
笔画和水平方向上投影的关系示意如下图:
因此书写时,就要求不同的数字之间尽量隔开。当然为了尽可能处理不经意的重叠,我们还可以为重叠部分相对每一笔画的位置设定一个阈值,如至少进入笔画一端的10%以内。
应用这样的规则后,我们就能比较好的把多个手写数字分割开,并能利用Visual Studio Tools for AI提供的批量推理功能,一次性对所有分割出的图形做推理。
多个手写数字识别的最终效果如图:
当然,我们对问题的定义还是非常理想化,分割算法也比较简单。在实际应用中,我们还经常要考虑非二值图形、噪点、非数字的判别等等。并且对手写数字的分割可能比我们设定的规则要复杂,因为在现实场景中,水平方向上的重叠可能会影响图形的涵义。
将两个手写数字分割开这一问题,实际上和经典的图像分割问题非常类似。虽然本文示例中的图像非常简单,但仍然可能具有相当复杂的语义需要处理。为此,我们可能需要引入更多的模型,或者扩展现有的模型来正确判断多个图形之间的关系。
进阶
那么,如果要识别多个连写的数字,或支持字母该怎么做呢?大家多用用也会发现,如果数字写得很小,或者没写到正中,识别起来正确率也会不高。要解决这些问题,做成真正的产品,就不止这一个模型了。比如在多个数字识别中,可能要根据经验来切分图,或者训练另一个模型来检测并分割数字。要支持字母,则需要重新训练一个包含手写字母的模型,并准备更多的字母的数据。要解决字太小的问题,还要检测一下字的大小,做合适的放大等等。
我们可以看到,一个训练出来的模型本身到一个实际的应用之间还有不少的功能要实现。希望我们这一系列的介绍,能够帮助大家将机器学习的概念带入到传统的编程领域中,做出更聪明的产品。
AI应用开发实战 - 手写识别应用入门的更多相关文章
- AI应用开发实战 - 手写算式计算器
扩展手写数字识别应用 识别并计算简单手写数学表达式 主要知识点 了解MNIST数据集 了解如何扩展数据集 实现手写算式计算器 简介 本文将介绍一例支持识别手写数学表达式并对其进行计算的人工智能应用的开 ...
- 【Win 10 应用开发】手写识别
记得前面(忘了是哪天写的,反正是前些天,请用力点击这里观看)老周讲了一个14393新增的控件,可以很轻松地结合InkCanvas来完成涂鸦.其实,InkCanvas除了涂鸦外,另一个大用途是墨迹识别, ...
- Tensorflow之基于MNIST手写识别的入门介绍
Tensorflow是当下AI热潮下,最为受欢迎的开源框架.无论是从Github上的fork数量还是star数量,还是从支持的语音,开发资料,社区活跃度等多方面,他当之为superstar. 在前面介 ...
- AI应用开发实战 - 定制化视觉服务的使用
AI应用开发实战 - 定制化视觉服务的使用 本篇教程的目标是学会使用定制化视觉服务,并能在UWP应用中集成定制化视觉服务模型. 前一篇:AI应用开发实战 - 手写识别应用入门 建议和反馈,请发送到 h ...
- 深度学习之PyTorch实战(3)——实战手写数字识别
上一节,我们已经学会了基于PyTorch深度学习框架高效,快捷的搭建一个神经网络,并对模型进行训练和对参数进行优化的方法,接下来让我们牛刀小试,基于PyTorch框架使用神经网络来解决一个关于手写数字 ...
- 机器学习实战kNN之手写识别
kNN算法算是机器学习入门级绝佳的素材.书上是这样诠释的:“存在一个样本数据集合,也称作训练样本集,并且样本集中每个数据都有标签,即我们知道样本集中每一条数据与所属分类的对应关系.输入没有标签的新数据 ...
- AI应用开发实战
AI应用开发实战 出发点 目前,人工智能在语音.文字.图像的识别与解析领域带来了跨越式的发展,各种框架.算法如雨后春笋一般,互联网上随处可见与机器学习有关的学习资源,各大mooc平台.博客.公开课都推 ...
- AI应用开发实战 - 从零开始搭建macOS开发环境
AI应用开发实战 - 从零开始搭建macOS开发环境 本视频配套的视频教程请访问:https://www.bilibili.com/video/av24368929/ 建议和反馈,请发送到 https ...
- [纯C#实现]基于BP神经网络的中文手写识别算法
效果展示 这不是OCR,有些人可能会觉得这东西会和OCR一样,直接进行整个字的识别就行,然而并不是. OCR是2维像素矩阵的像素数据.而手写识别不一样,手写可以把用户写字的笔画时间顺序,抽象成一个维度 ...
随机推荐
- Java中创建线程的三种方式及其优缺点
1.自定义一个继承Thread的类,由于Java的单继承特性,限制了该类的扩展性. 2.实现Runnable接口,重写run()方法. 3.实现Callable接口,重写call方法.线程执行体可以有 ...
- Intent传值的学习
今天学习了Intent传值的过程,有点安卓编程经验的都知道,Intent可以实现页面的跳转,可以从一个activity跳转到另一个activity,这个名义上说是界面跳转,其实这句话现在觉得说的很不严 ...
- What is the best way to handle Invalid CSRF token found in the request when session times out in Spring security
18.5.1 Timeouts One issue is that the expected CSRF token is stored in the HttpSession, so as soon a ...
- python中读写excel并存入mysql
为了一个突如其来的想法:用python简单解决就好.现在算是把这个项目需要的基础功能坑都填完了.剩下就是AI和数据展示方面的坑了. 今天遇到的坑是: 1.从excel读出的中文是乱码 2.中文写入my ...
- SpringCloud Eureka 报错 无法启动问题
1.SpringCloud Eureka 报错 无法启动基本上都是spring boot的版本与spring cloud的版本不匹配导致的. <dependencyManagement> ...
- ranker.go
package { start = utils.MinInt(options.OutputOffset, len(outputDocs)) end = ...
- iOS之LLDB常用调试命令
LLDB是个开源的内置于XCode的调试工具,这里来理一理常用用法.lldb对于命令的简称,是头部匹配方式,只要不混淆,你可以随意简称某个命令.结果为在xcode下验证所得,可能与其它平台有所误差. ...
- IOS-QQ第三方登录
iOS QQ第三方登实现 我们经常会见到应用登陆的时候会有QQ,微信,微博等的第三方登陆 如图: 下面我们主要讲一下qq的第三方登陆如何实现 首先,到官网注册: http://wiki.conne ...
- ajax封装函数和表单序列化
//表单序列化function iSerialize(form){ var parts={}; for(var i=0;i<form.elements.length;i++){ var file ...
- 【英国毕业原版】-《博尔顿大学毕业证书》Bolton一模一样原件
☞博尔顿大学毕业证书[微/Q:2544033233◆WeChat:CC6669834]UC毕业证书/联系人Alice[查看点击百度快照查看][留信网学历认证&博士&硕士&海归& ...