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

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

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

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

准备工作

一、 思路

通过上一篇文章搭建环境的介绍后,就能得到一个能识别单个手写数字的模型了,并且识别的准确度会在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;


namespace DrawDigit
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private Bitmap digitImage;//用来保存手写数字
        private bool isPainting;//当前是否处于绘制状态,当鼠标左键被按下时,处于绘制状态
        private int startX;//用于绘制线段,作为线段的初始端点x坐标
        private int startY;//用于绘制线段,作为线段的初始端点y坐标
        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,并记录下需要绘制的线段的起始坐标
            if (e.Button == MouseButtons.Left)
            {
                isPainting = true;
                startX = e.X;
                startY = e.Y;
            }
        }

        private void pictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            //当鼠标在移动,且当前处于绘制状态时,根据鼠标的实时位置与记录的起始坐标绘制线段,同时更新需要绘制的线段的起始坐标
            if (isPainting == true)
            {
                Graphics g = Graphics.FromImage(digitImage);
                Pen myPen = new Pen(Color.Black, 40);
                myPen.StartCap = System.Drawing.Drawing2D.LineCap.Round;
                myPen.EndCap = System.Drawing.Drawing2D.LineCap.Round;
                g.DrawLine(myPen, startX, startY, e.X, e.Y);
                pictureBox1.Image = digitImage;
                g.Dispose();
                startX = e.X;
                startY = e.Y;
            }
        }

        private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            //当鼠标左键没有被按下时,设置isPainting为false,同时归零起始坐标
            //同时开始处理图片进行推理
            //暂时不填写这部分的代码
        }
    }
}

步骤二:把模型包装成一个类

将模型包装成一个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)
{
    //当鼠标左键没有被按下时,设置isPainting为false,同时归零起始坐标
    //同时开始处理图片进行推理
    if (e.Button == MouseButtons.Left)
    {
        isPainting = false;
        Bitmap digitTmp = (Bitmap)digitImage.Clone();//复制digitImage
        //调整图片大小为Mnist模型可接收的大小:28×28
        using (Graphics g = Graphics.FromImage(digitTmp))
        {
            g.InterpolationMode = InterpolationMode.HighQualityBicubic;
            g.DrawImage(digitTmp, 0, 0, MnistImageSize, MnistImageSize);
        }
        //将图片转为灰阶图,并将图片的像素信息保存在list中
        var image = new List<float>(MnistImageSize * MnistImageSize);
        for (var x = 0; x < MnistImageSize; x++)
        {
            for (var y = 0; y < MnistImageSize; y++)
            {
                var color = digitTmp.GetPixel(y, x);
                var a = (float)(0.5 - (color.R + color.G + color.B) / (3.0 * 255));
                image.Add(a);
            }
        }
        //将图片信息包装为mnist模型规定的输入格式
        var batch = new List<IEnumerable<float>>();
        batch.Add(image);
        //将图片传送给mnist模型进行推理
        var result = model.Infer(batch);
        //将推理结果输出
        label1.Text = result.First().First().ToString();
    }
}

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

三、效果展示

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

赶紧试试效果怎么样~

注意

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

进阶

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

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

猜你喜欢

转载自blog.csdn.net/SoftwareTeacher/article/details/80770347