人間がデータ入力する前のドラフト入力としても使えるレベルではあるが、ちょっと弱い。入力補助として使いたいならばgoogle Cloud Vision APIやら、読取革命17などを使った方が高精度か。
A3左右レイアウトであまりうまく認識できていない例。個別の文字認識もいまいち。どちらかというと認識成績悪いパターンと思う。A4片面の太め文字の契約書などはもう少しきちんと読めるレベル。
バージョン違いが多発して、なかなか動かない。模範解答を最初に示す。ただし、この組み合わせはCPU利用。GPUで動かすにはさらに調整必要。Windows11で動きます。
import os
from pathlib import Path
from io import BytesIO
import time
import numpy as np
from PIL import Image
import fitz # PyMuPDF
import paddle
from paddleocr import PaddleOCR
import sys
import platform
import importlib
try:
from importlib import metadata as importlib_metadata # Py3.8+
except Exception:
import importlib_metadata # type: ignore
def _safe_dist_version(dist_names: list[str]) -> str:
“””複数のディストリ名候補から最初に見つかったバージョンを返す。”””
for name in dist_names:
try:
return importlib_metadata.version(name)
except Exception:
continue
return “unknown”
def _safe_module_version(module_name: str) -> str:
“””モジュールの __version__ を返す(無ければ unknown)。”””
try:
mod = importlib.import_module(module_name)
return getattr(mod, ‘__version__’, ‘unknown’)
except Exception:
return ‘not installed’
def _print_environment_versions() -> None:
print(“== Environment Info ==”)
print(f”Python: {sys.version.split()[0]} ({platform.platform()})”)
# CUDA可否
try:
compiled = bool(getattr(paddle, ‘is_compiled_with_cuda’, lambda: False)())
dev_cnt = 0
try:
dev_cnt = int(getattr(getattr(paddle, ‘device’, object()), ‘cuda’, object()).device_count())
except Exception:
dev_cnt = 0
print(f”Paddle CUDA compiled: {compiled}, CUDA devices: {dev_cnt}”)
except Exception as e:
print(f”Paddle CUDA check error: {e}”)
items: list[tuple[str, list[str] | None]] = [
# (表示名, ディストリ名候補). None の場合はモジュール名=表示名で __version__ を読む
(‘paddle (module)’, None),
(‘paddlepaddle (dist)’, [‘paddlepaddle’]),
(‘paddlepaddle-gpu (dist)’, [‘paddlepaddle-gpu’]),
(‘paddleocr’, [‘paddleocr’]),
(‘paddlex’, [‘paddlex’]),
(‘paddlenlp’, [‘paddlenlp’]),
(‘paddle2onnx’, [‘paddle2onnx’]),
(‘paddlehub’, [‘paddlehub’]),
(‘paddleaudio’, [‘paddleaudio’]),
(‘paddlespeech’, [‘paddlespeech’]),
(‘numpy’, [‘numpy’]),
(‘Pillow’, [‘Pillow’]),
(‘cv2 (opencv-python)’, [‘opencv-python’]),
(‘cv2 (opencv-python-headless)’, [‘opencv-python-headless’]),
(‘cv2 (opencv-contrib-python)’, [‘opencv-contrib-python’]),
(‘PyMuPDF’, [‘PyMuPDF’]),
(‘scikit-image’, [‘scikit-image’]),
(‘imgaug’, [‘imgaug’]),
(‘lmdb’, [‘lmdb’]),
(‘visualdl’, [‘visualdl’]),
(‘rapidfuzz’, [‘rapidfuzz’]),
(‘pdf2docx’, [‘pdf2docx’]),
(‘python-docx’, [‘python-docx’]),
]
for label, dists in items:
if dists is None:
# module name is taken from label prefix before space or entire label
module_name = ‘paddle’
ver = _safe_module_version(module_name)
print(f”- {label}: {ver}”)
else:
ver = _safe_dist_version(dists)
print(f”- {label}: {ver}”)
print(“======================”)
def _can_use_gpu() -> bool:
“””GPUが利用可能かを安全に判定(ビルド/デバイス両面を確認)。”””
try:
compiled = bool(getattr(paddle, ‘is_compiled_with_cuda’, lambda: False)())
device_count = 0
try:
device_count = int(getattr(getattr(paddle, ‘device’, object()), ‘cuda’, object()).device_count())
except Exception:
device_count = 0
return compiled and device_count > 0
except Exception:
return False
def _pdf_page_to_600dpi_png(page) -> bytes:
“””PDFページを600DPIのPNG画像バイト列に変換。”””
# 600 DPI のマトリックス計算 (72 DPI base)
dpi_scale = 600 / 72
mat = fitz.Matrix(dpi_scale, dpi_scale)
# 透明レイヤで欠落しないよう alpha=False を明示
pix = page.get_pixmap(matrix=mat, alpha=False)
return pix.tobytes(“png”)
def _png_bytes_to_bgr_ndarray(png_bytes: bytes) -> np.ndarray:
“””PNGバイト列をBGRのnumpy配列へ(PaddleOCRはBGR想定)。”””
img_rgb = Image.open(BytesIO(png_bytes)).convert(‘RGB’)
arr_rgb = np.array(img_rgb)
return arr_rgb[:, :, ::-1]
def _extract_text_lines(result) -> list[str]:
“””PaddleOCR結果からテキスト行のみを抽出(複数戻り形式に耐性)。”””
lines: list[str] = []
if isinstance(result, list) and result and isinstance(result[0], list):
for item in result[0]:
try:
txt = item[1][0]
if txt:
lines.append(txt)
except Exception:
continue
return lines
if isinstance(result, list):
for item in result:
if isinstance(item, dict):
data = item.get(‘data’) or item.get(‘res’) or item.get(‘result’)
if isinstance(data, list):
for ent in data:
if isinstance(ent, dict):
txt = ent.get(‘text’) or ent.get(‘transcription’)
if txt:
lines.append(txt)
elif isinstance(ent, (list, tuple)) and len(ent) >= 2:
try:
txt = ent[1][0]
if txt:
lines.append(txt)
except Exception:
pass
return lines
def _try_init_ocr(lang_code: str, available_gpu: bool) -> tuple[PaddleOCR, bool]:
“””指定言語でOCRを初期化。GPUが使えればGPU→失敗時CPUの順で試す。”””
# 共通オプション(ページ全体の検出を重視)
common_kwargs = dict(
lang=lang_code,
ocr_version=’PP-OCRv4′,
use_angle_cls=True, # 角度補正を有効化
det_limit_side_len=4096, # 長辺の縮小上限を拡大(既定 960 → 4096)
det_limit_type=’min’, # 短辺基準で縮小→細長ページでも欠落を抑制
det_db_thresh=0.30, # 検出しきい値を下げて薄い文字も拾う
det_db_box_thresh=0.30, # ボックス確定のしきい値
det_db_unclip_ratio=2.0, # 少し広めにボックスを展開
rec_batch_num=64, # 認識のバッチサイズ(環境に応じて調整OK)
)
# GPU 試行
if available_gpu:
try:
paddle.set_device(‘gpu’)
ocr = PaddleOCR(**common_kwargs)
return ocr, True
except Exception:
pass
# CPU フォールバック
paddle.set_device(‘cpu’)
ocr = PaddleOCR(**common_kwargs)
return ocr, False
def _create_ocr_with_fallback() -> tuple[PaddleOCR, str, bool]:
“””GPU/言語を順に試して、動作するPaddleOCRインスタンスを返す。”””
available_gpu = _can_use_gpu()
last_err: Exception | None = None
for lang_code in [‘japan’, ‘ch’]:
try:
ocr, using_gpu = _try_init_ocr(lang_code, available_gpu)
return ocr, lang_code, using_gpu
except Exception as e:
last_err = e
continue
# ここまで来たら致命的
raise RuntimeError(f”PaddleOCRの初期化に失敗しました: {last_err}”)
def pdf_to_text_with_paddleocr():
print(“PaddleOCRを初期化中…”)
ocr, selected_lang, using_gpu = _create_ocr_with_fallback()
print(f”PaddleOCRの初期化完了: lang='{selected_lang}’, GPU={‘ON’ if using_gpu else ‘OFF’}”)
# 入出力フォルダ
pdf_folder = Path(“pdf”)
txt_folder = Path(“txt”)
png_folder = Path(“png”)
txt_folder.mkdir(exist_ok=True)
png_folder.mkdir(exist_ok=True)
print(f”出力フォルダ ‘{txt_folder}’ を準備しました。”)
# PDF一覧
pdf_files = list(pdf_folder.glob(“*.pdf”))
total_files = len(pdf_files)
if total_files == 0:
print(f”‘{pdf_folder}’ フォルダにPDFファイルが見つかりません。”)
return
print(f”処理対象のPDFファイル数: {total_files}個”)
print(“-” * 50)
for file_index, pdf_file in enumerate(pdf_files, 1):
print(f”\n[{file_index}/{total_files}] 処理中: {pdf_file.name}”)
try:
doc = fitz.open(pdf_file)
except Exception as e:
print(f” × PDFを開けませんでした: {e}”)
continue
total_pages = doc.page_count
print(f” 総ページ数: {total_pages}ページ”)
all_text: list[str] = []
for page_num in range(total_pages):
print(f” ページ {page_num + 1}/{total_pages} を処理中…”, end=” “)
page = doc.load_page(page_num)
# PNG作成開始時間
png_start_time = time.time()
# PDFページを600DPIのPNG画像に変換
png_bytes = _pdf_page_to_600dpi_png(page)
# PNGを書き出し(後で同じ画像をOCRに渡す)
png_path = png_folder / f”{pdf_file.stem}_p{page_num + 1:04}.png”
try:
with open(png_path, ‘wb’) as pf:
pf.write(png_bytes)
except Exception as e:
print(f” × PNG保存失敗: {e}”)
# PNG作成終了時間
png_end_time = time.time()
png_duration = png_end_time – png_start_time
print(f”PNG作成時間: {png_duration:.2f}秒”, end=” “)
# PNG画像をBGR配列に変換してOCR処理
img_bgr = _png_bytes_to_bgr_ndarray(png_bytes)
# 互換性のため常に ocr() API を使用(角度補正もON)
result = ocr.ocr(img_bgr, cls=True)
text_lines = _extract_text_lines(result)
# ページ区切りを追加
if page_num == 0:
all_text.append(f”\n///// page {page_num + 1} /////\n”)
else:
all_text.append(f”\n\n///// page {page_num + 1} /////\n”)
all_text.extend(text_lines)
print(f”完了 ({len(text_lines)}行のテキストを検出)”)
doc.close()
output_file = txt_folder / f”{pdf_file.stem}.txt”
try:
with open(output_file, ‘w’, encoding=’utf-8′) as f:
# 改行を削除して連続したテキストとして保存
f.write(“”.join(all_text))
print(f” ✓ 保存完了: {output_file}”)
except Exception as e:
print(f” × 書き込み失敗: {e}”)
print(f” 進捗: {file_index}/{total_files} ({(file_index/total_files)*100:.1f}%)”)
print(“\n” + “=” * 50)
print(f”すべての処理が完了しました! ({total_files}個のファイルを処理)”)
if __name__ == “__main__”:
_print_environment_versions()
pdf_to_text_with_paddleocr()