【Tesseract.js】で手書き文字認識を試してみる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/022443d8-1af1-6fbb-6f4a-0ed379bca5ee.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/f897ac67-e906-abe4-e03e-9333f92ac68f.png)
※HTMLの`canvas`要素に手書き文字を書き込んで、`Tesseract.js`に文字認識してもらっています。

# はじめに
　先日、[【Tesseract.js】画像から数字を読み取って数独を解くWebアプリを作ってみた](https://qiita.com/fruit_ole/items/4576deeda11e86ee88e3)　という記事を投稿しました。いろいろ調べてみると、`Tesseract.js`は手書き文字もある程度認識できるということだったので、どの程度使えるのか試してみました。
　今回も詳しい説明は省略します。

# やったこと
- HTMLの`canvas`要素に文字を書けるようにする
- `canvas`に書いた手書き文字を`Tesseract.js`に認識させる
- 英語、日本語、数字、アルファベット、ひらがな、カタカナを選択できるようにして、文字認識の条件を指定できるようにする

# ソースコード

- `index.html`：ブラウザに表示させるHTML
<details><summary>ソースコードを表示</summary><div>

```html:index.html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <style>
        body {background: whitesmoke;}
        #canvas {
            display: block;
            background-image: url(ichimatsu_silver.png);
            background-size: 150px;
            border: solid black 0.5px;
            box-sizing: border-box;
        }
        #text {font-weight: bold; color: red}
    </style>
    <script src='https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js'></script>
    <script src="vector2.js"></script>
    <script src="paint.js"></script>
    <title>手書き文字認識</title>
</head>
<body>
    <div>
        <input type="radio" name="mode" value="english" checked>英語
        <input type="radio" name="mode" value="japanese">日本語
        <input type="radio" name="mode" value="number">数字
        <input type="radio" name="mode" value="alphabet">アルファベット
        <input type="radio" name="mode" value="hiragana">ひらがな
        <input type="radio" name="mode" value="katakana">カタカナ
    </div>
    <canvas id="canvas" width="300" height="300"></canvas>
    <input type="button" id="clear_button" value="クリア">
    <input type="button" id="recognize_button" value="読み取る">
    <span id="output"></span>
    <br>
    認識した文字：<span id="text"></span>
    <script src="main.js"></script>
</body>
</html>
```
</div></details>

　ちなみに、`ichimatsu_silver.png`は[フリー素材](https://tsukatte.com/wp-content/uploads/2019/01/ichimatsu_silver.png)を使っています。（別に背景白でも問題ない）

- `main.js`：メインとなるjavascript

<details><summary>ソースコードを表示</summary><div>

```javascript:main.js
const canvas = document.getElementById('canvas');// canvas（手書き領域）
const pen = new Pen(canvas);// ペンをインスタンス化
const textZone = document.getElementById('text')// 結果を表示する場所
const out = document.getElementById('output')// 読み込み中/読み込み完了を表示する場所
const modes = document.getElementsByName('mode')// モードを選択するラジオボタン

// canvas上でマウスの座標を取得する関数
const getCoord = e => { return new Vector2(e.offsetX, e.offsetY) }
// Tesseract.jsを使うための準備
const { createWorker } = Tesseract;

// マウスが押されたとき
canvas.addEventListener('mousedown', e => {
    const coord = getCoord(e);
    pen.begin(coord)
}, false)

// マウスが動いたとき
canvas.addEventListener('mousemove', e => {
    const coord = getCoord(e);
    if(pen.isDrawing) {
        pen.push(coord)
    }
}, false)

// マウスが離されたとき
canvas.addEventListener('mouseup', e => {
    pen.close();
}, false)

// クリアボタンが押されたとき、canvas全体をクリアする
const clearButton = document.getElementById('clear_button')
clearButton.addEventListener('click', () => {
    const ctx = canvas.getContext('2d')
    ctx.clearRect(0, 0, canvas.width, canvas.height)
    pen.close()
    textZone.innerHTML = ''
    out.innerHTML = ''
}, false)

// 読み取るボタンが押されたとき、canvasの文字を認識する
const recognizeButton = document.getElementById('recognize_button')
recognizeButton.addEventListener('click', () => {
    out.innerHTML = '認識中...'
    let lang = 'eng', whitelist = '';
    modes.forEach(mode => {
        if(mode.checked) {
            if(mode.value == 'english') {
                lang = 'eng'
                whitelist = ''
            }
            if(mode.value == 'japanese') {
                lang = 'jpn'
                whitelist = ''
            }
            if(mode.value == 'number') {
                lang = 'eng'
                whitelist = '0123456789'
            }
            if(mode.value == 'alphabet') {
                lang = 'eng'
                whitelist = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
            }
            if(mode.value == 'hiragana') {
                lang = 'jpn'
                whitelist = 'ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてでとどなにぬねのはばぱひびぴふぶぷへべぺほぼぽまみむめもゃやゅゆょよらりるれろゎわゐゑをんゔゕゖ'
            }
            if(mode.value == 'katakana') {
                lang = 'jpn'
                whitelist = 'ァアィイゥウェエォオカガキギクグケゲコゴサザシジスズセゼソゾタダチヂッツヅテデトドナニヌネノハバパヒビピフブプヘベペホボポマミムメモャヤュユョヨラリルレロヮワヰヱヲンヴヵヶ'
            }
        }
    })
    recognize(lang, whitelist).then(text => {
        out.innerHTML = '認識完了！'
        console.log(text)
        textZone.innerHTML = text
    })
}, false)

async function recognize(lang = 'eng', whitelist = '') {
    const worker = createWorker({
        langPath: '...',
        logger: m => console.log(m),
    });
    await worker.load();
    // 言語を設定
    await worker.loadLanguage(lang);
    await worker.initialize(lang);
    // 限定する文字を設定
    await worker.setParameters({
        tessedit_char_whitelist: whitelist
    });
    // 認識
    const { data: { text } } = await worker.recognize(canvas);
    return text;
}
```

</div></details>

- `paint.js`：`canvas`に文字を書きこむためのクラス`Pen`を用意

<details><summary>ソースコードを表示</summary><div>

```javascript:paint.js
// クラスを用意
class Pen {
    _isDrawing = false;
    get isDrawing() { return this._isDrawing }
    set isDrawing(value) { this._isDrawing = value }
    constructor(canvas) {
        this.ctx = canvas.getContext('2d')
    }
    // 書き始める
    begin(coord) {
        this.isDrawing = true;
        this.lastCoord = coord;
        this.ctx.strokeStyle = 'black';
        this.ctx.lineWidth = 4;
    }
    // 書き進める
    push(coord) {
        this.ctx.beginPath()
        this.ctx.moveTo(this.lastCoord.x, this.lastCoord.y)
        this.ctx.lineTo(coord.x, coord.y)
        this.ctx.stroke()
        this.ctx.closePath()
        this.lastCoord = coord;
    }
    // 書き終える
    close() {
        this.isDrawing = false;
    }
}
```

</div></details>


- `vector2.js`：二次元ベクトルのクラス`Vector2`を用意（今回はほとんど使われない）

<details><summary>ソースコードを表示</summary><div>

```javascript:vector2.js
// クラスを用意
class Vector2 {
    // x
    get x() { return this._x }
    set x(value) { this._x = value }
    // y
    get y() { return this._y }
    set y(value) { this._y = value }
    constructor(x = 0, y = 0) {
        this.x = x;
        this.y = y;
    }
    clone() { return new Vector2(this.x, this.y) }
    add(v) {
        const x = this.x + v.x;
        const y = this.y + v.y;
        return new Vector2(x, y);
    }
    sub(v) {
        const x = this.x - v.x;
        const y = this.y - v.y;
        return new Vector2(x, y);
    }
    times(num) {
        const x = this.x * num;
        const y = this.y * num;
        return new Vector2(x, y);
    }
    magnitude() {
        return Math.sqrt(this.x * this.x + this.y * this.y)
    }
    get normalized() { return this.times(1/this.magnitude()) }
    static zero  = new Vector2( 0,  0)
    static one   = new Vector2( 1,  1)
    static right = new Vector2( 1,  0)
    static left  = new Vector2(-1,  0)
    static down  = new Vector2( 0,  1)
    static up    = new Vector2( 0, -1)
}
```

</div></details>

# 結果
　例によって、あまり精度は高くないです。マウスで書いた字とはいえ、もうちょっと精度が欲しいところ。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/11e58c50-5622-b858-428a-5e25a16dc1e9.png)
これは、認識してほしい、、
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/12ddf781-39ab-ce6f-414c-07c334e260ba.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/62ab9909-3e99-be72-640d-43999200e5f9.png)
これはDではない。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/2565a772-f563-f432-0226-58417460dc88.png)
これは5らしいです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/0363cab1-92c7-bae5-da44-f98baf75ff4b.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/cca58c7d-a72e-5ae5-d635-2db37aa92081.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/d33861f7-f50a-954f-41bf-d850b49850d9.png)
さかなー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/03ac41cf-8e11-fb58-b42e-16a114222648.png)
おしい
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/0e632677-9cbf-3ce5-d430-c97e4b270157.png)
おしい、漢字はほとんどうまくいかないです。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2729479/f6781582-5e74-dafe-bbd2-605aa13e32c3.png)
Cy 錫 粟 ン な M ペ ど′′` ヵ 0 和 (

# まとめ
　あまり使えないかもしれませんが、いろいろ試してみるのは楽しいです。皆さんもやってみてください。