macOSでOCRしたい

Mac

プレビュー.appで画像を開くと、認識した文字列をコピーできるけれど、あれをPythonから呼び出して大量の画像に対してバッチ処理したい。

で、macOSのVision Frameworkを叩くのは面倒だと思ったら、pyobjc-framework-Visionというラッパーがあり、それのインターフェイスになるPythonパッケージを見つけた。ちょちょいとPythonを書いて、と。

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import argparse
from ocrmac import ocrmac
import os
import glob
import magic
import math
from pillow_heif import register_heif_opener

register_heif_opener()


def format_annotations(annotations: list = []) -> str:
    SPACE = 1.0
    avg_font_width = sum([ann[2][2] for ann in annotations]) / len(annotations) * SPACE
    avg_font_height = sum([ann[2][3] for ann in annotations]) / len(annotations) * SPACE
    lines = []
    sv_x = 0
    sv_y = 0
    for ann in annotations:
        if (abs(ann[2][0] - sv_x) > avg_font_width) and (
            abs(ann[2][1] - sv_y) > avg_font_height
        ):
            lines.append(ann[0])
        else:
            spaces = math.floor(
                max(
                    abs(ann[2][0] - sv_x) / avg_font_width * 0.5,
                    abs(ann[2][1] - sv_y) / avg_font_height * 0.5,
                )
            )
            try:
                lines[len(lines) - 1] += " " * spaces + ann[0]
            except IndexError:
                lines.append(" " * spaces + ann[0])
        sv_x = ann[2][0]
        sv_y = ann[2][1]

    return "\n".join(lines)


def main() -> None:
    # Parse arguments
    parser = argparse.ArgumentParser()
    parser.add_argument("image_dir", help="Directory containing images to OCR")
    parser.add_argument("output_dir", help="Directory to output OCR results")
    args = parser.parse_args()

    # Get all images in the directory
    images = [
        f
        for f in glob.glob(os.path.join(os.path.realpath(args.image_dir), "*"))
        if magic.from_file(f, mime=True) in ["image/jpeg", "image/png", "image/heic"]
    ]
    for image in images:
        print(f"Processing {image}")
        annotations = ocrmac.OCR(
            image, framework="livetext", language_preference=["ja-JP"]
        ).recognize()
        if not annotations:
            print(f"Failed to OCR {image}")
            continue
        bn_no_ext = os.path.splitext(os.path.basename(image))[0]
        with open(
            os.path.join(os.path.realpath(args.output_dir), f"{bn_no_ext}.txt"), "w"
        ) as f:
            f.write(format_annotations(annotations))


if __name__ == "__main__":
    main()

テスト画像1

テスト結果1

日本国内の交差点は約100万あるとされている
ので、そこに接続しているエッジは  T字路で 3
本、2本の道路が交差していれば4本、さらに五
叉路や側道があるパターンでは、1 つのノードに
20 ぐらいのエッジが接続しているパターンも実
在します。
航空路線に例えると、ハブ空港が日本国内だけ
で約100万あり、エッジがその10倍・約1,000万
存在する、みたいなものです*3。そしてその巨大
なネットワーク内で乗り継ぎを数十回、数百回す
るというのが、私たちが日常行っている、自動車
の運転を含めた地上での移動です。
道路ネットワークよりもさらに大規模なネット
ワークとしては、SNS やウェブのリンクなども
考えられます。

テスト画像2

テスト結果2

PostgreSQL に航空路線のデータを格納する例を考えてみましょう。現在世界には約三千五百の空港があり、それらの空港を結ぶ航空路線
は数万に及びますが、羽田空港からシアトルタコマ空港への直行便は、航空会社の別を考慮しなければ一路線しかありません。条件は「羽田発
シアトルタコマ着」のみです。
しかし乗り継ぎを考慮すると、羽田からシアトルタコマへの経路は複数通り存在します。クエリの条件は「羽田発・空港(A)着」「空港(A)発シ
アトルタコマ着」です。さらに乗り継ぎが二回になると、「羽田発・空港(A)着」「空港(A)発-空港(B)着」「空港(B)着・シアトルタコマ着」とな
り、経路のパターンが増えることが分かります。空港(ノード)の数より、路線(エッジ)の数がかなり多くなるのはこういう理由です。
航空路線の場合、乗り継ぎ回数がそれほど増えることはないので、クエリもあまり難しくなるとは考えにくく、SQL で十分対応できるでしょ
う。

テスト画像3

テスト結果3

•この薬はあなたの症状に合わせ処方したものです。他の人には譲らないでください。
•お薬は指示どおりにお飲みください。自己判断で服用量や回数を加減しないでください。
•お薬はお子様の手の届かない場所に置いてください。
•この薬以外に他の医療機関の薬又は市販薬を服用している方は、医師又は薬剤師に申し
出てください。
口食前とは食事前30分位、食後とは食後30分位に服用することです。
口食間とは食後2時間位に服用することです。
口(糖尿病の方を除き)食事のとれない方でも食事をする時と同じ時間に服用してください。
口時間ごとに定められた薬は食事に関係なく服用してください。

改行や文字間のスペースなど、適当に書いたので上手くいかないパターンもあるかと思うけど、この年末にやろうと思っていることに関しては、このぐらいできてれば多分足りそう。何より、macOSだけで出来たので、ヨシ!

GitHubに放流しておいた。