コンテンツ

pythonからPaddlePaddle/PaddleOCRを試す

結論

2025.8現在のAcrobat Proに付いているOCR機能や10年前に買ったオンプレのABBYY FineReader の方が優秀
だが、無料の中では最高峰か?

なお、この業界での最強はCloud Vision API のテキスト認識機能(google drive ocrとは別物)とみんな言っている。

結論的には、私が使っている既存のOCRシステムから乗り換えならず。
だが、大量のバッチ処理が必要&それなりの精度で良し、という場面ではお役立ち。

 

PaddleOCRの良い点

・無料の中では最高峰?
・python、無料、オンプレという組み合わせなので、AIで5分でプログラム組めて100万件の業務用途などでも無料で無制限に使える
・多少の原稿傾きは許容され修正してくれる

 

PaddleOCRの問題点

・一番の問題は段組がうまく認識できないこと。A3左右にレイアウトされた契約書などは段組の認識が乱れて左右の文章が入り乱れる
・NTTがN七七になったり、ダスキンがダスキソになったりする。これは日本語ネイティブじゃない無料の限界か。むしろ日本人が作っていないのにたいしたものと考えるべきか。
・pdfを直接読み込みしようとすると一部の段組しか読み込みされない。600dpi画像に変換してから読み込みすればすべての文字が認識対象になる(たいした問題ではない)
・固有名詞などは、1ページ目では正しく認識できているが同じ固有名詞を2ページ目では読み間違えたりする(たとえば、らりるれろ(株)→らリるれろ(株)みたいなことになる)

 

PaddleOCRの使い道

人間がデータ入力する前のドラフト入力としても使えるレベルではあるが、ちょっと弱い。入力補助として使いたいならばgoogle Cloud Vision APIやら、読取革命17などを使った方が高精度か。

では、何に使うのか。
文書仕分けのための形態素分析、単語出現頻度カウント、全文検索用データ作成、みたいな統計処理用ならば十分(段組ぐちゃ などもあまり影響しない)

 

PaddleOCRの処理速度

A4片面ぎっしりの契約書をCPU処理して4-5秒ほど。(別途pdfからPNG作成が2秒ほど)

 

PaddleOCRの認識例

A3左右レイアウトであまりうまく認識できていない例。個別の文字認識もいまいち。どちらかというと認識成績悪いパターンと思う。A4片面の太め文字の契約書などはもう少しきちんと読めるレベル。

///// page 3 /////
からざる事由により,本物件が滅失又は毀損して本契約の履行が不可能となった第3条((売買対象面積)甲負担`キイ互いに書面により通知して、本契約を解除できる。但し、毀損が修復可能甲、こは,本物件の売買対象面積を末尾表示の登記記録記載面積とし、—-こは売買甲`ニイユ甲は乙に対し、その責任と負担においてそれを修復して引渡す。尚,実測面積と差異が生じたとしてもによる測量は行わない。「「2:前項により本契約が解除された場合甲は,乙に対し受領済みの金員を無利代金の増減の請求はできない。–2:章息にてすみやかに返還する。甲は、残代金支払い時之に、平成25年12月4日付の土地家屋調査士-保有する関係書類の全てを、乙に交付する。作製の確定実測図及び第10条((公租公課等の分担)甲`32本物件から生ずる収益又は本物件に対して賦課される固定資産税第4条((境界の明示)–平成25年12月4日付の土地家屋調都市計画税等の公租公課並びにガス,水道電気料金及び各種負担金等の諸負担甲は,こに対し残代金支払い時均に宛名名義の如何に拘らず、について.引渡し完了日の前日逐の分を甲の収益又は査士田村製の確定実測図及び隣接地所有者との土地境界確認書を交付し、本負担とし引渡し完了日以降の分を乙の収益又は負担として引渡し完了日に清算当該図面上の明示を以って足りるものとする。物件の境界の明示については、する。尚固定資産税á・都市計画税の起算日は平成27年1月1日とする。2:甲こは引渡し完了日に清算できない収益又は負担があるとき、当該清算額が第5条((所有権の移転の時期及び引渡し)–甲がこれを受領し確定後すみやかに清算する。本物件の所有権は乙が甲に対して売買代金全額を支払い3.こは、本件建物に関する公租公課の清算金を甲に支払うとき、清算金の8%相同時に甲は本物件を乙に引渡す。たときに甲から乙に移転し、–(以下「弓渡し完了日」2:当額を清算金に係る消費税及び地方消費税として前二項の清算金とともに甲に支甲、乙は、本物件の引渡しに際し引渡しを完了した日「払う。という。を記載した書面を作成する。第11条(瓊庇担保責任)第6条((付属物の帰属)-本物件引渡甲は本物件の隠れたる瑁庇について責任を負う。尚、こは、甲に本物件に付帯する一切の付属物(什器備品を含む。の所有権は乙に対し、対し、本物件について、当該取庇を発見したときすみやかに通知して、修復にしと同時に無償にて甲から乙に移転する。-急を要する場合を除いて立会う機会を与えなくてはならない。2.甲は乙に対し、前項の環庇について,引渡し完了日から2年以内に請求を受第 7条 (抵当権等の抹消)—その責任と負担においけたものに限り責任を負い所有権移転時逐にこは中に対し前項の瑁庇により生じた損害の暗甲は、

PaddleOCRの動かし方

バージョン違いが多発して、なかなか動かない。模範解答を最初に示す。ただし、この組み合わせはCPU利用。GPUで動かすにはさらに調整必要。Windows11で動きます。

== Environment Info ==
Python: 3.12.7 (Windows-11-10.0.26100-SP0)
Paddle CUDA compiled: False, CUDA devices: 0
– paddle (module): 2.6.2
– paddlepaddle (dist): 2.6.2
– paddlepaddle-gpu (dist): unknown
– paddleocr: 2.7.0.3
– paddlex: unknown
– paddlenlp: unknown
– paddle2onnx: unknown
– paddlehub: unknown
– paddleaudio: unknown
– paddlespeech: unknown
– numpy: 1.26.4
– Pillow: 11.0.0
– cv2 (opencv-python): unknown
– cv2 (opencv-python-headless): 4.10.0.84
– cv2 (opencv-contrib-python): unknown
– PyMuPDF: 1.26.3
– scikit-image: 0.25.2
– imgaug: 0.4.0
– lmdb: 1.7.3
– visualdl: 2.5.3
– rapidfuzz: 3.13.0
– pdf2docx: 0.5.8
– python-docx: 1.2.0

 

絶対に動くソースコード

これで動かなければあきらめろ。一発で動くソースコード。

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()

 

 

 

TOP