Visual Studio 完全AI手册 - 使用模型搭建应用
请听题:
当我们手握一个模型,我们可以:
A:拿模型来垫显示器?
B:把模型裱起来挂在墙上?
C:把模型拿去发朋友圈?
D:把模型拿去构建应用!
本教程的目的是将使用模型构建应用的过程压缩到极简,以便各位读者能够快速上手构建应用,在短期内构建信心。
零、前提条件
- 一台电脑,使用win10 64位操作系统
- 请确保鼠标、键盘、显示器都是好的
- 阅读了第一篇博客,能够在电脑上训练MNIST模型
一、思路
经过漫长的训练之后,我们手上有一个模型,这个模型能够用来识别手写的单个数字(0-9),并且识别的准确度是99%,那么我们要怎么使用这个模型来搭建应用呢?
一个正常且合理的思路是:
- 把模型包装成一个类,规定好识别功能的输入与输出格式 (这是最难的一步)
- 写一些代码,这些代码能够以某种格式获取手写的数字
- 再写一些代码,把第二步获得的
手写的数字
处理成第一步规定的输入格式
- 最后获得
输出
,把输出以某种形式展示出来
是不是很简单?只比把大象关进冰箱里多了一步。
说干就干,首先找个你觉得舒服的地点和姿势,打开你的Visual Studio 2017,咱们马上进入动手环节。
二、动手
步骤一:把模型包装成一个类
提问:我们要怎么把模型包装成一个类呢?
回答:Tools for AI早就给我们准备好了一个这样的工具,请看这个链接:
https://github.com/Microsoft/vs-tools-for-ai/blob/master/docs/zh-hans/docs/model-inference.md
首先,点击文件->新建->项目
在弹出的窗口左侧点击已安装->AI Tools->Inference
,然后选择模型推理类库
然后自己配置好这个项目的名称、位置,点击确定
VS会弹出一个模型推理类库创建向导,这个时候就需要我们选择自己之前训练好的模型了~
首先在模型路径里选择保存的模型文件的路径。
对于TensorFlow,我们可以选择检查点的.meta
文件,或者是保存的模型的.pb
文件
这里我们选择在Visual Studio 完全AI手册 - 从0开始配置环境这篇博客最后生成的output
目录下的检查点的model.ckpt-8000.meta
文件,同时需要自己配置一下推理接口,点击推理接口旁的添加
,按照下图修改接口,然后点击确定。
类名可以自己定义,因为我们用的是MNIST,那么类名就叫Mnist
好了,然后点击确定。
现在解决方案资源管理器
里,在解决方案MnistForm
下,就有了这样的一个东西:
双击Mnist.cs
,我们可以看到项目自动把模型进行了封装,生成了一个可以拿来用的infer
函数
然后我们在MnistForm下点击右键,点击生成,等待一会,这个项目就可以拿来用了~
步骤二:获取手写的数字
提问:那我们要怎么获取手写的数字呢?
回答:我们可以写一个简单的WinForm画图程序,让我们可以用鼠标手写数字,然后把图片保存下来
首先,我们在解决方案MnistForm下点击鼠标右键,选择添加->新建项目
,在弹出的窗口里选择Visual C#->Windows窗体应用
,名称不妨叫做DrawDigit
,点击确定,于是我们又多了一个项目,Visual Studio也自动弹出了一个窗口的设计图。
然后我们对这个窗口做一些简单的修改:
首先我们打开VS窗口左侧的工具箱,这个窗口程序需要以下三种组件:
- PictureBox:用来手写数字,并且把数字保存成图片
- Label:用来显示模型的识别结果
- Button:我们需要两个,一个用来清理PictureBox的手写结果,另一个用来把当前的结果传递给模型进行识别
那经过一些简单的选择与拖动还有调整大小
,这个窗口现在是这样的:
一些注意事项
- 这些组件都可以通过
右键->查看属性
,在属性里修改它们的设置 - 为了方便把PictureBox里的图片转化成Mnist能识别的格式,PictureBox的最好是一个正方形
- label默认是不能调整大小的,可以在
属性->布局
里修改AutoSize
为False
,并且在外观
中修改TextAlign
为MiddleCenter
,在Text里修改默认的"label1"
为" "
- 最好把这些组件的名字都修改好,当然也可以不改。。
经过一些简单的调整,这个窗口现在是这样的:
现在来让我们愉快地给这些组件添加事件!
还是在属性窗口,我们选择一个组件,右键->查看属性,点击闪电符号,给组键绑定对应的事件:
组件类型 | 事件 |
---|---|
pictureBox1 | 在鼠标 下的MouseDown 中输入picturebox1_MouseDown ,点击回车在 鼠标 下的MouseUp 中输入picturebox1_MouseUp ,点击回车在 鼠标 下的MouseMove 中输入picturebox1_MouseMove ,点击回车 |
button1(用于清除) | 在Click 下输入clean_click ,点击回车 |
button2(用于识别) | 在Click 下输入recognize_click ,点击回车 |
Form1 | 在行为 的load 下输入Form1_load ,点击回车 |
note:在开发的过程中请记得时不时
Ctrl+S
一下
添加完之后我们发现,VS给我们自动生成了几个空函数
接下来,我们来给DrawDigit添加引用,让它能使用MnistForm。在DrawDigit项目的引用上点击鼠标右键,点击添加引用
,在弹出的窗口中选择MnistForm
,点击确定。
然后我们开始补全对应的函数~废话少说上代码!
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 MnistForm; //引用MnistForm推理类库
namespace DrawDigit
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
private Bitmap digitImage;//用来保存手写数字
private int pbWidth;//手写数字图片的宽
private int pbHeight;//手写数字图片的高
private bool isPainting;//当前是否处于绘制状态,当鼠标左键被按下时,处于绘制状态
private int currentX;//用于绘制线段,作为线段的初始端点x坐标
private int currentY;//用于绘制线段,作为线段的初始端点y坐标
private Mnist model;//用于识别手写数字
private const int MnistImageSize = 28;//Mnist模型所需的输入图片大小
private void Form1_Load(object sender, EventArgs e)
{
//当窗口加载时,绘制一个白色方框
model = new Mnist();
pbWidth = pictureBox1.Width;
pbHeight = pictureBox1.Height;
digitImage = new Bitmap(pbWidth, pbHeight);
Graphics g = Graphics.FromImage(digitImage);
g.Clear(Color.White);
pictureBox1.Image = digitImage;
}
private void recognize_click(object sender, EventArgs e)
{
label1.Text = "";//清除label1上次显示的值
Bitmap digitTmp = 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();
}
private void clean_click(object sender, EventArgs e)
{
//当点击清除时,重新绘制一个白色方框,同时清除label1显示的文本
digitImage = new Bitmap(pbWidth, pbHeight);
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;
currentX = e.X;
currentY = 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, currentX, currentY, e.X, e.Y);
pictureBox1.Image = digitImage;
g.Dispose();
currentX = e.X;
currentY = e.Y;
}
}
private void pictureBox1_MouseUp(object sender, MouseEventArgs e)
{
//当鼠标左键没有被按下时,设置isPainting为false,同时归零起始坐标
if (e.Button == MouseButtons.Left)
{
isPainting = false;
currentX = 0;
currentY = 0;
}
}
}
}
步骤三:连接两个部分
这一步差不多就是这么个感觉:
I have an apple , I have a pen. AH~ , Applepen
这部分的代码请看上述代码中recognize_click
函数中的代码。
由于MNIST的模型的输入是一个2828的白字黑底的灰度图,因此我们首先要对图片进行一些处理。
首先将图片转为2828的大小。
然后将RGB图片转化为灰阶图,将灰阶标准化到[-0.5,0.5]区间内,转换为黑底白字。
最后将图片用mnist模型要求的格式包装起来,并传送给它进行推理。
三、可能出现的问题
BadImageFormatException
就像这样:
这时应该在DrawDigit项目上点击右键,选择属性,在生成一栏将平台目标中的首选32位
复选框取消即可。
三、效果展示
现在我们就有了一个简单的小程序,可以识别手写的数字了。
赶紧试试效果怎么样~