Friday, 29 October, 2021 UTC


Summary

背景
「OCR」という技術を耳にしたことがありますか?OCRは、Optical Character Recognition(光学文字認識)の略です。手書きや印刷された文字を光学的な手段でデータとして取り込み、文字認識することによってコンピュータープログラムなどで使用できるように変換する技術です。OCRは様々な分野で利用されています。使用例としては、車のナンバープレートを認識し盗難車を検知したり、書籍をデジタル化したりという例があります。Tesseract.jsは100以上の言語に対応するオープンソースのOCRライブラリです。Tesseract.jsは、C言語で開発されたTesseractOCRエンジンをJavaScriptのWebAssemblyにコンパイルしています。Tesseract.jsを使うと、ブラウザでOCRを簡単に利用できます。
Tesseract.jsの精度は完全ではありません。誤認識する可能性がありますので、これまで手作業で行っていた作業をTesseract.jsで自動化し効率を上げるためなど、補助的な用途での利用を推奨します。
本稿では、Tesseract.jsとReactを使用し、ブラウザ上で画像のOCR処理を行い、読み取ったテキストをSMSとして送信するアプリの作成方法をご紹介します。
本稿は前編と後編に分かれており、前編ではプロジェクトのセットアップからフロントエンドの構築までを、後編ではバックエンド側の構築と動作検証についてご紹介いたします。

後編はこちら:Tesseract.jsとReactでOCRコミュニケーションアプリを作る(後編)
目標
このチュートリアルを最後まで進めると、Tesseract.jsの基礎を学べるとともに、以下のようなReactを使ったOCRコミュニケーションアプリを作成できます。
アプリケーションの動作フローは以下を想定しています。
  1. 画像をアップロードする。
  2. Tesseract.jsで画像をOCR処理する。
  3. 必要に応じて読み取ったテキストを編集する。
  4. テキストを指定した電話番号にSMSとして送信する。
想定される技術知識
本稿では以下の知識を想定しています。
  • JavaScriptの基本知識
  • Node.jsの基本知識
  • Reactの基本知識
必要なツール
  • 安定バージョンのNode.jsとnpm
  • Twilioアカウント。アカウント作成方法はHelp Centerの「Twilioアカウントの作成方法」を参照してください。
  • Twilioの電話番号
アプリケーションの構造
作成するアプリケーションではフロントエンドとバックエンドを準備します。フロントエンド側では画像のアップロードボタン、OCR処理ボタン、テキストのエディタやSMSの送信ボタンなどを表示させます。バックエンド側はNode.jsとExpressを使ってSMSの送信処理を行います。
アプリケーションの具体的な構造は以下のとおりです。
フロントエンド:
  • App: プロジェクトの実行エントリーポイントとなるルートコンポーネント。
  • OcrReader: OCR処理する画像のアップロードとOCR処理ボタンを構成するコンポーネント。
  • SmsSender: OCR処理し読みとったテキストのエディタと、テキストのSMS送信ボタンを構成するコンポーネント。
バックエンド:
  • server.js: Node.jsとExpressでSMS送信処理を行うサーバーファイル。
大まかなアプリケーションの構造が理解できたところでプロジェクトの作成に進みましょう。
基本設定とReactの準備

create-react-appでReactアプリケーションを作成する

まずは、Reactアプリケーションを作成します。
ターミナルを開いて、以下のコマンドを実行してください。
npx create-react-app ocr-sms-sender cd ocr-sms-sender npm start 
このコマンドで、Reactアプリケーションの作成、ディレクトリへの移動、アプリケーションの起動を行います。
ブラウザでlocalhost:3000にアクセスします。問題なくアプリケーションが起動すると、以下のような画面が表示されます。
この時点で、一度ターミナルのプロセスを終了させてください。

依存パッケージをインストールする

次に、アプリケーションに必要な依存パッケージをインストールします。
ターミナルで以下のコマンドを実行してください。
npm install --save tesseract.js twilio express dotenv intl-tel-input 
インストールした依存パッケージの詳細は以下のとおりです。
  • tesseract.js: ブラウザで機能するJavaScriptOCRライブラリ。
  • twilio: Twilio Node ヘルパーライブラリ。Twilio APIに対するHTTPリクエストを、Node.jsを使って書けるようにするためのパッケージ。
  • express: Node.jsで使うウェブサーバーフレームワーク。本稿ではSMSの送信に使います。
  • dotenv: .envファイルに定義された値を環境変数として取り込むためのパッケージ。
  • intl-tel-input: International Telephone Input。国際電話の番号を入力して検証するためのJavaScriptプラグイン。
インストールが完了したら、次にフロントエンドを構築します。
フロントエンドを構築する
まずは、フロントエンドで構築するコンポーネントファイルを作成します。ターミナルで、/srcディレクトリの配下に、/componentsフォルダを作成してください。
/componentsフォルダの配下に、OcrReader.jsと、SmsSender.jsファイルを作成してください。

App.jsコンポーネントを構築する

ルートコンポーネントのApp.jsを構築します。create-react-appを実行した際に自動作成された/src 配下にある App.jsファイルを編集します。テキストエディタでApp.jsファイルを開いてください。
ファイルの内容を、以下のコードに変更してください。
import { useState } from "react" import OcrReader from "./components/OcrReader" import SmsSender from "./components/SmsSender" function App() { const [ocrData, setOcrData] = useState("") // 子コンポーネントからOCRデータをPropsとして受け取る const onReadOcrData = (ocrData) => { setOcrData(ocrData) } // 子コンポーネントで別の画像を使用するボタンが押されたことをPropsで検知する const onRemoveClicked = () => { setOcrData("") } return ( <div className="App"> <header>OCRアプリへようこそ!</header> <OcrReader onReadOcrData={onReadOcrData} onRemoveClicked={onRemoveClicked} /> {ocrData && <SmsSender readText={ocrData}/>} </div> ) } export default App 
ファイルを保存してください。
このコードでは、画像のOCR処理を担うOcrReaderコンポーネントと、OCR処理で読み取ったテキストの編集とSMS送信を担うSmsSenderコンポーネントをインポートしています。
App.jsでは、子コンポーネントのOcrReaderで読み取ったテキストをocrDataとしてpropsオブジェクトで兄弟コンポーネントのSmsSenderに渡します。
onReadOcrData関数で、ocrDataを受け取ります。<SmsSender>のJSXの属性で、ocrDatareadTextとしてpropsで渡します。
onRemoveClicked関数で、OcrReaderコンポーネントで「別の画像を使用する」ボタンがクリックされた際に、<SmsSender>に渡すテキストのデータも初期化します。

OCR処理コンポーネントを構築する

次に、OCR処理する画像の選択機能、画像の表示、OCR処理ボタンを担う関数コンポーネントのOcrReaderを構築します。OcrReader.jsを開いてください。
ファイルに以下のコードをペーストしてください。
import { useState } from "react" import { createWorker } from "tesseract.js" // 画像のOCR処理ステータス const STATUSES = { IDLE: "", FAILED: "OCR処理に失敗しました。", PENDING: "OCR処理中...", SUCCEEDED: "OCR処理完了", } export default OcrReader 
このコードでは、Tesseract.jsのcreateWorker関数をインポートをしています。
STATUSESで、Tesseract.jsの画像のOCR処理ステータスをオブジェクトとして定義します。export default OcrReaderで、コンポーネントを親コンポーネントのApp.jsにエキスポートします。
次に、コンポーネントのメインの関数、OcrReaderを定義します。STATUSESのブロックと、export default OcrReaderの間に、以下のコードをペーストしてください。
function OcrReader({onReadOcrData, onRemoveClicked}) { const [selectedImage, setSelectedImage] = useState(null) const [ocrState, setOcrState] = useState(STATUSES.IDLE) const worker = createWorker() // 画像のOCR処理 const readImageText = async() => { setOcrState(STATUSES.PENDING) try { await worker.load() // OCRで読み取りたい言語を設定 await worker.loadLanguage("jpn") await worker.initialize("jpn") const { data: { text } } = await worker.recognize(selectedImage) await worker.terminate() // 日本語テキストはスペースが入ってしまう可能性があるので、スペースを削除 const strippedText = text.replace(/\s+/g, "") onReadOcrData(strippedText) setOcrState(STATUSES.SUCCEEDED) } catch (err) { setOcrState(STATUSES.FAILED) } } } 
上記のコードを詳しく解説します。
OcrReader関数のパラメーターで、onReadOcrDataonRemoveClickedをpropsとして親コンポーネントに渡します。OCR処理する画像が選択されているかに関するステート(selectedImage)と、OCR処理の実行状況に関するステート(ocrState)を、useStateフックで定義します。 Tesseract.jsのworkerを変数として定義し、インスタンス化します。
Tesseract.jsでの画像のOCR処理をreadImageText非同期関数で定義します。
関数が呼び出されてすぐに、OCR処理ステータスをPENDINGに設定します。このステータスはTesseract.jsの処理ステータスが変わるたびに更新します。
workerインスタンスにはいくつかのメソッドが存在します。まずは、loadメソッドを呼び出します。
OCR処理で読み取りたい言語をloadLanguageメソッドで指定します。本稿では日本語を表すjpnを使用します。
OCR処理を初期化するための、initializeメソッドを呼び出します。パラーメーターで読み取る言語(jpn)を指定します。
OCR処理の準備ができたので、処理を実際に開始するためのrecognizeメソッドを呼び出します。パラメーターで読み取る画像を指定します。
最後に、OCR処理完了のタイミングでOCR処理の終了、クリーンアップを行うterminateメソッドを呼び出します。
Tesseract.jsでは、日本語を読み取る言語として指定すると、文字と文字の間に半角スペースが入ってしまうことがあります。これを防ぐために、text.replace(/+/g, ““)でスペースを取り除きます。
次に、readImageText関数のブロックの下に、以下のコードをペーストしてください。
// 別の画像を使用するボタンを押した時の処理 const handleRemoveClicked = () => { setSelectedImage(null) onRemoveClicked() setOcrState(STATUSES.IDLE) } 
このコードでは、「別の画像を使用する」ボタンがクリックされた際に、 setSelectedImageで選択された画像のステート、selectedImagenullに更新します。onRemoveClickedで親コンポーネントにステートを渡します。
最後に、コンポーネントのJSXを追加します。handleRemoveClicked関数のブロックの下に、以下のコードをペーストしてください。
return ( <div> {selectedImage && ( <div> <img src={URL.createObjectURL(selectedImage)} alt="scanned file" /> </div> )} <div> {selectedImage? <div className="button-container"> <button onClick={readImageText}>画像をOCR処理する</button> <button className="remove-button" disabled={ocrState === STATUSES.PENDING} onClick={handleRemoveClicked} > 別の画像を使用する </button> </div> : <> <p>画像ファイルをアップロードしてください。</p> <input type="file" name="ocr-image" onChange={(event) => { setSelectedImage(event.target.files[0]) }} /> <p>対応フォーマット:bmp、jpg、png、pbm</p> </> } </div> <div className="status"> {ocrState} </div> <br /> </div> ) 
ファイルを保存してください。
これで、OcrReaderコンポーネントが完成しました。OcrReaderコンポーネントの全コードは、Githubリポジトリを参照してください。

SMS送信コンポーネントを構築する

次に、SMSを送信する関数コンポーネントのSmsSenderを構築します。SmsSender.jsを開いてください。
ファイルに以下のコードをペーストしてください。
import { useEffect, useState, useRef } from "react" import "intl-tel-input/build/css/intlTelInput.css" import intlTelInput from "intl-tel-input" // SMS送信ステータス const STATUSES = { IDLE: "", FAILED: "メッセージ送信に失敗しました。", PENDING: "メッセージ送信中...", SUCCEEDED: "メッセージ送信完了", } export default SmsSender 
このコードでは、intl-tel-inputをインポートしています。また、STATUSESで、SMSの送信ステータスをオブジェクトとして定義します。
次に、コンポーネントのメインの関数、SmsSenderを定義します。STATUSESのブロックと、export default SmsSenderの間に、以下のコードをペーストしてください。
function SmsSender ({readText}) { const [smsText, setSmsText] = useState(readText) const [iti, setIti] = useState(null) const [smsSendingStatus, setSmsSendingStatus] = useState(STATUSES.IDLE) const inputRef = useRef(null) // International Telephone Inputを初期化 const init = () => intlTelInput(inputRef.current, { initialCountry: "jp" }) // レンダー後にInternational Telephone Inputを初期化 useEffect(() => { setIti(init()) }, []) // SMS送信リクエスト const sendSMS = async () => { setSmsSendingStatus(STATUSES.PENDING) const country = iti.getSelectedCountryData() const num = `+${country.dialCode}${iti.telInput.value}` await fetch("/send-sms", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ to: num, text: smsText }), }).then((response) => { // Check successful request status if (response.status === 200) { setSmsSendingStatus(STATUSES.SUCCEEDED) } else { setSmsSendingStatus(STATUSES.FAILED) } }).catch(() => { // Catch network errors setSmsSendingStatus(STATUSES.FAILED) }) } } 
SmsSender関数のパラメーターで、OcrReaderコンポーネントから渡されたreadTextをpropsとして親コンポーネントを通して受け取ります。送信するSMSのテキストのステート(smsText)、SMSを送信する電話番号のステート(iti)、SMSの送信処理のステート(smsSendingStatus)をuseStateフックで定義します。
inputRefを定義し、useRefでinput要素でユーザーが入力する電話番号にアクセスします。
init関数で、intl-tel-inputを初期化し、電話番号の入力ができるよう設定します。
useEffectフックで、レンダーの結果が画面に反映された後にintl-tel-inputを初期化するように設定します。
sendSMS関数で、後ほど作成するsend-smsエンドポイントに対してSMS送信リクエストを送信します。
fetchでHTTP POSTリクエストを送信します。ボディにOCR処理で読み取ったテキストを指定します。エンドポイントからのレスポンスをもとにSTATUSでSMS送信ステータスを更新します。
次に、ユーザーが「SMSメッセージを送信」ボタンの動作を定義するhandleSubmit関数を定義します。sendSMS関数のブロックの下に以下のコードをペーストしてください。
// 送信ボタンが押されたタイミングでSMS送信する const handleSubmit = e => { e.preventDefault() e.stopPropagation() sendSMS() } 
このコードでは、「SMSメッセージを送信」ボタンがクリックされたタイミングでsendSMS関数を呼び出します。
HTMLページ内でクリックイベントが発生し、処理が終了すると、画面遷移が起こります。これをpreventDefault()で防ぎます。
また、クリックイベントが発生するとイベントが親要素へと伝播していきます。stopPropagation()でこれ以上の伝播しないようにイベントの伝播を停止します。
最後に、コンポーネントのJSXを追加します。handleSubmit関数のブロックの下に、以下のコードをペーストしてください。
return ( <div> <form onSubmit={(e) => handleSubmit(e)}> <div>検知されたテキストを編集:</div> <div> <textarea rows="15" cols="45" name="name" defaultValue={readText} onChange={e => setSmsText(e.target.value)} /> </div> <input ref={inputRef} id="phone" name="phone" type="tel" /> <div> <button disabled={smsSendingStatus == "Sending Message..."} type="submit">SMSメッセージを送信</button> </div> </form> <div className="status"> {smsSendingStatus} </div> </div> ) 
ファイルを保存してください。
これで、SmsSenderコンポーネントが完成しました。SmsSenderコンポーネントの全コードは、Githubリポジトリを参照してください。

CSSを追加する

次に、アプリケーションのCSSを定義します。
テキストエディタで/srcの配下にあるindex.cssを開いてください。ファイルの内容を以下のコードに変更してください。
html * { font-family: 'Noto Sans Japanese', sans-serif; } .App { text-align: center; } header { color: #2F7AE5; font-size: 30px; } img { width: 280px; } textarea { border: 1px solid #ccc; } button { color: #fff; background: #2F7AE5; padding: 12px; border-radius: 5px; border: none; margin: 3px; cursor: pointer; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; -webkit-transition: all .3s; transition: all .3s; } button:hover { background-color: #1c4b8d; } input[type=text], input[type=tel] { padding: 12px 20px; margin: 8px 0; border: 1px solid #ccc; border-radius: 4px; box-sizing: border-box; width: 300px; } input[type=text] { height: 400px; } .button-container { display: flex; flex-direction: column; justify-content: center; align-items: center; } .remove-button { background: #7E7E7E; } .remove-button:hover { background: #414141; } .status { color: #2F7AE5; } /* International Telephone InputのCSS */ .iti__flag {background-image: url("/node_modules/intl-tel-input/build/img/flags.png");} @media (-webkit-min-device-pixel-ratio: 2), (min-resolution: 192dpi) { .iti__flag {background-image: url("/node_modules/intl-tel-input/build/img/[email protected]");} } 
ファイルを保存してください。
これで、フロントエンド側の準備ができました!
次のステップ
前編ではプロジェクトのセットアップからフロントエンド側の構築に関してご紹介しました。後編ではバックエンド側の構築方法と、アプリケーションの実装についてご説明いたします。
後半はこちら:Tesseract.jsとReactでOCRコミュニケーションアプリを作る(後編)
Twilio Blogに投稿してみたい方や、フィードバック、登壇、勉強会のお誘いなどお気軽にsnakajima[at]twilio.comまでご連絡ください。開発中のプロジェクトに関してはGithub(smwilk)を覗いてみて下さい。