AI應用開發實戰系列之三:手寫識別應用入門

AI應用開發實戰 - 手寫識別應用入門

手寫體識別的應用已經非常流行了,如輸入法,圖片中的文字識別等。但對於大多數開發人員來說,如何實現這樣的一個應用,還是會感覺無從下手。本文從簡單的MNIST訓練出來的模型開始,和大家一起入門手寫體識別。

在本教程結束後,會得到一個能用的AI應用,也許是你的第一個AI應用。雖然離實際使用還有較大的距離(具體差距在文章後面會分析),但會讓你對AI應用有一個初步的認識,有能力逐步搭建出能夠實際應用的模型。

建議和反饋,請發送到
https://github.com/Microsoft/vs-tools-for-ai/issues

聯繫我們
[email protected]

準備工作

一、 思路

通過上一篇文章搭建環境的介紹後,就能得到一個能識別單個手寫數字的模型了,並且識別的準確度會在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)
        {
            //當鼠標左鍵被按下時,記錄下需要繪製的線段的起始座標
            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)
            {
                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. 路徑中不能有中文字符,否則可能找不到模型。

進階

那麼,如果要識別多個連寫的數字,或支持字母該怎麼做呢?大家多用用也會發現,如果數字寫得很小,或者沒寫到正中,識別起來正確率也會不高。要解決這些問題,做成真正的產品,就不止這一個模型了。比如在多個數字識別中,可能要根據經驗來切分圖,或者訓練另一個模型來檢測並分割數字。要支持字母,則需要重新訓練一個包含手寫字母的模型,並準備更多的字母的數據。要解決字太小的問題,還要檢測一下字的大小,做合適的放大等等。

我們可以看到,一個訓練出來的模型本身到一個實際的應用之間還有不少的功能要實現。希望我們這一系列的介紹,能夠幫助大家將機器學習的概念帶入到傳統的編程領域中,做出更聰明的產品。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章