2024/12/31

【Python】tesseract.js+Flask+PywebviewでOCRを実装する

はじめに

画像から文字を抽出するOCR(Optical Character Recognition)は、紙の書類を電子化したり、スクリーンショットに写った文字を読み込んで活用したりと、さまざまなシーンで重宝される技術です。

PythonでOCRを実装する際、「Tesseract」というオープンソースのOCRエンジンの名前を耳にした方も多いのではないでしょうか。しかしながら、TesseractのインストールやOSごとの依存関係を調整する必要があるなど、環境構築で課題が生じる可能性があります。

本記事では、そうした問題を回避するため、tesseract.jsFlaskPywebviewを組み合わせて、Python環境で比較的容易にOCR機能を実装する方法をご紹介します。


1. Tesseractとは

Tesseractは、Googleが管理する高機能なオープンソースのOCRエンジンです。C++実装であり、多言語の文字認識に対応しています。Pythonから利用する場合、Tesseract本体をOSごとにインストールしたうえで、学習データ(.traineddata)の配置して、「pytesseract」や「pyocr」といったラッパーライブラリを通して画像解析を行うのが一般的です。

2. tesseract.jsとは

こうした導入や運用での煩雑さを軽減する一つの方法として、JavaScript版のtesseract.jsが挙げられます。tesseract.jsはWebAssembly技術を用いており、Webブラウザやブラウザ互換の環境であれば、Tesseract本体をインストールせずともOCRが実行できます。Python環境でも、後述のFlask+Pywebview構成などを使うことでGUIアプリケーションとして利用できます。

3. tesseract.js+Flask+Pywebviewで実装するOCR

3-1. 全体構成の概要

本記事で紹介する方法では、以下の三つの要素を組み合わせてOCRを実行します。

  1. Flask

    • Python製の軽量Webフレームワーク。
    • 今回の例ではファイルアップロードや簡易APIの実装などを行い、ブラウザ(Pywebview)に対する表示や処理の受け渡しを担います。
  2. Pywebview

    • Pythonコードから「ブラウザ相当のUIウィンドウ」を生成するライブラリ
    • Flaskで立ち上げたWebアプリにローカルでアクセスし、GUIアプリケーションのような操作感を提供します。
  3. tesseract.js

    • WebAssembly版Tesseract。
    • Pywebview上でレンダリングされるHTML環境に読み込み、画像中のテキストを認識します。

3-2. ディレクトリ構成例


project/
├── app.py
└── templates/ 
    └── index.html

3-3. 実装サンプルのコード紹介

以下に、主要ファイルのサンプルコードを示します。

app.py
from flask import Flask, render_template, request, jsonify
import webview
app = Flask(__name__)
@app.route('/')
def index():
return render_template('index.html')
@app.route('/process-ocr', methods=['POST'])
def process_ocr():
data = request.get_json()
ocr_text = data.get('text', '')
print('OCR結果:\n', ocr_text) # OCR結果をコンソールに出力
return jsonify({'status': 'success'})
if __name__ == '__main__':
window = webview.create_window('OCR アプリケーション', app)
webview.start()
view raw app.py hosted with ❤ by GitHub
index.html
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>OCR アプリケーション</title>
<script src='https://cdn.jsdelivr.net/npm/tesseract.js@5/dist/tesseract.min.js'></script>
<style>
.container {
max-width: 800px;
margin: 0 auto;
padding: 20px;
}
#imagePreview {
max-width: 100%;
margin: 20px 0;
display: none;
}
#result {
margin-top: 20px;
padding: 10px;
border: 1px solid #ccc;
min-height: 100px;
}
#status {
color: #666;
margin: 10px 0;
}
.button-group {
margin: 20px 0;
}
button {
padding: 8px 16px;
margin-right: 10px;
}
.preview-container {
margin: 20px 0;
text-align: center;
}
</style>
</head>
<body>
<div class="container">
<h1>OCR処理</h1>
<input type="file" id="imageInput" accept="image/*" onchange="previewImage(event)">
<div class="preview-container">
<img id="imagePreview" alt="プレビュー">
</div>
<div class="button-group">
<button onclick="processImage()">OCR実行</button>
<button onclick="clearAll()">クリア</button>
</div>
<div id="status"></div>
<div id="result"></div>
</div>
<script>
function previewImage(event) {
const file = event.target.files[0];
if (file) {
const preview = document.getElementById('imagePreview');
preview.style.display = 'block';
preview.src = URL.createObjectURL(file);
}
}
async function processImage() {
const file = document.getElementById('imageInput').files[0];
const statusDiv = document.getElementById('status');
const resultDiv = document.getElementById('result');
if (!file) {
alert('画像を選択してください');
return;
}
statusDiv.textContent = 'OCR処理中...';
try {
const worker = await Tesseract.createWorker();
const { data: { text } } = await worker.recognize(file);
await worker.terminate();
resultDiv.textContent = text;
statusDiv.textContent = 'OCR処理完了';
// OCR結果をPythonに送信
fetch('/process-ocr', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({text: text})
});
} catch (error) {
console.error('Error:', error);
statusDiv.textContent = 'エラーが発生しました';
}
}
function clearAll() {
document.getElementById('imageInput').value = '';
document.getElementById('imagePreview').style.display = 'none';
document.getElementById('result').textContent = '';
document.getElementById('status').textContent = '';
}
</script>
</body>
</html>
view raw index.html hosted with ❤ by GitHub

3-4. 動作画面

テストファイル:eng_bw.png

今後の課題・改良点

複数画像の取り込み

  • 現状のサンプルでは単一のファイル入力を想定しています。

並列処理(パフォーマンスの向上)

  • tesseract.jsはWebAssemblyで動作するため比較的高速ではあるものの、大量の画像を同時に処理したい場合や高速化が求められる場面では、並列処理などを検討して下さい。

まとめ

以上のように、tesseract.jsとFlask、およびPywebviewを組み合わせることで、Python環境でOCR機能を簡易的に実装できる方法をご紹介しました。
本記事のアプローチがPythonでOCR機能を開発する際の一助になれば幸いです。

2023/05/04

【Python】画像ファイルのPDF変換を並列処理で高速化する

これまでに何度も画像ファイルのPDF変換の記事を書いてきたのですが、これまでの変換プログラムの中で最も高速かつ簡単に動作していたのは、「img2pdf」でした。
img2pdfの使い方や速度比較などは、下記の過去記事にて紹介していますのでご参照ください。


これまで記事を作成する中で得られた経験から、concurrent.futuresで並列処理を実装すると、(動かすマシン次第ですが、)処理速度が結構早くなる可能性が見えてきました。
本記事では、並列処理を実装することでimg2pdfの処理速度を超えることを目指した結果をご紹介します。

本記事では、下記の三通りの比較を行います。
  • Method1: img2pdfを使用する
  • Method2: pikepdfとPillowを使用する
  • Method3: Method2をconcurrent.futuresで並列処理
前回までの記事と同様に、書籍をスキャンしたpngファイル(連番ファイル名)群を、pdfに変換することを想定しています。

実行環境は、以下の通りです。
MacBook Air(8コアCPU、10コアGPU、16コアNeural Engine搭載Apple M2チップ, 16GBメモリ)
------
Python 3.10.10
img2pdf 0.4.4
pikepdf 7.1.1
Pillow 9.3.0

Method1 : img2pdfを使う

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Method2: pikepdfとPillowを使用する

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

Method3: Method2をconcurrent.futuresで並列処理

ライブラリとして、下記のようにpic2pdfという名前で整理しました。
一部の関数はimg2pdfのように動かせます。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

10個の異なるpng画像ファイル群での出力ファイルサイズと実行時間の比較は、以下の通りです。


まとめ

今回作成したMethod2やMethod3による出力は、img2pdfで作成したPDFと完全に同じサイズのPDFにはなりませんでした。 img2pdfはロスレスらしいので、今回作成したものとは異なる処理でロスレスを実現しているみたいです。
なので、Method2やMethod3による出力はロスレスではなく、注意が必要です。

Method2は、img2pdfのソースコード(4000行超)と比較すればかなりシンプルに書くことができましたが、変換処理に2~3倍時間がかかっています。
それをconcurrent.futuresで並列処理を実装して、力技で高速化したのがMethod3です。あくまで、私の環境での計測結果になりますが、Method2よりも5倍以上早くなっています。
結果的に、img2pdfと比較して、半分程度で処理されるようになりました。また、プログレスバーを実装したので、処理時間が長い場合でも処理が動いていることが分かるようになりました。

今後も改良を重ねて、行こうと考えているので気になる方はGithubの方をチェックして下さい。

下記のようなPDF変換用のプログラムも作成していますので、今回の成果は、こちらで使用しようと考えています。

2023/03/26

【商品比較】 NTM-E50 vs YRM-N50

山善が販売している平台車の比較


山善が販売している平台車は複数ありますが、以下の2つの商品は、共に連結機能とスタッキング機能を売りにしています。
最大荷重量は異なりますが、画像や商品情報を見ても非常によく似ており、違いが分かりにくいです。

商品情報を表にすると以下の通り。

品番 NTM-E50 YRM-N50
サイズ 幅27.5×奥行41×高さ7.7cm 幅27.5×奥行41×高さ7.7cm
重量 1.2kg 1.2kg
最大荷重 80kgまで 100kgまで
材質 本体:PP
キャスター:エラストマー
本体:PP耐衝撃性
キャスター:ナイロン
オリーブドラブ
ダークブラウン
モカ
ローズピンク
オリーブドラブ(OD)
ブラック(GY)
原産国 日本 日本
特徴 縦・横方向へ簡単に連結が可能
スタッキング(積み重ね)が可能
360度回転の自在キャスター
縦・横方向へ簡単に連結が可能
スタッキング(積み重ね)が可能
360度回転の自在キャスター


結論:キャスターの素材のみが異なる。連結機能に互換性あり。


実際に、二つとも商品を購入してみたところ、連結機能に互換性がありました。


NTM-E50のエラストマー製キャスターは、家庭のフローリングなどの屋内の床で使用するには適しています。逆に、工場のような環境であれば、薬品類に強いナイロンキャスターのYRM-N50が適しています。
商品の色や最大荷重以外の違いとして、ここが最も大きな点です。

無印良品のポリプロピレン平台車


画像や商品情報を見る限り、NTM-E50と非常に似ています。
山善では販売していない白色相当…?


山善以外が販売元の商品にも同じ型のものがありそうです。この手の情報は公式がまとめて欲しいですが、事情があるのかも知れません。

2023/03/17

【Python】自作のPythonパッケージをPyPIに登録しました!

今回は、自作のPythonパッケージをPyPIに登録することができたので報告したいと思います。

作成したパッケージは、以前の記事などで、作ってきたものを拡張したもので、自炊した漫画の画像ファイルをpdfに変換するためのプログラムです。別の記事で作ったePubからpdfに変換するプログラムも組み込んでいます。

PyPIへの登録手順は、他のサイトでも紹介されているため、ここでは割愛します。詳しい手順は、参考記事などをご確認ください。

参考記事:
setup.pyファイルの書き方も初めてだったので、ドキュメントを読みながら苦労しました。結局、setup.pyファイルでミスをしてしまい、PyPIにファイルをあげて直ぐに更新版をリリースすることになってしまいました。
似たような名前のパッケージがすでに登録されていることもあるので、パッケージ名を考えるところが難しいかもしれません。

今回、私が登録したPythonパッケージのソースコードは以下のGitHubリポジトリで閲覧することができます。また、改良のアイデアやバグ報告などがあれば、Github上でIssueを立てていただければ幸いです。

2023/02/14

【Python】foliumでGoogle Mapのマーカーアイコンを表示する

以前もfolium関連で似たような記事を書きましたが、今回は別の機能を試してみます。

folium.features.CustomIcon()は、自作のアイコンなどを表示するための機能ですが、自前の画像でなくても指定できるようなので、Googleがホストしているアイコン画像を試してみました。
また、今回の記事からGithub Gistを使って、コードだけでなく、実行結果のマップのhtmlを記事に埋め込んでいます。過去の記事の一部も書き換えました。

参考: 実装例:
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
実行例:

注意点:ループを使用して、複数点に同じカスタムアイコンを設定する際には、ループ内でカスタムアイコンを初期化する必要がある。

参考:
Google Chart APIで数字のアイコンを設定した例:
二桁までの数字なら問題なく表示できそうです。
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
実行例: