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

課程內容
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore
2. 設定和需求
自修實驗室環境設定
- 登入 Google Cloud 控制台,然後建立新專案或重複使用現有專案。如果沒有 Gmail 或 Google Workspace 帳戶,請先建立帳戶。



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

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

這部虛擬機器搭載各種您需要的開發工具,並提供永久的 5GB 主目錄,而且可在 Google Cloud 運作,大幅提升網路效能並強化驗證功能。您可以在瀏覽器中完成本程式碼研究室的所有作業。您不需要安裝任何軟體。
3. 啟用 API
在本實驗室中,您將使用 Cloud Functions 和 Vision API,但首先必須在 Cloud 控制台或使用 gcloud 啟用這些服務。
如要在 Cloud 控制台中啟用 Vision API,請在搜尋列中搜尋 Cloud Vision API:

系統會將您導向 Cloud Vision API 頁面:

按一下 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 頁面。

為 bucket 命名
按一下 CREATE BUCKET 按鈕。

按一下「CONTINUE」。
選擇位置

在所選區域 (此處為 Europe) 建立多區域 bucket。
按一下「CONTINUE」。
選擇預設儲存空間級別

為資料選擇 Standard 儲存空間級別。
按一下「CONTINUE」。
設定存取權控管

由於您將使用可公開存取的圖片,因此希望儲存在這個 bucket 中的所有圖片都具有相同的統一存取權控管機制。
選擇 Uniform 存取權控管選項。
按一下「CONTINUE」。
設定保護/加密

保留預設值 (Google-managed key)),因為您不會使用自己的加密金鑰。
按一下 CREATE,最終完成值區建立程序。
將 allUsers 新增為儲存空間檢視者
前往 Permissions 分頁:

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

按一下「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:

如上一個步驟所述,測試您是否可以將圖片上傳至值區,並確認上傳的圖片可公開存取。
6. 測試 bucket 的公開存取權
返回儲存空間瀏覽器,您會在清單中看到自己的 bucket,並顯示「公開」存取權 (包括提醒您任何人都能存取該 bucket 內容的警告符號)。

現在值區已可接收圖片。
按一下 bucket 名稱,即可查看 bucket 詳細資料。

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

Public 存取權標籤旁邊也會顯示小小的連結圖示。點選後,瀏覽器會前往該圖片的公開網址,格式如下:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
其中 BUCKET_NAME 是您為 bucket 選擇的全域專屬名稱,後面則是圖片的檔案名稱。
按一下圖片名稱旁的核取方塊,即可啟用 DELETE 按鈕,並刪除第一張圖片。
7. 建立函式
在這個步驟中,您會建立函式來回應圖片上傳事件。
前往 Google Cloud 控制台的「Cloud Functions」專區。只要前往該頁面,系統就會自動啟用 Cloud Functions 服務。

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

函式分為兩種:
- 可透過網址叫用的 HTTP 函式 (即 Web API)。
- 可由某些事件觸發的背景函式。
您想建立背景函式,在上傳新檔案至 Cloud Storage 值區時觸發:

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

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

按一下 Select 選擇先前建立的 bucket,然後按一下 Save

按一下「下一步」前,您可以展開並修改「執行階段、建構作業、連線和安全性設定」下方的預設值 (256 MB 記憶體),然後更新為 1 GB。

點選 Next 後,即可調整「執行階段」、「原始碼」和「進入點」。
保留這個函式的 Inline editor:

選取其中一個 Java 執行階段,例如 Java 11:

原始碼包含 Java 檔案,以及提供各種中繼資料和依附元件的 pom.xml Maven 檔案。
保留預設的程式碼片段,這個片段會記錄上傳圖片的檔案名稱:

目前,請將要執行的函式名稱保留為 Example,以利測試。
按一下 Deploy 即可建立及部署函式。部署成功後,函式清單中會顯示綠色圓圈勾號:

8. 測試函式
在本步驟中,請測試函式是否會回應儲存空間事件。
從「漢堡」選單 (☰) 返回 Storage 頁面。
按一下圖片值區,然後按一下 Upload files 上傳圖片。

再次前往 Cloud 控制台中的 Logging > Logs Explorer 頁面。
在 Log Fields 選取器中,選取 Cloud Function 即可查看函式專屬記錄。向下捲動瀏覽記錄檔欄位,您甚至可以選取特定函式,更精細地查看函式相關記錄檔。選取 picture-uploaded 函式。
您應該會看到記錄項目,其中提到函式的建立時間、函式的開始和結束時間,以及實際的記錄陳述式:

記錄陳述式為 Processing file: pic-a-daily-architecture-events.png,表示與建立及儲存這張相片相關的事件確實已如預期觸發。
9. 準備資料庫
您會將 Vision API 提供的圖片資訊儲存到 Cloud Firestore 資料庫。這項服務是快速、全代管、無伺服器且雲端原生的 NoSQL 文件資料庫。前往 Cloud 控制台的「Firestore」部分,準備資料庫:

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

選擇多地區 (這裡選擇歐洲,但最好與函式和儲存空間值區位於相同地區)。
按一下 CREATE DATABASE 按鈕。
資料庫建立完成後,您應該會看到下列內容:

按一下 + START COLLECTION 按鈕,建立新的集合。
為集合命名 pictures。

您不需要建立文件,當新圖片儲存在 Cloud Storage 中,並由 Vision API 分析時,您會以程式輔助方式新增圖片。
按一下「Save」。
Firestore 會在新建立的集合中建立第一個預設文件,您可以安全地刪除該文件,因為其中不含任何實用資訊:

我們將在集合中以程式輔助方式建立的文件會包含 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,然後建立複合索引,如下所示:

按一下 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;
}
}

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. 部署函式
現在要部署函式。

按下 DEPLOY 按鈕,系統就會部署新版本,您可以查看進度:

13. 再次測試函式
成功部署函式後,請將圖片發布至 Cloud Storage,看看函式是否會遭到叫用、Vision API 會傳回什麼內容,以及中繼資料是否儲存在 Firestore 中。
返回 Cloud Storage,然後按一下我們在實驗室開始時建立的 bucket:

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

從「漢堡」選單 (☰) 導覽至「Explorer」Logging > Logs。
在 Log Fields 選取器中,選取 Cloud Function 即可查看函式專屬記錄。向下捲動瀏覽記錄檔欄位,您甚至可以選取特定函式,更精細地查看函式相關記錄檔。選取 picture-uploaded 函式。

在記錄清單中,確實可以看到函式已叫用:

記錄會顯示函式執行的開始和結束時間。中間則會顯示我們在函式中透過 console.log() 陳述式放置的記錄。我們看到:
- 觸發函式的事件詳細資料,
- Vision API 呼叫的原始結果,
- 我們在您上傳的圖片中找到的標籤,
- 主要顏色資訊,
- 圖片是否適合顯示
- 最終,這些圖片中繼資料會儲存在 Firestore 中。

再次從「漢堡」選單 (☰) 前往 Firestore 部分。在 Data 子專區 (預設顯示),您應該會看到 pictures 集合新增的文件,對應您剛上傳的圖片:

14. 清除 (選用)
如果您不打算繼續進行本系列的其他實驗室,可以清除資源來節省費用,並成為優質的雲端使用者。您可以按照下列方式個別清除資源。
刪除值區:
gsutil rb gs://${BUCKET_PICTURES}
刪除函式:
gcloud functions delete picture-uploaded --region europe-west1 -q
從集合中選取「刪除集合」,即可刪除 Firestore 集合:

或者,您也可以刪除整個專案:
gcloud projects delete ${GOOGLE_CLOUD_PROJECT}
15. 恭喜!
恭喜!您已成功實作專案的第一項重要服務!
涵蓋內容
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore