Monacaを使って何かアプリを作ってみたいと思っても、いざとなるとアイデアが出てこないかもしれません。また、最初から大きなアプリを作ろうと思うと、何から手を付けて良いのか分からないことでしょう。

そこで、この記事では手順を踏んで簡単なアプリを開発してみます。最初の一歩として、ぜひチャレンジしてみてください。

今回はTesseract.jsとNCMB(ニフクラmobile backend)を使って、多言語対応のOCRアプリを開発します。撮影した写真からテキスト情報を抜き出し、それを履歴として保存できます。

前回は画面に仕様と認証まで説明しました。

今回はOCR処理と履歴の表示を実装します。

■ Tesseract.jsを使って多言語対応のOCRアプリを作ろう【その1:画面の説明と認証まで】
https://press.monaca.io/atsushi/8004

プロジェクトのソースコードは、GitHubに配置しています。
https://github.com/monaca-samples/ocr

OCR画面の初期化

OCR画面では、DOM表示が完了したタイミングで前回説明した通り、認証のチェックを行います。

この処理は、ファイル「camera.html」にて行っています。

export default async function (props, {$f7, $f7router, $on, $store }) {
  // DOMが初期化完了した際に呼ばれるイベント
  $on("pageBeforeIn", async (e, page) => {
    // 認証していなければ、ログイン画面に移動
    if (!(await checkAuth())) {
      return $f7router.navigate({name: "Login"});
    }
  });
}

画面について (camera.html)

HTMLは次のような構造になっています。

また、用意しているアクションは次の3つです。

  • selectPhoto
    画像を選択した際に呼ばれる関数
  • startCrop
    画像のトリミングを開始する関数
  • setLang
    解析を行う言語を切り替える関数
  • startOcr
    OCR処理を開始する関数
  • save
    画像と解析結果のテキストを保存する関数

OCR対象の写真を選択 ( selectPhoto( ) )

写真が選択されるとアプリ画面のプレビューエリアに写真が表示されます。

// 画像が選択された際の処理
const selectPhoto = async (e) => {
  // この中に処理を記述します
}

まず、保存ボタンを一時的に無効にします。

// 保存ボタンを無効にする
$f7.$el.find('.save').css({'pointer-events': 'none'});

また、すでにテキストエリアに解析結果が入っている場合がありますので、それを消します。

// 結果を表示するテキストエリア
const ele = $f7.$el.find('[name="text"]');
ele.val(''); // 一旦内容を消す

続いて、画像を取得して、プレビュー表示のために画像タグにファイルを入れます。

// 指定されたファイルを読み込む
const file = e.target.files[0];
const src = await photoReader(file);

// プレビューに適用
$f7.$el.find('.preview').attr('src', src);

画像のトリミング( startCrop( ) )

画像のトリミングは、ライブラリ「Cropper.js」を利用します。

■ Cropper.js
https://fengyuanchen.github.io/cropperjs/

Cropper.jsのオブジェクト作成時、第一引数にプレビューで表示している画像を設定します。
この設定で、トリミング機能がアプリ内に表示されます。

    const startCrop = (e) => {
      e.preventDefault();

      const image = document.getElementById('img-preview');
      window.cropper = new Cropper(image, {
        dragMode: 'move',
        crop(event) {
          window.croppedValue = event.detail;
        }
      });
    }

解析言語の切り替え( setLang( ) )

setLang 関数では、日本語と英語の切り替えを行います。これは後で解析処理時に使われる情報です。

// 言語を切り替える
const setLang = (e) => {
  // ボタンからアクティブ表示を外す
  $f7.$el.find('.lang').removeClass('button-active');
  // 選択されたボタンに button-active クラスを追加する
  for (const dom of $f7.$el.find('.lang')) {
    if ($(dom).hasClass(e)) {
      $(dom).addClass('button-active');
    }
  }
}

OCR処理の実行 ( startOcr() )

プレビューエリアに設定したファイルを対象にOCR処理を行います。

OCR解析中にはログが送られてきます。

そこに書かれているパーセンテージをプログレスの値としてセットしていきます。

const file = $f7.$el.find('.preview').attr('src');
const progress = $f7.dialog.progress('Processing', 0);

// OCR処理の実行
const res = await Tesseract.recognize(file, lang, {
  logger: m => {
    if (m.status !== 'recognizing text') return;
    progress.setProgress(m.progress * 100) // OCR進捗を更新
  }
});

progress.close()

OCR結果をテキストエリアに表示

解析結果のテキストをテキストエリアに表示します。

さらにスタイルを使って、高さを調整します。

// 結果のテキスト
const text = res.data.text;
// テキストエリアに反映して、サイズを調整する
ele
  .val(res.data.text)
  .css('height', ${text.split(/\n/).length * 26}px);

ニフクラ mobile backendへの保存処理 ( save( ) )

OCR結果のテキストを保存

保存ボタンを押した際のイベントで、ニフクラ mobile backendのデータストアに保存を行います。

const save = async (e) => {
  // この中に実装します
}

データストアのクラスと、そのインスタンスを作成します。

これは通常のDBでいうテーブルおよび行に相当するデータです。

// OCRクラス(DBでいうテーブル相当)を用意
const OCR = ncmb.DataStore('OCR');

// OCRクラスのインスタンス(DBでいう行相当)を用意
const o = new OCR;

解析結果のテキストを設定します。

// 解析結果のテキストをセット
const text = $f7.$el.find('[name="text"]').val();
o.set('text', text);

ACL(アクセスコントロール)を準備します。

今回はログインユーザのみ、読み書き可能とします。

// ACL(アクセスコントロール)を用意
const acl = new ncmb.Acl;
acl
  .setUserReadAccess(user, true)   // 作成者のみ読み込み可能
  .setUserWriteAccess(user, true); // 作成者のみ編集可能

OCR対象の画像を保存

ニフクラ mobile backendのファイルストア(ファイルストレージ)に写真をアップロードします。

写真は、画像タグにdataURI形式で設定されています。

アップロード前に、dataURIからBlobにして、アップロードします。

アップロード自体は ncmb.File.upload の1行です。

また、ファイル名はなるべくユニークもものになるようにしておきます。

アップロードがうまくいったら、その際に指定したファイル名をOCRオブジェクトに設定します。

const src = $f7.$el.find('.preview').attr('src');
if (src.indexOf('data:') > -1) { // dataURIであれば続行
  // ファイルあり
  const blob = await (await fetch(src)).blob();
  // ファイル名は重複しないように生成
  const fileName = ${user.objectId}-${(new Date).getTime()}.${blob.type.split('/')[1]};
  // アップロード
  await ncmb.File.upload(fileName, blob, acl);
  // ファイル名を設定
  o.set('fileName', fileName)
}

後は save メソッドでデータストアに保存。

// ACLを設定して保存
await o
  .set("acl", acl)
  .save();

保存完了の通知

保存したらFramework7のトースト機能を使って、保存完了を通知します。

// トーストで通知
$f7.toast.create({
  text: "保存しました",
  position: "top",
  closeTimeout: 2000,
}).open();

履歴を一覧表示する

履歴の一覧表示は www/pages/list.html にて行います。

この画面のHTMLはとてもシンプルです。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="title">OCR履歴</div>
      </div>
    </div>
    <div class="page-content">
      <div class="list media-list">
        <ul id="histories">
        </ul>
      </div>
    </div>
  </div>
</template>
<style>
  .photo {
    object-fit: cover;
    width: 50px;
    height: 50px;
  }
</style>

タブ表示時のイベント

タブを表示された際のイベントで履歴データの取得、および表示を行います。

export default async function (props, {$f7, $f7router, $on, $store }) {
  // タブが表示された際に実行されるイベント
  $on("page:tabshow", async function(e, page) {
    // これまでのOCR履歴を取得
    const ary = await getAllOCR();
    // 履歴を表示
    showOCRs(ary);
  });
}

OCR履歴の取得処理

OCR履歴を取得する getAllOCR 関数は次のようになります。

ニフクラ mobile backendのデータストアからデータを取得します。

今回は絞り込み条件はなく、作成日の降順(新しいものが上)で20件取得しています。

// OCR履歴を取得する関数
const getAllOCR = () => {
  // OCRクラス(DBでいうテーブル相当)を用意
  const OCR = ncmb.DataStore("OCR");
  // OCRクラスのデータを検索
  return OCR
    .order("createDate") // 作成日の降順
    .limit(20)           // 20件
    .fetchAll();
}

履歴リストの表示処理

そして取り出したデータを showOCR 関数で表示します。

const showOCRs = (ary) => {
  // この中に記述します
}

まずHTMLを作成して、#histories の中にリスト表示します。

$f7.$el.find('#histories').html(ary.map(ocr => `
  <li>
    <a href="#" class="item-link item-content" data-object-id="${ocr.objectId}">
      <div class="item-media">
        <img src="assets/images/photo.png" class="photo" />
      </div>
      <div class="item-inner">
        <div class="item-title-row">
          <div class="item-title"></div>
          <div class="item-after">${ocr.text.length}文字</div>
        </div>
        <div class="item-text">${ocr.text}</div>
      </div>
    </a>
  </li>
`)
.join(''));

次にデータそれぞれについて、元画像を取得して表示します。

元画像は ncmb.File.download でダウンロードしていきます。

// 履歴ごとに写真を読み込んで、リストに適用する
for (const ocr of ary) {
  // 写真をダウンロード。awaitを使うと同期処理となり遅くなるため、非同期処理のPromiseで実行
  ncmb.File.download(ocr.fileName, 'blob')
    .then(async res => {
      // BlobをBase64に変換
      const src = await photoReader(res);
      // 画像を差し替える
      $f7.$el.find([data-object-id="${ocr.objectId}"] img.photo).attr('src', src);
    });
}

履歴リストをタップしたときの処理

最後にリストのタップイベントを設定します。

これはタップされたOCRデータの特定と、その画像データを表示画面( show.html )へ送る処理になります。

// 一覧表示したリストに対してタップイベントを追加
$f7.$el.find('#histories a').on('click', e => {
  // タップしたデータのキー(objectId)を取得
  const objectId = $(e.target).parents('a').data('object-id');
  // 画像も取得
  const img = $(e.target).parents('a').find('img.photo').attr('src');
  // 対象になる履歴データを取得
  const ocr = ary.filter(m => m.objectId === objectId)[0];
  // 表示画面に遷移
  $f7router.navigate(/${objectId}, {
    props: { ocr, img }
  });
});

OCR履歴の詳細表示

OCR詳細履歴の画面は、ファイル「show.html」にて、前の画面から受け取ったOCRオブジェクトを表示します。

HTMLは、表示処理のみです。

HTML表示のため、改行はbrタグに置き換えています。

<template>
  <div class="page">
    <div class="navbar">
      <div class="navbar-bg"></div>
      <div class="navbar-inner sliding">
        <div class="left">
          <a href="#" @click=${() => $f7router.back()} class="link">
            <i class="f7-icons">chevron_left</i> 戻る
          </a>
        </div>
        <div class="title">メモ</div>
      </div>
    </div>
    <div class="page-content">
      <div class="block">
        <div class="row">
          <div class="col-100">
            <div class="list">
              <ul>
                <li class="item-content item-input">
                  <div class="item-inner">
                    <div class="item-title item-label">元画像</div>
                    <div class="item-input-wrap">
                      <img src="${img}" class="preview" />
                    </div>
                  </div>
                </li>
                <li class="item-content item-input">
                  <div class="item-inner">
                    <div class="item-title item-label">解析結果</div>
                    <div class="item-input-wrap" innerHTML=${ocr.text.replace(/\r\n|\r|\n/g, '<br />')}>
                    </div>
                  </div>
                </li>
              </ul>
            </div>
          </div>
        </div>
      </div>      
    </div>
  </div>
</template>
<style>
  .preview {
    width: 100%;
    height: 200px;
    object-fit: cover;
  }
</style>

初期化時の処理

画面が表示された際には、OCRオブジェクトを表示します。

また、OCRした対象の画像を表示させるために、
ファイルをダウンロードし、画像タグの内容を置き換えます。

export default async (props, {$f7, $f7router, $on }) => {
  // 前画面から送られてきたOCR情報を取得
  const { ocr, img } = props;
  return $render;
}

これでOCRアプリの完成です。

まとめ

Tesseract.jsは優秀なOCRライブラリで、設定も不要で使うことができます。認識精度はコンピュータの出力した文字であれば、優秀と言ったところでしょう(手書き文字は苦手です)。手書きで書き写したりするのが面倒な情報は、OCRで入力代行できると便利そうです。

Framework7とMonacaを組み合わせることで、多彩な機能を持ったUIのアプリを開発できるようになります。ぜひトライしてください。