ブラウザ完結の日本語 OCR アプリを作って学んだこと(全 5 記事まとめ・設計と結果)

概要

今回はブラウザだけで完結する日本語 OCR アプリを作ってみた経緯と、全 5 記事に分けて書いた知見のまとめについて紹介していきます。

数週間かけて、Tesseract.js で日本語 OCR → Fuse.js で辞書マッチという仕組みを作ってみました。
細かいテクニックは個別の記事に分けて書いたので、ここでは全体像と、実際にどういう課題にぶつかって・何を学んだかを振り返ります。

各論は個別記事に、全体像と学びはこの記事にという構成です。
それではやっていきましょう!

目次

作ったもののイメージ

ユーザが写真を撮ると、OCR でテキストを抽出し、辞書と照合して該当する用語を一覧表示する、という UI を作りました。

作ったアプリの画面遷移イメージ(撮影 → OCR → 辞書マッチ → 結果表示)
ブラウザ完結 OCR アプリの画面フロー

サーバ側のコードは 1 行も無く、全部ブラウザで完結しています。
ホスティングは Cloudflare Pages の無料枠です!

なぜこの構成を選んだか

最初の要件は「スマホで撮った画像を素早くテキスト化して、用語の辞書に当てたい」というものでした。
候補として検討したのは以下です。

<検討した構成>
  • AWS Lambda + Tesseract → 月額料金と運用コスト
  • Google Cloud Vision API → 月の無料枠を超えると有料、画像がクラウドに飛ぶ懸念
  • ブラウザで Tesseract.js → 完全無料、画像は端末外に出ない

個人で細く長く動かすなら、維持コストがゼロに近いブラウザ完結が圧倒的に有利です。
Tesseract.js の精度がネックですが、前処理とマッチ側の工夫で「なんとか使える?ライン?」までは十分持っていけました。

ハマった順に振り返る

1. そのままだと日本語の精度が全然足りない

Tesseract.js に写真をそのまま食わせても、日本語の再現率は体感 4 〜 5 割といったところでした。
特にカタカナ固有名詞、細字、照明ムラに弱く、「原材料名」のような密集テキストはほぼ無理でした。

ここを突破するのに、Canvas で前処理を 4 段階にしたのが一番効きました。

Canvas で OCR 前処理:Sauvola 二値化とコントラスト伸長で認識精度を上げる

2. PSM(Page Segmentation Mode)は 1 つじゃダメ

Tesseract の PSM は画像解釈のヒントで、デフォルトの AUTO は万能ではありませんでした。
密なブロックは SINGLE_BLOCK、縦書きは SINGLE_BLOCK_VERT_TEXT など、複数走らせて結果をマージする戦略に変えたら取りこぼしが大きく減りました。

worker のセットアップ方法と併せて、こちらでまとめています。

Tesseract.jsでブラウザだけの日本語OCRを実装する方法(PSM二段階戦略)

3. ファジー検索は単一しきい値だと破綻する

OCR から拾ったトークンを Fuse.js で辞書に当てるとき、threshold: 0.3 を固定にしたら、短語で誤爆・長語で取りこぼしの両方が出ました。

段階しきい値・曖昧判定・長さ比キャップの 3 つを足した話は、下記に書いています。
Fuse.js でトークン長に応じた動的しきい値ファジー検索を作る方法

4. まとめて変更したら F1 が 14 ポイント落ちた

これが一番しんどかったです。
OCR 誤認識の補正パターンを一気に 10 種類追加したら、F1 スコアが 49.0% から 34.8% へ悪化しました。

まとめて入れたので、どのパターンが効いて・どれが副作用を出したのかが切り分け不能になり、ほぼ全部 revert して 1 件ずつ再投入する羽目に。

教訓は「1 変更 1 計測」の一言に尽きます。
補正パターンの書き方と失敗談は、以下にまとめました。
日本語 OCR の誤認識を正規表現で補正する方法(カタカナ混同と文脈パターン)

5. ベンチが無いと改善が進まない

最後に気付いたのが、数字で測れる環境が最初に要ることです。

F1 / CER / 4-gram recall の 3 指標を Vitest で出すベンチを作ってから、変更の良し悪しが即答できるようになり、改善スピードが 3 倍くらいになった感覚があります。
遠回りに見えて、ベンチマークを先に用意するのが結局近道でした。

作り方はこちらで紹介しています。
調教したOCR精度を測るベンチマークの作り方( F1 / CER / 4-gram recall)

実測結果(ダイジェスト)

各段階での集計 F1 の変化を、ざっくり表にまとめるとこんな感じです。
数値は所持画像セット(固有サンプル 8 枚)での値で、環境依存です。

フェーズ 施策 集計 F1
ベースライン Tesseract.js の素 + 単一しきい値 〜 0.30
前処理導入 Sauvola + コントラスト伸長 〜 0.45
PSM 二段階 AUTO + SINGLE_BLOCK 〜 0.49
動的しきい値 トークン長で threshold 切り替え 〜 0.52
誤認識補正 MISREAD_PAIRS + 慎重な MULTI_PATTERNS 〜 0.55

倍くらいになりましたが、実用としては厳しいレベルですね。。。
ここから先は、入力側の UX(撮影ガイド)や、ユーザが OCR 結果を後編集する仕組みで伸ばすのが費用対効果が高そうだなと感じています。

シリーズ記事リンク

気になる章から読んでもらえれば OK ですが、初めての方は「ベンチ → 前処理 → OCR → マッチング → 補正」の順で追うと、数字でブレ無く改善を追体験できると思います。

全体の学び

色々勉強になりましたが、今回の実験から学べるのは、以下の3つでした。

<3 つの学び>
  1. 数字で測れる環境を先に作る。そうでない調整は高確率で迷走する
  2. worker・モデルは重い。singleton で使い回す。UX はプログレスで吸収
  3. 1 変更 1 計測。まとめて入れた補正は、ほぼ確実にどこかで回帰する

抽象化すると「観測・計測・分離」になるので、OCR に限らず、他の機械学習チック or ヒューリスティック主体のプロジェクトでも同じ考え方が効きます。

次にやりたいこと

まだ手を付けてない領域もいろいろあります。

<今後の課題>
  • Tesseract に代わる PaddleOCR 系のブラウザ移植(精度向上の期待値が大きい)
  • ユーザが OCR 結果を直接編集してマッチし直す UI
  • 撮影時にリアルタイムで枠を合わせるガイド UI
  • 辞書側のメンテナンスを軽くするタクソノミ同期

どれもブログにできそうなテーマなので、気が向いたら続編を書こうと思います。

締め

1 つのアプリを作る過程で得られた知見を、個別テーマに分解してシリーズ化してみました。
ブラウザ完結の OCR は結構面白い分野だと思うので、同じことをやろうとしている方の参考になれば嬉しいです。

まぁ正直早さと精度を求めるならお金を払ってAI入りの優秀なOCRを使う方が早いとは思います。
ただ無料でOCRを使うのにはロマンがあるので、もっと頑張ってみたいですね!

各回はそれぞれの記事にリンクしていますので、興味のある領域からどうぞ。

以上となります。
ロマンを求めるのが男ってもんだろぉ!!!
それではお疲れさまでした。