プログラミング / 言語 / Python

OCR 精度向上と EasyOCR
— 画像前処理・代替エンジン

1. なぜ前処理が必要か

Tesseract は高品質な文書画像には強いですが、現実のスクリーンショット・写真・スキャンデータでは精度が落ちることがあります。OCR エンジンに渡す前に画像を「認識しやすい形」に加工することで、認識率を大幅に改善できます。

1

グレースケール化

カラー情報を除去し、輝度情報だけにする。計算コストを下げ、二値化の精度を上げる。

2

二値化(Binarization)

ピクセルを白か黒の2値に変換。背景と文字のコントラストを最大化する。

3

ノイズ除去(Denoising)

砂粒状のノイズ・影・汚れを除去する。誤認識の原因となるアーティファクトを消す。

4

解像度の拡大(Upscaling)

小さい文字は Tesseract が認識しにくい。300 DPI 以上を目安に拡大する。

5

傾き補正(Deskewing)

スキャンや撮影で斜めになった画像を水平に補正する。

2. Pillow による前処理

OpenCV なしでも Pillow だけで基本的な前処理が可能です。

グレースケール化 + 二値化

Python
import pytesseract
from PIL import Image, ImageFilter, ImageOps

# 元画像を開く
img = Image.open("sample.png")

# ① グレースケール化
gray = img.convert('L')

# ② 二値化(閾値 127 で白/黒に分ける)
binary = gray.point(lambda px: 255 if px > 127 else 0, '1')

# ③ OCR にかける
text = pytesseract.image_to_string(binary, lang='jpn', config='--psm 6')
print(text)

コントラスト強調 + シャープ化

Python
import pytesseract
from PIL import Image, ImageEnhance, ImageFilter

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

# コントラストを 2 倍に強調
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(2.0)

# シャープ化フィルタを適用
img = img.filter(ImageFilter.SHARPEN)

# グレースケール化
gray = img.convert('L')

text = pytesseract.image_to_string(gray, lang='jpn', config='--psm 6')
print(text)

解像度の拡大(Upscaling)

小さい画像は 2〜3 倍に拡大してから OCR にかけると認識率が向上します。

Python
import pytesseract
from PIL import Image

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

# 元のサイズを確認
print(f"元サイズ: {img.size}")  # (width, height)

# 2 倍に拡大(LANCZOS は高品質なリサンプリングフィルタ)
scale = 2
new_size = (img.width * scale, img.height * scale)
img_large = img.resize(new_size, Image.LANCZOS)
print(f"拡大後: {img_large.size}")

# グレースケール → OCR
gray = img_large.convert('L')
text = pytesseract.image_to_string(gray, lang='jpn', config='--psm 6')
print(text)
Tip: Tesseract は 300 DPI 以上の画像で最も安定した精度を発揮します。印刷物のスキャン画像なら DPI を確認し、不足していれば拡大してから OCR にかけましょう。

3. OpenCV のインストール

OpenCV(cv2)は Pillow より高度な画像処理アルゴリズムを多数備えています。適応的二値化・モルフォロジー処理・ノイズ除去など、OCR 前処理に特に有効な手法が揃っています。

bash
# opencv-python:基本機能
pip install opencv-python

# GUI 機能も含む場合(imshow などを使う場合)
# pip install opencv-python-headless  # サーバー環境では headless 版を推奨
Python
import cv2
print(cv2.__version__)  # 例: 4.10.x

4. OpenCV による高度な前処理

適応的二値化(Adaptive Thresholding)

固定閾値の二値化は照明ムラのある画像で失敗しやすいです。適応的二値化は局所的に最適な閾値を計算するため、影や照明の変化に強いです。

Python
import cv2
import pytesseract
from PIL import Image
import numpy as np

# OpenCV で画像を読み込む
img_cv = cv2.imread("sample.png")

# グレースケール変換
gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)

# 適応的二値化(照明ムラに強い)
binary = cv2.adaptiveThreshold(
    gray,
    maxValue=255,
    adaptiveMethod=cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
    thresholdType=cv2.THRESH_BINARY,
    blockSize=11,   # 近傍ブロックサイズ(奇数)
    C=2             # 定数(ブロックの平均から引く値)
)

# Pillow Image に変換して pytesseract に渡す
pil_img = Image.fromarray(binary)
text = pytesseract.image_to_string(pil_img, lang='jpn', config='--psm 6')
print(text)

ノイズ除去

Python
import cv2
import pytesseract
from PIL import Image

img_cv = cv2.imread("noisy_sample.png")
gray   = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)

# ガウシアンブラーでノイズを除去(軽度なノイズに有効)
blur = cv2.GaussianBlur(gray, (3, 3), 0)

# 二値化
_, binary = cv2.threshold(blur, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)

# 高度なノイズ除去(処理は重いが効果的)
# denoised = cv2.fastNlMeansDenoising(gray, h=10)

pil_img = Image.fromarray(binary)
text = pytesseract.image_to_string(pil_img, lang='jpn', config='--psm 6')
print(text)

大津の二値化(Otsu's Binarization)

ヒストグラム解析で最適な閾値を自動決定する方法です。手動で閾値を調整する手間が省けます。

Python
import cv2
import pytesseract
from PIL import Image

img_cv = cv2.imread("sample.png")
gray   = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)

# 大津の二値化(閾値 0 を指定 + THRESH_OTSU で自動決定)
thresh_val, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
print(f"自動決定された閾値: {thresh_val}")

pil_img = Image.fromarray(binary)
text = pytesseract.image_to_string(pil_img, lang='jpn', config='--psm 6')
print(text)

傾き補正(Deskewing)

Python
import cv2
import numpy as np
import pytesseract
from PIL import Image

def deskew(img_cv: np.ndarray) -> np.ndarray:
    """画像の傾きを検出して補正する"""
    gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)
    # エッジ検出
    edges = cv2.Canny(gray, 50, 150, apertureSize=3)
    # ハフ変換で直線を検出
    lines = cv2.HoughLinesP(edges, 1, np.pi / 180, threshold=100,
                            minLineLength=100, maxLineGap=10)
    if lines is None:
        return img_cv

    # 各直線の角度を計算
    angles = []
    for line in lines:
        x1, y1, x2, y2 = line[0]
        angle = np.degrees(np.arctan2(y2 - y1, x2 - x1))
        angles.append(angle)

    # 中央値の角度を補正角度として使用
    median_angle = np.median(angles)
    if abs(median_angle) > 45:
        median_angle = 90 - abs(median_angle)

    # アフィン変換で回転補正
    h, w = img_cv.shape[:2]
    center = (w // 2, h // 2)
    M = cv2.getRotationMatrix2D(center, median_angle, 1.0)
    rotated = cv2.warpAffine(img_cv, M, (w, h),
                             flags=cv2.INTER_CUBIC,
                             borderMode=cv2.BORDER_REPLICATE)
    print(f"補正角度: {median_angle:.2f}°")
    return rotated


img_cv = cv2.imread("tilted_sample.png")
corrected = deskew(img_cv)

pil_img = Image.fromarray(cv2.cvtColor(corrected, cv2.COLOR_BGR2RGB))
text = pytesseract.image_to_string(pil_img, lang='jpn', config='--psm 6')
print(text)

5. 前処理 + OCR の実践パターン

前処理は「すべてやれば良い」わけではなく、画像の特性に合わせて組み合わせます。以下のパイプライン関数は汎用的な出発点として使えます。

Python — 前処理パイプライン
import cv2
import pytesseract
import numpy as np
from PIL import Image

def preprocess_for_ocr(
    image_path: str,
    scale: float = 2.0,
    use_adaptive: bool = True
) -> Image.Image:
    """
    OCR 前処理パイプライン
    1. 解像度拡大
    2. グレースケール
    3. ノイズ除去
    4. 二値化(適応的 or 大津)
    """
    img_cv = cv2.imread(image_path)
    if img_cv is None:
        raise FileNotFoundError(f"画像が見つかりません: {image_path}")

    # ① 解像度拡大(scale 倍)
    h, w = img_cv.shape[:2]
    img_cv = cv2.resize(img_cv, (int(w * scale), int(h * scale)),
                        interpolation=cv2.INTER_LANCZOS4)

    # ② グレースケール
    gray = cv2.cvtColor(img_cv, cv2.COLOR_BGR2GRAY)

    # ③ ガウシアンブラーでノイズ除去
    blur = cv2.GaussianBlur(gray, (3, 3), 0)

    # ④ 二値化
    if use_adaptive:
        binary = cv2.adaptiveThreshold(
            blur, 255,
            cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
            cv2.THRESH_BINARY, 11, 2
        )
    else:
        _, binary = cv2.threshold(blur, 0, 255,
                                  cv2.THRESH_BINARY + cv2.THRESH_OTSU)

    return Image.fromarray(binary)


def extract_text(
    image_path: str,
    lang: str = 'jpn',
    psm: int = 6,
    scale: float = 2.0,
    use_adaptive: bool = True,
    min_conf: int = 60
) -> dict:
    """
    前処理 → OCR → 信頼度フィルタリング を行うまとめ関数
    Returns: {'text': str, 'words': list[dict]}
    """
    # 前処理
    processed = preprocess_for_ocr(image_path, scale, use_adaptive)

    # テキスト全文
    full_text = pytesseract.image_to_string(
        processed, lang=lang, config=f'--psm {psm}'
    ).strip()

    # 信頼度付き単語リスト
    df = pytesseract.image_to_data(
        processed, lang=lang, config=f'--psm {psm}',
        output_type=pytesseract.Output.DATAFRAME
    )
    words = []
    for _, row in df.iterrows():
        if row['conf'] >= min_conf and isinstance(row['text'], str) and row['text'].strip():
            words.append({
                'text': row['text'],
                'conf': int(row['conf']),
                'box': (int(row['left']), int(row['top']),
                        int(row['width']), int(row['height']))
            })

    return {'text': full_text, 'words': words}


# 使用例
if __name__ == '__main__':
    result = extract_text('sample.png', lang='jpn', scale=2.0)
    print("=== 抽出テキスト ===")
    print(result['text'])
    print(f"\n単語数: {len(result['words'])}")
    for w in result['words'][:5]:
        print(f"  '{w['text']}' conf={w['conf']} box={w['box']}")

6. EasyOCR — 代替エンジン

EasyOCR は深層学習(CRAFT + CRNN)ベースの OCR ライブラリです。Tesseract と異なり、外部バイナリのインストールが不要で、pip だけで導入できます。特に複雑なフォント・不規則なレイアウト・自然画像内のテキスト認識に強みがあります。

インストール

bash
# EasyOCR のインストール
pip install easyocr

# GPU(CUDA)を使う場合は PyTorch を GPU 版でインストール済みであること
# CPU のみで使う場合は上記だけで OK
注意: EasyOCR の初回実行時は言語モデルを自動ダウンロードします(数百 MB〜1 GB 程度)。ダウンロード先は ~/.EasyOCR/model/ です。ネットワーク環境を確認してから実行してください。

基本的な使い方

Python
import easyocr

# Reader を初期化(対応言語を指定)
# 初回は言語モデルをダウンロード(数分かかる場合あり)
reader = easyocr.Reader(['ja', 'en'])  # 日本語 + 英語

# 画像から文字を検出・認識
results = reader.readtext('sample.png')

# results は [(bounding_box, text, confidence), ...] のリスト
for bbox, text, conf in results:
    print(f"テキスト: {text!r:30s} 信頼度: {conf:.3f} 座標: {bbox}")
出力例
テキスト: 'Hello World' 信頼度: 0.987 座標: [[10, 5], [180, 5], [180, 35], [10, 35]] テキスト: '日本語テスト' 信頼度: 0.812 座標: [[10, 50], [220, 50], [220, 80], [10, 80]]
bounding_box の形式: EasyOCR の座標は [[x1,y1], [x2,y2], [x3,y3], [x4,y4]] の4点形式(左上→右上→右下→左下)です。Tesseract の left/top/width/height 形式とは異なります。

テキストだけをリストで取得

Python
import easyocr

reader = easyocr.Reader(['ja', 'en'])

# detail=0 にすると [text, text, ...] のシンプルなリストで返る
texts = reader.readtext('sample.png', detail=0)
print(texts)
# ['Hello World', '日本語テスト', ...]

# 改行で連結して全文を得る
full_text = '\n'.join(texts)
print(full_text)

bounding box の描画

Python
import easyocr
import cv2
import numpy as np

reader = easyocr.Reader(['ja', 'en'])
results = reader.readtext('sample.png')

img_cv = cv2.imread('sample.png')

for bbox, text, conf in results:
    if conf < 0.5:
        continue  # 信頼度が低いものはスキップ

    # bounding box を整数に変換
    pts = np.array(bbox, dtype=np.int32)

    # 四角形を描画
    cv2.polylines(img_cv, [pts], isClosed=True, color=(0, 0, 255), thickness=2)

    # テキストラベルを描画
    x, y = pts[0]
    cv2.putText(img_cv, f"{text} ({conf:.2f})",
                (x, y - 5), cv2.FONT_HERSHEY_SIMPLEX,
                0.5, (0, 0, 255), 1)

cv2.imwrite('output_easyocr.png', img_cv)
print("output_easyocr.png に保存しました")

7. Tesseract vs EasyOCR の比較

項目Tesseract + pytesseractEasyOCR
導入の手軽さ OS へのバイナリ導入が必要 pip だけで完結
日本語精度 前処理で改善可 DL モデルで安定
英語精度 文書画像では最高水準 同等レベル
処理速度 速い CPU でも高速 遅い CPU は重い(GPU で改善)
メモリ使用量 少ない 多い モデルが大きい
自然画像対応 文書画像向け 写真内テキストに強い
傾いたテキスト 苦手 前処理が必要 得意 自動補正
対応言語数 100+ 80+
カスタマイズ性 PSM/OEM/whitelist 等 パラメータは少なめ
選択指針:
文書・スクリーンショット・PDF スキャン → Tesseract(前処理で精度を補う)
自然画像・看板・レシート・手書き → EasyOCR(DL モデルが強み)
両方試して精度の良い方を採用するのが現実的なアプローチです。

8. 実践スクリプト — 全体まとめ

Tesseract と EasyOCR の両方を試し、結果を比較する実践スクリプトです。

Python — 実践スクリプト(全体)
"""
ocr_compare.py
画像から文字を抽出し、Tesseract と EasyOCR の結果を比較するスクリプト
"""

import sys
import cv2
import pytesseract
import easyocr
import numpy as np
from PIL import Image, ImageDraw


# === 前処理関数 ===================================================

def preprocess(image_path: str, scale: float = 2.0) -> tuple:
    """
    画像を前処理する
    Returns: (pil_img_for_tesseract, cv2_img_original)
    """
    img_cv = cv2.imread(image_path)
    if img_cv is None:
        raise FileNotFoundError(f"画像が見つかりません: {image_path}")

    # 拡大
    h, w = img_cv.shape[:2]
    img_large = cv2.resize(img_cv, (int(w * scale), int(h * scale)),
                           interpolation=cv2.INTER_LANCZOS4)

    # グレースケール + 適応的二値化
    gray = cv2.cvtColor(img_large, cv2.COLOR_BGR2GRAY)
    blur = cv2.GaussianBlur(gray, (3, 3), 0)
    binary = cv2.adaptiveThreshold(
        blur, 255,
        cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
        cv2.THRESH_BINARY, 11, 2
    )
    pil_img = Image.fromarray(binary)
    return pil_img, img_cv


# === Tesseract OCR ================================================

def run_tesseract(pil_img: Image.Image, lang: str = 'jpn+eng') -> dict:
    """Tesseract で OCR を実行"""
    # Windows の場合はパスを指定
    # pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'

    text = pytesseract.image_to_string(pil_img, lang=lang, config='--psm 6').strip()
    df = pytesseract.image_to_data(pil_img, lang=lang, config='--psm 6',
                                   output_type=pytesseract.Output.DATAFRAME)
    words = [
        {'text': r['text'], 'conf': int(r['conf'])}
        for _, r in df.iterrows()
        if r['conf'] >= 60 and isinstance(r['text'], str) and r['text'].strip()
    ]
    return {'text': text, 'words': words}


# === EasyOCR =====================================================

def run_easyocr(image_path: str, langs: list = None) -> dict:
    """EasyOCR で OCR を実行"""
    if langs is None:
        langs = ['ja', 'en']
    reader = easyocr.Reader(langs, gpu=False)  # CPU モード
    results = reader.readtext(image_path)
    words = [
        {'text': text, 'conf': round(conf, 3)}
        for _, text, conf in results
        if conf >= 0.5
    ]
    full_text = '\n'.join(w['text'] for w in words)
    return {'text': full_text, 'words': words}


# === bounding box 描画 ============================================

def draw_bboxes_tesseract(image_path: str, lang: str = 'jpn+eng',
                           scale: float = 2.0) -> Image.Image:
    """Tesseract の bounding box を描画した画像を返す"""
    pil_img, _ = preprocess(image_path, scale)
    df = pytesseract.image_to_data(pil_img, lang=lang,
                                   output_type=pytesseract.Output.DATAFRAME)
    draw = ImageDraw.Draw(pil_img.convert('RGB'))
    for _, row in df.iterrows():
        if row['conf'] >= 60 and isinstance(row['text'], str) and row['text'].strip():
            x, y, w, h = int(row['left']), int(row['top']), int(row['width']), int(row['height'])
            draw.rectangle([x, y, x + w, y + h], outline='red', width=2)
    return pil_img.convert('RGB')


# === メイン =======================================================

def main(image_path: str):
    print(f"対象画像: {image_path}\n")

    # 前処理
    pil_img, img_cv = preprocess(image_path, scale=2.0)

    # Tesseract
    print("=" * 50)
    print("【Tesseract OCR】")
    tess_result = run_tesseract(pil_img, lang='jpn+eng')
    print(tess_result['text'])
    print(f"→ 認識単語数: {len(tess_result['words'])}")

    # EasyOCR
    print("\n" + "=" * 50)
    print("【EasyOCR】")
    easy_result = run_easyocr(image_path, langs=['ja', 'en'])
    print(easy_result['text'])
    print(f"→ 認識単語数: {len(easy_result['words'])}")

    # bounding box 画像を保存
    bbox_img = draw_bboxes_tesseract(image_path, scale=2.0)
    bbox_img.save("output_bbox.png")
    print("\nbounding box 画像を output_bbox.png に保存しました")


if __name__ == '__main__':
    path = sys.argv[1] if len(sys.argv) > 1 else 'sample.png'
    main(path)

使い方:

bash
# 引数に画像パスを渡して実行
python ocr_compare.py sample.png

# 引数なしの場合は sample.png をデフォルトで使用
python ocr_compare.py
出力例
対象画像: sample.png ================================================== 【Tesseract OCR】 Hello, DevNotes Python OCR 入門 → 認識単語数: 5 ================================================== 【EasyOCR】 Hello, DevNotes Python OCR 入門 → 認識単語数: 5 bounding box 画像を output_bbox.png に保存しました
精度チューニングのチェックリスト:
□ 画像の解像度は十分か(300 DPI 以上推奨)
□ コントラストは明瞭か(背景色と文字色の差が大きいか)
□ 傾きや歪みはないか
□ ノイズ・汚れが多くないか
□ PSM(--psm)の値は画像に合っているか
□ 言語指定(lang)は正しいか
□ Tesseract で精度が出ない場合は EasyOCR を試したか