はじめに
OCR(Optical Character Recognition, 光学文字認識)は、画像中に含まれる文字を機械的に読み取り、デジタルテキストに変換する技術です。従来は印刷文字の単純な認識が中心でしたが、現在ではディープラーニングの進歩により、手書き文字、自然画像中の文字、複雑なレイアウトの文書まで高精度で認識できるようになっています。
本記事では、OCRに使われる代表的なAI技術をいくつかまとめました。
技術紹介
従来はルールベースで文字の特徴(エッジなど)を抽出したりして何とか頑張っていましたが、ディープラーニングの進化とともに精度や処理速度が向上していきました。
OCRは大きく以下の2つのステップに分けられます。
- テキスト検出 (Text Detection):画像中の「どこに文字があるか」を検出
- テキスト認識 (Text Recognition):検出した領域から「何の文字か」を認識
ここでは、OCRに使われているディープラーニング技術として代表的なものを紹介します。
簡単な説明にとどめていますので、詳しい解説を知りたい方は論文を参照ください。
代表的なテキスト検出技術
EAST (2017)
入力画像からend-to-endでテキスト領域を回帰する手法です。
従来は、「文字っぽい候補領域を大量生成 → 特徴抽出 → 文字かどうか分類する」といったような複雑なパイプラインでしたが、シンプルなend-to-end構造にすることよって大量の候補領域生成を排除し、高速な検出が可能になりました。処理の流れとしては、「画像をFCNに入力して多数のボックスを出力 → NMSで最終結果のボックスを得る」という簡単なものになっています。
CRAFT (2019)
EASTのような手法は、単語単位やテキストライン単位で検出を行うため、曲がったテキストなどに弱いという課題があります。CRAFTではこれを解決するために、文字領域をピクセル単位で予測するのに加えて、文字間のつながりも予測して単語としてリンクする仕組みを持っています。
各ピクセルが文字領域に属する確率マップと、文字間のつながりを表す確率マップ(Affinity Score)を出力します。Affinity Scoreを使って文字をグルーピングすることで、単語単位のボックスを生成することができます。
DBNet (2020)
従来は「テキスト候補領域を確率マップとして出力 → 後処理で二値化」していました。二値化する際には人手で閾値を決める必要がありますが、最適な値を選び取るのが難しいという問題がありました。
DBNetではこの閾値自体をニューラルネットに組み込んで学習可能にし、確率マップと閾値マップを出力します。この両者を使った微分可能な二値化モジュールによって、後処理が簡素化されるだけでなく検出精度と処理速度も向上しました。
代表的なテキスト認識技術
CRNN (2015)
Convolutional Recurrent Neural Network の略。CNNで特徴を抽出し、RNNで文字列の並び(文脈)を捉え、最後にCTC(Connectionist Temporal Classification)で文字列を出力する構成です。テキスト認識のベースラインとして広く用いられています。
CTCは、入力系列と出力系列の長さが一致しない場合のアライメント(対応関係)を考えなくてよいようにする損失関数です。
例えば「hello」というラベルに対して、出力系列が h-e-ll-oo- のように余分な長さや繰り返しを含んでいても、正しく “hello” にマッピングできるようになります。
こちらの記事が分かりやすかったです。
https://harald-scheidl.medium.com/intuitively-understanding-connectionist-temporal-classification-3797e43a86c
ASTER (2018)
画像をSTN(Spatial Transformer Network)で正規化してから認識するアプローチです。STNは入力画像を幾何的に変換するためのネットワークで、曲がったテキストをまっすぐに補正してから認識用のネットワークに入力する構成になっています。
画像の変換にはTPS変換(Thin Plate Spline)という手法が使われています。TPS変換は、対応する制御点をもとに画像全体を滑らかに曲げ伸ばしする補間手法で、単純なアフィン変換よりも柔軟に変形できるのが特徴です。STNは、このTPS変換で使用する制御点の位置を予測します。
TrOCR (2022)
Microsoftが提案したTransformerベースのOCRモデルです。画像エンコーダ(Vision Transformerなど)とテキストデコーダ(Transformer)を組み合わせたエンコーダ・デコーダ構造になっており、入力画像を系列化した特徴量に変換し、それを自己注意機構によって文脈を考慮しながらテキストに変換します。
従来のCNN+RNNベースのOCRは特徴抽出と系列モデリングを段階的に行っていましたが、TrOCRではTransformerの自己注意により長距離依存関係を直接扱えるため、より複雑なレイアウトや文脈にも対応しやすくなりました。
一方で、TransformerはSelf-Attentionの計算量が高く、モデルサイズが大きいため処理速度が比較的遅くなりがちです。
精度検証
Pythonから利用できるモデルをいくつか使って、簡単に精度を比較してみました。
評価には ICDAR 2015 のデータセットを使用しています。
ここでは画像の文字領域を事前にクロップした画像を入力にして、テキスト認識の精度を確認しました。
使用したモデル
ここでは以下の4つのモデルを使用しました。
1. AWS Textract
Amazonが提供するクラウドベースのOCRサービスです。単なる文字認識だけでなく、文書全体のレイアウト解析(表、フォーム、段組みなど)も得意です。従量課金制のため利用には注意が必要です。
2. TrOCR
上述したテキスト認識のモデルです。テキスト検出からやりたい場合は別のモデルを併用する必要があります。また、Transformerベースで処理速度に不安が残るため、リアルタイム処理が求められるケースでの利用は難しいかもしれません。
3. PaddleOCR
Baiduが開発したオープンソースのOCRフレームワークです。検出から認識まで一通りそろっており、多言語対応も充実しています。精度と速度の両面で高い性能を期待でき、自前でファインチューニングすることも可能なため、実サービスとして開発する際の良い選択肢になりそうです。
4. EasyOCR
コミュニティ主導で開発されているPyTorchベースのOCRライブラリです。インストールしてすぐ使える手軽さが魅力で、100以上の言語に対応しています。研究用のベースラインや簡単なアプリケーション試作には便利ですが、最新の深層学習モデルと比べると精度は控えめのようです。
結果
| モデル | Character Accuracy | Word Accuracy | 処理速度 (秒/画像) |
|---|---|---|---|
| AWS Textract | 85.47% | 74.77% | 1.270 |
| TrOCR | 81.89% | 60.04% | 1.649 |
| PaddleOCR | 67.47% | 54.79% | 0.014 |
| EasyOCR | 52.15% | 27.35% | 0.068 |
※Character Accuracy: 文字レベルの一致割合
※Word Accuracy: 完全一致した単語の割合
精度的にはAWS Textractが最も高精度な結果となりました。ユーザーが手を加えられる余地がない完成されたサービスなので、予想通りの結果と言えそうです。
他3つのパラメータ調整などは全くやっていないので、改善の余地は大いにあるかと思います。PaddleOCRとEasyOCRについては、テキスト検出とテキスト認識のどちらも行う設定にしてしまったので、こちらをテキスト認識だけ行うようにすればもう少し精度が上がるかもしれません。
また、今回使用したデータは自然画像で解像度が非常に低いケースも多く、比較的難しめのタスクになっていたことも悪精度の原因として考えられそうです。
処理速度についてはPaddleOCRが圧倒的に速く、クラウドのAWS TextractとTransformerベースのTrOCRは1枚に1秒以上かかっていました。通信にかかるオーバーヘッドを考えるとAWS TextractよりもTrOCRの方が速くなる気がしましたが、TrOCRが思ったよりも遅かったです(GPUはRTX 4600SUPERを使用)。リアルタイム処理が求められる場合は、PaddleOCRを用途に合わせてファインチューニングするなどして精度を上げていく必要がありそうです。
まとめ
本記事ではOCRに使われている代表的な技術を紹介し、実際にいくつかのモデルを使って精度を確認してみました。
最後に、実装したPythonスクリプトの一部を記載しておきます。
AWS TextractについてはAWS側の設定がいくつか必要ですが、他3つのモデルは簡単な実装ですぐに使うことができるので試してみてください。
from abc import ABC, abstractmethod
from typing import List, Tuple, Optional
import numpy as np
from PIL import Image
import io
class Detector(ABC):
"""OCR検出・認識のための抽象基底クラス"""
@abstractmethod
def predict(self, image: np.ndarray) -> Tuple[List[np.ndarray], List[str], List[float]]:
"""
画像からテキスト検出・認識を実行
Args:
image: 入力画像 (numpy array, RGB format)
Returns:
Tuple containing:
- boxes: バウンディングボックスの座標配列のリスト (各要素は4点または2点の座標)
- texts: 認識されたテキスト文字列のリスト
- scores: 信頼度スコア(0.0-1.0)のリスト
"""
pass
class DetectorFactory:
@staticmethod
def create_detector(model_type: str, **kwargs) -> Detector:
"""
指定されたモデルタイプのDetectorインスタンスを生成
Args:
model_type: モデルタイプ ("paddleocr", "easyocr", "trocr", "textract")
**kwargs: 各Detectorクラスの初期化引数
Returns:
Detectorインスタンス
Raises:
ValueError: 未対応のモデルタイプが指定された場合
"""
model_type = model_type.lower()
if model_type == "paddleocr":
return PaddleOCRDetector(**kwargs)
elif model_type == "easyocr":
return EasyOCRDetector(**kwargs)
elif model_type == "trocr":
return TrOCRDetector(**kwargs)
elif model_type == "textract":
return TextractDetector(**kwargs)
else:
raise ValueError(f"Unsupported model type: {model_type}. "
f"Supported types: paddleocr, easyocr, trocr, textract")
# ========================ここから各Detectorの実装========================================
class PaddleOCRDetector(Detector):
def __init__(self,
use_doc_orientation_classify: bool = False,
use_doc_unwarping: bool = False,
use_textline_orientation: bool = False,
lang: str = "en",
):
"""
Args:
use_doc_orientation_classify: 文書の向き分類を使用するか
use_doc_unwarping: 文書の歪み補正を使用するか
use_textline_orientation: テキスト行の向き検出を使用するか
lang: 言語設定
"""
try:
from paddleocr import PaddleOCR
self.ocr = PaddleOCR(
use_doc_orientation_classify=use_doc_orientation_classify,
use_doc_unwarping=use_doc_unwarping,
use_textline_orientation=use_textline_orientation,
lang=lang
)
except ImportError:
raise ImportError("PaddleOCR is not installed. Please install it with: pip install paddleocr")
def predict(self, image: np.ndarray) -> Tuple[List[np.ndarray], List[str], List[float]]:
res = self.ocr.predict(image)
boxes = res[0]["rec_boxes"] # shape: (N, 4) - [x1, y1, x2, y2]
texts = res[0]["rec_texts"]
scores = res[0]["rec_scores"]
# boxesをnumpy arrayのリストに変換
box_list = [box.astype(np.int32) for box in boxes]
return box_list, texts, scores
class EasyOCRDetector(Detector):
def __init__(self, languages: List[str] = ['en'], gpu: bool = True):
"""
Args:
languages: 認識する言語のリスト
gpu: GPU使用フラグ
"""
try:
import easyocr
self.reader = easyocr.Reader(languages, gpu=gpu)
except ImportError:
raise ImportError("EasyOCR is not installed. Please install it with: pip install easyocr")
def predict(self, image: np.ndarray) -> Tuple[List[np.ndarray], List[str], List[float]]:
results = self.reader.readtext(image)
boxes = []
texts = []
scores = []
for result in results:
box = np.array(result[0], dtype=np.int32) # 4点の座標
text = result[1]
score = result[2]
boxes.append(box)
texts.append(text)
scores.append(score)
return boxes, texts, scores
class TrOCRDetector(Detector):
def __init__(self,
trocr_model: str = "microsoft/trocr-base-printed",
device: Optional[str] = None,
use_text_detection: bool = False
)
"""
Args:
trocr_model: 使用するTrOCRモデル名
device: 使用するデバイス("cuda", "cpu", Noneで自動選択)
use_text_detection: 前段でテキスト検出を行うかどうか(Falseの場合、画像全体を認識)
"""
self.use_text_detection = use_text_detection
try:
import torch
from transformers import TrOCRProcessor, VisionEncoderDecoderModel
if device is None:
self.device = "cuda" if torch.cuda.is_available() else "cpu"
else:
self.device = device
self.processor = TrOCRProcessor.from_pretrained(trocr_model)
self.model = VisionEncoderDecoderModel.from_pretrained(trocr_model).to(self.device)
# テキスト検出を使用する場合のみTextDetectionを初期化
if self.use_text_detection:
from paddleocr import TextDetection
self.text_detector = TextDetection()
else:
self.text_detector = None
except ImportError as e:
raise ImportError(f"Required packages not installed: {e}")
def predict(self, image: np.ndarray) -> Tuple[List[np.ndarray], List[str], List[float]]:
import torch
boxes = []
texts = []
scores = []
if self.use_text_detection and self.text_detector is not None:
# PaddleOCRでテキスト領域を検出してから各領域でTrOCRによる認識を実行
detection_results = self.text_detector.predict(image)
pred_polys = detection_results[0]["dt_polys"]
# 各検出領域でTrOCRによる認識を実行
for poly in pred_polys:
poly = np.array(poly).astype(int)
x1, y1 = np.min(poly, axis=0)
x2, y2 = np.max(poly, axis=0)
# 領域をクロップ
crop_img = image[y1:y2, x1:x2]
# TrOCRで認識
pixel_values = self.processor(images=crop_img, return_tensors="pt").pixel_values.to(self.device)
with torch.no_grad():
generated_ids = self.model.generate(pixel_values, max_length=256)
text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
boxes.append(poly.astype(np.int32))
texts.append(text)
scores.append(1.0) # TrOCRはスコアを返さないので1.0を設定
else:
# テキスト検出を行わず、画像全体をTrOCRで認識
pixel_values = self.processor(images=image, return_tensors="pt").pixel_values.to(self.device)
with torch.no_grad():
generated_ids = self.model.generate(pixel_values, max_length=256)
text = self.processor.batch_decode(generated_ids, skip_special_tokens=True)[0].strip()
# 画像全体のバウンディングボックスを作成
h, w = image.shape[:2]
whole_image_box = np.array([[0, 0], [w, 0], [w, h], [0, h]], dtype=np.int32)
boxes.append(whole_image_box)
texts.append(text)
scores.append(1.0)
return boxes, texts, scores
class TextractDetector(Detector):
def __init__(self, region_name: str = "ap-northeast-2"):
"""
Args:
region_name: AWS リージョン名
"""
try:
import boto3
self.textract = boto3.client("textract", region_name=region_name)
except ImportError:
raise ImportError("boto3 is not installed. Please install it with: pip install boto3")
def predict(self, image: np.ndarray) -> Tuple[List[np.ndarray], List[str], List[float]]:
pil_image = Image.fromarray(image)
buf = io.BytesIO()
pil_image.save(buf, format="PNG")
image_bytes = buf.getvalue()
# Textractで処理
response = self.textract.detect_document_text(Document={"Bytes": image_bytes})
boxes = []
texts = []
scores = []
h, w = image.shape[:2]
for block in response.get("Blocks", []):
if block.get("BlockType") == "LINE":
# バウンディングボックスを取得(正規化座標)
bbox = block.get("Geometry", {}).get("BoundingBox", {})
text = block.get("Text", "")
confidence = block.get("Confidence", 0.0) / 100.0 # 0-1の範囲に正規化
# 正規化座標を画像座標に変換
x1 = int(bbox.get("Left", 0) * w)
y1 = int(bbox.get("Top", 0) * h)
x2 = int((bbox.get("Left", 0) + bbox.get("Width", 0)) * w)
y2 = int((bbox.get("Top", 0) + bbox.get("Height", 0)) * h)
# 矩形の4点を定義
box = np.array([[x1, y1], [x2, y1], [x2, y2], [x1, y2]], dtype=np.int32)
boxes.append(box)
texts.append(text)
scores.append(confidence)
return boxes, texts, scores
# 使用例
if __name__ == "__main__":
# Detectorを作成
detector = DetectorFactory.create_detector("paddleocr", lang="en")
# 画像を読み込んで予測
image = np.array(Image.open("img.jpg"))
boxes, texts, scores = detector.predict(image)
print(f"検出されたテキスト数: {len(texts)}")
for text, score in zip(texts, scores):
print(f"テキスト: {text}, スコア: {score:.3f}")
