Tesseract.js で OCR してみた

# はじめに

自分は HTML＋JavaScript で「蔵書台帳」アプリを作っています。書籍の ISBN をバーコード読取できるようにしています。
ところが ISBN がバーコード表示されていないことがあって、表記された ISBN を OCR できないかと思っていました。
改めて調べてみると、ブラウザ上の JavaScript コードで OCR できるんですね。

JavaScript で OCR できるライブラリは幾つもありましたが、これがよさそうでした。

[GitHub - naptha/tesseract.js: Pure Javascript OCR for more than 100 Languages](https://github.com/naptha/tesseract.js/ )

紹介している記事も多く、幾つか試してみました。

# Tesseract.js を使ってみた

* Tesseract 3.0.3

## インストールする

今回は CDN を使いました。

```html:
    <script src="https://unpkg.com/tesseract.js@v3.0.3/dist/tesseract.min.js"></script>
```

## Tesseract.js を使ってみた

Tesseract.js を使ってみます。

以下の記事を参考にしました。
[javascript製のOCRライブラリ「tesseract.js」を試してみた | cupOF Interests](https://co.bsnws.net/article/198)

画像を ```img``` タグで表示します。併せて読取の結果を表示する項目も用意します。

```html:app.html
    <img id="target" src="https://tesseract.projectnaptha.com/img/eng_bw.png">
    <p id="progress"></p>
    <p id="result"></p>
```

JavaScript のコードで Tesseract.js の機能を利用します。

```javascript:app.js
    window.onload = function(){
        var buf = document.querySelector('#target');
        Tesseract.recognize(
            buf,
            'eng',
            { 
                logger: function(m) {
                    document.querySelector('#progress').textContent = m.status;
                }
            }
        )
        .then(function(result){
            document.querySelector('#result').textContent = result.data.text;
        });
    };
```

```Tesseract.recognize()``` で読取します。
最初の引数に読取したい画像データを渡します。ここでは ```img``` タグのエレメント。
読取できた結果は ```.then()``` に処理を記述します。

# カメラ画像の特定の場所の文字を OCR したい

上記の例は画像ファイルの内容を読取しましたが、
カメラで撮影しながらリアルタイムで読取できないでしょうか。
また、カメラで撮影した画像の中の全ての文字を読取したいのではないので、
画像にガイドになる四角を表示して、そこに収まった文字だけ読取するようにできないでしょうか。

## JavaScript のコードでカメラ画像にブラウザ上に表示する

まず、JavaScript のコードでカメラ画像にブラウザ上に表示します。

以下の記事を参考にしました。
[【JavaScript】Webカメラの映像をブラウザに表示する方法 | アールエフェクト](    https://reffect.co.jp/html/javascript-webcamera)

カメラ画像を表示するエリアを用意します。```video``` タグを使います。

```app.html
    <video id="video"></video>
    <p id="result"></p>
```
```app.css
    #video {
        width: 100%;
        height: auto;
    }
```

JavaScript のコードでカメラ画像を表示します。

```app.js
    var video = document.querySelector('#video');
    navigator.mediaDevices.getUserMedia({
        video: {
            facingMode: 'environment'
        },
        audio: false
    })
    .then(function(stream){
        video.srcObject = stream;
        video.play();
    })
    .catch(function(e){
        document.querySelector('#result').textContent = JSON.stringify(e);
    });
```

```navigator.mediaDevices.getUserMedia()``` を使用します。

上記のコードは ```video``` タグにカメラ画像を表示していますが、その内容を ```canvas``` タグにコピーして表示するようにします。

以下の記事を参考にしました。
[Webカメラの映像をcanvasに表示させる - Qiita](https://qiita.com/chelcat3/items/02c77b55d080d770530a)

カメラ画像を表示するエリアを ```video``` タグから ```canvas``` タグに変えます。

```app.html
    <canvas id="canvas"></canvas>
    <p id="result"></p>
```
```app.css
    #canvas {
        width: 100%;
        height: auto;
    }
```

JavaScript のコードでカメラ画像を表示します。

```javascript:app.js
    var video = document.createElement('video');
    var canvas = document.querySelector('#canvas');
    navigator.mediaDevices.getUserMedia({
        video: {
            facingMode: 'environment'
        },
        audio: false
    })
    .then(function(stream){
        video.srcObject = stream;
        video.play();
        setInterval(function(){
            canvas.width = video.videoWidth;
            canvas.height = video.videoHeight;
            var ctx = canvas.getContext('2d');
            ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);
        }, 200);
    })
    .catch(function(e){
        document.querySelector('#result').textContent = JSON.stringify(e);
    });
```

```video``` タグは JavaScript コードで生成します。
```canvas``` の表示するサイズは CSS で指定しますが、画像データのサイズはコードで ```video``` のサイズと同じに指定します。
```setInterval()``` を使って 0.2 秒ごとに ```video``` の内容を ```canvas``` にコピーします。

## カメラ画像の特定の場所に四角を描く

カメラ画像を ```canvas``` に表示するようにしたので、ここに四角を描きます。
画面の中央に高さ 100 ピクセルの四角を描くことにします。

```javascript:app.js
        (前略)
        ctx.drawImage(video, 0, 0, video.videoWidth, video.videoHeight, 0, 0, canvas.width, canvas.height);

        var box = {
            x: 50,
            h: 100
        };
        box.y = (canvas.height - box.h) / 2;
        box.w = (canvas.width - box.x * 2);

        ctx.beginPath();
        ctx.strokeStyle = 'red';
        ctx.lineWidth = 2;
        ctx.rect(
            box.x, box.y, box.w, box.h
        );
        ctx.stroke();
```

さらに、上記の四角の場所の内容を別の ```canvas``` にコピーします。

```javascript:app.js
    (前略)
    var canvas = document.querySelector('#canvas');
    var buf = document.createElement('canvas');
    document.body.append(buf);
```
```javascript:
        (前略)
        ctx.stroke();

        buf.width = box.w;
        buf.height = box.h;
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);
```
## カメラ画像の特定の場所の文字だけ読取する

上記のコードで切取できた画像データを Tesseract.js に渡して OCR します。

以下の記事を参考にしました。
[【JavaScript】ブラウザだけでカメラ撮影した文字を読み取る（OCR） – console dot log]
(https://blog.capilano-fw.com/?p=8085)

```javascript:app.js
        (前略)
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);

        Tesseract.recognize(
            buf,
            'eng',
            { 
                logger: function(e) {
                    document.querySelector('#progress').textContent = e.status;
                }
            }
        )
        .then(function(result){
            document.querySelector('#result').textContent = result.data.text;
        });
```

このコードでは、```Tesseract.recognize()``` が 0.2 秒ごとに呼出されます。
ところが ```recognize()``` して読取できて ```.then()``` が呼出されるまでに、0.2 秒以上掛かります。
ある ```recognize()``` の呼出が完了する前に次の ```recognize()``` が呼出されるので、まずいですね。
フラグ ```isRecognizing``` を用意して、```recognize()``` が完了するまでは新たに ```recognize()``` を呼出しないようにします。

```javascript:app.js
    var isRecognizing = false;

    setInterval(function(){
        (中略)
        buf.getContext('2d').drawImage(canvas, box.x, box.y, box.w, box.h, 0, 0, box.w, box.h);

        if (isRecognizing) return;
        isRecognizing = true;

        Tesseract.recognize(
            buf,
            'eng',
            { 
                logger: function(e) {
                    document.querySelector('#progress').textContent = e.status;
                }
            }
        )
        .then(function(result){
            document.querySelector('#result').textContent = result.data.text;

            isRecognizing = false;
        });
    }, 200);
```