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

1. 概要

最初の Codelab では、バケットに画像をアップロードします。これにより、関数によって処理されるファイル作成イベントが生成されます。この関数は 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 を生成できます。または、ご自身でお試しになることもできます。このステップを終えた後は変更できず、プロジェクト期間中は維持されます。
  • なお、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 コンソールまたは gcloud で有効にする必要があります。

Cloud コンソールで 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. バケットを作成する(コンソール)

画像用の Storage バケットを作成します。これを行うには、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] をクリックします。

保護/暗号化の設定

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. バケットへの公開アクセスをテストする

Storage ブラウザに戻ると、バケットに [Public] と表示されているバケットがリストに表示されています。アクセス(そのバケットのコンテンツには誰でもアクセスできることを示す警告マークを含む)を付与する必要があります。

89e7a4d2c80a0319.png

これで、バケットで写真を受け取る準備が整いました。

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

131387f12d3eb2d3.png

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

e87584471a6e9c6d.png

Public アクセスラベルの横には、小さなリンクアイコンも表示されます。その画像をクリックすると、ブラウザはその画像の公開 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)と Region(バケットのリージョン選択との整合性を保つ必要があります):

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 ランタイムの 1 つ(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 から提供された写真に関する情報を Cloud Firestore データベースに保存します。Cloud Firestore データベースは、高速、フルマネージド、サーバーレス、クラウドネイティブの NoSQL ドキュメント データベースです。Cloud コンソールの 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)
  • 作成日(日付): この画像のメタデータが保存されたときのタイムスタンプ
  • 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 に保存する関数を更新します。

「ハンバーガー」から(gcr)メニューから 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 には、

  • ラベル検出: 画像の内容を把握
  • 画像プロパティ: 画像の興味深い属性を指定します(画像の主な色を確認できます)。
  • セーフサーチ: 画像が表示しても安全かどうかを知ることができます(アダルト / 医療 / 際どい / 暴力的なコンテンツを含む画像は使用できません)。

この時点で、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);
}

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

画像が安全に見せることができるか確認しましょう。

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

「ハンバーガー」から(gcr)メニューで、Logging > Logs Explorer に移動します。

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

次のステップ