Inside of LOVOT

GROOVE X 技術ブログ

実は使えた!iPhoneのようなOCR機能をCLIツールで!

こんにちは、GROOVE X SWチームの Junya Fukuda です。今回は小ネタです。

概要

  • macOSのVision Frameworkを使うとローカルで高精度OCRができる
  • CLIツールとして実装すれば大量の画像を一気に処理できる
  • Swift + Pythonで簡単に自動化できる
  • 注意:Macでしか動きません!

参考リンク

たくさんの画像から文字列をガッと取得したい

大量の画像があって、そこからテキストを抽出したいケースがあったりしませんか?

  • 会議のホワイトボード写真からテキストを抽出したい
  • 紙の資料をスキャンしてテキスト化したい
  • スクリーンショットからエラーメッセージを集めたい

今回のちょっとしたお話では、1000枚を超える画像の文字起こしでした。一つずつ手作業で文字起こしするのはとても辛いです。テクノロジーに頼りたい。

サンプル画像です。絵文字も下手すぎる https://tech.groove-x.com/entry/20231224/1703428678 より

既存のOCR手段と、その微妙なところ

対話型 AI サービスでOCR

ChatGPTやGeminiなどの対話型 AI サービスは画像認識もできるので、「これ読んで!」と画像をアップロードすれば文字を抽出してくれます。とても便利なのですが、気になる点もあります。

  • 精度が微妙に不安定(特に日本語)
  • 画像をアップロードするのに手間がかかる
    • 大量の画像をまとめて処理するのに工夫が必要
  • 会社の機密情報をアップロードして大丈夫か心配になる

クラウドのOCRサービス

Vision AI | Google CloudAmazon Textract | AWSなどのサービスも選択肢の候補になりますが、やはり気になる点もあります。

  • 精度は高い
  • 無料の範囲でも利用できるが、数が多いと費用がかかる
  • やはり機密情報の扱いを気にする必要がある

iPhoneの標準機能

iPhoneなら画像から文字を抽出する機能があって便利ですが、

  • でも1枚ずつしか処理できない
  • バッチ処理とか自動化ができない
  • データの収集・整理が大変

セキュリティを気にせず、大量の画像をまとめて、簡単に、というと手段が限られてしまいます。

Apple Vision Frameworkという救世主

実は、Appleが提供しているVision Frameworkというものを使えば、macOS上で高精度なOCRができます!しかもローカル処理なのでセキュリティの心配もありません。 さらに、XCodeではCLIツールも作れてしまうので、大量の画像処理も自動化できてしまいます。

XcodeでCLIツールを作ろう

Xcodeを使えば、Swift言語でコマンドラインツールが簡単に作れます。

  1. Xcodeを開いて「Command Line Tool」プロジェクトを作成
  2. 言語はSwiftを選択
  3. 「Vision」と「AppKit」フレームワークを追加

[ここにXcodeのプロジェクト作成画面] alt: Xcodeでコマンドラインツールを作成している画面

OCRを実装する

コードの全体はこのような感じです。

import Foundation
import Vision
import AppKit // NSImageを使用するためにインポート

// OCRを実行する関数
func performOCR(on imagePath: String) {
    let fileURL = URL(fileURLWithPath: imagePath)
    
    // 画像をロード
    guard let image = NSImage(contentsOf: fileURL) else {
        print("Failed to load the image at \(imagePath)")
        return
    }
    
    guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else {
        print("Failed to convert the image to CGImage.")
        return
    }
    
    let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
    let request = VNRecognizeTextRequest { (request, error) in
        if let error = error {
            print("Error during text recognition: \(error.localizedDescription)")
            return
        }
        
        guard let observations = request.results as? [VNRecognizedTextObservation] else {
            print("No text found in the image.")
            return
        }
        
        // 抽出した文字列を表示
        let recognizedText = observations.compactMap { $0.topCandidates(1).first?.string }.joined(separator: "\n")
        print("Recognized Text:")
        print(recognizedText)
    }
    
    request.recognitionLevel = .accurate  // 精度優先(ちょっと遅い)
    request.recognitionLanguages = ["ja", "en"]  // 日本語と英語を認識
    request.usesLanguageCorrection = true  // 言語的な文脈も考慮する

    do {
        try requestHandler.perform([request])
    } catch {
        print("Failed to perform text recognition: \(error.localizedDescription)")
    }
}

// コマンドライン引数を取得
let arguments = CommandLine.arguments

if arguments.count < 2 {
    print("Usage: ocr_tool <image_path>")
    exit(1)
}

// 画像パスを引数から取得
let imagePath = arguments[1]
performOCR(on: imagePath)

実装のポイントはいくつかあります。

  • NSImageを使って画像を読み込む(macOS特有のクラス)
  • VNRecognizeTextRequestで実際のOCR処理
  • 認識パラメータを調整して精度を上げる
    • .accurateモード:速度より精度重視(遅いけど正確)
    • 日本語と英語の両方を認識対象に
    • 言語的な文脈を考慮して補正

実際に使ってみよう テックブログやお知らせの画像から文字起こししてみた!

1枚の画像を文字起こしする場合は、以下のような使い方になります。いろいろ試してみましょう。

./ocr-sample path/to/your/image.jpg

手書き編

まずは記事冒頭、わたしの書いたひどい絵から。

https://tech.groove-x.com/entry/20231224/1703428678 より

...

さっそくうまくいきませんでした。これは元の画像の字が汚すぎるからに違いない。次の画像を試しましょう。同じく手書きの画像を試します。

みんなで育てよう!GX Standard!~LOVOTの品質基準を取り巻くお話~ https://tech.groove-x.com/entry/20241211 より

うまくいきました!手書きでもはっきりした文字であれば読み取ることができそうです!

たくさんの文字を含む画像編

では次にたくさんの文字を含む画像を試してみましょう。

チームの自己組織化を進める活動 https://tech.groove-x.com/entry/20241216 より

わりと文字起こしできていそうです。ですが、順序のある表は苦手かもしれません。手直しが必要なケースもありそうです。

整った画像の文字起こし

LOVOTのWebサイト lovot.life の LOVOT NEWSより以下の画像を試してみます!

教育機関向け割引プラン『LOVOT Education プラン』提供中!LOVOTで社会課題を解決するアイデアコンテストなども実施! https://lovot.life/blog/article/q8_u1xh80

100点の文字起こしができています!きれいな画像で整った文字であれば得意そうですね!

まとめて処理

Swiftで作ったツールを使って、大量の画像を一気に処理するPythonスクリプトも作りました。

大量の画像を一気に処理するPythonスクリプト

import argparse
import concurrent.futures
import subprocess
from pathlib import Path
from typing import Optional


def run_ocr_sample(image_path: Path) -> Optional[str]:
    """ Pythonスクリプトから ocr-sample コマンドを呼び出して結果を取得する

    :param image_path: 画像ファイルのパス(Pathオブジェクト)
    :return: OCRの出力結果(文字列)またはエラーの場合は None
    """
    try:
        result = subprocess.run(
            ["./ocr-sample", str(image_path)], capture_output=True, text=True
        )
        if result.returncode != 0:
            print(f"❌ Error for {image_path}:")
            print(f"💥 {result.stderr}")
            return None
        return result.stdout.strip()
    except FileNotFoundError:
        print(
            "🚫 The 'ocr-sample' command was not found. Make sure it's in the same directory as this script."
        )
        return None
    except Exception as e:
        print(f"⚠️ An unexpected error occurred for {image_path}: {e}")
        return None


def process_images_in_directory(directory: Path, output_file: Path) -> None:
    """ 指定したディレクトリ以下の画像をOCRにかけて、結果をまとめて保存する

    :param directory: 画像が保存されているディレクトリのパス(Pathオブジェクト)
    :param output_file: OCR結果を保存するファイルのパス(Pathオブジェクト)
    """
    supported_extensions = {".png", ".jpg", ".jpeg", ".tiff", ".bmp"}
    results: list[str] = []

    # 対象の画像ファイルをリストアップ
    image_paths = [
        image_path
        for image_path in directory.rglob("*")
        if image_path.is_file() and image_path.suffix.lower() in supported_extensions
    ]

    # 並列にOCR処理を実行
    with concurrent.futures.ThreadPoolExecutor() as executor:
        future_to_image = {
            executor.submit(run_ocr_sample, image_path): image_path
            for image_path in image_paths
        }
        for future in concurrent.futures.as_completed(future_to_image):
            image_path = future_to_image[future]
            try:
                ocr_output = future.result()
            except Exception as exc:
                print(f"⚠️ {image_path} generated an exception: {exc}")
            else:
                if ocr_output:
                    results.append(f"--- {image_path} ---\n{ocr_output}\n")
                    print(f"🔍 Processed: {image_path}")
                else:
                    print(f"❌ OCR failed for {image_path}")

    # 結果をファイルに保存
    output_file.write_text("".join(results), encoding="utf-8")
    print(f"✅ OCR processing completed. Results saved to {output_file}")


if __name__ == "__main__":
    # 引数をチェック ディレクトリと結果を出力するファイルを指定
    parser = argparse.ArgumentParser(
        description="Process images with OCR and save the results."
    )
    parser.add_argument(
        "input_directory", type=Path, help="Directory containing images to process"
    )
    parser.add_argument("output_file", type=Path, help="File path to save OCR results")
    args = parser.parse_args()

    process_images_in_directory(args.input_directory, args.output_file)

これで、指定したディレクトリ内の画像をまとめて処理できます。結果は1つのテキストファイルにまとめられるので、あとで検索したりコピペしたりするのも簡単です。

もちろんPythonスクリプトを書かずに、Swift側を拡張することもできます!(ソースコードが手元にあるので拡張できるのがいいですね)

# Pythonスクリプトを実行
$ python process_images.py ./input_dir ./out.txt
🔍 Processed: input_dir/image2.jpg
🔍 Processed: input_dir/image1.jpg
🔍 Processed: input_dir/image1.png
🔍 Processed: input_dir/image2.png
✅ OCR processing completed. Results saved to out.txt

処理時間は画像の数や複雑さによりますが、サクッと全部まとめて処理してくれます。 試しに1000枚の画像を行ったところ、6分ほどで実行できました。

このツールの良いところ

  1. セキュリティ面が安心: すべてローカル処理なので、機密情報も安全
  2. 無料: Macを持っていれば追加コスト不要
  3. まとめて処理: 大量の画像もバッチ処理できる
  4. 高精度: Apple純正の技術で精度が高い
  5. カスタマイズ自由: 自分好みにコードを改造できる

注意点とコツ

  • macOSでしか動きません: Windowsユーザーには残念ながら使えない...
  • 文字の順序は苦手: 表などの読み取りは苦手 手直しが必要なケースも
  • 画像品質が重要: ピンボケや暗すぎる画像は認識率が下がります
  • 特殊フォントは苦手: 手書き文字やデザインフォントは認識しづらいことも
  • 処理に時間がかかる: 高精度モードだと、結構じっくり処理します(気長に待ちましょう)

まとめ

今回紹介したApple Vision Frameworkを使ったOCRツールは、セキュリティを確保しながら大量の画像からテキストを抽出できる便利なソリューションです。何より、自分で作ったツールなので、必要に応じて機能を追加したり調整したりできるのが魅力だと思います。

似たような課題で困っている方がいれば、ぜひ試してみてください!思ったより簡単に実装できます。もし改良版を作った方がいたら、ぜひ教えてください!