📝

Google Cloud Vision API で縦書き横書き混合の日本語テキスト検出は難しい

2023/09/12に公開

画像内にある日本語のテキストを読み取りたくなったので、Google Cloud Vision API による OCR を試してみました。

Google Cloud Vision API とは

https://cloud.google.com/vision

Use our game-changing fully managed development environment Vertex AI Vision to create your own computer vision applications or derive insights from images and videos with pre-trained APIs, AutoML, or custom models.

事前トレーニング済みのモデルを使って画像ラベリング、光学式文字認識、不適切なコンテンツのタグ付けなどができます。

試してみる

環境

  • @google-cloud/vision: v4.0.2
  • Node.js: v18
  • TypeScript: v5.2.2
  • ts-node: v10.9.1

事前準備

https://cloud.google.com/vision/docs/ocr?hl=ja

に従って、ローカル環境にある画像ファイルからテキストを検出する実装を行います。
事前準備として下記が完了したものとします。

  • Google Cloud project の作成
  • Google Cloud project の課金の有効化
    • Google Cloud Vision API には無料で使える分がありますが、クレジットカード情報の登録は必須です
  • Google Cloud Vision API の有効化
  • ローカル環境での認証情報の設定

実装

今回はプロジェクトルートにある /imagesディレクトリに対象画像を配置し、OCR の結果を /detected ディレクトリに json ファイルとして格納することにしました。

実装の工夫として、API の Rate Limit にひっかからないように 30 枚処理するごとに 1 秒待機しています。

また画像内のテキストの言語が日本語だとわかっているので、languageHintsja を指定しています。
languageHints は間違った指定をすると読み取り精度が下がるので、言語が不明の場合は何も指定しないことが推奨されます。)

src/index.ts
import { resolve, parse } from "path";
import { readdirSync, existsSync, mkdirSync, writeFileSync } from "fs";
import { setTimeout } from "timers/promises";
import { ImageAnnotatorClient } from "@google-cloud/vision";

/**
 * 画像からテキストを抽出するクラス
 */
export class TextFromImages {
  /** 1 秒間に処理する画像数 */
  private static readonly _IMAGES_PER_SECOND = 30;
  /** 画像を配置するディレクトリ */
  private static readonly _INPUT_DIR = resolve(__dirname, "../images");
  /** テキスト検出した結果を配置するディレクトリ */
  private static readonly _DETECTED_DIR = resolve(__dirname, "../detected");

  /**
   * /images ディレクトリに配置されている画像内のテキストを抽出して json 形式で /detected に出力する
   */
  async extractTextFromImages() {
    const imageNames = readdirSync(TextFromImages._INPUT_DIR);
    if (imageNames.length === 0) {
      return;
    }

    // imageNames を 30 個ずつに分割する
    const chunkedImageNames = ((baseArray, size) =>
      baseArray.flatMap((_, index, array) =>
        index % size ? [] : [array.slice(index, index + size)],
      ))(imageNames, TextFromImages._IMAGES_PER_SECOND);

    if (!existsSync(TextFromImages._DETECTED_DIR)) {
      mkdirSync(TextFromImages._DETECTED_DIR);
    }

    const client = new ImageAnnotatorClient();
    for (const filenames of chunkedImageNames) {
      for (const filename of filenames) {
        const [result] = await client.textDetection({
          image: {
            source: {
              filename: resolve(TextFromImages._INPUT_DIR, filename),
            },
          },
          // 値を省略すると自動言語検出が有効になるため、通常は値を空にすることで最善の結果が得られる
          // 画像内のテキストの言語がわかっている場合は、ヒントを設定すると結果が少し良くなる
          imageContext: {
            languageHints: ["ja"],
          },
        });

        writeFileSync(
          resolve(TextFromImages._DETECTED_DIR, `${parse(filename).name}.json`),
          JSON.stringify(result),
        );
      }
      // API 制限に引っかからないように 30 回 API を呼ぶごとに 1 秒待つ
      await setTimeout(1000);
    }
  }
}

(async () => {
  const textFromImages = new TextFromImages();
  console.log("Start extracting text from images...");
  await textFromImages.extractTextFromImages();
  console.log("Done!");
})();

実際の動作検証

プロジェクトルートの /images ディレクトリに対象画像を配置し、プロジェクトルートで下記のコマンドを実行します。

npx ts-node src/index.ts

実行結果にはいくつか項目がありますが、文字認識結果は textAnnotationsfullTextAnnotation という二つの項目に格納されます。

textAnnotations のほうは単語単位でテキストの座標を取得できます。
たとえば今回の場合は「春」「は」「あけぼの」「。」のような区切りでテキストの座標を取得できました。

一方 fullTextAnnotation のほうはより詳細なデータを得られます。

  • 一文字ずつのテキストの座標
  • Google Cloud Vision API がどこからどこまでを一行と認識したか
  • 改行が検出された位置

などです。
用途に応じて結果を使い分けると良いと思います。

横書きの日本語

まずは日本語をどの程度読み取ってくれるのか確認です。

textAnnotations.descriptionfullTextAnnotation.text には読み取った全文が改行込みで格納されており、今回は以下のように読み取れました。

春はあけぼの。 やうやう白\nくなりゆく山ぎは、すこし\nあかりて、紫だちたる雲の\nほそくたなびきたる。

完璧に正しく読み取れています。

ちなみに文字背景の白い部分の不透明度を低くして写真が透けるようにした場合、本来存在しない謎の文字を読み取ってしまいました。文字の背景はなるべく単色がいいようです。

縦書きの日本語

日本語と言えば縦書き。
先ほどの画像では改行をしっかり読み取っていましたが、縦書きでも改行を認識できるのでしょうか?

結果は下記の通り、縦書きを認識してくれることがわかりました。

春はあけぼ\nの。 やうやう\n白くなりゆく\n山ぎは、すこ\nしあかりて、\n紫だちたる雲\nのほそくたな\nびきたる。

縦書き横書き混合の日本語

日本語の場合、デザインによっては縦書き・横書き混合のこともあります。
できたら縦書きと横書きをそれぞれ正しく読み取ってほしいところですが……

結果は下記の通り。

春はあけぼの。\nたたのちてしはゆ\nる\nなほた\nあ\nくなり\nびそる紫かす山な\n雲だりこ\nや

残念ながら駄目でした。

また読み取り方向を正しく認識できなくとも文字自体は全部読み取れるものだと想像していましたが、実際には一部の文字が読み取れていませんでした。

この後、縦書き部分に罫線を引いて見たり、横書きと縦書きの間の余白を広くしてみたり、縦書きと横書きの間を線で区切ったりしてみましたが、どうしても縦書きと横書きを正しく認識させることはできず、すべての文字を読み取ることもできませんでした……。

ランダムな文字列

縦書き単体・横書き単体では全文字読み取れたのに、縦書き・横書き混合の場合は読み取れた文字の数自体が減ったことは興味深いです。

もしかしたら縦書き単体・横書き単体のときもすべての文字を読み取れていたわけではなくて、読み取れた文字から文章を推測して読み取り結果を補正していたのかもしれません。
仮にそうだとすると Google Cloud Vision API はランダムな文字の羅列を読み取るのは苦手なのでしょうか?

結果は下記の通り。

壁はかっこうのおじぎ勢らでゴーシュがわからか\nっこうたた。それでもう少しいい気だですにとっ\nてねこますた。 楽たたことたはんそれではドレミ\nファの粗末会のときをはうまく同じたましが、 こ\nこじゃ狸へ結んれのました。

完璧に読み取れています。

となると文章として成立しているかどうかは関係なくて、本来の読み取り方向に沿っているかどうかで読み取り精度に差が出るようです。

終わりに

今回、Google Cloud Vision API を使用して画像内の日本語テキストを読み取ってみて、次のことがわかりました。

  • 縦書きでも横書きでも読み取れる
    • ただし文字部分の背景が複雑な画像だと本来存在しない文字を読み取ってしまうので、なるべく背景は単色が良い
    • 縦書きと横書きを混ぜると読み取り失敗する可能性が高くなる
  • 縦書きと横書きが混ざっていないのならランダムな文字列でも読み取れる

条件によっては精度に不安があるものの、基本的には精度よく読み取ることができたので、今後活用していきたいです!

chot Inc. tech blog

Discussion