🧘

Gemini Pro Vision による OCR を Cloud Functions で実行する

2024/01/22に公開

こんにちは、クラウドエースのデータソリューション部所属の坂田です。

2023 年 12 月に Google の新たな生成 AI モデルとして「Gemini」が発表されました。Gemini はテキストや画像など様々な入力に対応する「マルチモーダルモデル」です。

今回は、Gemini の性能や使用感を確かめるため、Gemini による OCR(光学的文字認識)の処理を Cloud Functions で実行する仕組みを作ってみたので、その方法を紹介します。

Gemini とは

Gemini は、テキスト、画像、動画を入力できるマルチモーダルモデルで、Google Cloud の Vertex AI から API で利用できます。

Vertex AI には、「Gemini Pro」と「Gemini Pro Vision」の 2 種類のモデルが用意されています。Gemini Pro はテキストのみを入力でき、Gemini Pro Vision はテキストだけでなく画像、動画を入力できます。なお、どちらも出力はテキストのみです。
詳しくはこちら。
https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/overview?hl=ja

今回は、Gemini Pro Vision の API を活用し、画像からテキストを抽出する OCR(光学的文字認識)を Cloud Functions で実行します。

作るもの

今回は、食品の裏側などに記載されている「食品表示」の画像から、食品名や原材料名などのテキストを JSON として抽出するという処理を Gemini Pro Vision で行います。

alt
OCR のイメージ

食品表示は、法律などによって項目や記載方法が定められていますが、食品の種類によって項目や記載方法が異なります。生成 AI の柔軟性を活かし、様々な形式の食品表示の画像からでもテキストを抽出することができるようにします。

以下のような構成の OCR の仕組みを構築します。

  1. Cloud Storage に食品表示の画像をアップロードする
  2. アップロードによって Cloud Functions が起動し、Gemini Pro Vision にリクエストが送信される
  3. Gemini Pro Vision が画像から抽出したテキストを、BigQuery に保存する

alt
構成図

作ってみる

1. Cloud Storage に食品表示の画像をアップロードする

まずは、Cloud Storage に食品表示の画像をアップロードします。今回は、家にあった食品の食品表示をスマホで撮影し、その画像を Cloud Storage にアップロードします。

以下のようなコマンドで Cloud Storage にアップロードできます。

gsutil cp <画像のパス> gs://<バケット名>/<画像のファイル名>

なお、Gemini Pro Vision は、画像のファイル形式として JPEG と PNG をサポートしています。また、画像とテキストを含むプロンプトは全体で最大 4MB までのため、画像が大きすぎる場合はリサイズする必要があります。
詳細な仕様はこちら。
https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/send-multimodal-prompts?hl=ja#image-requirements

2. Cloud Functions の関数を作成する

次に、Cloud Functions で OCR の処理を行う関数を作成します。今回は Python で関数を作成します。

以下がソースコード全文です。

Cloud Functions 関数のコード全文
import json
import functions_framework
import vertexai

from cloudevents.http import CloudEvent
from flask import abort
from google.cloud import bigquery
from vertexai.preview.generative_models import GenerativeModel, Part


PROJECT_ID = "my-project"       # TO CHANGE
LOCATION = "asia-northeast1"    # TO CHANGE
TABLE_ID = f"{PROJECT_ID}.my_dataset.my_table" # TO CHANGE


def generate_text_multimodal(image: Part) -> str:
    # Vertex AI の初期化
    vertexai.init(project=PROJECT_ID, location=LOCATION)

    # プロンプトの定義
    prompt = """
    あなたはOCRを行うプログラムです。
    食品のパッケージの画像から、「食品表示ラベル」と呼ばれる黒枠で囲われた表を特定し、食品表示ラベルの情報を出力例に従ってJSON形式で出力してください。
    なお、JSONの項目と食品表示ラベルの表記は以下のような対応となっており、当てはまる項目が食品表示ラベルに無い場合はnullを出力してください。

    【項目の対応】
    ・名称:name
    ・原材料名:ingredients
    ・内容量:amount
    ・保存方法:how_to_store
    ・消費期限:expiration_date
    ・賞味期限:best_before
    ・製造者:manufacture
    ・販売者:vendor
    
    【出力例】
    {
    "name": "納豆",
    "ingredients": "大豆、納豆菌",
    "amount": "45g×3",
    "how_to_store": "冷蔵庫(10℃以下)にて保存",
    "expiration_date": "上面の下部記載",
    "best_before": null,
    "manufacture": "〇〇株式会社",
    "vendor": null
    }
    """

    # Gemini Pro Visionモデルにプロンプトを入力してテキストを生成
    model = GenerativeModel("gemini-pro-vision")
    response = model.generate_content([image, prompt])

    return response.text


def parse_response(response: str) -> list[dict]:
    # テキストを JSON 形式に変換
    response = response.replace(' ```json\n', '')
    response = response.replace('\n```', '')
    response = json.loads(response)

    return [response]


def insert_to_bigquery(data: dict):
    client = bigquery.Client(project=PROJECT_ID, location=LOCATION)

    # BigQuery テーブル・スキーマ定義
    schema = [
        bigquery.SchemaField("name", "STRING", "NULLABLE", "名称"),
        bigquery.SchemaField("ingredients", "STRING", "NULLABLE", "原材料名"),
        bigquery.SchemaField("amount", "STRING", "NULLABLE", "内容量"),
        bigquery.SchemaField("how_to_store", "STRING", "NULLABLE", "保存方法"),
        bigquery.SchemaField("expiration_date", "STRING", "NULLABLE", "消費期限"),
        bigquery.SchemaField("best_before", "STRING", "NULLABLE", "賞味期限"),
        bigquery.SchemaField("manufacture", "STRING", "NULLABLE", "製造者"),
        bigquery.SchemaField("vendor", "STRING", "NULLABLE", "販売者")
    ]
    table = bigquery.Table(TABLE_ID, schema=schema)

    # BigQuery テーブルへのデータ挿入
    errors = client.insert_rows_json(table, json_rows=data)

    return errors


@functions_framework.cloud_event
def main(cloud_event: CloudEvent) -> tuple:    
    data = cloud_event.data

    bucket = data["bucket"]
    name = data["name"]

    # 画像の読み込み
    try:
        image = Part.from_uri(f"gs://{bucket}/{name}", mime_type="image/jpeg")
    except Exception as e:
        print(json.dumps(dict(severity="ERROR", message="Failed to read image: {}".format(e))))
        return abort(400)

    # Gemini Pro Vision によるテキスト生成
    response_row_data = generate_text_multimodal(image)

    # テキストを JSON 形式に変換
    json_data = parse_response(response_row_data)

    # BigQuery テーブルへデータ挿入
    errors = insert_to_bigquery(json_data)

    if errors == []:
        print(json.dumps(dict(severity="DEFAULT", message="New rows have been added.")))
        return "OK"
    else:
        print(json.dumps(dict(severity="ERROR", message="Encountered errors while inserting rows: {}".format(errors))))
        return abort(400)

一部抜粋してソースコードを解説します。

Cloud Storage からの画像の読み込み

以下の部分で画像を読み込んでいます。

try:
    image = Part.from_uri(f"gs://{bucket}/{name}", mime_type="image/jpeg")
except Exception as e:
    print(json.dumps(dict(severity="ERROR", message="Failed to read image: {}".format(e))))
    return abort(400)

Vertex AI の Python ライブラリで用意されている Part.from_uri() を使って画像を読み込んでいます。

プロンプトの定義

Gemini Pro Vision にプロンプトを入力する処理は以下です。

def generate_text_multimodal(image: Part) -> str:
    # Vertex AI の初期化
    vertexai.init(project=PROJECT_ID, location=LOCATION)

    # プロンプトの定義
    prompt = """
    あなたはOCRを行うプログラムです。
    食品のパッケージの画像から、「食品表示ラベル」と呼ばれる黒枠で囲われた表を特定し、食品表示ラベルの情報を出力例に従ってJSON形式で出力してください。
    なお、JSONの項目と食品表示ラベルの表記は以下のような対応となっており、当てはまる項目が食品表示ラベルに無い場合はnullを出力してください。

    【項目の対応】
    ・名称:name
    ・原材料名:ingredients
    ・内容量:amount
    ・保存方法:how_to_store
    ・消費期限:expiration_date
    ・賞味期限:best_before
    ・製造者:manufacture
    ・販売者:vendor
    
    【出力例】
    {
    "name": "納豆",
    "ingredients": "大豆、納豆菌",
    "amount": "45g×3",
    "how_to_store": "冷蔵庫(10℃以下)にて保存",
    "expiration_date": "上面の下部記載",
    "best_before": null,
    "manufacture": "クラウドエース株式会社",
    "vendor": null
    }
    """

    # Gemini Pro Vision モデルにプロンプトを入力してテキストを生成
    model = GenerativeModel("gemini-pro-vision")
    response = model.generate_content([image, prompt])

    return response.text

プロンプトは、公式ドキュメントのプロンプトガイドを参考に作成しました。

プロンプトのポイントは以下の 3 つです。

  • 出力形式を明示する(今回は JSON)
  • 出力例を示す
  • 画像はプロンプトの冒頭に挿入する

ポイント 3 つ目の画像の位置について、model.generate_content([image, prompt]) でモデルにリクエストを送信していますが、引数のリストにおける画像とプロンプトの順番で画像の位置が決定されます。
もし、プロンプトの途中に画像を挿入したい場合は、model.generate_content([prompt1, image, prompt2]) などとすることで実現できます。

また、どのモデルを使用するかは、vertexai.preview.generative_modelsGenerativeModel("gemini-pro-vision") の部分で指定しています。

モデルのレスポンスのパース

以下のコードで、モデルのレスポンスの文字列を JSON 形式にパースしています。

def parse_response(response: str) -> list[dict]:
    # テキストを JSON 形式に変換
    response = response.replace(' ```json\n', '')
    response = response.replace('\n```', '')
    response = json.loads(response)

    return [response]

モデルの理想的なレスポンスは綺麗な(正しい)JSON 形式ですが、プロンプトエンジニアリングが甘かったためか、綺麗な JSON で返してくれなかったため、レスポンスの文字列を整形してから JSON に変換しています。

3. BigQuery テーブルを作成する

BigQuery への挿入は以下のコードで処理しています。

# BigQuery テーブルへのデータ挿入
errors = client.insert_rows_json(table, json_rows=data)

BigQuery ライブラリのデータ挿入用の API insert_rows_json() はあらかじめテーブルを作成しておく必要があるため、コードを実行する前にテーブルを作成します。

モデルが抽出するテキストの項目に合わせて、以下のスキーマでテーブルを作成します。

フィールド名 データ型 モード
name STRING NULLABLE
ingredients STRING NULLABLE
amount STRING NULLABLE
how_to_store STRING NULLABLE
expiration_date STRING NULLABLE
best_before STRING NULLABLE
manufacture STRING NULLABLE
vendor STRING NULLABLE

以下のコマンドでテーブルを作成します。

bq mk \
--table \
--schema name:STRING,ingredients:STRING,amount:STRING,how_to_store:STRING,expiration_date:STRING,best_before:STRING,manufacture:STRING,vendor:STRING \
<プロジェクトID>:<データセット名>.<テーブル名>

4. Cloud Functions の関数をデプロイする

Cloud Functions の関数を以下のコマンドでデプロイします。--trigger-bucket を設定することで、Cloud Storage トリガーが有効になります。

gcloud functions deploy <関数名> \
--gen2 \
--runtime python39 \
--trigger-bucket <トリガーとなる Cloud Storage バケット名> \
--region <リージョン> \
--source <関数のソースコードのディレクトリ> \
--entry-point main

なお、ソースコードのディレクトリには以下のような requirements.txt を配置しておきます。

functions-framework
google-cloud-aiplatform
google-cloud-bigquery

動かしてみる

では、実際に動かしてみます。

まず、Cloud Storage に食品表示の画像をアップロードします。今回のコードは JPG 形式の画像のみをサポートしているので、JPG 画像をアップロードします。

gsutil cp ./image.jpg gs://<バケット名>/image.jpg

アップロードしたのは、以下のような画像です。
(一部モザイクをいれていますが、実際にアップロードしたものにはモザイクは入れていません)

alt

画像をアップロードすると Cloud Functions が起動し、Gemini Pro Vision にリクエストが送信されます。そして、Gemini Pro Vision は画像からテキストを抽出し、抽出した情報が BigQuery に挿入されます。

規約により、抽出結果はお見せできませんが、しっかりとプロンプトの指示通りにテキストが抽出されていました。また、上で挙げた画像以外にも、食品表示の形式が異なる食品の画像を数種類試してみましたが、どの画像も OCR を実施できました。

ただ、抽出されたテキストの中には、画像には存在しないテキストも含まれていました。例えば、原材料名「ingredients」に画像には存在しない原材料が含まれているといった形です。

Gemini Pro Vision はあくまで生成 AI のため、生成 AI のデメリットが出てしまうという結果となりました。

まとめ

今回は、Gemini Pro Vision による OCR の処理を Cloud Functions で実行する方法を紹介しました。

OCR 自体は実現できましたが、抽出されたデータのうち、いくつかは誤ったデータとなってしまいました。
高い精度で OCR を行いたい場合は、Gemini Pro Vision ではなく、Google Cloud の Vision APIDocument AI といったプロダクトを使用するのが良さそうです。

一方で、生成 AI は高い柔軟性によって様々な画像・場面に対応できるという強みがあります。
OCR の精度は、以下のような要素で左右されると思われます。

  • プロンプト
  • 画像の解像度
  • 写真の撮り方
  • OCR で抽出するテキストの言語

上記について、色々と試してみると良い精度が出るかもしれません。

参考文献

https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/send-multimodal-prompts?hl=ja

https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/design-multimodal-prompts?hl=ja

https://cloud.google.com/vertex-ai/docs/generative-ai/multimodal/sdk-for-gemini/gemini-sdk-overview-reference?hl=ja

関連記事

Gemini の紹介
https://zenn.dev/cloud_ace/articles/84d1cea10dec1f

Discussion