【JavaScript】ブラウザだけでカメラ撮影した文字を読み取る(OCR)

こんにちは❗フリーランス・エンジニアの 九保すこひ です。

さてさて、このところLaravelの新バージョンがリリースされたこともあってほぼLaravel記事ばかりでしたが、やはり開発者として「気になること」もたまにはやってみたいということで、今回は「ある驚く機能」をブラウザで実装してみたいと思います。

その機能とは・・・・・

OCR(画像から文字を読み取る)

機能です。

実は、OCRは次の記事で紹介をしたことがあります。

📝 無料でできる!PHPで画像からテキストを読み取る方法

しかし、これはPHPからコマンドを実行する方法なので、サーバーが必須でした。

しかし、この間すごいパッケージを発見してしまったんです。

その名も、「tesseract.js」です。

なんと、このパッケージは純粋にブラウザのJavaScriptだけでOCRを実現するというスグレモノなんです。

そこで❗

開発者として、どうしてもやってみたくなったので、今回は需要は度外視してtesseract.jsをご紹介します(笑)

ぜひ「プログラムの読み物」的な感じで読んでいただけると嬉しいです😊✨
(最後にソースコード一式をダウンロードできますよ)

「見つけたときは、まさか
ブラウザでOKとは思いませんでした👍」

実行環境: Google Chrome 85、Vue 3、Tailwind CSS

やりたいこと

今回実装したい内容は次のとおりです。

  • カメラの映像をリアルタイム表示
  • 好きなところでスナップショットをとる
  • スナップショットをOCRにかけてテキストを取得

なお、今回の目的は本に書かれているISBNコードの取得です。

↓↓↓こういうやつですね。

また、今回はLaravel 8.xのログイン機能で採用されたTailwind CSSと、こちらも新バージョンがリリースになったVue 3で実装していきます。

では実際にやっていきましょう。

HTML部分をつくる

今回のコードは比較的短いのでわかりやすいようにHTMLJavaScriptに分けてご紹介します。

<html>
<head>
    <!-- ③ viewportを追加してスマホでも見やすくする -->
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, shrink-to-fit=no">
    <!-- ① CDNでCSSを読み込み -->
    <link href="https://unpkg.com/tailwindcss@^1.0/dist/tailwind.min.css" rel="stylesheet">
</head>
<body>
    <div id="app">
        <div ref="canvas-container" style="width:100%;margin:0 auto;">
            <canvas ref="canvas"></canvas>
        </div>
        <div class="text-center pt-3">
            <!-- ② Status によって表示ブロックを切り替える -->
            <!-- play: カメラの映像をキャンバスにリアルタイムで表示 -->
            <div v-if="status=='play'">
                <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold px-8 py-3 rounded" @click="takeSnapshot">
                    スナップショットを取る
                </button>
            </div>
            <!-- pause: スナップショットを撮ったので一次停止 -->
            <div v-if="status=='pause'">
                <button class="bg-blue-500 hover:bg-blue-700 text-white font-bold px-8 py-3 mr-1 rounded" @click="runOcr">
                    ISBNを読み取る
                </button>
                <button class="bg-transparent hover:bg-blue-500 text-blue-700 font-semibold hover:text-white py-2 px-4 ml-1 border border-blue-500 hover:border-transparent rounded" @click="playVideo">
                    戻る
                </button>
            </div>
            <!-- pause: スナップショットをOCRにかけてテキストを取得 -->
            <div v-if="status=='reading'">
                読み取り中です...
            </div>
        </div>
    </div>
    <!-- ① CDNでJavaScriptを読み込み -->
    <script src="https://unpkg.com/vue@3.0.0/dist/vue.global.prod.js"></script>
    <script src='https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js'></script>
    <script>

        // ここにJavaScript

    </script>
</body>
</html>

では番号順にHTMLをご紹介します。

① CDNでJavaScriptを読み込み

今回は前述しましたとおり以下のフレームワーク、パッケージを読み込みます。

  • Tailwind CSS
  • Vue 3
  • tesseract.js

本来はnpmでインストールし、ビルドするのですが今回はわかりやすいようにすべてCDNを使いました。(tesseract.jsにもCDNがあるのでお手軽につかえますね👍)

※なお、現状(2020.9.30)、Laravel 8.xVue 3をインストールし「シングル・ファイル・コンポーネント」ビルドしようとすると「Vue packages version mismatch」というエラーが出ます。これはLaravel mix の version 6 で修正されるようです。

② Status によって表示ブロックを切り替える

ここは変数statusの変化で表示切り替えをする部分です。
変化する内容は以下のとおりです。

  • play: カメラの映像をリアルタイムで表示
  • pause: スナップショットをとった時。カメラは一時停止します
  • reading: 取得したスナップショットからテキストを読み取り中

③ viewportを追加してスマホでも見やすくする

カメラの解像度がいい方がOCRの精度もあがるので、スマホのためにviewportを追加しておきます。

※なくても問題ないですが、ボタンがちっちゃくなってしまって操作しにくいと思います。

JavaScript部分をつくる

続いてJavaScript部分です。

Vue.createApp({
    data() {
        return {
            video: null,
            canvas: null,
            context: null,
            dataUrl: '',
            status: 'none'
        }
    },
    methods: {
        // ① カメラとキャンバスの準備
        initialize() {

            this.status = 'initialize';

            navigator.mediaDevices.getUserMedia({
                video: {
                    facingMode: {
                        ideal: 'environment'
                    }
                }
            })
            .then(stream => {

                this.canvas = this.$refs.canvas;
                this.context = this.canvas.getContext('2d');

                this.video = document.createElement('video');
                this.video.addEventListener('loadedmetadata', () => { // メタデータが取得できるようになったら実行

                    const canvasContainer = this.$refs['canvas-container'];
                    this.canvas.width = canvasContainer.offsetWidth;
                    this.canvas.height = parseInt(this.canvas.width * this.video.videoHeight / this.video.videoWidth);
                    this.render();

                });
                // iOSのために以下3つの設定が必要
                this.video.setAttribute('autoplay', '');
                this.video.setAttribute('muted', '');
                this.video.setAttribute('playsinline', '');
                this.video.srcObject = stream;
                this.playVideo();

            })
            .catch(error => console.log(error));

        },
        render() {

            if(this.video.readyState === this.video.HAVE_ENOUGH_DATA) {

                this.context.drawImage(this.video, 0, 0, this.canvas.width, this.canvas.height);

            }

            requestAnimationFrame(this.render);

        },
        runOcr() { // ③ スナップショットからテキストを抽出

            this.status = 'reading';

            Tesseract.recognize(this.dataUrl, 'eng', {
                logger: log => {

                    console.log(log);

                }
            })
            .then(result => {

                alert(result.data.text);

            })
            .catch(error => console.log(error))
            .finally(() => {

                this.playVideo();

            });

        },
        playVideo() {

            this.video.play();
            this.status = 'play';

        },
        pauseVideo() {

            this.video.pause();
            this.status = 'pause';

        },
        takeSnapshot() { // ② スナップショットを取る(カメラは一時停止)

            // this.makeSound(); // 音を鳴らす
            this.pauseVideo();
            this.dataUrl = this.canvas.toDataURL();

        },
        makeSound() { // ④ おまけ:スナップショットをとるときに音をならす

            document.querySelectorAll('.notification-iframe').forEach(el => el.remove()); // 全ての通知用 iFrame を削除

            // soundタグは使わず iFrame で直接音声ファイルへアクセスする
            const iFrame = document.createElement('iframe');
            iFrame.setAttribute('src', '/audios/insight.ogg');
            iFrame.setAttribute('allow', 'autoplay');
            iFrame.style.display = 'none';
            iFrame.className = 'notification-iframe';
            document.body.appendChild(iFrame);

        }
    },
    mounted() {

        this.initialize();

    }
}).mount('#app');

今回はいつもと違い、新バージョンのVue 3を使っているので、今までのVueコードとは少し違っています。Vue 3での変更点は以下のURLを参考にしてください。

📝 Vue 3の新しい機能と変更点

ではコードの内容です。

① カメラとキャンバスの準備

まずnavigator.mediaDevices.getUserMedia()でPCやスマホについている「ウェブカメラ」を起動します。

ご注意: Google Chromeでは、ウェブカメラはHTTPS接続が必須になっています。

そして、起動モードは以下のようにしていますが、これはPCの場合、フロントカメラを使い、スマホの場合はバックカメラを使うようにするためです。

video: {
    facingMode: {
        ideal: 'environment'
    }
}

ただし、ここは環境によって変わると思います。もしうまく行かない場合は以下のコードに入れ替えてみてください。

video: true

カメラが起動できたら、次は映像を表示するcanvasの設定です。

Vue$refsを使って、<canvas> ... </canvas>のすぐ外側にある<div> ... </div>にアクセスし、その横幅とウェブカメラの縦横比から表示すべきサイズを割り出しています。

キャンバスのサイズ変更が終わったら、this.render()を呼び出し実際にカメラの映像を表示することになります。

なお、以下の3行はiPhone(Safari)で実行するために必要な設定です。

this.video.setAttribute('autoplay', '');
this.video.setAttribute('muted', '');
this.video.setAttribute('playsinline', '');

② スナップショットを取る

続いて、「スナップショットを取る」ボタンがクリックされたときに実行される部分です。

・・・とは言っても、ここではウェブカメラを止め、現在表示されている画像のdataURLを取得しているだけです。(音を鳴らす部分は後でご紹介します)

③ スナップショットからテキストを抽出

ここが今回の本題、「OCRで画像からテキストを抽出する部分」になります。

内容としては、以下の部分で途中経過をログ表示し、

logger: log => {

    console.log(log);

}

さらに、読み取りが完了したらthen()内でalert()を実行しています。

.then(result => {

    alert(result.data.text);

})

これでOCR機能は完了です😊✨

④ おまけ:スナップショットをとるときに音をならす

正直なところ、この部分は必須ではありませんが、やはりスナップショットをとったときに音が鳴った方がわかりやすいだろうなと思い、「おまけ」として追加しました。

※ただし、いきなり音がなると迷惑かと思ったので、初期状態では無効にしています。利用する場合は、takeSnapshot()内にあるthis.makeSound();のコメントアウトを解除してください。

中身としてはiFrameに直接音声ファイルへアクセスさせているのですが、なぜこの形にしているかというと、実は<audio> ... </audio>タグを使うと必ずクリックしないと音を鳴らせないという制約があるためです。

今回の場合はクリックを伴っているので問題はないですが、もし非同期で実装する場合はうまくいかなくなってしまいますので、あえてiFrameの形で実装しました。

ちなみに、今回使った通知音は以下のURLからダウンロードさせていただきました。(フリーですが、ライセンスはクリエイティブ・コモンズです)

📝 Notifications Sounds

お疲れ様でした😊✨

ダウンロードする

今回実際に開発したソースコードを以下からダウンロードできます。

ブラウザだけでカメラ撮影した文字を読み取る

※ただし、zipファイルに含まれている音声ファイルはhttps://notificationsounds.com/sound-effects/insight-578 からダウンロードしたもので、こちらのライセンスはクリエイティブ・コモンズです。ご注意ください。

テストしてみる

では、実際にスマホと使って本に書かれているISBNを読み取ってみましょう。

スナップショットをとって、OCRを実行すると・・・・・・

はい❗

うまくISBNが取得できました。(その他の部分は、微妙なところもありますが・・・)

ちなみに:精度について

ちなみに、やはり一番重要なのは「どれぐらいの精度で文字を読み取れるか」だと思いますので、以下のような画像を用意して実際に読み取ってみることにしました。

結果はこのとおりです。

10pxだと完全にダメで一切読み取りができません。
15pxになると、一部読み取ることはできていますが、あまり精度が高いとはいえません。

それでも20px以上になると正しく認識され、さらに変な文字が入ってくることもありませんでした。

また、どうやらバーコードが写り込んでしまうと精度が悪くなるようですので、もし実際に使う場合は、カメラ映像の中に横長の赤枠を表示し、その中の画像データだけをOCRにかける、などの工夫が必要になってくるかもしれません。

なお、サーバー版tesseractの精度は以下のURLからご覧ください。

📝 無料でできる!PHPで画像からテキストを読み取る方法

おわりに

ということで、今回はブラウザだけでOCRを実装してみました。

これまでは、サーバーへ画像を送信しないといけなかったことを考えると、とてもお手軽になったんじゃないでしょうか。

ただ、気になる部分はやはり「精度」と「読み取りの速度が遅い」という部分です。サーバー側のtesseractはそれほど、「うーん、遅いな・・・」と感じることはありませんでしたが、ブラウザ版は読み取る内容によってはある程度時間が必要になるため、「お手軽」と「実行速度」のどちらを優先させるか考える必要があると思います。

ともあれ、それほどコードも複雑ではありませんし、気軽にOCRを使えるのでぜひ皆さんも一度チャレンジしてみてくださいね。

ではでは〜❗

「趣味のピアノで
コード進行を勉強中です👍」

この記事が役立ちましたらシェアお願いします😊✨ by 九保すこひ
また、わかりにくい部分がありましたらお問い合わせからお気軽にご連絡ください。
(また、個人レッスンも承ってます👍)
このエントリーをはてなブックマークに追加       follow us in feedly