Google Cloud FunctionsのOCRチュートリアルでイベント駆動なシステムの構築を体験する

GCPのCloud FunctionsのチュートリアルでOCRを使ったサービスの構築を試します。GCPでイベント駆動なシステムを構築する場合のパターンが簡単に分かるので、これからCloud Functionsなどを使っていく方にとってはためになる記事かと思います。
2020.05.08

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

はじめに

CX事業本部東京オフィスの佐藤智樹です。

GCPのCloudFunctionsのチュートリアルでOCRを使ったサービスの構築があったので試した内容を記事にします。

上記のチュートリアルでは入力画像の言語を認識して、画像内部のテキストを認識・翻訳した結果をストレージに保存するシステムが作れます。チュートリアルそのままではなく、プロジェクト作成などの操作に慣れるため事前準備や事後処理はチュートリアルから多少変更したり、適宜簡単な解説をいれたりします。

GCPでCloud Functionsなどのサービスをこれから触っていく方向けに、GCPだとイベント駆動なシステムはどんな風に構築するのか伝われば幸いです。

システム構成

先に作成するシステムの構成を引用します。

  1. 任意の言語のテキストを含む画像が Cloud Storage にアップロードされます。
  2. Cloud 関数がトリガーされ、Vision API を使用してテキストを抽出し、ソース言語を検出します。
  3. Pub/Sub トピックにメッセージが発行されることで、テキストが翻訳のためにキューに配置されます。翻訳は、ソース言語とは異なるターゲット言語ごとにキューに配置されます。
  4. ターゲット言語がソース言語と一致する場合、翻訳キューがスキップされ、テキストは結果キュー(別の Pub/Sub トピック)に送信されます。
  5. Cloud 関数が、Translation API を使用して翻訳キューのテキストを翻訳します。翻訳結果は結果キューに送信されます。
  6. 別の Cloud 関数が、翻訳されたテキストを結果キューから Cloud Storage に保存します。
  7. 結果は、翻訳ごとに txt ファイルとして Cloud Storage に保存されます。

光学式文字認識(OCR)のチュートリアル Google Cloud Functions に関するドキュメント

アップロードされた画像内部のテキストが認識対象の言語の場合は③、⑤をスキップし、認識対象の言語でない場合は③、⑤で翻訳してから結果を保存します。チュートリアルの設定ファイルでは英語と日本語の他3ヶ国語が設定されており、5ヵ国語分のファイルが生成されます。本稿では日本語の画像を使って認識結果を確認します。

チュートリアルの実行

チュートリアルの実行内容について記載していきます。

事前準備

最初にプロジェクトを作成します。新しいプロジェクトの配下にCloud Functionsなどのリソースを作ればプロジェクトを削除することで今回作成したリソースを漏れなく削除できます。

ダッシュボードの画面を開いて「プロジェクトを作成」を選択します。

以下の画像のように「20200506-CloudFunctions-OCR」という名前でプロジェクトを作成します。G suiteなどに所属したアカウントでなければ「組織なし」で作成します。

次に左上のナビゲーションメニューから「お支払い」を選択してプロジェクトに対する課金が有効になっているのか確認します。

「Billing health checks」が「すべての問題を解決しました」になっていることを確認します。

Cloud Shellを有効化してリソース作成の準備をします。Cloud Shellは以下の画像の赤枠部分から有効化できます。

Cloud Shellにはpythonやgitなどのコマンドが含まれているので今回はCloud Shellでリソースの作成を行います。ローカルでもgcloudコマンドをインストールすれば同様にできます。(ただしCloud Shellの場合は、Cloud Functionsデプロイのためにコンポーネントのアップデートが初回に3~4分ほどかかります)

Cloud Shellにプロジェクトの指定がされないとき(コンソールにプロジェクトのIDがない場合)があるので、その場合は以下のコマンドでプロジェクトをCloud Shellに設定します。PROJECT_IDはダッシュボードから確認してください。

$ gcloud config set project [PROJECT_ID]

後コンポーネントをアップデートします。(ローカル環境の場合はsudo不要)

$ sudo gcloud components update

sudoコマンドを付けない場合は以下のようなエラーになります。

ERROR: (gcloud.components.update) You cannot perform this action because you do not have perm
ission to modify the Google Cloud SDK installation directory [/google/google-cloud-sdk].

画像をアップロードするバケット(Cloud Store)を作成

認識する画像をアップロードするためのバケットを作成します。「YOUR_IMAGE_BUCKET_NAME」にはグローバルで一意になるような名前で作成します。

$ gsutil mb gs://20200505-cloudfunctions-ocr-image

次に認識後の文字列を保存するためのバケットを作成します。「YOUR_TEXT_BUCKET_NAME」には上記と同じようにグローバルで一意になるような名前で作成します。

$ gsutil mb gs://20200505-cloudfunctions-ocr-text

ソースの準備

gitリポジトリから今回使用するソースをコピーします。

$ mkdir work
$ cd work
$ git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

コピーしたら使用するソースのディレクトリに移動します。

$ cd python-docs-samples/functions/ocr/app/

ディレクトリ配下の設定ファイルを変更します。下記の設定ファイルでCloud Pub/Subのトピック名や翻訳対象の言語などを設定します。トピック名はラテン文字しか含めないので注意して設定してください。

{
    "RESULT_TOPIC": "CloudFunctionsResultTopic",
    "RESULT_BUCKET": "20200505-cloudfunctions-ocr-text",
    "TRANSLATE_TOPIC": "CloudFunctionsTranslateTopic",
    "TRANSLATE": true,
    "TO_LANG": ["en", "fr", "es", "ja", "ru"]
}

以下で簡単に項目の設定について説明します。

  • RESULT_TOPIC:概要図④のトピック名
  • TRANSLATE_TOPIC:概要図③のトピック名
  • RESULT_BUCKET:概要図⑥で最終的に翻訳結果が保存されるバケット名
  • TRANSLATE:特に実装のソース内では未使用
  • TO_LANG:翻訳先の言語

コンポーネントのインポート部分

ソースの内容を確認します。それぞれのCloud Functionsに渡すファイル自体は同じもので、イベントの発火口となる関数だけを変えてデプロイします。

まずGCPの別サービスとやりとりするための設定を以下の部分でインポートしています。(ひとつだけ注意が必要で「import translate」の部分がチュートリアル通りだとデプロイできないので「translate_v2」をimportするように変更してください。変更しない場合はデプロイ時にソースの有効さをチェックしているのかデプロイできずにentry-pointのエラーが出ます。)

main.py

import base64
import json
import os

from google.cloud import pubsub_v1
from google.cloud import storage
from google.cloud import translate_v2 as translate
from google.cloud import vision

# Cloud Vision API用のクライアント
vision_client = vision.ImageAnnotatorClient()
# Cloud Translation API用のクライアント
translate_client = translate.Client()
# Cloud Pub/Subへアクセス用のクライアント
publisher = pubsub_v1.PublisherClient()
# Cloud Storageへアクセス用のクライアント
storage_client = storage.Client()

project_id = os.environ['GCP_PROJECT']

# 設定ファイルのインポート
with open('config.json') as f:
    data = f.read()
config = json.loads(data)

画像処理部分

画像の認識を行うのは以下の部分です。Cloud Storageに画像がアップロードされたことを検知して起動されます。

main.py

def process_image(file, context):
        """Cloud Function triggered by Cloud Storage when a file is changed.
        Args:
            file (dict): Metadata of the changed file, provided by the triggering
                                     Cloud Storage event.
            context (google.cloud.functions.Context): Metadata of triggering event.
        Returns:
            None; the output is written to stdout and Stackdriver Logging
        """
        bucket = validate_message(file, 'bucket')
        name = validate_message(file, 'name')

        detect_text(bucket, name)

        print('File {} processed.'.format(file['name']))

次の関数でCloud Vision APIを使って画像からテキストを抜き出します。抜き出したテキストが設定ファイルで決めた言語以外の言語の場合は翻訳用のCloud Pub/Subのトピックにテキストを渡します。

main.py

def detect_text(bucket, filename):
        print('Looking for text in image {}'.format(filename))

        futures = []

        text_detection_response = vision_client.text_detection({
            'source': {'image_uri': 'gs://{}/{}'.format(bucket, filename)}
        })
        annotations = text_detection_response.text_annotations
        if len(annotations) > 0:
            text = annotations[0].description
        else:
            text = ''
        print('Extracted text {} from image ({} chars).'.format(text, len(text)))

        detect_language_response = translate_client.detect_language(text)
        src_lang = detect_language_response['language']
        print('Detected language {} for text {}.'.format(src_lang, text))

        # Submit a message to the bus for each target language
        for target_lang in config.get('TO_LANG', []):
            topic_name = config['TRANSLATE_TOPIC']
            if src_lang == target_lang or src_lang == 'und':
                topic_name = config['RESULT_TOPIC']
            message = {
                'text': text,
                'filename': filename,
                'lang': target_lang,
                'src_lang': src_lang
            }
            message_data = json.dumps(message).encode('utf-8')
            topic_path = publisher.topic_path(project_id, topic_name)
            future = publisher.publish(topic_path, data=message_data)
            futures.append(future)
        for future in futures:
            future.result()

テキスト翻訳部分

既定の言語でなかった場合Cloud Transration APIを使ったこちらの関数で翻訳します。翻訳した結果はCloud Storage保存のためのトピックに送信します。Cloud Pub/Subから受けるデータはJSONの場合base64形式からデコードする必要があるので最初にデコードしてからデータを取り込んでいます。

main.py

def translate_text(event, context):
        if event.get('data'):
            message_data = base64.b64decode(event['data']).decode('utf-8')
            message = json.loads(message_data)
        else:
            raise ValueError('Data sector is missing in the Pub/Sub message.')

        text = validate_message(message, 'text')
        filename = validate_message(message, 'filename')
        target_lang = validate_message(message, 'lang')
        src_lang = validate_message(message, 'src_lang')

        print('Translating text into {}.'.format(target_lang))
        translated_text = translate_client.translate(text,
                                                     target_language=target_lang,
                                                     source_language=src_lang)
        topic_name = config['RESULT_TOPIC']
        message = {
            'text': translated_text['translatedText'],
            'filename': filename,
            'lang': target_lang,
        }
        message_data = json.dumps(message).encode('utf-8')
        topic_path = publisher.topic_path(project_id, topic_name)
        future = publisher.publish(topic_path, data=message_data)
        future.result()

翻訳されたテキストを保存する

Cloud Pub/Subに送られた翻訳後のテキストデータをCloud Storageに保存します。

main.py

def save_result(event, context):
        if event.get('data'):
            message_data = base64.b64decode(event['data']).decode('utf-8')
            message = json.loads(message_data)
        else:
            raise ValueError('Data sector is missing in the Pub/Sub message.')

        text = validate_message(message, 'text')
        filename = validate_message(message, 'filename')
        lang = validate_message(message, 'lang')

        print('Received request to save file {}.'.format(filename))

        bucket_name = config['RESULT_BUCKET']
        result_filename = '{}_{}.txt'.format(filename, lang)
        bucket = storage_client.get_bucket(bucket_name)
        blob = bucket.blob(result_filename)

        print('Saving result to {} in bucket {}.'.format(result_filename,
                                                         bucket_name))

        blob.upload_from_string(text)

        print('File saved.')

ソースをデプロイ

次に以下のコマンドでソースをデプロイします。「20200505-cloudfunctions-ocr-image」は事前に作成した画像を格納するバケット名を指定してください。entry-pointにはイベントから実行される関数を指定します。

$ gcloud functions deploy ocr-extract --runtime python37 --trigger-bucket 20200505-cloudfunctions-ocr-image --entry-point process_image

Cloud Shellの場合は以下のようにプロジェクトでAPIを有効化するかとCloud Functions作成許可が聞かれるので 両方とも「y」で許可します。2分ほどでデプロイは終わります。

API [cloudfunctions.googleapis.com] not enabled on project
[99999999999]. Would you like to enable and retry (this will take a
few minutes)? (y/N) y

Allow unauthenticated invocations of new function [ocr-extract]? (y/N) y

他の関数についても以下のコマンドでデプロイします。「CloudFunctionsTranslateTopic」はシステム概要の③のトピックを指定してください。「CloudFunctionsResultTopic」は⑤のトピックを指定します。「ocr-extract」のデプロイと同じように「Allow unauthenticated ~」と聞かれるので許可してください。

$ gcloud functions deploy ocr-translate --runtime python37 --trigger-topic CloudFunctionsTranslateTopic --entry-point translate_text
$ gcloud functions deploy ocr-save --runtime python37 --trigger-topic CloudFunctionsResultTopic --entry-point save_result

以上でデプロイは完了です。

初回実行時の注意

チュートリアルには載っていないですが、初めて「Cloud Vision API」と「Cloud Translate API」を動かす場合はAPIの有効化が必要です。Webコンソールの上部検索欄か左のタブから該当のAPIを選択して以下の画面をそれぞれ開いて「有効化」をクリックします。有効化しない場合は実行時に関数がクラッシュします。

動作確認

画像をアップロードして、動作確認を行います。今回はcloneしたソース内に画像ファイルがあるのでそちらを利用します。

$ gsutil cp ../images/sign.png gs://20200505-cloudfunctions-ocr-image

実行すると概要⑦のCloud Storage(今回は20200505-cloudfunctions-ocr-text)に認識後のテキストが保存されます。

入力画像

出力テキスト(英語と日本語のみ掲載)

sign.png_en.txt

|Stop line

sign.png_jp.txt

|ストップライン

ログを確認したところサンプルの画像は中国語(繁体)「zh-TW」として認識されます。妥協して今回は以下のようにconfig.jsonの「TO_LANG」を修正し漢字が出力されることを確認します。

config.json

{
    "RESULT_TOPIC": "CloudFunctionsResultTopic",
    "RESULT_BUCKET": "20200505-cloudfunctions-ocr-text",
    "TRANSLATE_TOPIC": "CloudFunctionsTranslateTopic",
    "TRANSLATE": true,
    "TO_LANG": ["en", "fr", "es", "ja", "ru", "zh-TW"]
}

修正して再度ソースをデプロイします。もう一度バケットにファイルをアップロードし、出力されたテキストファイルをダウンロードして確認すると漢字が認識されたことを確認できます。

sign.png_zh-TW.txt

|停止線

事後処理

最後に以下のコマンドで、プロジェクトを削除すると全てのリソースが削除できます。

$ gcloud projects delete [PROJECT_ID]

もし戻したい場合は30日以内なら以下のコマンドで一部のリソースを復元できます。プロジェクトの削除と復元に関する詳細はこちらのマニュアルで確認してください。

$ gcloud projects undelete [PROJECT_ID]

感想

GCPでのイベント駆動なシステムの構築がある程度体験できました。トピックの作成がいらなかったり、権限設定などを細かくしなくても最初に色々試したりできるのはとっつきやすく感じます。特にCloud Shellは個人環境を汚す必要もなく、仮想マシンの立ち上げもなしでCLIからリソースの操作ができるのでかなり良い体験でした。

チュートリアルで挙げた点に注意すればCloud Vision APIなど他には無い魅力的な機能を使えるので、一部の欲しい機能だけGCPで個別のロジック含めてAPI化して利用するのも有りかと思います。これからGCPでCloud Functionsを始める方などの参考になれば幸いです。