1
1

More than 1 year has passed since last update.

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

Posted at

image.png
image.png
※HTMLのcanvas要素に手書き文字を書き込んで、Tesseract.jsに文字認識してもらっています。

はじめに

 先日、【Tesseract.js】画像から数字を読み取って数独を解くWebアプリを作ってみた という記事を投稿しました。いろいろ調べてみると、Tesseract.jsは手書き文字もある程度認識できるということだったので、どの程度使えるのか試してみました。
 今回も詳しい説明は省略します。

やったこと

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

ソースコード

  • index.html:ブラウザに表示させる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>

 ちなみに、ichimatsu_silver.pngフリー素材を使っています。(別に背景白でも問題ない)

  • main.js:メインとなる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;
}
  • paint.jscanvasに文字を書きこむためのクラスPenを用意
ソースコードを表示
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;
    }
}
  • vector2.js:二次元ベクトルのクラスVector2を用意(今回はほとんど使われない)
ソースコードを表示
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)
}

結果

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

image.png
これは、認識してほしい、、
image.png
image.png
これはDではない。
image.png
これは5らしいです。
image.png
image.png
image.png
さかなー
image.png
おしい
image.png
おしい、漢字はほとんどうまくいかないです。
image.png
Cy 錫 粟 ン な M ペ ど′′` ヵ 0 和 (

まとめ

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

1
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
1