Pic-a-daily: ラボ 1 - 画像を保存および分析する(Java)

1. 概要

最初のコードラボでは、バケットに画像をアップロードします。これにより、関数によって処理されるファイル作成イベントが生成されます。この関数は Vision API を呼び出して画像分析を行い、結果をデータストアに保存します。

d650ca5386ea71ad.png

学習内容

  • Cloud Storage
  • Cloud Functions
  • Cloud Vision API
  • Cloud Firestore

2. 設定と要件

セルフペース型の環境設定

  1. Google Cloud Console にログインして、プロジェクトを新規作成するか、既存のプロジェクトを再利用します。Gmail アカウントも Google Workspace アカウントもまだお持ちでない場合は、アカウントを作成してください。

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • プロジェクト名は、このプロジェクトの参加者に表示される名称です。Google API では使用されない文字列です。この設定はいつでも変更できます。
  • プロジェクト ID は、すべての Google Cloud プロジェクトにおいて一意でなければならず、不変です(設定後は変更できません)。Cloud コンソールでは一意の文字列が自動生成されます。通常は、この内容を意識する必要はありません。ほとんどの Codelab では、プロジェクト ID(通常は PROJECT_ID と識別されます)を参照する必要があります。生成された ID が好みではない場合は、ランダムに別の ID を生成できます。または、ご自身で試して、利用可能かどうかを確認することもできます。このステップ以降は変更できず、プロジェクトを通して同じ ID になります。
  • なお、3 つ目の値として、一部の API が使用するプロジェクト番号があります。これら 3 つの値について詳しくは、こちらのドキュメントをご覧ください。
  1. 次に、Cloud のリソースや API を使用するために、Cloud コンソールで課金を有効にする必要があります。この Codelab の操作をすべて行って、費用が生じたとしても、少額です。このチュートリアルの終了後に請求が発生しないようにリソースをシャットダウンするには、作成したリソースを削除するか、プロジェクト全体を削除します。Google Cloud の新規ユーザーは、300 米ドル分の無料トライアル プログラムをご利用いただけます。

Cloud Shell の起動

Google Cloud はノートパソコンからリモートで操作できますが、この Codelab では、Google Cloud Shell(Cloud 上で動作するコマンドライン環境)を使用します。

Google Cloud Console で、右上のツールバーにある Cloud Shell アイコンをクリックします。

55efc1aaa7a4d3ad.png

プロビジョニングと環境への接続にはそれほど時間はかかりません。完了すると、次のように表示されます。

7ffe5cbb04455448.png

この仮想マシンには、必要な開発ツールがすべて用意されています。永続的なホーム ディレクトリが 5 GB 用意されており、Google Cloud で稼働します。そのため、ネットワークのパフォーマンスと認証機能が大幅に向上しています。この Codelab での作業はすべて、ブラウザ内から実行できます。インストールは不要です。

3. API を有効にする

このラボでは、Cloud Functions と Vision API を使用しますが、まず Cloud Console または gcloud で有効にする必要があります。

Cloud Console で Vision API を有効にするには、検索バーで Cloud Vision API を検索します。

cf48b1747ba6a6fb.png

Cloud Vision API のページが表示されます。

ba4af419e6086fbb.png

[ENABLE] ボタンをクリックします。

または、gcloud コマンドライン ツールを使用して Cloud Shell で有効にすることもできます。

Cloud Shell で、次のコマンドを実行します。

gcloud services enable vision.googleapis.com

オペレーションが正常に完了したことを確認します。

Operation "operations/acf.12dba18b-106f-4fd2-942d-fea80ecc5c1c" finished successfully.

Cloud Functions も有効にします。

gcloud services enable cloudfunctions.googleapis.com

4. バケットを作成する(コンソール)

写真用のストレージ バケットを作成します。この操作は、Google Cloud Platform コンソール(console.cloud.google.com)または Cloud Shell やローカル開発環境の gsutil コマンドライン ツールを使用して行うことができます。

ハンバーガー メニュー(☰)から Storage ページに移動します。

1930e055d138150a.png

バケットに名前を付ける

[CREATE BUCKET] ボタンをクリックします。

34147939358517f8.png

[CONTINUE] をクリックします。

場所を選択

197817f20be07678.png

任意のリージョン(ここでは Europe)にマルチリージョン バケットを作成します。

[CONTINUE] をクリックします。

デフォルトのストレージ クラスを選択する

53cd91441c8caf0e.png

データの Standard ストレージ クラスを選択します。

[CONTINUE] をクリックします。

アクセス制御を設定する

8c2b3b459d934a51.png

一般公開されている画像を扱うため、このバケットに保存されているすべての画像に同じ均一なアクセス制御を設定します。

Uniform アクセス制御オプションを選択します。

[CONTINUE] をクリックします。

Set Protection/Encryption

d931c24c3e705a68.png

独自の暗号鍵を使用しないため、デフォルト(Google-managed key))のままにします。

CREATE をクリックして、バケットの作成を完了します。

allUsers をストレージ ビューアとして追加

[Permissions] タブに移動します。

d0ecfdcff730ea51.png

次のように、Storage > Storage Object Viewer ロールを持つ allUsers メンバーをバケットに追加します。

e9f25ec1ea0b6cc6.png

[SAVE] をクリックします。

5. バケットを作成する(gsutil)

Cloud Shell で gsutil コマンドライン ツールを使用してバケットを作成することもできます。

Cloud Shell で、一意のバケット名の変数を設定します。Cloud Shell では、GOOGLE_CLOUD_PROJECT が一意のプロジェクト ID に設定されています。これをバケット名の末尾に追加できます。

次に例を示します。

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

ヨーロッパに標準のマルチリージョン ゾーンを作成します。

gsutil mb -l EU gs://${BUCKET_PICTURES}

均一なバケットレベルのアクセスを確保します。

gsutil uniformbucketlevelaccess set on gs://${BUCKET_PICTURES}

バケットを公開します。

gsutil iam ch allUsers:objectViewer gs://${BUCKET_PICTURES}

コンソールの Cloud Storage セクションに移動すると、公開 uploaded-pictures バケットが表示されます。

a98ed4ba17873e40.png

前の手順で説明したように、バケットに写真をアップロードでき、アップロードした写真が一般公開されていることをテストします。

6. バケットへの公開アクセスをテストする

ストレージ ブラウザに戻ると、バケットがリストに表示され、アクセス権が [公開] になっています(バケットのコンテンツに誰でもアクセスできることを示す警告アイコンも表示されます)。

89e7a4d2c80a0319.png

これで、バケットで写真を受け取れるようになりました。

バケット名をクリックすると、バケットの詳細が表示されます。

131387f12d3eb2d3.png

ここで、Upload files ボタンを試して、バケットに写真を追加できることをテストします。ファイル選択ポップアップが表示され、ファイルの選択を求められます。選択すると、バケットにアップロードされ、この新しいファイルに自動的に割り当てられた public アクセス権が再び表示されます。

e87584471a6e9c6d.png

Public アクセスラベルの横に、小さなリンクアイコンも表示されます。クリックすると、ブラウザはその画像の公開 URL に移動します。この URL の形式は次のようになります。

https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png

BUCKET_NAME は、バケットに選択したグローバルに一意の名前と、写真のファイル名です。

画像名の横にあるチェックボックスをオンにすると、DELETE ボタンが有効になり、最初の画像を削除できます。

7. 関数を作成する

このステップでは、写真のアップロード イベントに応答する関数を作成します。

Google Cloud コンソールの Cloud Functions セクションに移動します。このページにアクセスすると、Cloud Functions サービスが自動的に有効になります。

9d29e8c026a7a53f.png

Create function をクリックします。

名前を選択します(例: picture-uploaded)とリージョン(バケットのリージョン選択と一致させる必要があります)。

4bb222633e6f278.png

関数には次の 2 種類があります。

  • URL(ウェブ API など)で呼び出すことができる HTTP 関数。
  • イベントによってトリガーできるバックグラウンド関数。

Cloud Storage バケットに新しいファイルがアップロードされたときにトリガーされるバックグラウンド関数を作成します。

d9a12fcf58f4813c.png

バケットでファイルが作成または更新されたときにトリガーされるイベントである Finalize/Create イベントタイプに関心があります。

b30c8859b07dc4cb.png

前に作成したバケットを選択して、この特定のバケットでファイルが作成または更新されたときに通知されるように Cloud Functions に指示します。

cb15a1f4c7a1ca5f.png

Select をクリックして、前に作成したバケットを選択し、Save をクリックします。

c1933777fac32c6a.png

[次へ] をクリックする前に、[ランタイム、ビルド、接続、セキュリティの設定] でデフォルト(256 MB のメモリ)を開いて変更し、1 GB に更新できます。

83d757e6c38e10.png

Next をクリックすると、ランタイムソースコードエントリ ポイントを調整できます。

この関数の Inline editor を保持します。

b6646ec646082b32.png

Java ランタイムのいずれか(Java 11 など)を選択します。

f85b8a6f951f47a7.png

ソースコードは、Java ファイルと、さまざまなメタデータと依存関係を提供する pom.xml Maven ファイルで構成されています。

デフォルトのコード スニペットはそのままにします。アップロードされた写真のファイル名がログに記録されます。

9b7b9801b42f6ca6.png

ここでは、テスト用に実行する関数の名前を Example にします。

Deploy をクリックして関数を作成し、デプロイします。デプロイが成功すると、関数のリストに緑色の丸で囲まれたチェックマークが表示されます。

3732fdf409eefd1a.png

8. 関数をテストする

このステップでは、関数がストレージ イベントに応答することを確認します。

ハンバーガー(☰)メニューから Storage ページに戻ります。

画像バケットをクリックし、Upload files をクリックして画像をアップロードします。

21767ec3cb8b18de.png

Cloud コンソール内で再度移動して、Logging > Logs Explorer ページに移動します。

Log Fields セレクタで Cloud Function を選択して、関数専用のログを表示します。[ログフィールド] を下にスクロールすると、特定の関数を選択して、関数に関連するログの詳細なビューを表示することもできます。picture-uploaded 関数を選択します。

関数の作成、関数の開始時間と終了時間、実際のログステートメントに関するログ項目が表示されます。

e8ba7d39c36df36c.png

ログステートメントは Processing file: pic-a-daily-architecture-events.png となっています。これは、この写真の作成と保存に関連するイベントが想定どおりにトリガーされたことを意味します。

9. データベースを準備する

Vision API から提供された画像に関する情報は、高速、フルマネージド、サーバーレス、クラウドネイティブの NoSQL ドキュメント データベースである Cloud Firestore データベースに保存します。Cloud Console の Firestore セクションに移動して、データベースを準備します。

9e4708d2257de058.png

Native mode または Datastore mode の 2 つのオプションがあります。ネイティブ モードを使用します。ネイティブ モードでは、オフライン サポートやリアルタイム同期などの追加機能を利用できます。

SELECT NATIVE MODE をクリックします。

9449ace8cc84de43.png

マルチリージョンを選択します(ここではヨーロッパですが、理想的には関数とストレージ バケットと同じリージョンを少なくとも選択します)。

[CREATE DATABASE] ボタンをクリックします。

データベースが作成されると、次のようになります。

56265949a124819e.png

+ START COLLECTION ボタンをクリックして、新しいコレクションを作成します。

名前のコレクション pictures

75806ee24c4e13a7.png

ドキュメントを作成する必要はありません。新しい写真が Cloud Storage に保存され、Vision API によって分析されると、それらの写真がプログラムで追加されます。

[Save] をクリックします。

Firestore は、新しく作成されたコレクションに最初のデフォルト ドキュメントを作成します。このドキュメントには有用な情報は含まれていないため、安全に削除できます。

5c2f1e17ea47f48f.png

コレクションにプログラムで作成されるドキュメントには、次の 4 つのフィールドが含まれます。

  • name(文字列): アップロードされた写真のファイル名。ドキュメントのキーでもあります。
  • labels(文字列の配列): Vision API によって認識されたアイテムのラベル
  • color(文字列): ドミナント カラーの 16 進数カラーコード(例: #ab12ef)
  • created(日付): この画像のメタデータが保存されたときのタイムスタンプ
  • thumbnail(ブール値): この写真のサムネイル画像が生成された場合に存在し、true になる省略可能なフィールド

Firestore で検索してサムネイルが利用可能な写真を見つけ、作成日で並べ替えるため、検索インデックスを作成する必要があります。

Cloud Shell で次のコマンドを使用してインデックスを作成できます。

gcloud firestore indexes composite create \
  --collection-group=pictures \
  --field-config field-path=thumbnail,order=descending \
  --field-config field-path=created,order=descending

または、Cloud コンソールで、左側のナビゲーション列の Indexes をクリックし、次のように複合インデックスを作成することもできます。

ecb8b95e3c791272.png

Create をクリックします。インデックスの作成には数分かかることがあります。

10. 関数を更新する

Functions ページに戻り、Vision API を呼び出して写真を分析し、メタデータを Firestore に保存するように関数を更新します。

ハンバーガー メニュー(☰)から Cloud Functions セクションに移動し、関数名をクリックして Source タブを選択し、EDIT ボタンをクリックします。

まず、Java 関数の依存関係を一覧表示する pom.xml ファイルを編集します。Cloud Vision API Maven 依存関係を追加するようにコードを更新します。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>cloudfunctions</groupId>
  <artifactId>gcs-function</artifactId>
  <version>1.0-SNAPSHOT</version>

  <properties>
    <maven.compiler.target>11</maven.compiler.target>
    <maven.compiler.source>11</maven.compiler.source>
  </properties>

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

  <dependencies>
    <dependency>
      <groupId>com.google.cloud.functions</groupId>
      <artifactId>functions-framework-api</artifactId>
      <version>1.0.4</version>
      <type>jar</type>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-firestore</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>google-cloud-storage</artifactId>
    </dependency>
  </dependencies>

  <!-- Required for Java 11 functions in the inline editor -->
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>3.8.1</version>
        <configuration>
          <excludes>
            <exclude>.google/</exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>

依存関係が最新の状態になったので、カスタムコードで Example.java ファイルを更新して、関数のコードを操作します。

Example.java ファイルにカーソルを合わせ、鉛筆アイコンをクリックします。パッケージ名とファイル名を src/main/java/fn/ImageAnalysis.java に置き換えます。

ImageAnalysis.java 内のコードを次のコードで置き換えます。これについては次のステップで説明します。

package fn;

import com.google.cloud.functions.*;
import com.google.cloud.vision.v1.*;
import com.google.cloud.vision.v1.Feature.Type;
import com.google.cloud.firestore.*;
import com.google.api.core.ApiFuture;

import java.io.*;
import java.util.*;
import java.util.stream.*;
import java.util.concurrent.*;
import java.util.logging.Logger;

import fn.ImageAnalysis.GCSEvent;

public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    private static final Logger logger = Logger.getLogger(ImageAnalysis.class.getName());

    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException, ExecutionException {
        String fileName = event.name;
        String bucketName = event.bucket;

        logger.info("New picture uploaded " + fileName);

        try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
            List<AnnotateImageRequest> requests = new ArrayList<>();
            
            ImageSource imageSource = ImageSource.newBuilder()
                .setGcsImageUri("gs://" + bucketName + "/" + fileName)
                .build();

            Image image = Image.newBuilder()
                .setSource(imageSource)
                .build();

            Feature featureLabel = Feature.newBuilder()
                .setType(Type.LABEL_DETECTION)
                .build();
            Feature featureImageProps = Feature.newBuilder()
                .setType(Type.IMAGE_PROPERTIES)
                .build();
            Feature featureSafeSearch = Feature.newBuilder()
                .setType(Type.SAFE_SEARCH_DETECTION)
                .build();
                
            AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
                .addFeatures(featureLabel)
                .addFeatures(featureImageProps)
                .addFeatures(featureSafeSearch)
                .setImage(image)
                .build();
            
            requests.add(request);

            logger.info("Calling the Vision API...");
            BatchAnnotateImagesResponse result = vision.batchAnnotateImages(requests);
            List<AnnotateImageResponse> responses = result.getResponsesList();

            if (responses.size() == 0) {
                logger.info("No response received from Vision API.");
                return;
            }

            AnnotateImageResponse response = responses.get(0);
            if (response.hasError()) {
                logger.info("Error: " + response.getError().getMessage());
                return;
            }

            List<String> labels = response.getLabelAnnotationsList().stream()
                .map(annotation -> annotation.getDescription())
                .collect(Collectors.toList());
            logger.info("Annotations found:");
            for (String label: labels) {
                logger.info("- " + label);
            }

            String mainColor = "#FFFFFF";
            ImageProperties imgProps = response.getImagePropertiesAnnotation();
            if (imgProps.hasDominantColors()) {
                DominantColorsAnnotation colorsAnn = imgProps.getDominantColors();
                ColorInfo colorInfo = colorsAnn.getColors(0);

                mainColor = rgbHex(
                    colorInfo.getColor().getRed(), 
                    colorInfo.getColor().getGreen(), 
                    colorInfo.getColor().getBlue());

                logger.info("Color: " + mainColor);
            }

            boolean isSafe = false;
            if (response.hasSafeSearchAnnotation()) {
                SafeSearchAnnotation safeSearch = response.getSafeSearchAnnotation();

                isSafe = Stream.of(
                    safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
                    safeSearch.getSpoof(), safeSearch.getViolence())
                .allMatch( likelihood -> 
                    likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
                );

                logger.info("Safe? " + isSafe);
            }

            // Saving result to Firestore
            if (isSafe) {
                FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
                Firestore pictureStore = firestoreOptions.getService();

                DocumentReference doc = pictureStore.collection("pictures").document(fileName);

                Map<String, Object> data = new HashMap<>();
                data.put("labels", labels);
                data.put("color", mainColor);
                data.put("created", new Date());

                ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

                logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
            }
        }
    }

    private static String rgbHex(float red, float green, float blue) {
        return String.format("#%02x%02x%02x", (int)red, (int)green, (int)blue);
    }

    public static class GCSEvent {
        String bucket;
        String name;
    }
}

968749236c3f01da.png

11. 関数を調べる

それでは、さまざまな興味深い部分を詳しく見ていきましょう。

まず、Maven の pom.xml ファイルに特定の依存関係を含めます。Google Java クライアント ライブラリは、依存関係の競合を解消するために Bill-of-Materials(BOM) を公開しています。これを使用すると、個々の Google クライアント ライブラリのバージョンを指定する必要がなくなります。

  <dependencyManagement>
    <dependencies>
      <dependency>
        <groupId>com.google.cloud</groupId>
        <artifactId>libraries-bom</artifactId>
        <version>26.1.1</version>
        <type>pom</type>
        <scope>import</scope>
      </dependency>
    </dependencies>
  </dependencyManagement>

次に、Vision API のクライアントを準備します。

...
try (ImageAnnotatorClient vision = ImageAnnotatorClient.create()) {
...

次に、関数の構造を示します。受信したイベントから必要なフィールドを取得し、定義した GCSEvent 構造にマッピングします。

...
public class ImageAnalysis implements BackgroundFunction<GCSEvent> {
    @Override
    public void accept(GCSEvent event, Context context) 
            throws IOException, InterruptedException,     
    ExecutionException {
...

    public static class GCSEvent {
        String bucket;
        String name;
    }

シグネチャだけでなく、Cloud Functions をトリガーしたファイルとバケットの名前を取得する方法にも注目してください。

参考までに、イベント ペイロードの例を以下に示します。

{
  "bucket":"uploaded-pictures",
  "contentType":"image/png",
  "crc32c":"efhgyA==",
  "etag":"CKqB956MmucCEAE=",
  "generation":"1579795336773802",
  "id":"uploaded-pictures/Screenshot.png/1579795336773802",
  "kind":"storage#object",
  "md5Hash":"PN8Hukfrt6C7IyhZ8d3gfQ==",
  "mediaLink":"https://www.googleapis.com/download/storage/v1/b/uploaded-pictures/o/Screenshot.png?generation=1579795336773802&alt=media",
  "metageneration":"1",
  "name":"Screenshot.png",
  "selfLink":"https://www.googleapis.com/storage/v1/b/uploaded-pictures/o/Screenshot.png",
  "size":"173557",
  "storageClass":"STANDARD",
  "timeCreated":"2020-01-23T16:02:16.773Z",
  "timeStorageClassUpdated":"2020-01-23T16:02:16.773Z",
  "updated":"2020-01-23T16:02:16.773Z"
}

Vision クライアント経由で送信するリクエストを準備します。

ImageSource imageSource = ImageSource.newBuilder()
    .setGcsImageUri("gs://" + bucketName + "/" + fileName)
    .build();

Image image = Image.newBuilder()
    .setSource(imageSource)
    .build();

Feature featureLabel = Feature.newBuilder()
    .setType(Type.LABEL_DETECTION)
    .build();
Feature featureImageProps = Feature.newBuilder()
    .setType(Type.IMAGE_PROPERTIES)
    .build();
Feature featureSafeSearch = Feature.newBuilder()
    .setType(Type.SAFE_SEARCH_DETECTION)
    .build();
    
AnnotateImageRequest request = AnnotateImageRequest.newBuilder()
    .addFeatures(featureLabel)
    .addFeatures(featureImageProps)
    .addFeatures(featureSafeSearch)
    .setImage(image)
    .build();

Vision API の 3 つの主要な機能をリクエストしています。

  • ラベル検出: 写真の内容を理解する
  • 画像プロパティ: 画像の興味深い属性(画像のドミナント カラー)を取得します。
  • セーフサーチ: 画像を表示しても安全かどうかを判断します(アダルト コンテンツ、医療コンテンツ、わいせつなコンテンツ、暴力的なコンテンツが含まれていないかどうかを判断します)。

この時点で、Vision API を呼び出すことができます。

...
logger.info("Calling the Vision API...");
BatchAnnotateImagesResponse result = 
                            vision.batchAnnotateImages(requests);
List<AnnotateImageResponse> responses = result.getResponsesList();
...

参考までに、Vision API からのレスポンスは次のようになります。

{
  "faceAnnotations": [],
  "landmarkAnnotations": [],
  "logoAnnotations": [],
  "labelAnnotations": [
    {
      "locations": [],
      "properties": [],
      "mid": "/m/01yrx",
      "locale": "",
      "description": "Cat",
      "score": 0.9959855675697327,
      "confidence": 0,
      "topicality": 0.9959855675697327,
      "boundingPoly": null
    },
     - - - 
  ],
  "textAnnotations": [],
  "localizedObjectAnnotations": [],
  "safeSearchAnnotation": {
    "adult": "VERY_UNLIKELY",
    "spoof": "UNLIKELY",
    "medical": "VERY_UNLIKELY",
    "violence": "VERY_UNLIKELY",
    "racy": "VERY_UNLIKELY",
    "adultConfidence": 0,
    "spoofConfidence": 0,
    "medicalConfidence": 0,
    "violenceConfidence": 0,
    "racyConfidence": 0,
    "nsfwConfidence": 0
  },
  "imagePropertiesAnnotation": {
    "dominantColors": {
      "colors": [
        {
          "color": {
            "red": 203,
            "green": 201,
            "blue": 201,
            "alpha": null
          },
          "score": 0.4175916016101837,
          "pixelFraction": 0.44456374645233154
        },
         - - - 
      ]
    }
  },
  "error": null,
  "cropHintsAnnotation": {
    "cropHints": [
      {
        "boundingPoly": {
          "vertices": [
            { "x": 0, "y": 118 },
            { "x": 1177, "y": 118 },
            { "x": 1177, "y": 783 },
            { "x": 0, "y": 783 }
          ],
          "normalizedVertices": []
        },
        "confidence": 0.41695669293403625,
        "importanceFraction": 1
      }
    ]
  },
  "fullTextAnnotation": null,
  "webDetection": null,
  "productSearchResults": null,
  "context": null
}

エラーが返されなかった場合は、次に進むことができます。そのため、次のような if ブロックがあります。

AnnotateImageResponse response = responses.get(0);
if (response.hasError()) {
     logger.info("Error: " + response.getError().getMessage());
     return;
}

写真で認識されたもの、カテゴリ、テーマのラベルを取得します。

List<String> labels = response.getLabelAnnotationsList().stream()
    .map(annotation -> annotation.getDescription())
    .collect(Collectors.toList());

logger.info("Annotations found:");
for (String label: labels) {
    logger.info("- " + label);
}

写真のドミナント カラーを知りたいとします。

String mainColor = "#FFFFFF";
ImageProperties imgProps = response.getImagePropertiesAnnotation();
if (imgProps.hasDominantColors()) {
    DominantColorsAnnotation colorsAnn = 
                               imgProps.getDominantColors();
    ColorInfo colorInfo = colorsAnn.getColors(0);

    mainColor = rgbHex(
        colorInfo.getColor().getRed(), 
        colorInfo.getColor().getGreen(), 
        colorInfo.getColor().getBlue());

    logger.info("Color: " + mainColor);
}

また、ユーティリティ関数を使用して、赤 / 緑 / 青の値を CSS スタイルシートで使用できる 16 進数のカラーコードに変換しています。

写真を表示しても安全かどうかを確認しましょう。

boolean isSafe = false;
if (response.hasSafeSearchAnnotation()) {
    SafeSearchAnnotation safeSearch = 
                      response.getSafeSearchAnnotation();

    isSafe = Stream.of(
        safeSearch.getAdult(), safeSearch.getMedical(), safeSearch.getRacy(),
        safeSearch.getSpoof(), safeSearch.getViolence())
    .allMatch( likelihood -> 
        likelihood != Likelihood.LIKELY && likelihood != Likelihood.VERY_LIKELY
    );

    logger.info("Safe? " + isSafe);
}

アダルト、なりすまし、医療、暴力、わいせつな属性が「可能性が高い」または「可能性が非常に高い」でないかどうかを確認しています。

セーフサーチの結果が問題なければ、メタデータを Firestore に保存できます。

if (isSafe) {
    FirestoreOptions firestoreOptions = FirestoreOptions.getDefaultInstance();
    Firestore pictureStore = firestoreOptions.getService();

    DocumentReference doc = pictureStore.collection("pictures").document(fileName);

    Map<String, Object> data = new HashMap<>();
    data.put("labels", labels);
    data.put("color", mainColor);
    data.put("created", new Date());

    ApiFuture<WriteResult> writeResult = doc.set(data, SetOptions.merge());

    logger.info("Picture metadata saved in Firestore at " + writeResult.get().getUpdateTime());
}

12. 関数をデプロイする

関数をデプロイします。

604f47aa11fbf8e.png

DEPLOY ボタンを押すと、新しいバージョンがデプロイされます。進行状況は次のとおりです。

13da63f23e4dbbdd.png

13. 関数を再度テストする

関数が正常にデプロイされたら、Cloud Storage に写真を投稿し、関数が呼び出されたかどうか、Vision API が何を返すか、メタデータが Firestore に保存されているかどうかを確認します。

Cloud Storage に戻り、ラボの開始時に作成したバケットをクリックします。

d44c1584122311c7.png

バケットの詳細ページで、Upload files ボタンをクリックして写真をアップロードします。

26bb31d35fb6aa3d.png

ハンバーガー(☰)メニューから、Logging > Logs エクスプローラに移動します。

Log Fields セレクタで Cloud Function を選択して、関数専用のログを表示します。[ログフィールド] を下にスクロールすると、特定の関数を選択して、関数に関連するログの詳細なビューを表示することもできます。picture-uploaded 関数を選択します。

b651dca7e25d5b11.png

ログのリストを見ると、関数が呼び出されたことがわかります。

d22a7f24954e4f63.png

ログには、関数実行の開始と終了が示されます。その間には、console.log() ステートメントで関数に配置したログが表示されます。次のような出力が表示されます。

  • 関数をトリガーするイベントの詳細。
  • Vision API 呼び出しの未加工の結果。
  • アップロードした写真で見つかったラベル、
  • ドミナント カラーの情報、
  • 写真を表示しても安全かどうか、
  • 最終的に、写真に関するメタデータは Firestore に保存されます。

9ff7956a215c15da.png

もう一度ハンバーガー メニュー(☰)から Firestore セクションに移動します。Data サブセクション(デフォルトで表示)に、新しく追加されたドキュメントを含む pictures コレクションが表示されます。これは、先ほどアップロードした写真に対応しています。

a6137ab9687da370.png

14. クリーンアップ(省略可)

シリーズの他のラボを続行する予定がない場合は、リソースをクリーンアップして費用を節約し、クラウドのマナーを守りましょう。リソースを個別にクリーンアップするには、次の操作を行います。

バケットを削除します。

gsutil rb gs://${BUCKET_PICTURES}

関数を削除します。

gcloud functions delete picture-uploaded --region europe-west1 -q

コレクションから [コレクションを削除] を選択して、Firestore コレクションを削除します。

410b551c3264f70a.png

または、プロジェクト全体を削除することもできます。

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. 完了

おめでとうございます!プロジェクトの最初の主要なサービスを実装できました。

学習した内容

  • Cloud Storage
  • Cloud Functions
  • Cloud Vision API
  • Cloud Firestore

次のステップ