
九保すこひです(フリーランスのITコンサルタント、エンジニア)
さてさて、このところLaravel
の新バージョンがリリースされたこともあってほぼLaravel
記事ばかりでしたが、やはり開発者として「気になること」もたまにはやってみたいということで、今回は「ある驚く機能」をブラウザで実装してみたいと思います。
その機能とは・・・・・
OCR(画像から文字を読み取る)
機能です。
実は、OCR
は次の記事で紹介をしたことがあります。
しかし、これは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部分をつくる
今回のコードは比較的短いのでわかりやすいようにHTML
とJavaScript
に分けてご紹介します。
<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.x
でVue 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を参考にしてください。
ではコードの内容です。
① カメラとキャンバスの準備
まず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からダウンロードさせていただきました。(フリーですが、ライセンスはクリエイティブ・コモンズです)
お疲れ様でした
ダウンロードする
今回実際に開発したソースコードを以下からダウンロードできます。
ブラウザだけでカメラ撮影した文字を読み取る※ただし、zipファイル
に含まれている音声ファイルはhttps://notificationsounds.com/sound-effects/insight-578 からダウンロードしたもので、こちらのライセンスはクリエイティブ・コモンズです。ご注意ください。
テストしてみる
では、実際にスマホと使って本に書かれているISBNを読み取ってみましょう。
スナップショットをとって、OCR
を実行すると・・・・・・
はい
うまくISBN
が取得できました。(その他の部分は、微妙なところもありますが・・・)
ちなみに:精度について
ちなみに、やはり一番重要なのは「どれぐらいの精度で文字を読み取れるか」だと思いますので、以下のような画像を用意して実際に読み取ってみることにしました。
結果はこのとおりです。
10px
だと完全にダメで一切読み取りができません。
15px
になると、一部読み取ることはできていますが、あまり精度が高いとはいえません。
それでも20px
以上になると正しく認識され、さらに変な文字が入ってくることもありませんでした。
また、どうやらバーコードが写り込んでしまうと精度が悪くなるようですので、もし実際に使う場合は、カメラ映像の中に横長の赤枠を表示し、その中の画像データだけをOCR
にかける、などの工夫が必要になってくるかもしれません。
なお、サーバー版tesseract
の精度は以下のURLからご覧ください。
おわりに
ということで、今回はブラウザだけでOCR
を実装してみました。
これまでは、サーバーへ画像を送信しないといけなかったことを考えると、とてもお手軽になったんじゃないでしょうか。
ただ、気になる部分はやはり「精度」と「読み取りの速度が遅い」という部分です。サーバー側のtesseract
はそれほど、「うーん、遅いな・・・」と感じることはありませんでしたが、ブラウザ版は読み取る内容によってはある程度時間が必要になるため、「お手軽」と「実行速度」のどちらを優先させるか考える必要があると思います。
ともあれ、それほどコードも複雑ではありませんし、気軽にOCR
を使えるのでぜひ皆さんも一度チャレンジしてみてくださいね。
ではでは〜
「趣味のピアノで
コード進行を勉強中です」