Article

無料のOCRツールで書類デジタル化

高田晃太郎
無料のOCRツールで書類デジタル化

IDCは企業データの80〜90%が非構造化だと報告しており¹、McKinseyはナレッジワーカーが情報探索に全労働時間の約20%を費やすと示しています²。ここでいう非構造化とは、表や列に整理されていないテキストや画像、PDFなどのこと。紙の請求書や契約、手書き申請書といったアナログ資産は、依然として検索不能・分析不能のボトルネックです。SaaSやRPAが当たり前になった今でも、入口のOCR品質次第で後段の自動化が詰まる現実は変わりません。コストに敏感なチームが有償エンジンに踏み切れないとき、TesseractやPaddleOCR、OCR.space、Google Driveの無料OCRをどう組み合わせれば実務品質に届くのか。本稿では、OSSと無料枠APIを活用し、前処理(OCR前に画像を整える工程)・精度評価(CER/WERなどの簡潔な指標)・スケール・ガバナンスまでをCTO視点で通しで設計します。

無料OCRの実力と選定基準を技術・ビジネス両軸で捉える

まず現場品質のラインを決めることが重要です。帳票や契約の検索性向上だけなら全文精度よりもCER(Character Error Rate、文字誤り率)を一定以下に抑え、検索キーや合計金額などのフィールドだけは確実に抽出できれば業務は流れます。反対に仕訳自動化や与信の自動判定まで見据えるなら、版面認識(ページの構造を理解すること)やレイアウト解析、表検出の堅牢性が必要になります。無料の選択肢は大きく二つに分かれます。ローカル実行のOSSエンジンとしてはTesseractとPaddleOCRが代表格です。クラウドの無料枠ではOCR.spaceのフリープランやGoogle Driveのドキュメント変換が実務で使えるレベルにあります。セキュリティの観点では、機微情報はローカル実行を第一選択にし、匿名化やマスキングが可能な場合のみクラウド無料枠を補助的に使うのが安全です。ビジネス面では、OSSはランニングコストがゼロでも、前処理・辞書・監視の内製コストが乗るため、最初からROIの見立てを作っておくと意思決定がぶれません。

環境・前提のベースライン

本稿のコードはPython 3.10系で動作確認できる構成を前提にします。日本語と英語の混在を想定し、300dpiスキャンのA4片面を標準ケースとします。GPUが使える環境ではPaddleOCRの推論速度が伸びますが、CPUのみでも使える設計を示します。以降、Tesseract(活字に強い定番OSS)とPaddleOCR(深層学習ベースでレイアウトに強いOSS)、そしてOCR.spaceとGoogle Drive OCR(無料枠API)を場面に応じて使い分けます。

ローカル実行:TesseractとPaddleOCRの実装と前処理

ローカル実行の利点は、データを外部に出さずに済むことと、バッチ処理時の従量課金が発生しないことです。課題は前処理とレイアウト耐性で、ここを怠ると無料OCRの印象は一気に悪化します。従って最初に画質と幾何補正のパイプラインを確立します。

OpenCVでの歪み補正と二値化:精度の土台を作る

スキャンやスマホ撮影では傾きと陰影がエラーの主要因です。以下は自動傾き補正、コントラスト強調、Otsu二値化までを一つの関数にまとめた例です。異常入力に備え、例外処理とタイムアウト境界を意識します。専門用語は多いですが、やっていることは「見やすく、まっすぐな白黒画像に整える」だけです。

import cv2
import numpy as np
from typing import Tuple

def preprocess(path: str) -> Tuple[np.ndarray, float]:
    img = cv2.imread(path, cv2.IMREAD_GRAYSCALE)
    if img is None:
        raise FileNotFoundError(f"Image not found: {path}")
    blur = cv2.GaussianBlur(img, (5, 5), 0)
    clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8)).apply(blur)
    thresh = cv2.threshold(clahe, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)[1]
    coords = np.column_stack(np.where(thresh == 0))
    angle = 0.0
    if coords.size > 0:
        rect = cv2.minAreaRect(coords)
        angle = rect[-1]
        angle = -(90 + angle) if angle < -45 else -angle
    (h, w) = img.shape[:2]
    M = cv2.getRotationMatrix2D((w // 2, h // 2), angle, 1.0)
    rotated = cv2.warpAffine(thresh, M, (w, h), flags=cv2.INTER_CUBIC, borderMode=cv2.BORDER_REPLICATE)
    return rotated, angle

帳票の表線がOCRの誤認識を誘発する場合は、モルフォロジー演算(膨張・収縮の画像処理)で細い線を抑止します。過剰に消すと文字が欠落するため、画像の解像度に応じてカーネルサイズを調整します。

def suppress_grid(bin_img: np.ndarray) -> np.ndarray:
    kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (1, 3))
    eroded = cv2.erode(bin_img, kernel, iterations=1)
    dilated = cv2.dilate(eroded, kernel, iterations=1)
    return dilated

Tesseract + pytesseract:日本語・英語混在の基本形

Tesseractはオープンソースとして長年活発に開発されてきた主要なOCRエンジンの一つです³。以下は日本語と英語を同時に認識し、時間計測とエラー処理を持たせた最小実装です。縦書き文書はjpn_vert、横書き中心はjpnを使います。PSM(Page Segmentation Mode、ページのレイアウト仮定)やOEM(OCR Engine Mode、エンジンの選択)は簡単に言えば「どんな紙面だと仮定するか」「どの内部エンジンを使うか」を指定するものです。

import shutil
import time
import pytesseract
from pytesseract import Output

def ocr_tesseract(img_path: str) -> dict:
    if not shutil.which("tesseract"):
        raise RuntimeError("Tesseract binary not found in PATH")
    img, angle = preprocess(img_path)
    lang = "jpn+eng"
    start = time.perf_counter()
    try:
        text = pytesseract.image_to_string(img, lang=lang, config="--oem 3 --psm 6")
        data = pytesseract.image_to_data(img, lang=lang, output_type=Output.DICT, config="--oem 3 --psm 6")
    except pytesseract.TesseractError:
        # 日本語辞書が未導入などのケースを想定し、英語のみでフォールバック
        text = pytesseract.image_to_string(img, lang="eng", config="--oem 3 --psm 6")
        data = pytesseract.image_to_data(img, lang="eng", output_type=Output.DICT, config="--oem 3 --psm 6")
    dur = time.perf_counter() - start
    return {"text": text, "angle": angle, "time_sec": round(dur, 3), "boxes": data}

PSMは6(単一の均一ブロック)を起点に、帳票であれば4(複数段落)や11(スパーステキスト)も試すと安定する場合があります。文字候補の信頼度はimage_to_dataのconfで得られるため、閾値で除外して後段のルールベース補正へ回すのが定石です。

PaddleOCR:深層学習ベースでレイアウト耐性を高める

PaddleOCRはディープラーニングの検出+認識で構成され、80言語以上をサポートするオープンソースのドキュメントOCRシステムです⁴,⁷。GPUがあれば推論速度も十分実用的です。以下は日本語モデルを使った例で、タイムアウトと例外処理を含めています。レイアウトが崩れた撮影画像や文字領域がバラつく文書に強みがあります。

from paddleocr import PaddleOCR
import time

def ocr_paddle(img_path: str, use_gpu: bool = False) -> dict:
    ocr = PaddleOCR(lang='japan', use_angle_cls=True, show_log=False, use_gpu=use_gpu)
    start = time.perf_counter()
    try:
        result = ocr.ocr(img_path, cls=True)
    except Exception as e:
        raise RuntimeError(f"PaddleOCR failed: {e}")
    dur = time.perf_counter() - start
    lines = []
    for page in result:
        for line in page:
            txt = line[1][0]
            conf = float(line[1][1])
            if conf >= 0.5:
                lines.append(txt)
    return {"text": "\n".join(lines), "time_sec": round(dur, 3), "lines": len(lines)}

両者の併用も有効です。スキャン品質が高く文字密度が高い場合はTesseract、撮影由来や曲がり、領域混在が激しい場合はPaddleOCRといった役割分担で、精度とスループットをバランスさせます。

精度を可視化する:CER/WERと正規化

導入判断では感覚値ではなく指標で比較します。CER(文字誤り率:正解テキストの文字数に対する誤りの割合)とWER(単語誤り率:単語単位の誤り割合)を測り、キーフィールドの抽出成功率も別軸で見ると良いでしょう。以下はjiwerを使った簡易評価です。正規化(大文字小文字や空白・句読点のゆらぎを揃える)を入れると比較が安定します。

from jiwer import wer, cer
import regex as re

def normalize(s: str) -> str:
    s = s.lower()
    s = re.sub(r"\s+", " ", s)
    s = re.sub(r"[,、]", ",", s)
    return s.strip()

def eval_wer(ref: str, hyp: str) -> float:
    return wer(normalize(ref), normalize(hyp))

def eval_cer(ref: str, hyp: str) -> float:
    return cer(normalize(ref), normalize(hyp))

クラウド無料枠:OCR.spaceとGoogle Drive変換の実務適用

オンプレ優先が原則でも、低リスク文書のピーク処理や、多言語の暫定対応には無料枠APIが役立ちます。無料枠はリクエスト数やファイルサイズの上限があるため、バッチ時はレート制御とリトライ、エラー分類を実装しておきます。個人情報や機微データは投入しない設計を徹底します。

OCR.space API:無料でも手軽に高品質テキスト

OCR.spaceはFree API Keyで小規模利用が可能です⁶。以下はリトライとエラー分類を含む実装です。送信前にローカル前処理を挟むと精度が上がります。

import requests
import time

API_URL = "https://api.ocr.space/parse/image"

def ocr_ocrspace(img_path: str, api_key: str, language: str = "jpn,eng") -> dict:
    for attempt in range(3):
        with open(img_path, 'rb') as f:
            files = {"file": f}
            data = {"language": language, "isTable": True}
            headers = {"apikey": api_key}
            resp = requests.post(API_URL, files=files, data=data, headers=headers, timeout=60)
        if resp.status_code == 200:
            j = resp.json()
            if j.get("IsErroredOnProcessing"):
                # API側のエラーはメッセージを透過
                raise RuntimeError(j.get("ErrorMessage"))
            text = "\n".join(p.get("ParsedText", "") for p in j.get("ParsedResults", []))
            return {"text": text, "credits": j.get("OCRExitCode")}
        if resp.status_code in (429, 503):
            time.sleep(2 ** attempt)
            continue
        resp.raise_for_status()
    raise TimeoutError("OCR.space retry exceeded")

テーブル抽出やPDF対応も可能ですが、無料枠の制限に注意します。APIの応答構造は時折変わるため、スキーマ緩和と監視を入れておくと運用が安定します。

Google Driveのドキュメント変換でOCRする

Google Driveはアップロード時にGoogleドキュメントへ変換することでOCRできます⁵。品質は版面次第ですが、英数字中心の印刷物なら十分実用です。以下はPythonクライアントの例です。変換専用のプロジェクト・サービスアカウントを用意し、処理後は原本を保持せずテキストのみを取り出します。

from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from google.oauth2.service_account import Credentials

SCOPES = ['https://www.googleapis.com/auth/drive']

def ocr_google_drive(service_account_json: str, img_path: str) -> str:
    creds = Credentials.from_service_account_file(service_account_json, scopes=SCOPES)
    service = build('drive', 'v3', credentials=creds)
    file_metadata = {"name": "ocr_upload", "mimeType": "application/vnd.google-apps.document"}
    media = MediaFileUpload(img_path, mimetype='image/png', resumable=True)
    try:
        file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
        doc_id = file.get('id')
        export = service.files().export(fileId=doc_id, mimeType='text/plain').execute()
        service.files().delete(fileId=doc_id).execute()
    except Exception as e:
        raise RuntimeError(f"Drive OCR failed: {e}")
    return export.decode('utf-8')

セキュリティの観点では、変換用の専用プロジェクトとサービスアカウントを分離し、機微ドキュメントは投入しない方針を徹底します。ログには原文を残さず、抽出メタデータのみを記録すると漏洩リスクを抑えられます。

実務で回るパイプライン設計:抽出、正規化、スケール、コスト

OCRを価値に変えるには、単にテキスト化するだけでなく、検索キーや勘定科目、日付、金額といった構造化までをつなげる必要があります。無料エンジンを活かすための設計ポイントを、コードとともに具体化します。

キーフィールド抽出とポストプロセス

請求書の合計金額や日付は正規表現と辞書で堅実に抽出します。下記は通貨・日付の抽出例で、OCRの揺れを想定し全角・半角と区切り記号のゆらぎを吸収します。こうした軽量なルールで「必要なところだけ確実に拾う」設計が、無料エンジンの価値を引き出します。

import regex as re

DATE_RE = re.compile(r"(20\d{2})[./年\-](\d{1,2})[./月\-](\d{1,2})")
AMOUNT_RE = re.compile(r"(合計||税込)\D*([0-90-9,,]+)")

def extract_fields(text: str) -> dict:
    t = re.sub(r"[\u3000\s]+", " ", text)
    date = None
    m = DATE_RE.search(t)
    if m:
        y, mo, d = m.groups()
        date = f"{int(y):04d}-{int(mo):02d}-{int(d):02d}"
    amount = None
    m2 = AMOUNT_RE.search(t)
    if m2:
        raw = m2.group(2).replace(',', ',')
        amount = int(re.sub(r"[^0-9]", "", raw))
    return {"date": date, "amount": amount}

手書きの氏名や住所の補正は辞書ベースの候補生成が効きます。日本語のサジェストにはMeCabやSudachi辞書を活用すると誤字の収束が早まります。高コストな大規模言語モデルのコールは避け、固有名詞については頻度辞書を持つのが無料路線では現実的です。

スループットとSLA:計測コードで見積もりを固める

性能は画像の質とモデルでぶれます。疑似本番のバッチで実測し、ページ毎の処理時間分布を把握します。以下はTesseractの単純ベンチマークで、ページ毎の秒数と毎分処理枚数をログします。少数枚の評価では分布が安定しないため、可能なら数百ページ単位で測るのが無難です。

import glob
import statistics as stats

def bench(paths):
    if not paths:
        return {"avg_sec": 0.0, "p95_sec": 0.0, "pages_per_min": 0.0}
    times = []
    for p in paths:
        r = ocr_tesseract(p)
        times.append(r["time_sec"])
    avg = stats.mean(times)
    p95 = stats.quantiles(times, n=20)[18] if len(times) >= 20 else max(times)
    ppm = 60.0 / avg if avg > 0 else 0
    return {"avg_sec": round(avg, 3), "p95_sec": round(p95, 3), "pages_per_min": round(ppm, 1)}

if __name__ == "__main__":
    report = bench(glob.glob("scans/*.png"))
    print(report)

一般的な報告では、300dpiの活字A4でCPUのみのTesseractが1ページあたりおおよそ0.6〜1.2秒、PaddleOCR(CPU)が0.8〜1.5秒、GPU利用時は0.2〜0.5秒程度まで短縮できるケースがみられます。スキャン品質やノイズで大きく前後するため、必ず手元のサンプルで測定しSLAに落とすことが重要です。

ジョブ制御とスケールアウト:キューとコンテナ

一時的な大量投入には、ワーカーを水平展開できる構成が向きます。画像前処理とOCRを別キューに分け、CPUバインドなTesseractを多並列、GPU前提のPaddleOCRを少並列で動かすと資源効率が上がります。以下は最小のDockerfile例で、同一イメージをワーカー数だけ横に並べられます。

FROM python:3.10-slim
RUN apt-get update && apt-get install -y tesseract-ocr tesseract-ocr-jpn tesseract-ocr-eng libglib2.0-0 libsm6 libxrender1 libxext6 && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
CMD ["python", "worker.py"]

クラウド無料枠APIを組み合わせる場合は、非同期に投げて失敗時のフォールバックをローカルに切り替える二段構えにすると、上限に当たったときのユーザー影響を最小化できます。ジョブのトレーサビリティはメッセージIDとストレージのキーを揃えておくのが運用のコツです。

コスト・ガバナンス:無料の定義を誤解しない

無料エンジンは実行コストがゼロなだけで、学習データの更新や辞書メンテナンス、監視・リトライの開発工数は発生します。開発初期に、対象文書のリスト、必要精度、処理量ピーク、セキュリティ区分を短いドキュメントにまとめ、月次レビューで指標と逸脱を見直す習慣を作ると継続的に品質が上がります。クラウド無料枠の利用は、個人情報や健康・財務データを除外し、匿名化したテストデータに限る運用にするとコンプライアンス対応が容易です。

ケーススタディの組み立て:請求書5000枚/月を無料で捌く

具体的なシナリオを描いてみます。月間5000枚の請求書が届くバックオフィスで、初期は全て無料エンジンで構成し、ピーク日だけクラウド無料枠を併用します。平常時はTesseractを主力に、表が多く撮影混在するロットのみPaddleOCRへ振り分けます。前処理はOpenCVで一律に実施し、合計金額・請求日・取引先名を抽出、社内マスタとファジーマッチで正規化します。日次で100枚のゴールドデータをサンプリングし、CERとフィールド正答率を自動計測します。平均処理時間が閾値を超えたらワーカーを一段増やし、クラウド無料枠のレートを一段上げ、終業までに必ず当日分を消化します。こうしたループを最初に作っておけば、無料構成でも安定したSLAを提示できます。

失敗に強い設計:エラーを前提にする

OCRは失敗します。読み取り不能のページは必ず出ます。それを例外ではなく仕様として扱い、信頼度の低いページは「人に回す」キューへ送るのが現実解です。ログには入力画像のハッシュとバージョン化した前処理・モデルの情報を残し、再処理時は完全に同じ条件を再現できるようにします。人手検証の結果は必ず辞書やルールの改善に反映し、小さなPDCAを高速に回すことで、無料エンジンの限界を少しずつ押し広げられます。

監視と可視化:技術の成果を経営言語に変換する

ダッシュボードには、枚数、平均・P95処理時間、CER、フィールド正答率、手戻り率、そして人手の節約時間を並べます。経営層にはコスト回避額とサイクルタイム短縮を示し、現場には日々の品質指標で早期異常検知を促します。無料であっても、意思決定につながる可視化がなければ価値は伝わりません。

セキュリティと法務の基本線:無料でも守るべきこと

社外APIを使う場合はデータ処理場所、保存期間、転送の暗号化を必ず確認します。匿名化・マスキングは前処理に組み込み、原本の保管は社内ストレージに限定します。OSS利用ではライセンスの確認を怠らず、モデルや辞書の配布条件に抵触しないようにリポジトリ構成を分けておきます。監査に備え、処理フローとアクセス権限を簡潔に図示した資料を常に最新化しておくとガバナンスの説明コストが大きく下がります。

まとめに向けた実装リストの最小核

最後に、実装の最小核として、前処理、ローカルOCR(Tesseract・PaddleOCR)、クラウド補助(OCR.space・Google Drive OCR)、フィールド抽出、評価、そしてベンチ計測までのコード断片を本稿に収めました。これらを小さなワーカーに詰めて横に並べるだけで、無料でも十分に実用的なドキュメントデジタル化ラインが立ち上がります。

まとめ:無料を武器に、精度と運用で勝つ

無料のOCRは魔法ではありませんが、適切な前処理と指標管理、そして役割分担さえ整えれば、紙のボトルネックを現実的なコストで解消できます。Tesseractで堅実に基盤をつくり、PaddleOCRで難画像を押さえ、必要に応じてOCR.spaceやGoogle Driveの無料枠を補助に使う。抽出したテキストは正規表現と辞書で整え、CERやフィールド正答率を継続計測して改善を回す。そんな地に足のついた設計が、今日のバックオフィスと分析基盤をつなぎます。明日、どの文書からデジタル化を始めますか。まずは10枚のゴールドデータを作り、ここで示したコードを走らせ、あなたの環境での数字を手に入れてください。数字が出れば、次の投資判断は迷いません。

参考文献

  1. StorageNewsletter. What Every Executive Needs To Know About Unstructured Data (2023). https://www.storagenewsletter.com/2023/09/07/what-every-executive-needs-to-know-about-unstructured-data/
  2. Forbes Technology Council. Reality Check: Still Spending More Time Gathering Instead Of Analyzing? (2019). https://www.forbes.com/councils/forbestechcouncil/2019/12/17/reality-check-still-spending-more-time-gathering-instead-of-analyzing/
  3. Ars Technica. Google open-sources Tesseract OCR engine (2006). https://arstechnica.com/information-technology/2006/09/7664/
  4. PaddleOCR GitHub repository. https://github.com/PaddlePaddle/PaddleOCR
  5. Google Drive Help. Convert PDF and photo files to text. https://support.google.com/drive/answer/176692?hl=en
  6. OCR.space OCR API documentation. https://ocr.space/ocrapi
  7. Gigazine. 「PaddleOCR」で高精度なOCRを無料で実現する方法(2021). https://gigazine.net/news/20210919-paddleocr/