每天多拍:研究室 1 - 儲存與分析圖片 (Java)

1. 總覽

在第一個程式碼研究室中,您會將圖片上傳至 bucket。這會產生檔案建立事件,並由函式處理。函式會呼叫 Vision API 進行圖片分析,並將結果儲存在資料存放區。

d650ca5386ea71ad.png

課程內容

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

2. 設定和需求

自修實驗室環境設定

  1. 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • 專案名稱是這個專案參與者的顯示名稱。這是 Google API 未使用的字元字串。你隨時可以更新該位置資訊。
  • 專案 ID 在所有 Google Cloud 專案中不得重複,且設定後即無法變更。Cloud 控制台會自動產生不重複的字串,通常您不需要在意這個字串。在大多數程式碼研究室中,您需要參照專案 ID (通常會標示為 PROJECT_ID)。如果您不喜歡產生的 ID,可以產生另一個隨機 ID。你也可以嘗試自訂名稱,看看是否可用。完成這個步驟後就無法變更,且專案期間都會維持這個設定。
  • 請注意,部分 API 會使用第三個值,也就是「專案編號」。如要進一步瞭解這三種值,請參閱說明文件
  1. 接著,您需要在 Cloud 控制台中啟用帳單,才能使用 Cloud 資源/API。完成本程式碼研究室的費用應該不高,甚至完全免費。如要關閉資源,避免產生本教學課程以外的費用,您可以刪除自己建立的資源,或刪除整個專案。Google Cloud 新使用者可參加價值$300 美元的免費試用計畫。

啟動 Cloud Shell

雖然可以透過筆電遠端操作 Google Cloud,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是可在雲端執行的指令列環境。

Google Cloud 控制台中,點選右上工具列的 Cloud Shell 圖示:

55efc1aaa7a4d3ad.png

佈建並連線至環境的作業需要一些時間才能完成。完成後,您應該會看到如下的內容:

7ffe5cbb04455448.png

這部虛擬機器搭載各種您需要的開發工具,並提供永久的 5GB 主目錄,而且可在 Google Cloud 運作,大幅提升網路效能並強化驗證功能。您可以在瀏覽器中完成本程式碼研究室的所有作業。您不需要安裝任何軟體。

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. 建立 bucket (主控台)

建立圖片的儲存空間值區。您可以透過 Google Cloud Platform 主控台 ( console.cloud.google.com) 或 Cloud Shell 或本機開發環境的 gsutil 指令列工具執行這項操作。

在「漢堡」選單 (☰) 中,前往 Storage 頁面。

1930e055d138150a.png

為 bucket 命名

按一下 CREATE BUCKET 按鈕。

34147939358517f8.png

按一下「CONTINUE」。

選擇位置

197817f20be07678.png

在所選區域 (此處為 Europe) 建立多區域 bucket。

按一下「CONTINUE」。

選擇預設儲存空間級別

53cd91441c8caf0e.png

為資料選擇 Standard 儲存空間級別。

按一下「CONTINUE」。

設定存取權控管

8c2b3b459d934a51.png

由於您將使用可公開存取的圖片,因此希望儲存在這個 bucket 中的所有圖片都具有相同的統一存取權控管機制。

選擇 Uniform 存取權控管選項。

按一下「CONTINUE」。

設定保護/加密

d931c24c3e705a68.png

保留預設值 (Google-managed key)),因為您不會使用自己的加密金鑰。

按一下 CREATE,最終完成值區建立程序。

將 allUsers 新增為儲存空間檢視者

前往 Permissions 分頁:

d0ecfdcff730ea51.png

allUsers 成員新增至 bucket,並指派 Storage > Storage Object Viewer 角色,如下所示:

e9f25ec1ea0b6cc6.png

按一下「SAVE」。

5. 建立 bucket (gsutil)

您也可以使用 Cloud Shell 中的 gsutil 指令列工具建立 bucket。

在 Cloud Shell 中,為不重複的值區名稱設定變數。Cloud Shell 已將 GOOGLE_CLOUD_PROJECT 設為專屬專案 ID。你可以將該值附加至 bucket 名稱。

例如:

export BUCKET_PICTURES=uploaded-pictures-${GOOGLE_CLOUD_PROJECT}

在歐洲建立標準多區域:

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

確認統一值區層級存取權:

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

將 bucket 設為公開:

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

前往控制台的 Cloud Storage 部分,您應該會看到公開 uploaded-pictures bucket:

a98ed4ba17873e40.png

如上一個步驟所述,測試您是否可以將圖片上傳至值區,並確認上傳的圖片可公開存取。

6. 測試 bucket 的公開存取權

返回儲存空間瀏覽器,您會在清單中看到自己的 bucket,並顯示「公開」存取權 (包括提醒您任何人都能存取該 bucket 內容的警告符號)。

89e7a4d2c80a0319.png

現在值區已可接收圖片。

按一下 bucket 名稱,即可查看 bucket 詳細資料。

131387f12d3eb2d3.png

您可以在該處嘗試 Upload files 按鈕,測試是否能將圖片新增至值區。檔案選擇器彈出式視窗會要求你選取檔案。選取後,系統會將檔案上傳至儲存空間,並再次顯示自動指派給這個新檔案的public存取權。

e87584471a6e9c6d.png

Public 存取權標籤旁邊也會顯示小小的連結圖示。點選後,瀏覽器會前往該圖片的公開網址,格式如下:

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

其中 BUCKET_NAME 是您為 bucket 選擇的全域專屬名稱,後面則是圖片的檔案名稱。

按一下圖片名稱旁的核取方塊,即可啟用 DELETE 按鈕,並刪除第一張圖片。

7. 建立函式

在這個步驟中,您會建立函式來回應圖片上傳事件。

前往 Google Cloud 控制台的「Cloud Functions」專區。只要前往該頁面,系統就會自動啟用 Cloud Functions 服務。

9d29e8c026a7a53f.png

按一下 Create function

選擇名稱 (例如 picture-uploaded),以及「Region」(區域) (請務必與 bucket 的區域選擇保持一致):

4bb222633e6f278.png

函式分為兩種:

  • 可透過網址叫用的 HTTP 函式 (即 Web API)。
  • 可由某些事件觸發的背景函式。

您想建立背景函式,在上傳新檔案至 Cloud Storage 值區時觸發:

d9a12fcf58f4813c.png

您感興趣的事件類型是 Finalize/Create,也就是在 Bucket 中建立或更新檔案時觸發的事件:

b30c8859b07dc4cb.png

選取先前建立的 bucket,讓 Cloud Functions 在這個 bucket 中建立 / 更新檔案時收到通知:

cb15a1f4c7a1ca5f.png

按一下 Select 選擇先前建立的 bucket,然後按一下 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 提供的圖片資訊儲存到 Cloud Firestore 資料庫。這項服務是快速、全代管、無伺服器且雲端原生的 NoSQL 文件資料庫。前往 Cloud 控制台的「Firestore」部分,準備資料庫:

9e4708d2257de058.png

系統會提供兩個選項:Native modeDatastore mode。使用原生模式,即可享有離線支援和即時同步等額外功能。

按一下 SELECT NATIVE MODE

9449ace8cc84de43.png

選擇多地區 (這裡選擇歐洲,但最好與函式和儲存空間值區位於相同地區)。

按一下 CREATE DATABASE 按鈕。

資料庫建立完成後,您應該會看到下列內容:

56265949a124819e.png

按一下 + START COLLECTION 按鈕,建立新的集合

為集合命名 pictures

75806ee24c4e13a7.png

您不需要建立文件,當新圖片儲存在 Cloud Storage 中,並由 Vision API 分析時,您會以程式輔助方式新增圖片。

按一下「Save」。

Firestore 會在新建立的集合中建立第一個預設文件,您可以安全地刪除該文件,因為其中不含任何實用資訊:

5c2f1e17ea47f48f.png

我們將在集合中以程式輔助方式建立的文件會包含 4 個欄位:

  • name (字串):上傳圖片的檔案名稱,也是文件的鍵
  • 標籤 (字串陣列):Vision API 辨識項目的標籤
  • color (字串):主色的十六進位顏色代碼 (即 #ab12ef)
  • created (日期):儲存這張圖片中繼資料的時間戳記
  • 縮圖 (布林值):選用欄位,如果系統已為這張相片產生縮圖,這個欄位就會存在並設為 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」按鈕。

首先,請編輯 pom.xml 檔案,其中列出 Java 函式的依附元件。更新程式碼,新增 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 函式的檔案和 bucket 名稱。

如要參考,以下是事件酬載的樣子:

{
  "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 樣式表中使用。

讓我們檢查圖片是否安全無虞:

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,然後按一下我們在實驗室開始時建立的 bucket:

d44c1584122311c7.png

進入值區詳細資料頁面後,按一下 Upload files 按鈕即可上傳圖片。

26bb31d35fb6aa3d.png

從「漢堡」選單 (☰) 導覽至「Explorer」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

後續步驟