OCR 精度向上と EasyOCR
— 画像前処理・代替エンジン
1. なぜ前処理が必要か
Tesseract は高品質な文書画像には強いですが、現実のスクリーンショット・写真・スキャンデータでは精度が落ちることがあります。OCR エンジンに渡す前に画像を「認識しやすい形」に加工することで、認識率を大幅に改善できます。
グレースケール化
カラー情報を除去し、輝度情報だけにする。計算コストを下げ、二値化の精度を上げる。
二値化(Binarization)
ピクセルを白か黒の2値に変換。背景と文字のコントラストを最大化する。
ノイズ除去(Denoising)
砂粒状のノイズ・影・汚れを除去する。誤認識の原因となるアーティファクトを消す。
解像度の拡大(Upscaling)
小さい文字は Tesseract が認識しにくい。300 DPI 以上を目安に拡大する。
傾き補正(Deskewing)
スキャンや撮影で斜めになった画像を水平に補正する。
2. Pillow による前処理
OpenCV なしでも Pillow だけで基本的な前処理が可能です。
グレースケール化 + 二値化
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)
コントラスト強調 + シャープ化
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 にかけると認識率が向上します。
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)
3. OpenCV のインストール
OpenCV(cv2)は Pillow より高度な画像処理アルゴリズムを多数備えています。適応的二値化・モルフォロジー処理・ノイズ除去など、OCR 前処理に特に有効な手法が揃っています。
# opencv-python:基本機能
pip install opencv-python
# GUI 機能も含む場合(imshow などを使う場合)
# pip install opencv-python-headless # サーバー環境では headless 版を推奨
import cv2
print(cv2.__version__) # 例: 4.10.x
4. OpenCV による高度な前処理
適応的二値化(Adaptive Thresholding)
固定閾値の二値化は照明ムラのある画像で失敗しやすいです。適応的二値化は局所的に最適な閾値を計算するため、影や照明の変化に強いです。
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)
ノイズ除去
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)
ヒストグラム解析で最適な閾値を自動決定する方法です。手動で閾値を調整する手間が省けます。
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)
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 の実践パターン
前処理は「すべてやれば良い」わけではなく、画像の特性に合わせて組み合わせます。以下のパイプライン関数は汎用的な出発点として使えます。
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 だけで導入できます。特に複雑なフォント・不規則なレイアウト・自然画像内のテキスト認識に強みがあります。
インストール
# EasyOCR のインストール
pip install easyocr
# GPU(CUDA)を使う場合は PyTorch を GPU 版でインストール済みであること
# CPU のみで使う場合は上記だけで OK
~/.EasyOCR/model/ です。ネットワーク環境を確認してから実行してください。
基本的な使い方
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}")
[[x1,y1], [x2,y2], [x3,y3], [x4,y4]] の4点形式(左上→右上→右下→左下)です。Tesseract の left/top/width/height 形式とは異なります。
テキストだけをリストで取得
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 の描画
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 + pytesseract | EasyOCR |
|---|---|---|
| 導入の手軽さ | 中 OS へのバイナリ導入が必要 | 高 pip だけで完結 |
| 日本語精度 | 中 前処理で改善可 | 高 DL モデルで安定 |
| 英語精度 | 高 文書画像では最高水準 | 高 同等レベル |
| 処理速度 | 速い CPU でも高速 | 遅い CPU は重い(GPU で改善) |
| メモリ使用量 | 少ない | 多い モデルが大きい |
| 自然画像対応 | 低 文書画像向け | 高 写真内テキストに強い |
| 傾いたテキスト | 苦手 前処理が必要 | 得意 自動補正 |
| 対応言語数 | 100+ | 80+ |
| カスタマイズ性 | 高 PSM/OEM/whitelist 等 | 中 パラメータは少なめ |
文書・スクリーンショット・PDF スキャン → Tesseract(前処理で精度を補う)
自然画像・看板・レシート・手書き → EasyOCR(DL モデルが強み)
両方試して精度の良い方を採用するのが現実的なアプローチです。
8. 実践スクリプト — 全体まとめ
Tesseract と EasyOCR の両方を試し、結果を比較する実践スクリプトです。
"""
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)
使い方:
# 引数に画像パスを渡して実行
python ocr_compare.py sample.png
# 引数なしの場合は sample.png をデフォルトで使用
python ocr_compare.py
□ 画像の解像度は十分か(300 DPI 以上推奨)
□ コントラストは明瞭か(背景色と文字色の差が大きいか)
□ 傾きや歪みはないか
□ ノイズ・汚れが多くないか
□ PSM(--psm)の値は画像に合っているか
□ 言語指定(lang)は正しいか
□ Tesseract で精度が出ない場合は EasyOCR を試したか