C#で簡易手書き文字認識システムを作った

C#で数字だけを認識する、手書き文字認識システムを作りました。
非常に簡易的なものです。
以下の説明は、VisualStudioを使用して作成する場合の説明になります。

```TegakiLib.cs
using System;
using System.Drawing;
using System.Windows.Forms;


namespace TegakiLib
{
    public class Tegaki : PictureBox
    {
        private Bitmap canvas;
        private Graphics g;
        private Pen p;
        private bool writing = false;
        private int pointCount = 0;
        private bool isInit = true;
        private int initPositionX = 0;
        private int initPositionY = 0;

        /// <summary>
        /// up=1,down=2
        /// left=1,right=2
        /// </summary>
        private int[][,] hanbetu = new int[10][,];
        private int[] refIndex = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };

        public Action<int> exec;

        public Tegaki()
        {
            exec = (int i) => { };
            InitTegaki();
        }
        public Tegaki(Action<int> func)
        {
            exec = func;
            InitTegaki();
        }

        private void InitTegaki()
        {
            canvas = new Bitmap(500, 500);
            g = Graphics.FromImage(canvas);
            p = new Pen(Color.Cyan, 5);

            hanbetu[0] = new int[,] { { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[1] = new int[,] { { 2, 0, 0, 0 }, { 2, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[2] = new int[,] { { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[3] = new int[,] { { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[4] = new int[,] { { 0, 1, 0, 0 }, { 1, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[5] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[6] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 0, 1, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[7] = new int[,] { { 1, 0, 0, 0 }, { 0, 2, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[8] = new int[,] { { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 0, 1, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 9, 9, 0, 0 } };
            hanbetu[9] = new int[,] { { 0, 1, 0, 0 }, { 2, 0, 0, 0 }, { 0, 2, 0, 0 }, { 1, 0, 0, 0 }, { 2, 0, 0, 0 }, { 9, 9, 0, 0 } };
        }

        protected override void OnMouseDown(MouseEventArgs e)
        {
            writing = true;
            base.OnMouseDown(e);
        }

        protected override void OnMouseLeave(EventArgs e)
        {
            drawFin();
            base.OnMouseLeave(e);
        }

        protected override void OnMouseMove(MouseEventArgs e)
        {
            mouseMoving();
            base.OnMouseMove(e);
        }

        protected override void OnMouseUp(MouseEventArgs e)
        {
            drawFin();
            base.OnMouseUp(e);
        }

        private void drawFin()
        {
            writing = false;
            isInit = true;
            int j;

            g.Clear(Color.FromArgb(64, 0, 64));
            this.Image = canvas;
            for (int i = 0; i <= 9; i++)
            {
                refIndex[i] = 0;
                for (j = 0; j < hanbetu[i].Length / 4; j++)
                {
                    hanbetu[i][j, 2] = 0;
                    hanbetu[i][j, 3] = 0;
                }
            }
        }

        private void mouseMoving()
        {
            if (!writing) return;

            int up = 1, down = 2, left = 1, right = 2;
            int ud = 10, lr = 10;
            int writtenNumber = 0;
            bool m = false;

            Point point = this.PointToClient(Control.MousePosition);

            g.DrawRectangle(p, point.X, point.Y, 5, 5);
            this.Image = canvas;

            if (isInit)
            {
                initPositionX = point.X;
                initPositionY = point.Y;
                isInit = false;
            }
            pointCount++;
            if (pointCount >= 5)
            {
                if ((point.X - initPositionX) >= 30)
                {
                    lr = right;
                    isInit = true;
                }
                if ((point.X - initPositionX) <= -30)
                {
                    lr = left;
                    isInit = true;
                }
                if ((point.Y - initPositionY) >= 30)
                {
                    ud = down;
                    isInit = true;
                }
                if ((point.Y - initPositionY) <= -30)
                {
                    ud = up;
                    isInit = true;
                }
                pointCount = 0;
                //isInit = true;

                for (int i = 0; i <= 9; i++)
                {
                    if (hanbetu[i][refIndex[i], 0] == ud || hanbetu[i][refIndex[i], 1] == lr)
                    {
                        hanbetu[i][refIndex[i], 2] = point.X;
                        hanbetu[i][refIndex[i], 3] = point.Y;

                        refIndex[i]++;
                        if (hanbetu[i][refIndex[i], 0] == 9 || hanbetu[i][refIndex[i], 1] == 9)
                        {
                            switch (i)
                            {
                                case 0:
                                    if ((hanbetu[0][0, 2] < hanbetu[0][2, 2]) && (hanbetu[0][1, 3] > hanbetu[0][3, 3]) &&
                                       (hanbetu[0][0, 3] - 10 > hanbetu[0][3, 3]))
                                    {
                                        writtenNumber = 0;
                                        m = !m;
                                    }
                                    break;
                                case 1:
                                    if (((hanbetu[1][0, 2] + 50 >= hanbetu[1][2, 2]) && (hanbetu[1][0, 2] - 50 <= hanbetu[1][2, 2])) &&
                                            (hanbetu[4][2, 2] == 0) && (hanbetu[4][2, 3] == 0) &&
                                            (hanbetu[9][4, 2] == 0) && (hanbetu[9][4, 3] == 0) &&
                                            (hanbetu[7][0, 2] == 0) && (hanbetu[7][0, 3] == 0))
                                    {
                                        writtenNumber = 1;
                                        m = !m;
                                    }
                                    break;
                                case 2:
                                    writtenNumber = 2;
                                    m = !m;
                                    break;
                                case 3:
                                    if ((hanbetu[3][0, 3] < hanbetu[3][1, 3]) && (hanbetu[3][2, 3] < hanbetu[3][3, 3]))
                                    {
                                        writtenNumber = 3;
                                        m = !m;
                                    }
                                    break;
                                case 4:
                                    if ((hanbetu[4][1, 3] < hanbetu[4][0, 3]) && (hanbetu[4][1, 2] < hanbetu[4][2, 2]))
                                    {
                                        writtenNumber = 4;
                                        m = !m;
                                    }
                                    break;
                                case 5:
                                    writtenNumber = 5;
                                    m = !m;
                                    break;
                                case 6:
                                    if ((hanbetu[6][1, 2] < hanbetu[6][0, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][2, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][3, 2]) &&
                                       (hanbetu[6][1, 2] < hanbetu[6][4, 2]) &&
                                       (hanbetu[6][0, 3] + 30 < hanbetu[6][4, 3]))
                                    {
                                        writtenNumber = 6;
                                        m = !m;
                                    }
                                    break;
                                case 7:
                                    if ((hanbetu[9][3, 2] == 0) && (hanbetu[9][3, 3] == 0))
                                    {
                                        writtenNumber = 7;
                                        m = !m;
                                    }
                                    break;
                                case 8:
                                    if ((hanbetu[8][0, 3] < hanbetu[8][1, 3]) && (hanbetu[8][2, 3] > hanbetu[8][3, 3]))
                                    {
                                        writtenNumber = 8;
                                        m = !m;
                                    }
                                    break;
                                case 9:
                                    if (hanbetu[9][0, 3] < hanbetu[9][2, 3])
                                    {
                                        writtenNumber = 9;
                                        m = !m;
                                    }
                                    break;
                            }
                            if(m) exec(writtenNumber);
                        }
                    }
                }
            }
        }
    }
}
```

`PictureBox`クラスの派生`Tegaki`クラスです。
上のソースコードをそのままコピーして、DLLにもできますし、プロジェクトの一部にしてもかまいません。
フォームのデザイン画面を開いてみると、ツールボックスに`Tegaki`が追加されます。

##使い方(プログラム例)

![00001.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/01e1a105-ae97-41a8-6365-210b1c6afb62.png)

`Tegaki`のサイズは500✕500のみ対応しています。プロパティでサイズを500✕500にしてください。
背景色は、何でもいいですが、勝手にRGB(64,0,64)に変わってしまいます。（手抜きですが・・・）
他にもラベルを2つ追加しました。

```Form2.cs
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Forms;

namespace TegakiNumber
{
    public partial class Form2 : Form
    {
        int j = 0;
        public Form2()
        {
            InitializeComponent();
            tegaki1.exec = exec;
        }

        private void exec(int i)
        {
            label1.Text = "現在の判別値" + i.ToString();
            j = i;
        }

        private void tegaki1_MouseUp(object sender, MouseEventArgs e)
        {
            label2.Text = "確定した判別値" + j.ToString();
        }
    }
}
```

フォームのソースコードです。
コンストラクターで、
`tegaki1.exec = exec;`
とありますが、`Tegaki`クラスの中で`public Action<int> exec;`と定義されています。
これはint型を1つ引数にとって、戻り値を返さない関数を意味しています。

つまり`Tegaki.exec`に`Form2.exec(int i)`を渡しています。
この関数は、手書き入力中に、今現在判別している数字が変化したら実行されます。
その数字が引数`i`に渡されます。
`label1`は、入力中の現在の判別値を表示します。

この簡易文字認識システムは、マウスクリックからマウスが離されるまでの軌跡で判断します。
よって、`label2`は、`tegaki1`の`MouseUp`イベントで、確定した判別値を返します。

##使い方(数字の書き方)

先ほど説明したように、マウスクリックからマウスが離されるまでの軌跡で判断します。
つまり、一筆書きが基本です。
「4」は、
![00002.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/0a598720-38b2-0ec2-8f0e-7894ee4baf77.png)
の順に書きます。
「5」は、
![00003.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/4cbbc58c-9827-1f8b-f3f3-30f5a150c734.png)
と書きます。
「7」は、
![00004.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/9479c8db-75d9-0790-62bc-afdccf8e7934.png)
最初の縦棒は必須です。

##実際の実行中の画面

![00006.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/0fa52996-de15-0c08-7516-71888a854c33.png)

「3」を書いてる途中の画面です。まだ途中なので、現在の判別値が「2」と判定されています。

![00005.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/590410/c733192c-03c3-9719-232c-bccf9ce4aeb2.png)

「6」を書いたときの画面です。（マウスクリックを離すと手書き文字が消えるので、確定した判別値が前回のままになっています。）

以上、数字だけに対応した簡易的な手書き文字認識システムでした。