EasyOCR によるシーンテキスト検出・認識(英語・日本語対応)(ソースコードと実行結果)

Python開発環境,ライブラリ類

ここでは、最低限の事前準備について説明する。機械学習や深層学習を行う場合は、NVIDIA CUDA、Visual Studio、Cursorなどを追加でインストールすると便利である。これらについては別ページ https://www.kkaneko.jp/cc/dev/aiassist.htmlで詳しく解説しているので、必要に応じて参照してください。

Python 3.12 のインストール

インストール済みの場合は実行不要。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要である。

REM Python をシステム領域にインストール
winget install --scope machine --id Python.Python.3.12 -e --silent
REM Python のパス設定
set "PYTHON_PATH=C:\Program Files\Python312"
set "PYTHON_SCRIPTS_PATH=C:\Program Files\Python312\Scripts"
echo "%PATH%" | find /i "%PYTHON_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_PATH%" /M >nul
echo "%PATH%" | find /i "%PYTHON_SCRIPTS_PATH%" >nul
if errorlevel 1 setx PATH "%PATH%;%PYTHON_SCRIPTS_PATH%" /M >nul

関連する外部ページ

Python の公式ページ: https://www.python.org/

AI エディタ Windsurf のインストール

Pythonプログラムの編集・実行には、AI エディタの利用を推奨する。ここでは,Windsurfのインストールを説明する。

管理者権限でコマンドプロンプトを起動(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行して、Windsurfをシステム全体にインストールする。管理者権限は、wingetの--scope machineオプションでシステム全体にソフトウェアをインストールするために必要となる。

winget install --scope machine Codeium.Windsurf -e --silent

関連する外部ページ

Windsurf の公式ページ: https://windsurf.com/

必要なライブラリのインストール

コマンドプロンプトを管理者として実行(手順:Windowsキーまたはスタートメニュー > cmd と入力 > 右クリック > 「管理者として実行」)し、以下を実行する


pip install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu126
pip install easyocr opencv-python pillow

EasyOCR によるシーンテキスト検出・認識(英語・日本語対応)プログラム

概要

本プログラムは、視覚情報から文字を認識し理解する。具体的には、カメラや動画に映る日本語文字、英語、数式を検出し、その内容をテキストデータとして抽出する。この過程では、画像中の文字領域の特定と、その領域内の文字パターンの認識という2段階の処理を行う。

主要技術

参考文献

[1] JaidedAI. (2020). EasyOCR: Ready-to-use OCR with 80+ supported languages. GitHub repository. https://github.com/JaidedAI/EasyOCR

ソースコード


# EasyOCR によるシーンテキスト検出・認識(英語・日本語対応)プログラム
# 特徴技術名: EasyOCR
# 出典: Y. Baek et al., "Character Region Awareness for Text Detection," 2019 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR), 2019, pp. 9365-9374, doi: 10.1109/CVPR.2019.00959.
# 特徴機能: CRAFTアルゴリズムによる文字レベルテキスト検出とアフィニティ予測。個々の文字領域を特定し、隣接する文字間の空間的関係を学習することで、任意の形状、湾曲、変形したテキストを含む複雑な屋外シーン画像から高精度にテキストを検出可能。従来の単語レベルのバウンディングボックス検出に比べて柔軟性が大幅に向上。
# 学習済みモデル: CRAFT(Character Region Awareness for Text detection)テキスト検出モデル、CRNN(Convolutional Recurrent Neural Network)テキスト認識モデル。ResNet特徴抽出、LSTM系列ラベリング、CTC復号化による構成。80+言語対応、屋外シーンテキスト検出に特化した事前学習済みモデル。PyTorchベース、モデルハブから自動ダウンロード対応。URL: https://www.jaided.ai/easyocr/modelhub
# 認識可能文字・数字・記号:
#   英字: A-Z(大文字)、a-z(小文字)
#   数字: 0-9
#   日本語: ひらがな(あ-ん)、カタカナ(ア-ン)、漢字(常用漢字・人名用漢字を含む広範な漢字)
#   記号: スペース、句読点(. , ; : ! ? " ' ` 。、)、演算子(+ - * / = < > )、括弧(( ) [ ] { } )、その他記号(@ # $ % & _ | \ / ~ ^ )など、ASCII印刷可能文字(ASCII 32-126)対応
#   特殊文字: 改行、タブなどの制御文字は除く
# 方式設計
#     関連利用技術: OpenCV(動画処理、画像表示、ファイル選択)、PyTorch(深層学習フレームワーク)、urllib(サンプル動画ダウンロード)、tkinter(ファイル選択ダイアログ)
#     入力と出力: 入力: 動画(ユーザは「0:動画ファイル,1:カメラ,2:サンプル動画」のメニューで選択.0:動画ファイルの場合はtkinterでファイル選択.1の場合はOpenCVでカメラが開く.2の場合はhttps://github.com/opencv/opencv/blob/master/samples/data/vtest.aviを使用)、出力: OpenCV画面でリアルタイム表示、バウンディングボックス付きテキスト検出結果、1秒間隔でprint()による処理結果表示、result.txtファイル保存
#     処理手順: 1. EasyOCRリーダー初期化(英語・日本語対応)、2. 動画フレーム取得、3. CRAFTによる文字レベルテキスト検出とバウンディングボックス生成、4. CRNNによる英語・日本語テキスト認識、5. 結果をフレームに描画(バウンディングボックス+認識テキスト)、6. リアルタイム表示と結果保存
#     前処理、後処理: 前処理: フレーム前処理(必要に応じたリサイズ)、後処理: 信頼度による結果フィルタリング、バウンディングボックス座標の正規化
#     追加処理: 信頼度閾値によるテキスト検出結果のフィルタリング(低信頼度結果を除去し精度向上)
#     調整を必要とする設定値: 信頼度閾値(confidence_threshold):テキスト検出の信頼度しきい値、デフォルト0.5、高い値ほど厳格
# 将来方策: 信頼度閾値の自動調整機能(動的に最適な閾値を学習し設定する機能の実装)
# その他の重要事項: GPU使用可能時の自動検出、メモリ使用量の監視、最新版EasyOCR 1.7.2対応、英語と日本語の同時認識対応
# 前準備: pip install easyocr opencv-python pillow

import cv2
import easyocr
import tkinter as tk
from tkinter import filedialog
import os
import time
from PIL import Image, ImageDraw, ImageFont
import numpy as np
import torch

# 設定値
CONFIDENCE_THRESHOLD = 0.5  # テキスト検出の信頼度閾値
FONT_SIZE = 20  # 日本語フォントサイズ
FONT_COLOR = (0, 255, 0)  # テキスト表示色(緑)
PRINT_INTERVAL = 1.0  # 結果表示間隔(秒)
TEXT_PAD = 2  # テキスト背景のパディング
CHAR_W = 15  # 文字幅の概算値

# 画像鮮明化フィルタ(エッジ強調)
SHARPEN_KERNEL = np.array([[-1, -1, -1],
                           [-1,  9, -1],
                           [-1, -1, -1]])

all_results = []
last_print_time = time.time()

print('屋外シーンテキスト検出・認識システム(英語・日本語対応)')
print('=' * 55)
print('認識可能文字・数字・記号:')
print('  英字: A-Z(大文字)、a-z(小文字)')
print('  数字: 0-9')
print('  日本語: ひらがな、カタカナ、漢字')
print('  記号: スペース、句読点(. , ; : ! ? " \' ` 。、)、演算子(+ - * / = < > )')
print('        括弧(( ) [ ] { } )、その他記号(@ # $ % & _ | \\ / ~ ^ )など')
print('        ASCII印刷可能文字(ASCII 32-126)対応')
print('  特徴: 屋外シーンの任意形状・湾曲・変形テキストに対応')
print('=' * 55)
print('操作方法: qキーで終了')

# GPU/CPU自動選択(より確実な判定)
gpu_ok = torch.cuda.is_available()
if gpu_ok:
    try:
        torch.cuda.current_device()  # GPUアクセス確認
        device = 'cuda'
    except:
        gpu_ok = False
        device = 'cpu'
else:
    device = 'cpu'

print(f'使用デバイス: {device}')
print('EasyOCR(英語・日本語)を初期化中...')

try:
    reader = easyocr.Reader(['en', 'ja'], gpu=gpu_ok)
    print('EasyOCR初期化完了')
except Exception as e:
    print(f'EasyOCR初期化エラー: {e}')
    exit()

# フォント読み込み(日本語対応)
font_loaded = False
try:
    font = ImageFont.truetype('C:/Windows/Fonts/msgothic.ttc', FONT_SIZE)
    font_loaded = True
except Exception as e:
    print(f'フォント読み込みエラー: {e}')
    print('日本語フォントが利用できないため、OpenCVの描画機能を使用します')
    font = None


def video_processing(frame):
    global all_results, last_print_time
    try:
        # 画像鮮明化
        sharpened_frame = cv2.filter2D(frame, -1, SHARPEN_KERNEL)

        results = reader.readtext(sharpened_frame)
        processed_frame = frame.copy()
        detected_texts = []

        # フォントが読み込めた場合のみPillow描画を使用
        if font_loaded and font is not None:
            img_pil = Image.fromarray(cv2.cvtColor(processed_frame, cv2.COLOR_BGR2RGB))
            draw = ImageDraw.Draw(img_pil)

        for detection in results:
            if len(detection) == 3:
                bbox, text, confidence = detection
                if confidence >= CONFIDENCE_THRESHOLD:
                    bbox = [[int(x), int(y)] for x, y in bbox]

                    # バウンディングボックスの点数を確認
                    if len(bbox) >= 3:  # 最低3点あれば多角形を描画可能
                        if font_loaded and font is not None:
                            # Pillowでバウンディングボックス描画
                            bbox_points = [(bbox[i][0], bbox[i][1]) for i in range(len(bbox))]
                            bbox_points.append(bbox_points[0])  # 閉じた図形にする
                            draw.line(bbox_points, fill=FONT_COLOR, width=2)

                            # テキスト表示位置の計算(画面内に収まるよう調整)
                            text_y = max(0, bbox[0][1] - 25)  # Y座標が負にならないよう調整
                            text_x = bbox[0][0]
                            display_text = f'{text} ({confidence:.2f})'

                            # 背景の黒色矩形を描画
                            text_width = len(display_text) * CHAR_W
                            draw.rectangle([text_x - TEXT_PAD, text_y - TEXT_PAD,
                                          text_x + text_width + TEXT_PAD, text_y + FONT_SIZE + TEXT_PAD],
                                         fill=(0, 0, 0))

                            # テキスト描画
                            draw.text((text_x, text_y), display_text, font=font, fill=FONT_COLOR)
                        else:
                            # OpenCVでバウンディングボックス描画
                            pts = np.array(bbox, np.int32)
                            pts = pts.reshape((-1, 1, 2))
                            cv2.polylines(processed_frame, [pts], True, FONT_COLOR, 2)

                            # OpenCVでテキスト描画(ASCII文字のみ表示可能)
                            text_y = max(25, bbox[0][1])  # Y座標が画面内に収まるよう調整
                            display_text = f'Text ({confidence:.2f})'  # 日本語は表示できないため簡略化
                            cv2.putText(processed_frame, display_text, (bbox[0][0], text_y - 5),
                                       cv2.FONT_HERSHEY_SIMPLEX, 0.5, FONT_COLOR, 1)

                    detected_texts.append(f'{text} (信頼度: {confidence:.2f})')

        # Pillow描画を使用した場合、OpenCV形式に変換
        if font_loaded and font is not None:
            processed_frame = cv2.cvtColor(np.array(img_pil), cv2.COLOR_RGB2BGR)

        current_time = time.time()
        if current_time - last_print_time >= PRINT_INTERVAL:
            if detected_texts:
                timestamp = time.strftime('%H:%M:%S')
                result_line = f'[{timestamp}] 検出結果: {", ".join(detected_texts)}'
                print(result_line)
                all_results.append(result_line)
            else:
                print('テキストが検出されませんでした')
            last_print_time = current_time

        return processed_frame
    except Exception as e:
        print(f'テキスト検出エラー: {e}')
        return frame


print('0: 動画ファイル')
print('1: カメラ')
print('2: サンプル動画')

choice = input('選択: ')
temp_file = None

if choice == '0':
    root = tk.Tk()
    root.withdraw()
    path = filedialog.askopenfilename()
    if not path:
        exit()
    cap = cv2.VideoCapture(path)
elif choice == '1':
    cap = cv2.VideoCapture(0, cv2.CAP_DSHOW)
    cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
elif choice == '2':
    import urllib.request
    url = 'https://github.com/opencv/opencv/raw/master/samples/data/vtest.avi'
    filename = 'vtest.avi'
    try:
        urllib.request.urlretrieve(url, filename)
        temp_file = filename
        cap = cv2.VideoCapture(filename)
    except Exception as e:
        print(f'動画のダウンロードに失敗しました: {url}')
        print(f'エラー: {e}')
        exit()
else:
    print('無効な選択です')
    exit()

# メイン処理
try:
    while True:
        cap.grab()
        ret, frame = cap.retrieve()
        if not ret:
            break

        processed_frame = video_processing(frame)
        cv2.imshow('Video', processed_frame)

        if cv2.waitKey(1) & 0xFF == ord('q'):
            break
finally:
    cap.release()
    cv2.destroyAllWindows()
    if temp_file:
        os.remove(temp_file)

    if all_results:
        with open('result.txt', 'w', encoding='utf-8') as f:
            for result in all_results:
                f.write(result + '\n')
        print('result.txtに保存')
    else:
        print('保存する結果がありません')