AI应用开发实战 - 手写识别应用入门

手写体识别的应用已经非常流行了,如输入法,图片中的文字识别等。但对于大多数开发人员来说,如何实现这样的一个应用,还是会感觉无从下手。本文从简单的MNIST训练出来的模型开始,和大家一起入门手写体识别。

在本教程结束后,会得到一个能用的AI应用,也许是你的第一个AI应用。虽然离实际使用还有较大的距离(具体差距在文章后面会分析),但会让你对AI应用有一个初步的认识,有能力逐步搭建出能够实际应用的模型。

建议和反馈,请发送到
https://github.com/Microsoft/vs-tools-for-ai/issues

联系我们
OpenmindChina@microsoft.com

准备工作

  • 使用win10 64位操作系统的计算机
  • 参考上一篇博客AI应用开发实战 - 从零开始配置环境。在电脑上训练并导出MNIST模型。

一、 思路

通过上一篇文章搭建环境的介绍后,就能得到一个能识别单个手写数字的模型了,并且识别的准确度会在98%,甚至99%以上了。那么我们要怎么使用这个模型来搭建应用呢?

大致的步骤如下:

  1. 实现简单的界面,将用户用鼠标或者触屏的输入变成图片
  2. 将生成的模型包装起来,成为有公开数据接口的类。
  3. 将输入的图片进行规范化,成为数据接口能够使用的格式。
  4. 最后通过模型来推理(inference)出图片应该是哪个数字,并显示出来。

是不是很简单?

二、动手

步骤一:获取手写的数字

提问:那我们要怎么获取手写的数字呢?

回答:我们可以写一个简单的WinForm画图程序,让我们可以用鼠标手写数字,然后把图片保存下来。

首先,我们打开Visual Studio,选择文件->新建->项目

在弹出的窗口里选择Visual C#->Windows窗体应用,项目名称不妨叫做DrawDigit,解决方案名称不妨叫做MnistForm,点击确定。

此时,Visual Studio也自动弹出了一个窗口的设计图。

在DrawDigit项目上点击右键,选择属性,在生成一栏将平台目标从Any CPU改为x64

否则,DrawDigit(首选32位)与它引用的MnistForm(64位)的编译平台不一致会引发System.BadImageFormatException的异常。

然后我们对这个窗口做一些简单的修改:

首先我们打开VS窗口左侧的工具箱,这个窗口程序需要以下三种组件:

  1. PictureBox:用来手写数字,并且把数字保存成图片
  2. Label:用来显示模型的识别结果
  3. Button:用来清理PictureBox的手写结果

那经过一些简单的选择与拖动还有调整大小,这个窗口现在是这样的:

一些注意事项

  1. 这些组件都可以通过右键->查看属性,在属性里修改它们的设置
  2. 为了方便把PictureBox里的图片转化成Mnist能识别的格式,PictureBox的需要是正方形
  3. 可以给这些控件起上有意义的名称。
  4. 可以调整一下label控件大小、字体等,让它更美观。

经过一些简单的调整,这个窗口现在是这样的:

现在来让我们愉快地给这些组件添加事件!

还是在属性窗口,我们选择某个组件,右键->查看属性,点击闪电符号,给组件绑定对应的事件。每次绑定后,会跳到代码部分,生成一个空函数。点回设计视图继续操作即可。

组件类型 事件
pictureBox1 Mouse下双击MouseDownMouseUpMouseMove来生成对应的响应事件函数。
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();}}

最后让我们尝试一下运行~

三、效果展示

现在我们就有了一个简单的小程序,可以识别手写的数字了。

赶紧试试效果怎么样~

注意

  1. 路径中不能有中文字符,否则可能找不到模型。

扩展

尝试识别多个数字

我们已经支持了单个手写数字的识别,那能不能支持多个手写数字的识别呢?同时写下多个数字,正是现实中更为常见的情形。相比之下,如果只能一次识别一个手写数字,应用就会有比较大的局限性。
首先,我们可以尝试在现有的应用里一次写下两个数字,看看识别效果(为了更好的展示效果,将笔画的宽度由40调整为20。这一改动对单个数字的识别并无大的影响):

识别效果不尽人意。
右上角展示的结果准确地反应了模型对我们手写输入的推理结果(即result.First().First().ToString()),然而这一结果并不像我们期望的那样是“42”。
了解MNIST数据集的读者们可能已经意识到了,这是“理所当然”的。归根结底,这一问题的症结在于:作为我们AI应用核心的AI模型,本身并不具备识别多个数字的能力——当前案例中我们使用的AI模型是基于MNIST数据集训练的(训练过程请回顾我们之前的博客AI应用开发实战 - 从零开始配置环境),而MNIST数据集只覆盖了单个的手写数字;并且,我们并未对笔迹图形作额外的处理。
结果是在写下多个数字的情况下,我们实际上在“强行”让AI模型做超出其适应性范围的判断。这属于AI模型的误用。其结果自然难以令人满意。
那么,为了增强应用的可用性,我们能不能改善它、让它能识别多个数字呢?我们很自然地想到,既然MNIST模型已经能很好地识别单个数字,那我们只需要把多个数字分开,一个一个地让MNIST模型进行识别就好了。这样,我们就引入了一个新的子问题,即是“多个手写数字的分割”。

子问题:分割多个手写数字

我们注意到本文介绍的应用有一个特点,那就是最终用作输入的图形,是用户当场写下的,而非通过图片文件导入的静态图片,即我们拥有笔画产生过程中的全部动态信息,比如笔画的先后顺序,笔画的重叠关系等等。考虑到这些信息,我们可以设计一种基本的分割规则:在水平面上的投影相重叠的笔画,我们就认为它们同属于一个数字。
笔画和水平方向上投影的关系示意如下图:

因此书写时,就要求不同的数字之间尽量隔开。当然为了尽可能处理不经意的重叠,我们还可以为重叠部分相对每一笔画的位置设定一个阈值,如至少进入笔画一端的10%以内。
应用这样的规则后,我们就能比较好的把多个手写数字分割开,并能利用Visual Studio Tools for AI提供的批量推理功能,一次性对所有分割出的图形做推理。
多个手写数字识别的最终效果如图:

当然,我们对问题的定义还是非常理想化,分割算法也比较简单。在实际应用中,我们还经常要考虑非二值图形、噪点、非数字的判别等等。并且对手写数字的分割可能比我们设定的规则要复杂,因为在现实场景中,水平方向上的重叠可能会影响图形的涵义。
将两个手写数字分割开这一问题,实际上和经典的图像分割问题非常类似。虽然本文示例中的图像非常简单,但仍然可能具有相当复杂的语义需要处理。为此,我们可能需要引入更多的模型,或者扩展现有的模型来正确判断多个图形之间的关系。

进阶

那么,如果要识别多个连写的数字,或支持字母该怎么做呢?大家多用用也会发现,如果数字写得很小,或者没写到正中,识别起来正确率也会不高。要解决这些问题,做成真正的产品,就不止这一个模型了。比如在多个数字识别中,可能要根据经验来切分图,或者训练另一个模型来检测并分割数字。要支持字母,则需要重新训练一个包含手写字母的模型,并准备更多的字母的数据。要解决字太小的问题,还要检测一下字的大小,做合适的放大等等。

我们可以看到,一个训练出来的模型本身到一个实际的应用之间还有不少的功能要实现。希望我们这一系列的介绍,能够帮助大家将机器学习的概念带入到传统的编程领域中,做出更聪明的产品。

转载于:https://www.cnblogs.com/ms-uap/p/9182530.html

AI应用开发实战 - 手写识别应用入门相关推荐

  1. AI应用开发实战 - 手写算式计算器

    扩展手写数字识别应用 识别并计算简单手写数学表达式 主要知识点 了解MNIST数据集 了解如何扩展数据集 实现手写算式计算器 简介 本文将介绍一例支持识别手写数学表达式并对其进行计算的人工智能应用的开 ...

  2. AI应用开发实战系列之三:手写识别应用入门

    AI应用开发实战 - 手写识别应用入门 手写体识别的应用已经非常流行了,如输入法,图片中的文字识别等.但对于大多数开发人员来说,如何实现这样的一个应用,还是会感觉无从下手.本文从简单的MNIST训练出 ...

  3. 机器学习实战-手写识别系统

    在实现了K近邻算法后,书中给出了一个实例,今天来学习一下使用K近邻分类器的手写识别系统.书中原带的文件已经过处理转换为了文本格式,方便了许多. 先看一下原带文件 准备数据 #将图像转换为测试向量 de ...

  4. 【机器学习实战】k近邻算法实战——手写识别系统

    文章目录 手写识别系统 步骤: 准备数据:将图像转换为测试向量 测试算法:使用k-近邻算法识别手写数字 [完整代码] 手写识别系统 为了简单起见,这里构造的系统只能识别数字0到9,参见图2-6.需要识 ...

  5. Tensorflow之基于MNIST手写识别的入门介绍

    Tensorflow是当下AI热潮下,最为受欢迎的开源框架.无论是从Github上的fork数量还是star数量,还是从支持的语音,开发资料,社区活跃度等多方面,他当之为superstar. 在前面介 ...

  6. cnn神经网络可以用于数据拟合吗_使用Keras搭建卷积神经网络进行手写识别的入门(包含代码解读)...

    本文是发在Medium上的一篇博客:<Handwritten Equation Solver using Convolutional Neural Network>.本文是原文的翻译.这篇 ...

  7. AI应用开发实战(转)

    AI应用开发实战 - 从零开始配置环境 与本篇配套的视频教程请访问:https://www.bilibili.com/video/av24421492/ 建议和反馈,请发送到 https://gith ...

  8. AI应用开发实战系列之四 - 定制化视觉服务的使用

    AI应用开发实战 - 定制化视觉服务的使用 本篇教程的目标是学会使用定制化视觉服务,并能在UWP应用中集成定制化视觉服务模型. 前一篇:AI应用开发实战 - 手写识别应用入门 建议和反馈,请发送到 h ...

  9. python基于轻量级CNN模型开发构建手写藏文数字识别系统

    最近做的很多工作都是跟手写性质的数据集有关的,比如:手写汉字.手写甲骨文.手写数字.手写字母等等,今天主要做的实践是对藏文中的手写数字进行识别分析,在我之前的博文中有很多相关的实践分析,感兴趣的话可以 ...

最新文章

  1. CV:基于Keras利用CNN主流架构之mini_XCEPTION训练情感分类模型hdf5并保存到指定文件夹下
  2. 不只是用于研究:使用Nvivo获取各种定性数据
  3. c 语言注释格式化,linux 格式化C语言的源文件 命令:indent
  4. Spring Webflux – Kotlin DSL –实现的演练
  5. Windows Hook机制(转贴一)
  6. mysql8.0.20 64位安装教程_MySQL8.0.20压缩版本安装教程图文详解
  7. (90)FPGA仿真计数器激励
  8. 设置VS2015背景图片(转载)
  9. Linux 错误: $'\r': command not found错误解决
  10. Loadrunner教程
  11. GreenPlum分区表原理
  12. 2020年10月最新免费加速下载百度网盘文件方法
  13. 【车牌识别】基于模板匹配算法实现新能源车牌识别matlab源码
  14. [Inferior 1 (process 17260) exited normally] Debugger finished with status 0
  15. MySQL中的删除:drop,delete,truncate的区别和联系
  16. 知识图谱嵌入的Translate模型汇总(TransE,TransH,TransR,TransD)
  17. 隐马尔科夫模型(HMM)理解与总结
  18. G1垃圾回收器学习笔记
  19. 全球及中国导电聚合物(CP)型电子鼻发展前景与投资战略分析报告2022~2028年
  20. 张海宁:首个 CNCF 中国开源项目 Harbor 的修炼之道

热门文章

  1. java 运行环境变量_java 环境变量配置与第一个程序运行
  2. php satellite_object_to_string,php object如何转string
  3. mysql反向生成uml类图_UML类图自动生成,太爽了
  4. 【ruoyi若依】Caused by: java.lang.NoClassDefFoundError: com/sun/jna/platform/win32/VersionHelpers
  5. oracle查看执行最慢与查询次数最多的sql语句
  6. java读取对象失败_jsp exception对象获取异常信息
  7. java短_Java中的最短代码和最低延迟
  8. 不刷新页面的tab_SwiftUI小技巧之如何解决Tab切换后页面重置和List刷新bug
  9. 正式发布python版本的年份_飞书首次举办产品发布会,新版本“”正式发布
  10. 特斯拉员工入职3天就“偷”代码,悄悄备份6300多Python脚本