每日图片:实验 1 - 存储和分析图片

1. 概览

在第一个 Codelab 中,您将在存储分区中上传图片。这将生成将由函数处理的文件创建事件。该函数将调用 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 控制台会自动生成一个唯一字符串;通常您不在乎这是什么在大多数 Codelab 中,您都需要引用项目 ID(它通常标识为 PROJECT_ID)。如果您不喜欢生成的 ID,可以再随机生成一个 ID。或者,您也可以尝试自己的项目 ID,看看是否可用。完成此步骤后便无法更改该 ID,并且该 ID 在项目期间会一直保留。
  • 此外,还有第三个值,即某些 API 使用的项目编号,供您参考。如需详细了解所有这三个值,请参阅文档
  1. 接下来,您需要在 Cloud 控制台中启用结算功能,以便使用 Cloud 资源/API。运行此 Codelab 应该不会产生太多的费用(如果有费用的话)。如需关停资源,以免产生超出本教程范围的结算费用,您可以删除自己创建的资源或删除整个项目。Google Cloud 的新用户符合参与 $300 USD 免费试用计划的条件。

启动 Cloud Shell

虽然可以通过笔记本电脑对 Google Cloud 进行远程操作,但在此 Codelab 中,您将使用 Google Cloud Shell,这是一个在云端运行的命令行环境。

Google Cloud 控制台 中,点击右上角工具栏中的 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. 创建存储分区(控制台)

为图片创建存储分区。您可以通过 Google Cloud Platform 控制台 ( console.cloud.google.com) 执行此操作,也可以通过 Cloud Shell 中的 gsutil 命令行工具或本地开发环境执行此操作。

来自“汉堡”(☰) 菜单,前往 Storage 页面。

1930e055d138150a

为存储分区命名

点击 CREATE BUCKET 按钮。

34147939358517f8

点击 CONTINUE

选择位置

197817f20be07678

在您选择的区域中创建一个多区域存储分区(此处为 Europe)。

点击 CONTINUE

选择默认存储类别

53cd91441c8caf0e

为您的数据选择 Standard 存储类别。

点击 CONTINUE

设置访问权限控制

8c2b3b459d934a51

由于您将使用可公开访问的图片,因此您希望存储在此存储分区中的所有图片都拥有相同的统一访问权限控制。

选择 Uniform 访问权限控制选项。

点击 CONTINUE

设置保护/加密

d931c24c3e705a68.png

保留默认值(Google-managed key),因为您不会使用自己的加密密钥。

点击 CREATE,最终完成存储分区的创建。

将 allUsers 添加为存储空间查看者

转到 Permissions 标签页:

d0ecfdcff730ea51.png

向存储分区添加一个角色为 Storage > Storage Object ViewerallUsers 成员,如下所示:

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 浏览器,您会在列表中看到您的存储分区,其中显示“公开”访问权限(包括一个警告标志,提醒您任何人都可以访问该存储分区的内容)。

89e7a4d2c80a0319

您的存储分区现在已准备好接收图片。

如果您点击存储分区名称,会看到存储分区详细信息。

131387f12d3eb2d3.png

在该页面上,您可以尝试使用 Upload files 按钮,测试是否可以向存储分区添加图片。系统会显示一个文件选择器弹出式窗口,要求您选择文件。选择后,系统会将其上传到您的存储分区,您可以再次看到自动归因于这个新文件的 public 访问权限。

e87584471a6e9c6d.png

Public 访问权限标签旁边,您还会看到一个小链接图标。点击该图片后,浏览器会转到该图片的公共网址,该网址的格式为:

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

BUCKET_NAME 是您为存储分区选择的全局唯一名称,然后是您的照片的文件名。

点击图片名称旁的复选框,即可启用 DELETE 按钮,您可以删除第一张图片。

7. 创建函数

在此步骤中,您将创建一个函数来响应图片上传事件。

访问 Google Cloud 控制台的 Cloud Functions 部分。访问该文件后,Cloud Functions 服务将自动启用。

9d29e8c026a7a53f

点击 Create function

选择一个名称(例如picture-uploaded)和区域(请务必与为存储分区选择的区域一致):

4bb222633e6f278

函数有两种:

  • 可通过网址(即网络 API)调用的 HTTP 函数;
  • 可由某个事件触发的后台函数。

您希望创建一个后台函数,并在有新文件上传到我们的 Cloud Storage 存储分区时触发:

d9a12fcf58f4813c.png

您对 Finalize/Create 事件类型感兴趣,即在存储分区中创建或更新文件时会触发的事件:

b30c8859b07dc4cb.png

选择之前创建的存储分区,以指示 Cloud Functions 函数在此特定存储分区中创建 / 更新文件时接收通知:

cb15a1f4c7a1ca5f.png

点击 Select 以选择您之前创建的存储分区,然后点击 Save

c1933777fac32c6a.png

在点击“下一步”之前,您可以展开并修改运行时、构建、连接和安全设置下的默认设置(256 MB 内存),并将其更新为 1GB。

83d757e6c38e10

点击 Next 后,您可以调整运行时源代码入口点

保留此函数的 Inline editor

7dccb5a3fa66363d.png

选择以下 Node.js 运行时之一:

21defc3b0accd5b4.png

源代码由 index.js JavaScript 文件以及提供各种元数据和依赖项的 package.json 文件组成。

保留默认代码段:它会记录所上传照片的文件名:

465aca96eb8ca5f9

目前,出于测试目的,请将要执行的函数的名称保留到 helloGCS 中。

点击 Deploy 以创建和部署函数。部署成功后,您应该会在函数列表中看到一个绿色圆圈的对勾标记:

e9d78025d16651aa.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

系统提供两个选项:Native modeDatastore mode。使用原生模式,该模式可提供离线支持和实时同步等额外功能。

点击 SELECT NATIVE MODE

9449ace8cc84de43.png

选择一个多区域(这里位于欧洲,但最好至少与您的函数和存储分区位于同一区域)。

点击 CREATE DATABASE 按钮。

创建数据库后,您应该会看到以下内容:

56265949a124819e

点击 + START COLLECTION 按钮创建新集合

名称集合“pictures”。

75806ee24c4e13a7

您无需创建文档。当新图片存储在 Cloud Storage 中并由 Vision API 进行分析时,您将以编程方式添加这些图片。

点击 Save

Firestore 会在新创建的集合中创建第一个默认文档,您可以安全地删除该文档,因为它不包含任何实用信息:

5c2f1e17ea47f48f

在集合中以编程方式创建的文档将包含 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,然后创建复合索引,如下所示:

ecb8b95e3c791272.png

点击 Create。 索引创建过程可能需要几分钟时间。

10. 更新函数

返回 Functions 页面,更新该函数以调用 Vision API 来分析图片,并将元数据存储在 Firestore 中。

来自“汉堡”(☰) 菜单中,前往 Cloud Functions 部分,点击函数名称,选择 Source 标签页,然后点击 EDIT 按钮。

首先,修改 package.json 文件,其中列出了 Node.JS 函数的依赖项。更新代码以添加 Cloud Vision API NPM 依赖项:

{
  "name": "picture-analysis-function",
  "version": "0.0.1",
  "dependencies": {
    "@google-cloud/storage": "^1.6.0",
    "@google-cloud/vision": "^1.8.0",
    "@google-cloud/firestore": "^3.4.1"
  }
}

现在依赖项已是最新状态,接下来您需要更新 index.js 文件来处理函数的代码。

index.js 中的代码替换为以下代码。我们将在下一步中对此进行说明。

const vision = require('@google-cloud/vision');
const Storage = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');

const client = new vision.ImageAnnotatorClient();

exports.vision_analysis = async (event, context) => {
    console.log(`Event: ${JSON.stringify(event)}`);

    const filename = event.name;
    const filebucket = event.bucket;

    console.log(`New picture uploaded ${filename} in ${filebucket}`);

    const request = {
        image: { source: { imageUri: `gs://${filebucket}/${filename}` } },
        features: [
            { type: 'LABEL_DETECTION' },
            { type: 'IMAGE_PROPERTIES' },
            { type: 'SAFE_SEARCH_DETECTION' }
        ]
    };

    // invoking the Vision API
    const [response] = await client.annotateImage(request);
    console.log(`Raw vision output for: ${filename}: ${JSON.stringify(response)}`);

    if (response.error === null) {
        // listing the labels found in the picture
        const labels = response.labelAnnotations
            .sort((ann1, ann2) => ann2.score - ann1.score)
            .map(ann => ann.description)
        console.log(`Labels: ${labels.join(', ')}`);

        // retrieving the dominant color of the picture
        const color = response.imagePropertiesAnnotation.dominantColors.colors
            .sort((c1, c2) => c2.score - c1.score)[0].color;
        const colorHex = decColorToHex(color.red, color.green, color.blue);
        console.log(`Colors: ${colorHex}`);

        // determining if the picture is safe to show
        const safeSearch = response.safeSearchAnnotation;
        const isSafe = ["adult", "spoof", "medical", "violence", "racy"].every(k => 
            !['LIKELY', 'VERY_LIKELY'].includes(safeSearch[k]));
        console.log(`Safe? ${isSafe}`);

        // if the picture is safe to display, store it in Firestore
        if (isSafe) {
            const pictureStore = new Firestore().collection('pictures');
            
            const doc = pictureStore.doc(filename);
            await doc.set({
                labels: labels,
                color: colorHex,
                created: Firestore.Timestamp.now()
            }, {merge: true});

            console.log("Stored metadata in Firestore");
        }
    } else {
        throw new Error(`Vision API error: code ${response.error.code}, message: "${response.error.message}"`);
    }
};

function decColorToHex(r, g, b) {
    return '#' + Number(r).toString(16).padStart(2, '0') + 
                 Number(g).toString(16).padStart(2, '0') + 
                 Number(b).toString(16).padStart(2, '0');
}

11. 探索函数

下面我们来详细了解一下各个有趣的部分。

首先,我们需要为 Vision、Storage 和 Firestore 所需的模块:

const vision = require('@google-cloud/vision');
const Storage = require('@google-cloud/storage');
const Firestore = require('@google-cloud/firestore');

然后,我们为 Vision API 准备一个客户端:

const client = new vision.ImageAnnotatorClient();

接下来我们看一下函数的结构。我们将其设为异步函数,因为我们使用的是 Node.js 8 中引入的 async / await 功能:

exports.vision_analysis = async (event, context) => {
    ...
    const filename = event.name;
    const filebucket = event.bucket;
    ...
}

请注意签名,以及检索触发 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 客户端发送的请求:

const request = {
    image: { source: { imageUri: `gs://${filebucket}/${filename}` } },
    features: [
        { type: 'LABEL_DETECTION' },
        { type: 'IMAGE_PROPERTIES' },
        { type: 'SAFE_SEARCH_DETECTION' }
    ]
};

我们要求 Vision API 的 3 项主要功能如下:

  • 标签检测:了解照片中的内容
  • 图片属性:提供图片的一些有趣属性(我们关注的是图片的主色)
  • 安全搜索:了解图片是否可以安全显示(其中不得包含成人 / 医疗 / 少儿不宜 / 暴力内容)

此时,我们可以调用 Vision API:

const [response] = await client.annotateImage(request);

以下是 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 代码块:

if (response.error === null) {
    ...
} else {
    throw new Error(`Vision API error: code ${response.error.code},  
                     message: "${response.error.message}"`);
}

我们将获取图片中识别出的事物、类别或主题的标签:

const labels = response.labelAnnotations
    .sort((ann1, ann2) => ann2.score - ann1.score)
    .map(ann => ann.description)

我们首先按分数从高到低对标签进行排序。

我们想要了解图片的主色:

const color = response.imagePropertiesAnnotation.dominantColors.colors
    .sort((c1, c2) => c2.score - c1.score)[0].color;
const colorHex = decColorToHex(color.red, color.green, color.blue);

我们再次按分数对颜色进行排序,并获取第一种颜色。

我们还使用实用函数将红色 / 绿色 / 蓝色值转换为可以在 CSS 样式表中使用的十六进制颜色代码。

我们来检查一下图片是否可以安全显示:

const safeSearch = response.safeSearchAnnotation;
const isSafe = ["adult", "spoof", "medical", "violence", "racy"]
    .every(k => !['LIKELY', 'VERY_LIKELY'].includes(safeSearch[k]));

我们正在检查成人 / 仿冒 / 医疗 / 暴力 / 少儿不宜属性,以了解相应属性是不太可能还是极有可能

如果安全搜索的结果没问题,我们就可以将元数据存储在 Firestore 中:

if (isSafe) {
    const pictureStore = new Firestore().collection('pictures');
            
    const doc = pictureStore.doc(filename);
    await doc.set({
        labels: labels,
        color: colorHex,
        created: Firestore.Timestamp.now()
    }, {merge: true});
}

12. 部署函数

现在来部署函数了。

274c1e2fca6c0bd9

点击 DEPLOY 按钮,系统就会部署新版本,您可以看到进度:

4e0ac812a9124e7c

13. 再次测试函数

成功部署函数后,您要将图片发布到 Cloud Storage,看看函数是否被调用、Vision API 会返回什么,以及元数据是否存储在 Firestore 中。

返回 Cloud Storage,点击我们在实验开始时创建的存储分区:

d44c1584122311c7.png

进入存储分区详情页面后,点击 Upload files 按钮以上传图片。

26bb31d35fb6aa3d.png

来自“汉堡”(☰) 菜单中,前往 Logging > Logs Explorer。

Log Fields 选择器中,选择 Cloud Function 以查看函数专用的日志。向下滚动日志字段,您甚至可以选择特定函数,以便更细致地查看与函数相关的日志。选择 picture-uploaded 函数。

b651dca7e25d5b11.png

事实上,在日志列表中,我可以看到我们的函数被调用了:

d22a7f24954e4f63.png

日志会指示函数执行的开始和结束。之间,我们可以看到通过 console.log() 语句放入函数中的日志。我们可以看到:

  • 触发函数的事件的详细信息
  • Vision API 调用的原始结果,
  • 在我们上传的照片中找到的标签
  • 主色信息
  • 照片是否可以安全显示
  • 最后,有关照片的元数据会存储在 Firestore 中。

9ff7956a215c15da

同样是(☰) 菜单,前往“Firestore”部分。在 Data 子部分(默认显示)中,您应该会看到 pictures 集合,其中添加了一个新文档,与您刚刚上传的图片相对应:

a6137ab9687da370.png

14. 清理(可选)

如果您不打算继续完成本系列中的其他实验,可以清理资源以节省成本,并成为一个整体优秀的云公民。您可以按以下步骤逐个清理资源。

删除存储分区:

gsutil rb gs://${BUCKET_PICTURES}

删除函数:

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

选择“从集合中删除集合”,以删除 Firestore 集合:

410b551c3264f70a

或者,您也可以删除整个项目:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. 恭喜!

恭喜!您已成功实现该项目的第一个密钥服务!

所学内容

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

后续步骤