プログラミング

画像から文字を読み取るOCR:Tesseractの導入とpytesseractの使い方

スキャンした書類や写真の中の文字は、そのままではただの画像です。コピーもできず、検索にもかかりません。この画像の中の文字を読み取って、機械で扱えるテキストに変換する技術をOCR(Optical Character Recognition:光学文字認識)と呼びます。

この記事では、無料で使えるOCRエンジンTesseractをパソコンに入れ、PythonのライブラリpytesseractからOCRを実行するところまでをまとめます。画像を1枚渡して、その中の文字をテキストとして取り出す、という最小の流れを実際に動かします。日本語の読み取りにも対応させます。

Pythonの環境がまだの場合は、当サイトの「Python Install ManagerでのPython環境構築」や「uvのインストールと使い方」を先にご覧ください。

動作確認

Tesseract 5.5.0/Python 3.12/pytesseract 0.3.13/Pillow 12.3.0で確認しています。

本文の記述は2026年7月時点のものです。

目次

はじめに

この記事の目的は、Tesseract(テッセラクト)を使ったOCRを、自分のパソコンで動かせるようになることです。具体的には、次の3つになります。

  • OCRエンジンTesseractの本体をインストールする。
  • 日本語を読み取るための言語データを追加する。
  • pytesseractを使い、Pythonから画像を渡して文字を読み取る。

Tesseractは、画像を渡すと中の文字をテキストにして返してくれるソフトです。無料でオフラインで動き、100以上の言語に対応しています。まずこれを動かせるようにしておくと、スキャンした書類のテキスト化など、画像から文字を扱うさまざまな場面で使えます。

Pythonのライブラリは次の2つを使います。

pip install pytesseract pillow

# uvの場合
uv add pytesseract pillow

また、この記事では説明のために、読み取る対象の画像をその場で作ります。そのために日本語のフォントファイル(TTF)を1つ使います。手元に無ければ、IPAexフォントからIPAexゴシックを入手し、ipaexg.ttfを作業フォルダに置いてください。手元にスキャン画像や写真がある場合は、そのファイルを読み込ませれば、画像を作る部分は不要です。

OCRとTesseractの位置づけ

はじめに、OCRがどういう場面で必要になるかを整理します。

パソコンで扱う文書には、「文字データを持っている文書」と「画像でしかない文書」があります。WordやGoogleドキュメントからPDFにしたもの、Webページなどは、見た目が文字であるだけでなく、データとしても文字を持っています。こういう文書は、OCRを使わなくてもプログラムで文字を取り出せます。

一方、紙をスキャナで読み取ったPDFや、書類を撮った写真は、データとしては色のついた点(ピクセル)の集まり、つまり画像でしかありません。人間の目には文字に見えても、機械にとっては「文字が書かれた絵」であって、文字データは入っていません。こういう画像から文字を読み取るのがOCRの仕事です。画像を見て「これは『在』という字だ」と推定し、文字コードに起こします。人が画像を見て書き写す作業を、機械にやらせるイメージです。

Tesseractは、このOCRを行うエンジンのひとつです。オープンソースで、Apache License 2.0のもとで無料で使えます。もともとはHewlett-Packardで開発され、2005年にオープンソースとして公開されました。バージョン4でニューラルネットワーク(LSTM)を使う認識エンジンが加わり、精度が上がっています。現在の安定版はバージョン5です。

Tesseractには画面(GUI)がなく、基本はコマンドラインから使うソフトです。Pythonから使うときは、Tesseractを呼び出す橋渡しをしてくれるライブラリpytesseractを間に挟みます。

インストールするものは、次の3つに分かれます。この順に進めます。

入れるものどういうものか入れ方
Tesseract本体OCRを行うエンジンOSごとの方法(Homebrew、apt、インストーラ)
言語データ言語ごとの学習済みモデルOSごとの方法、またはファイルを配置
pytesseractPythonから本体を呼ぶライブラリpip/uv addでインストール

Tesseract本体をインストールする

まず、OCRエンジンであるTesseract本体をインストールします。これはPythonのライブラリではなく、OSにインストールするソフトなので、pipではなくOSごとの方法で入れます。使っているOSのところをご覧ください。

macOS

Homebrewで入れられます。

brew install tesseract

Linux (Ubuntu/Debian)

aptで入れられます。

sudo apt update
sudo apt install tesseract-ocr

Windows

Windowsには、新しいバージョンの公式インストーラが用意されていません。そのため、マンハイム大学図書館(UB Mannheim)が配布しているインストーラを使うのが定番です。これはTesseractの公式ドキュメントでも案内されている入手先です。

次のページから、インストーラ(.exe)をダウンロードして実行します。

UB MannheimのTesseract配布ページ:
https://github.com/UB-Mannheim/tesseract/wiki

インストーラの指示に従って進めれば本体が入ります。途中で追加の言語データを選ぶ画面(Additional language data)があり、そこで日本語を選んでおきます

インストール先のフォルダ(初期設定ではC:\Program Files\Tesseract-OCR)は、後で使うので控えておきます。

インストール後、環境変数PATHにC:\Program Files\Tesseract-OCR(インストール先)を追加すると、どこからでもtesseractコマンドを呼べるようになります。

インストールの確認

どのOSでも、インストールできたかはバージョン表示で確認できます。ターミナル(Windowsならコマンドプロンプト)で次を実行します。

tesseract --version

次のように、バージョン番号などが表示されれば成功です。

tesseract v5.5.0.20241111
 leptonica-1.85.0
  libgif 5.2.2 : libjpeg 8d (libjpeg-turbo 3.0.4) : libpng 1.6.44 : libtiff 4.7.0 : ...

日本語の言語データを追加する

Tesseractは、読み取る言語ごとに「言語データ(学習済みモデル)」を必要とします。本体を入れただけでは、多くの場合、英語(eng)と向き検出用(osd)しか入っていません。日本語を読み取るには、日本語の言語データ(jpn)を追加します。

いま入っている言語は、次のコマンドで一覧できます。

tesseract --list-langs

jpnが並んでいなければ、追加が必要です。追加方法はOSによって異なります。

macOSは、Homebrewで追加の言語データをまとめて入れられます。

brew install tesseract-lang

Linux(Ubuntu/Debian)は、日本語のパッケージを入れます。公式の命名はtesseract-ocr-<言語コード>で、日本語ならjpnです。

sudo apt install tesseract-ocr-jpn

Windowsは、UB Mannheimのインストーラの途中で追加の言語データを選ぶ画面があり、そこで日本語(Japanese)を選んでおけば一緒に入っています。入れ忘れた場合は、後から言語データファイル(jpn.traineddata)だけを入手し、Tesseractのtessdataフォルダ(初期設定ではC:\Program Files\Tesseract-OCR\tessdata)に置くこともできます。

追加できたら、もう一度tesseract --list-langsを実行して、一覧にjpnが現れることを確認します。次はLinuxでの例で、表示されるパスや、jpn以外に並ぶ言語は環境によって異なります(macOSのHomebrewではsnumも一緒に入ります)。

List of available languages in "C:\Program Files\Tesseract-OCR/tessdata/" (5):
eng
jav
jpn
jpn_vert
osd

言語データの版によって結果が変わる

同じjpn.traineddataという名前でも、配布元によって中身が違います。Tesseractの公式にはtessdata(従来のエンジン用のモデルも含む版)、tessdata_fast(速度重視)、tessdata_best(精度重視)という複数のリポジトリがあります。同じ画像を同じコードで読ませても版が異なると結果が変わることがあります

pytesseractをインストールする

本体と言語データが入ったら、Pythonから呼び出すためのpytesseractを入れます。また、画像を読み込むためにPillowも一緒に入れます。

pip install pytesseract pillow

# uvの場合
uv add pytesseract pillow

pytesseractはあくまで「Tesseract本体を呼び出す橋渡し」なので、本体が入っていないと動きません。先に本体を入れておくことが前提です。

インストールできたら、PythonからTesseractのバージョンを取得できるか試して、本体とpytesseractが正しくつながっているかを確認します。

import pytesseract

print(pytesseract.get_tesseract_version())  # 本体のバージョンが返れば連携できている
print(pytesseract.get_languages())          # 使える言語の一覧

筆者の環境では、次のように表示されました。

5.5.0.20241111
['eng', 'jav', 'jpn', 'jpn_vert', 'osd']

get_languages()の一覧にjpnが含まれていれば、日本語の読み取りも使えます。

本体が見つからないというエラーがでた場合

pytesseractがTesseract本体の場所を見つけられないと、次のエラーになります。

TesseractNotFoundError: tesseract is not installed or it's not in your PATH. 
See README file for more information.

多くはPATHが通っていないことが原因です。PATHを設定し直すのが本筋ですが、コード側で本体の場所を直接指定することもできます。次のように、実行ファイルのパスを教えます(下はWindowsの例で、パスは自分の環境に合わせます)。

import pytesseract
pytesseract.pytesseract.tesseract_cmd = r"C:\Program Files\Tesseract-OCR\tesseract.exe"

画像から文字を読み取る(最小の例)

実際に画像から文字を読み取ります。ここでは説明のために、Pythonで文字を書いた画像をその場で作り、それをOCRにかけます。

pytesseract.image_to_string()が、画像を渡すとその中の文字をテキストとして返す関数です。これがpytesseractの基本になります。

from PIL import Image
import pytesseract

text = pytesseract.image_to_string(Image.open("ファイルパス"))
print(repr(text))

Image.open()を使って画像を開いてから渡していますが、これはpytesseractの最も基本的な使い方です。この記事の後の例では、開いた画像を2回読ませたり、縮小してから読ませたりするので、その形にも揃えています。画像を一度読むだけなら、pytesseract.image_to_string("ファイルパス")のようにファイルのパスを直接渡すこともできます。

サンプル画像を作る部分も含めると以下になります。

from PIL import Image, ImageDraw, ImageFont
import pytesseract

# 説明のため、文字を書いた画像をその場で作る
img = Image.new("RGB", (500, 80), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("ipaexg.ttf", 28)
draw.text((20, 20), "Hello, Tesseract!", fill="black", font=font)
img.save("hello.png")

# 画像を渡して、中の文字をテキストにする (ここで、OCRを実施)
text = pytesseract.image_to_string(Image.open("hello.png"))
print(repr(text))

実行すると、次の画像が生成されます。

そして、この画像に対してOCRが実行されて、画像の中の文字がテキストとして返ってきました。筆者の環境での結果を示します。

'Hello, Tesseract!\n'

画像の中に「絵」として書かれていた文字が、プログラムで扱えるテキストになりました。これがOCRの基本の動きです。

なお、ここでImageFont.truetype()によるフォントの指定を省くと、Pillowは小さなビットマップフォントで描画します。その画像をOCRにかけると、筆者の環境では'Hella Tesseract\n'と読み間違えました。文字が小さければ、英語1単語でも読み違えるということです。OCRに渡す画像の質が結果を左右する、という点は大事です。

日本語を読み取る

日本語を読み取るときは、lang引数で言語を指定します。指定しないと既定の英語として読もうとするため、日本語はうまく読めません。日本語ならlang="jpn"を渡します。

from PIL import Image, ImageDraw, ImageFont
import pytesseract

lines = [
    "にゃんにゃん運輸 就業規則(抜粋)",
    "第3条 在宅勤務は週3日まで認める。",
    "第7条 経費精算は毎月20日を締切とする。",
]
img = Image.new("RGB", (700, 200), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("ipaexg.ttf", 28)
y = 20
for line in lines:
    draw.text((20, y), line, fill="black", font=font)
    y += 50
img.save("kitei.png")

print(pytesseract.image_to_string(Image.open("kitei.png"), lang="jpn"))

実行すると、次の画像が作成されます。

そして、この画像に対してOCRが実行されて、画像の中の文字がテキストとして返ってきました。筆者の環境での結果を示します。

にゃんにゃん運輸 就業規則 (抜粋)
第3条 在宅勤務は週3日まで認める。
第7条 経費精算は毎月20日を締切とする。

3行とも正しく読めています。ただし、細かく見ると1点だけ原文と違います。元の画像は全角の(抜粋)ですが、読み取り結果は半角の(抜粋)になりました。OCRは字の形から文字を推定するので、形が似ている文字は、原文と別の文字に割り当てられることがあります。読み取ったテキストが原文と1文字も違わない、とは期待しないほうが安全です。

画像の中身に合わせてページ分割モード(PSM)を選ぶ

OCRは、文字を読む前に「この画像のどこに、どういう向きで、どうまとまった文章があるか」を解析します。この解析の仕方を指定するのがPSM(Page Segmentation Mode、ページ分割モード)です。

モード一覧

PSMは全部で次の14種類があります(tesseract --help-psmで一覧を確認できます)。config引数で--psm 6のように番号を指定します。

モード(--psm内容備考
0ページの向きと文字種の判定(OSD)のみを行う
1OSDつきの、自動ページ分割
2自動ページ分割を行うが、OSDも文字認識もしない(未実装)
3完全に自動のページ分割、ただしOSDなし(デフォルト)◎よく使う
4大きさがまちまちな、1段の文章とみなす
5縦方向にそろった、均一なテキストのかたまりとみなす
6均一なテキストのかたまりとみなす◎よく使う
7画像を1行のテキストとして扱う◎よく使う
8画像を1つの単語として扱う
9画像を、円の中に書かれた1つの単語として扱う
10画像を1文字として扱う
11まばらなテキスト。順序を問わず、できるだけ多くの文字を見つける
12OSDつきの、まばらなテキスト
13生の行。Tesseract固有の処理を省いて、画像を1行のテキストとして扱う

種類は多いですが、横書きの文書を扱うときに実際に使うのは、備考に「よく使う」と付けた3つ(既定の3、かたまり扱いの6、1行扱いの7)でほぼ足ります。残りは、1文字だけ・単語だけを読む用途(8〜10)、縦書き用(5)、ページの向きの判定を絡めるもの(0〜2、12)などです。いずれも入力の形が決まった特殊な場面向けで、通常の文書を読むときには使いません。

表に出てくる「ページの向きと文字種の判定」は、TesseractではまとめてOSD(Orientation and Script Detection)と呼ばれます。ページが90度単位で回転していないか(スキャンで紙が横や逆さまになっていないか)と、どの文字体系(日本語・ラテン文字など)で書かれているかを推定する処理です。文字を読む前の下準備にあたり、文書がすべて正しい向きだと分かっているなら、気にする必要はありません。

日本語の縦書きには専用の言語データが要る

縦書き用のモード5は、日本語では、これまで使ってきたjpnのままだと正しく読めません。縦書き専用の言語データjpn_vertを別に用意し、lang="jpn_vert"とモード5を組み合わせる必要があります。

判定モード

モード0とモード2は、文字を読み取るためのモードではなく、ページの向きや文字体系を判定するためのモードです。文字を取り出すのではなく、上で説明したOSDの結果を得ることが目的です。この用途には、pytesseractに専用の関数image_to_osd()があります。

from PIL import Image
import pytesseract

# 説明のため、正立の画像と、上下逆さまにした画像を用意する
img = Image.open("kitei.png")            # 「日本語を読み取る」の節で作った画像
upside_down = img.rotate(180)

for label, im in [("正立の画像", img), ("上下逆さまの画像", upside_down)]:
    print(f"--- {label} ---")
    print(pytesseract.image_to_osd(im))

筆者の環境では、次のように、画像がどの向きで置かれているかが検出できました。

--- 正立の画像 ---
Page number: 0
Orientation in degrees: 0
Rotate: 0
Orientation confidence: 5.47
Script: Japanese
Script confidence: 2.21

--- 上下逆さまの画像 ---
Page number: 0
Orientation in degrees: 180
Rotate: 180
Orientation confidence: 5.40
Script: Japanese
Script confidence: 2.21

Orientation in degreesが「画像が何度回転しているか」、Rotateが「正立に戻すには何度回せばよいか」、Scriptが「どの文字体系か」を表します。傾いたスキャン画像を、文字認識にかける前に自動で回転補正するといった前処理で使えます。

一方、文字を読み取る関数であるimage_to_stringにモード0や2を渡すと、これらは文字を返さないモードなので、エラーになります。この2つは、この記事で行うような「画像から文字を取り出す」用途では使いません。

文字列の取得(モード3とモード6)

よく使う3つの中でも、既定のPSM 3とPSM 6は結果が大きく変わることがあります。差がはっきり出る例を見てみます。申請書のように、項目がページの中でまばらに置かれた画像です。

from PIL import Image, ImageDraw, ImageFont
import pytesseract

# 項目がまばらに置かれた申請書のような画像を作る
img = Image.new("RGB", (700, 400), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("ipaexg.ttf", 24)
draw.text((30, 30), "申請番号", fill="black", font=font)
draw.text((500, 30), "A-1234", fill="black", font=font)
draw.text((30, 330), "承認印", fill="black", font=font)
img.save("shinsei.png")

img = Image.open("shinsei.png")
print("=== 既定(PSM 3)===")
print(pytesseract.image_to_string(img, lang="jpn"))
print("=== PSM 6 ===")
print(pytesseract.image_to_string(img, lang="jpn", config="--psm 6"))

実行すると次の画像が作成されます。

そして、この画像に対してOCRが実行されて、次の結果が得られました。

=== 既定(PSM 3)===
寺 騙

品り
由

A-1234

=== PSM 6 ===
申請番号                                      A-1234
承認印

既定のPSM 3では、申請番号承認印も原形をとどめていません。文字がまばらに離れているため、レイアウトの解析に失敗し、別々の文字の並びとして読もうとした結果です(どんな文字に化けるかはTesseractのバージョンによって変わりますが、崩れること自体は変わりません)。一方、画像全体をひとかたまりとして扱うPSM 6では、3つの項目がすべて正しく読み取れました。

では、PSM 6を常に指定すればよいかというと、そうではありません。今度は2段組みのページを読ませてみます。

from PIL import Image, ImageDraw, ImageFont
import pytesseract

# 2段組みのページを作る(A4を300dpiで読み取ったくらいの大きさ)
img = Image.new("RGB", (1700, 1100), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("ipaexg.ttf", 40)

left = ["第1章 在宅勤務規程", "", "第1条(目的)", "本規程は、在宅勤務の",
        "取扱いを定めるもので", "ある。", "", "第2条(対象者)",
        "勤続6か月以上の社員", "を対象とする。"]
right = ["第3条(日数)", "在宅勤務は週3日まで", "認める。", "", "第4条(申請)",
         "前月末までに所属長へ", "申請する。", "", "第5条(設備)",
         "機器は会社が貸与する。"]

for column, x in ((left, 120), (right, 950)):
    y = 100
    for line in column:
        draw.text((x, y), line, fill="black", font=font)
        y += 66
img.save("two_col.png")

img = Image.open("two_col.png")
print("=== 既定(PSM 3)===")
print(pytesseract.image_to_string(img, lang="jpn"))
print("=== PSM 6 ===")
print(pytesseract.image_to_string(img, lang="jpn", config="--psm 6"))

実行すると次の画像が得られます。

結果は逆転しました。

=== 既定(PSM 3)===
第1章 在宅勤務規程

第1条(目的)

本規程は、在宅勤務の
取扱いを定めるもので
ある。

第2条(対象者)
勤続6か月以上の社員
を対象とする。

第3条(日数)
在宅勤務は週3日まで
認める。

第4条(申請)
前月末までに所属長へ
申請する。

第5条(設備)
機器は会社が貸与する。


=== PSM 6 ===
第1章 在宅勤務規程 第3条(日数)
在宅勤務は週3日まで

第1条(目的) 認める。

本規程は、在宅勤務の

取扱いを定めるもので 第4条(申請)

ある。 前月末までに所属長へ
申請する。

第2条(対象者)

勤続6か月以上の社員 第5条(設備)

を対象とする。 機器は会社が貸与する。

既定のPSM 3は2つの段をきちんと認識し、左の段を読み終えてから右の段に移り、第1条から第5条まで正しい順序で並べています。逆にPSM 6は、画像全体をひとかたまりと決めてかかるため、左の段と右の段が1行ずつ交互に混ざりました

つまり、PSMは「精度を上げる設定」ではなく「画像の中身をTesseractに教える設定」です。ひとかたまりの文章や切り出した領域ならPSM 6、段組みのあるページなら既定のPSM 3、というように、入力の性質に合わせて選びます。うまく読めないときは、パラメータを総当たりする前に、渡している画像がどういうものかを見直すのが大事です。

OCRの結果は完璧ではない

ここまでの例はきれいな画像だったので、ほぼ正確に読めました。しかし実際に扱うのは、スキャンした紙や、撮影した書類です。解像度が低かったり、かすれていたり、傾いていたりします。そのとき何が起きるかを見ておきます。

先ほどの画像(以下)を40%に縮小して、粗いスキャンを模してから読ませます。

from PIL import Image
import pytesseract

img = Image.open("kitei.png")                    # 700x200 (作成済みの画像)
small = img.resize((280, 80), Image.LANCZOS)     # 縮小する

print("=== 元の画像(700x200)===")
print(pytesseract.image_to_string(img, lang="jpn", config="--psm 6"))
print("=== 縮小した画像(280x80)===")
print(pytesseract.image_to_string(small, lang="jpn", config="--psm 6"))

結果は次のようになりました。

=== 元の画像(700x200)===
にゃんにゃん運輸 就業規則 (抜粋)
第3条 在宅勤務は週3日まで認める。
第7条 経費精算は毎月20日を締切とする。

=== 縮小した画像(280x80)===
にゃんにゃん連和夫則 (抜和)
第3条 在宅動務は周3日まで認める。
第7休 経精算は毎月20日を締切とする。

運輸連和に、就業規則夫則に、抜粋抜和に、週3日周3日など様々な場所で間違いが起こりました。どの文字をどう間違えるかはTesseractのバージョンによって変わりますが、粗い画像では誤読が出るという点は同じです。

注目したいのは、この結果がエラーにならないことです。プログラムは正常に終了し、それらしい日本語の文字列を返してきます。「週3日」が「周3日」になっていても、機械は間違いに気づきません。OCRは推定なので、間違えるときも、もっともらしく間違えます。

このことは、OCRを使った後の処理に効いてきます。たとえば読み取ったテキストを検索できるようにする場合、週3日という語が失われていれば、その語では引けなくなります。OCRを通したテキストは、そのまま信用するのではなく、後で整える処理を挟むのが前提です。

まず入力画像の質を上げる

OCRの精度は、モードの指定よりも、入力画像の質に大きく左右されます。Tesseractの公式ドキュメントにも、読み取り結果を良くするには入力画像の質を上げる必要がある、と明記されています。有効なのは、解像度を上げる(紙のスキャンなら300dpi程度)、傾きを直す、影やムラを取って白黒にする(二値化)といった処理です。パラメータをいじる前に、元画像を見直すほうが効くことが多くあります。

発展的内容

一歩進んだ機能を3つ紹介します。実務で必要になったときに、こういう選択肢があると思い出せる程度に触れておきます。

信頼度や座標を得る

image_to_data()は、読み取った各単語について、位置(座標)と信頼度(0〜100の値。100に近いほど自信がある)を返します。信頼度が低い箇所を機械的に抽出できるので、「OCRの怪しい部分を人間が見直す」といった使い方に向きます。

from PIL import Image
import pytesseract

img = Image.open("kitei.png")
data = pytesseract.image_to_data(img, lang="jpn", config="--psm 6",
                                  output_type=pytesseract.Output.DICT)

# 信頼度と単語を並べて表示(空の行を除く)
for text, conf in zip(data["text"], data["conf"]):
    if text.strip():
        print(f"{conf:>3}  {text}")

筆者の環境では、次のように表示されました(一部抜粋)。

 93  に
92 ゃ
96 ん
93 に
93 ゃ
96 ん
95 運輸
95 就業
79 規則
79 (抜粋
93 )
96 第
92 3
90 条
96 在宅

全体は90前後ですが、規則(抜粋だけ低めです。この画像はきれいなので実際には誤読していませんが、スキャン画像でOCRをかけたとき、信頼度が低い箇所は誤読の可能性が高いので、人間が確認する対象を絞り込むのに使えます。

検索可能なPDFを作る

スキャンした紙のPDFは画像しか持たないので、そのままではテキスト検索できません。Tesseractは、元の画像の見た目を保ったまま、その上に読み取ったテキストを透明な層として重ねた検索可能PDFを作れます。この形式のPDFなら、pypdfなどでテキストを抽出できます。

from PIL import Image
import pytesseract

img = Image.open("kitei.png")
pdf_bytes = pytesseract.image_to_pdf_or_hocr(img, lang="jpn", extension="pdf",
                                              config="--psm 6")
with open("kitei_searchable.pdf", "wb") as f:
    f.write(pdf_bytes)

作られたkitei_searchable.pdfをpypdfで読むと、テキストが取り出せることが確認できます(Pythonにpypdfライブラリの追加が必要)。

from pypdf import PdfReader
print(PdfReader("kitei_searchable.pdf").pages[0].extract_text())
に ゃ ん に ゃ ん 運輸 就業 規則 (抜粋 )
第 3 条 在宅 勤務 は 週 3 日 まで 認め る 。
第 7 条 経費 精算 は 毎月 20 日 を 締切 と する 。

extension="hocr"にすると、単語ごとに位置情報を持ったHTML(hOCR形式)が得られます。ほかにもimage_to_boxes(文字ごとの座標)やimage_to_alto_xml(ALTO XML形式)といった関数もあります。

複数の言語を同時に指定する

日本語のビジネス文書には、英単語や英語の型番が混じることがよくあります。この場合、lang="jpn"だけで読ませると、日本語部分は正しく読めても英語部分が壊れることがあります。逆にlang="eng"だけだと、日本語部分が壊れます。

こういうときは、+でつないで複数の言語を同時に指定できます。

from PIL import Image, ImageDraw, ImageFont
import pytesseract

# 説明のため、日本語と英語が混じった画像を作る
img = Image.new("RGB", (900, 100), "white")
draw = ImageDraw.Draw(img)
font = ImageFont.truetype("ipaexg.ttf", 30)
draw.text((20, 30), "Please review the 就業規則 by tomorrow", fill="black", font=font)
img.save("mixed.png")

img = Image.open("mixed.png")
print("--- lang='eng' のみ ---")
print(pytesseract.image_to_string(img, lang="eng", config="--psm 6"))
print("--- lang='jpn+eng' ---")
print(pytesseract.image_to_string(img, lang="jpn+eng", config="--psm 6"))

筆者の環境では、次の結果になりました。

--- lang='eng' のみ ---
Please review the RLSekHA! by tomorrow

--- lang='jpn+eng' ---
Please review the 就業規則 by tomorrow

処理時間に上限をつける

大量の画像を一括で処理するとき、たまに極端に時間がかかる画像に当たることがあります。ノイズが多く、Tesseractが解析に手間取るケースです。この1枚に何分もかかると、全体の処理が止まってしまいます。

pytesseractにはtimeout引数があり、指定した秒数を超えたら処理を打ち切ってエラーを返してくれます。

from PIL import Image
import pytesseract

img = Image.open("kitei.png")

try:
    # 0.001秒でタイムアウトさせる(実際の運用ではもっと長い値にする)
    text = pytesseract.image_to_string(img, lang="jpn", config="--psm 6",
                                        timeout=0.001)
    print(text)
except RuntimeError as e:
    print(f"タイムアウトしました: {e}")

上のコードでは、あえて極端に短い0.001秒を指定してタイムアウトを起こしています。実行すると、次のようになります。

タイムアウトしました: Tesseract process timeout

実際には、1枚あたり何秒までなら許容するかを決めて、そのくらいの値を指定します。タイムアウトするとRuntimeErrorが飛ぶので、tryで受けておけば、その1枚をスキップして次の画像の処理を続けられます。大量のスキャンPDFを一括処理するようなバッチ処理での保険として便利です。

OCRエンジンを切り替える

Tesseractには2種類のOCRエンジンが載っています。バージョン3までの伝統的なLegacyエンジンと、バージョン4から加わったLSTMエンジン(ニューラルネットワークを使うもの)です。既定はLSTMで、多くの場合これで十分です。

エンジンは--oem(OCR Engine Mode)で指定します。値は次の4種類です。

内容
0Legacyエンジンのみを使う
1LSTMエンジンのみを使う
2Legacy+LSTMの両方を使う
3使えるものから自動で選ぶ(既定)

使い方は次の通りです。

from PIL import Image
import pytesseract

img = Image.open("kitei.png")
print(pytesseract.image_to_string(img, lang="jpn", config="--oem 1 --psm 6"))

ただし、注意点があります。多くの環境では、Legacyエンジン(--oem 0--oem 2)は使えません。これは言語データが対応していないためです。Legacyエンジンを使うには、Legacy対応の言語データ(tessdataリポジトリから取得)に差し替える必要があります。対応していない場合は次のようなエラーがでます。

Error: Tesseract (legacy) engine requested, but components are not present in
C:\\Program Files\\Tesseract-OCR/tessdata/jpn.traineddata!!
Failed loading language 'jpn' Tesseract couldn't load any languages! Could not initialize tesseract.

LSTMのほうが精度が高いのが一般的で、あえて切り替える場面は多くありません。「切り替える機能自体はあるが、通常は既定(LSTM)のままでよい」と思っておけば十分です。

まとめ

Tesseractを使ったOCRを、環境構築から実際の読み取りまで確認しました。要点を以下にまとめます。

  • OCRは、画像でしかない文書(スキャンや写真)から文字を読み取ってテキストにする技術である。文字データを持つ文書には不要で、画像の文書に対して使う。
  • 入れるものは「Tesseract本体」「言語データ」「pytesseract」の3つ。本体と言語データはOSごとの方法で、pytesseractはpipまたはuvで入れる。
  • 日本語を読み取るには言語データjpnを追加し、lang="jpn"で指定する。
  • 基本はpytesseract.image_to_string(画像)で、画像の中の文字をテキストとして受け取れる。
  • PSM(ページ分割モード)は「画像の中身をTesseractに教える設定」であり、常に良いモードがあるわけではない。まばらな配置や切り出した領域なら--psm 6、1行だけなら--psm 7、段組みのあるページなら既定のPSM 3、というように入力に合わせて選ぶ。
  • OCRは推定なので、間違えるときもエラーにならず、もっともらしく間違える。読み取り結果は、後で整えることを前提にする。
  • 精度を上げたいときは、まず入力画像の質(解像度・傾き・コントラスト)を見直す。
  • image_to_stringのほかに、信頼度つきで結果を得るimage_to_dataや、検索可能PDFを作るimage_to_pdf_or_hocrもある。用途に応じて使い分ける。
  • 日本語と英語が混じる文書はlang="jpn+eng"と複数言語を同時に指定でき、大量画像の一括処理ではtimeoutで処理時間の上限をかけられる。

参考になりましたら幸いです。

参考資料

-プログラミング
-