1. 概览
在第一个 Codelab 中,您将在存储分区中上传图片。这将生成将由函数处理的文件创建事件。该函数将调用 Vision API 以执行图片分析并将结果保存在数据存储区中。
学习内容
- Cloud Storage
- Cloud Functions
- Cloud Vision API
- Cloud Firestore
2. 设置和要求
自定进度的环境设置
- 登录 Google Cloud 控制台,然后创建一个新项目或重复使用现有项目。如果您还没有 Gmail 或 Google Workspace 账号,则必须创建一个。
- 项目名称是此项目参与者的显示名称。它是 Google API 尚未使用的字符串。您可以随时对其进行更新。
- 项目 ID 在所有 Google Cloud 项目中必须是唯一的,并且不可变(一经设置便无法更改)。Cloud 控制台会自动生成一个唯一字符串;通常您不在乎这是什么在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为
PROJECT_ID
)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且该 ID 在项目期间会一直保留。 - 此外,还有第三个值,即某些 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档。
- 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。如需关停资源,以免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。
启动 Cloud Shell
虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。
在 Google Cloud 控制台 中,点击右上角工具栏中的 Cloud Shell 图标:
预配和连接到环境应该只需要片刻时间。完成后,您应该会看到如下内容:
这个虚拟机已加载了您需要的所有开发工具。它提供了一个持久的 5 GB 主目录,并且在 Google Cloud 中运行,大大增强了网络性能和身份验证功能。您在此 Codelab 中的所有工作都可以在浏览器中完成。您无需安装任何程序。
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. 创建存储分区(控制台)
为图片创建存储分区。您可以通过 Google Cloud Platform 控制台 ( console.cloud.google.com) 执行此操作,也可以通过 Cloud Shell 中的 gsutil 命令行工具或本地开发环境执行此操作。
前往 Storage
来自“汉堡”(☰) 菜单,前往 Storage
页面。
为存储分区命名
点击 CREATE BUCKET
按钮。
点击 CONTINUE
。
选择位置
在您选择的区域中创建一个多区域存储分区(此处为 Europe
)。
点击 CONTINUE
。
选择默认存储类别
为您的数据选择 Standard
存储类别。
点击 CONTINUE
。
设置访问权限控制
由于您将使用可公开访问的图片,因此您希望存储在此存储分区中的所有图片都拥有相同的统一访问权限控制。
选择 Uniform
访问权限控制选项。
点击 CONTINUE
。
设置保护/加密
保留默认值(Google-managed key)
,因为您不会使用自己的加密密钥。
点击 CREATE
,最终完成存储分区的创建。
将 allUsers 添加为存储空间查看者
转到 Permissions
标签页:
向存储分区添加一个角色为 Storage > Storage Object Viewer
的 allUsers
成员,如下所示:
点击 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
存储分区:
如上一步中所述,测试您是否可以将照片上传到存储分区,以及上传的照片是否可公开访问。
6. 测试对存储分区的公开访问权限
返回 Storage 浏览器,您会在列表中看到您的存储分区,其中显示“公开”访问权限(包括一个警告标志,提醒您任何人都可以访问该存储分区的内容)。
您的存储分区现在已准备好接收图片。
如果您点击存储分区名称,会看到存储分区详细信息。
在该页面上,您可以尝试使用 Upload files
按钮,测试是否可以向存储分区添加图片。系统会显示一个文件选择器弹出式窗口,要求您选择文件。选择后,系统会将其上传到您的存储分区,您可以再次看到自动归因于这个新文件的 public
访问权限。
在 Public
访问权限标签旁边,您还会看到一个小链接图标。点击该图片后,浏览器会转到该图片的公共网址,该网址的格式为:
https://storage.googleapis.com/BUCKET_NAME/PICTURE_FILE.png
BUCKET_NAME
是您为存储分区选择的全局唯一名称,然后是您的照片的文件名。
点击图片名称旁的复选框,即可启用 DELETE
按钮,您可以删除第一张图片。
7. 创建函数
在此步骤中,您将创建一个函数来响应图片上传事件。
访问 Google Cloud 控制台的 Cloud Functions
部分。访问该文件后,Cloud Functions 服务将自动启用。
点击 Create function
。
选择一个名称(例如picture-uploaded
)和区域(请务必与为存储分区选择的区域一致):
函数有两种:
- 可通过网址(即网络 API)调用的 HTTP 函数;
- 可由某个事件触发的后台函数。
您希望创建一个后台函数,并在有新文件上传到我们的 Cloud Storage
存储分区时触发:
您对 Finalize/Create
事件类型感兴趣,即在存储分区中创建或更新文件时会触发的事件:
选择之前创建的存储分区,以指示 Cloud Functions 函数在此特定存储分区中创建 / 更新文件时接收通知:
点击 Select
以选择您之前创建的存储分区,然后点击 Save
在点击“下一步”之前,您可以展开并修改运行时、构建、连接和安全设置下的默认设置(256 MB 内存),并将其更新为 1GB。
点击 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 数据库中。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(字符串):上传的图片的文件名,这也是文档的键
- labels(字符串数组):Vision API 识别出的项目的标签
- color(字符串):主色的十六进制颜色代码(即#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
,然后创建复合索引,如下所示:
点击 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 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 样式表中使用的十六进制颜色代码。
我们来检查一下图片是否可以安全显示:
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
,点击我们在实验开始时创建的存储分区:
进入存储分区详情页面后,点击 Upload files
按钮以上传图片。
来自“汉堡”(☰) 菜单中,前往 Logging > Logs
Explorer。
在 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