异步处理图片

本教程演示了如何使用 Cloud Run for Anthos、Cloud Vision API 和 ImageMagick 检测上传到 Cloud Storage 存储桶的令人反感的图片并对其进行模糊处理。本教程是基于将 Pub/Sub 与 Cloud Run for Anthos 搭配使用教程撰写的。

本教程将介绍如何修改现有示例应用。如果需要,您还可以下载已完成的示例

目标

  • 编写、构建异步数据处理服务并将其部署到 Cloud Run for Anthos
  • 通过将文件上传到 Cloud Storage 来调用该服务,从而创建 Pub/Sub 消息。
  • 使用 Cloud Vision API 检测暴力或成人内容。
  • 使用 ImageMagick 对令人反感的图片进行模糊处理。
  • 上传一张肉食僵尸的图片来测试服务。

费用

在本文档中,您将使用 Google Cloud 的以下收费组件:

您可使用价格计算器根据您的预计使用情况来估算费用。 Google Cloud 新用户可能有资格申请免费试用

准备工作

  1. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

  2. Make sure that billing is enabled for your Google Cloud project.

  3. Enable the Cloud Run for Anthos and Cloud Vision APIs.

    Enable the APIs

  4. 安装并初始化 gcloud CLI。
  5. 安装 kubectl 组件:
    gcloud components install kubectl
  6. 更新组件:
    gcloud components update
  7. 如果您使用的是 Cloud Run for Anthos,请按照设置 Cloud Run for Anthos 中的说明创建新集群。
  8. 按照将 Pub/Sub 与 Cloud Run for Anthos 搭配使用教程中的说明,设置 Pub/Sub 主题、安全推送订阅和初始 Cloud Run for Anthos 服务来处理消息。

设置 gcloud 默认值

如需为您的 Cloud Run for Anthos 服务配置 gcloud 默认值,请执行以下操作:

  1. 设置默认项目:

    gcloud config set project PROJECT_ID

    PROJECT_ID 替换为您在本教程中使用的项目名称。

  2. 为您的集群配置 gcloud:

    gcloud config set run/platform gke
    gcloud config set run/cluster CLUSTER-NAME
    gcloud config set run/cluster_location REGION

    您需要将其中的:

    • CLUSTER-NAME 替换为您使用的集群名称。
    • REGION 替换为您选择的受支持的集群位置。

了解操作顺序

本教程中的数据流遵循以下步骤:

  1. 用户将图片上传到 Cloud Storage 存储分区。
  2. Cloud Storage 将关于新文件的消息发布到 Pub/Sub。
  3. Pub/Sub 将消息推送到 Cloud Run for Anthos 服务。
  4. Cloud Run for Anthos 服务检索 Pub/Sub 消息中引用的图片文件。
  5. Cloud Run for Anthos 服务使用 Cloud Vision API 来分析图片。
  6. 如果检测到暴力或成人内容,则 Cloud Run for Anthos 服务会使用 ImageMagick 对图片进行模糊处理。
  7. Cloud Run for Anthos 服务将经过模糊处理的图片上传到其他 Cloud Storage 存储桶,以供使用。

后续对经过模糊处理的图片的使用将留给读者自行练习。

设置 Cloud Storage 存储分区

  1. 创建一个 Cloud Storage 存储分区以上传图片,其中 INPUT_BUCKET_NAME 是全局唯一的存储分区名称:

    gsutil mb gs://INPUT_BUCKET_NAME

    Cloud Run for Anthos 服务仅从此存储桶读取内容。

  2. 创建另一个 Cloud Storage 存储分区以接收经过模糊处理的图片,其中 BLURRED_BUCKET_NAME 是全局唯一的存储分区名称:

    gsutil mb gs://BLURRED_BUCKET_NAME

    Cloud Run for Anthos 服务会将经过模糊处理的图片上传到此存储桶。使用单独的存储分区可防止处理后的图片重新触发服务。

在以下步骤中,您将创建并部署一项服务,用于处理上传到 INPUT_BUCKET_NAME 的文件通知。请在部署并测试服务后启用通知递送,以避免过早调用新服务。

修改 Pub/Sub 教程示例代码

本教程基于使用 Pub/Sub 教程中汇编的代码构建而成。如果您尚未完成这一基础教程,请立即查看并完成(可跳过清理步骤),然后返回此处来添加图片处理行为。

添加图片处理代码

为了便于阅读和测试,图片处理代码与请求处理是彼此独立的。要添加图片处理代码,请执行以下操作:

  1. 切换到 Pub/Sub 教程示例代码的目录。

  2. 添加代码以导入图片处理依赖项,包括与 Google Cloud 服务、ImageMagick 和文件系统集成的库。

    Node.js

    在编辑器中打开一个新的 image.js 文件,然后复制以下内容:
    const gm = require('gm').subClass({imageMagick: true});
    const fs = require('fs');
    const {promisify} = require('util');
    const path = require('path');
    const vision = require('@google-cloud/vision');
    
    const {Storage} = require('@google-cloud/storage');
    const storage = new Storage();
    const client = new vision.ImageAnnotatorClient();
    
    const {BLURRED_BUCKET_NAME} = process.env;

    Python

    在编辑器中打开一个新的 image.py 文件,然后复制以下内容:
    import os
    import tempfile
    
    from google.cloud import storage, vision
    from wand.image import Image
    
    storage_client = storage.Client()
    vision_client = vision.ImageAnnotatorClient()

    Go

    在编辑器中打开一个新的 imagemagick/imagemagick.go 文件,然后复制以下内容:
    
    // Package imagemagick contains an example of using ImageMagick to process a
    // file uploaded to Cloud Storage.
    package imagemagick
    
    import (
    	"context"
    	"errors"
    	"fmt"
    	"log"
    	"os"
    	"os/exec"
    
    	"cloud.google.com/go/storage"
    	vision "cloud.google.com/go/vision/apiv1"
    	visionpb "google.golang.org/genproto/googleapis/cloud/vision/v1"
    )
    
    // Global API clients used across function invocations.
    var (
    	storageClient *storage.Client
    	visionClient  *vision.ImageAnnotatorClient
    )
    
    func init() {
    	// Declare a separate err variable to avoid shadowing the client variables.
    	var err error
    
    	storageClient, err = storage.NewClient(context.Background())
    	if err != nil {
    		log.Fatalf("storage.NewClient: %v", err)
    	}
    
    	visionClient, err = vision.NewImageAnnotatorClient(context.Background())
    	if err != nil {
    		log.Fatalf("vision.NewAnnotatorClient: %v", err)
    	}
    }
    

    Java

    在编辑器中打开一个新的 src/main/java/com/example/cloudrun/ImageMagick.java 文件,然后复制以下内容:
    import com.google.cloud.storage.Blob;
    import com.google.cloud.storage.BlobId;
    import com.google.cloud.storage.BlobInfo;
    import com.google.cloud.storage.Storage;
    import com.google.cloud.storage.StorageOptions;
    import com.google.cloud.vision.v1.AnnotateImageRequest;
    import com.google.cloud.vision.v1.AnnotateImageResponse;
    import com.google.cloud.vision.v1.BatchAnnotateImagesResponse;
    import com.google.cloud.vision.v1.Feature;
    import com.google.cloud.vision.v1.Feature.Type;
    import com.google.cloud.vision.v1.Image;
    import com.google.cloud.vision.v1.ImageAnnotatorClient;
    import com.google.cloud.vision.v1.ImageSource;
    import com.google.cloud.vision.v1.SafeSearchAnnotation;
    import com.google.gson.JsonObject;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.Paths;
    import java.util.ArrayList;
    import java.util.List;
    
    public class ImageMagick {
    
      private static final String BLURRED_BUCKET_NAME = System.getenv("BLURRED_BUCKET_NAME");
      private static Storage storage = StorageOptions.getDefaultInstance().getService();

  3. 添加代码以接收作为事件对象的 Pub/Sub 消息并控制图片处理。

    该事件包含最初上传的图片的相关数据。此代码通过查看 Cloud Vision 对暴力或成人内容的分析结果来确定图片是否需要进行模糊处理。

    Node.js

    // Blurs uploaded images that are flagged as Adult or Violence.
    exports.blurOffensiveImages = async event => {
      // This event represents the triggering Cloud Storage object.
      const object = event;
    
      const file = storage.bucket(object.bucket).file(object.name);
      const filePath = `gs://${object.bucket}/${object.name}`;
    
      console.log(`Analyzing ${file.name}.`);
    
      try {
        const [result] = await client.safeSearchDetection(filePath);
        const detections = result.safeSearchAnnotation || {};
    
        if (
          // Levels are defined in https://cloud.google.com/vision/docs/reference/rest/v1/AnnotateImageResponse#likelihood
          detections.adult === 'VERY_LIKELY' ||
          detections.violence === 'VERY_LIKELY'
        ) {
          console.log(`Detected ${file.name} as inappropriate.`);
          return blurImage(file, BLURRED_BUCKET_NAME);
        } else {
          console.log(`Detected ${file.name} as OK.`);
        }
      } catch (err) {
        console.error(`Failed to analyze ${file.name}.`, err);
        throw err;
      }
    };

    Python

    # Blurs uploaded images that are flagged as Adult or Violence.
    def blur_offensive_images(data):
        file_data = data
    
        file_name = file_data["name"]
        bucket_name = file_data["bucket"]
    
        blob = storage_client.bucket(bucket_name).get_blob(file_name)
        blob_uri = f"gs://{bucket_name}/{file_name}"
        blob_source = vision.Image(source=vision.ImageSource(image_uri=blob_uri))
    
        # Ignore already-blurred files
        if file_name.startswith("blurred-"):
            print(f"The image {file_name} is already blurred.")
            return
    
        print(f"Analyzing {file_name}.")
    
        result = vision_client.safe_search_detection(image=blob_source)
        detected = result.safe_search_annotation
    
        # Process image
        if detected.adult == 5 or detected.violence == 5:
            print(f"The image {file_name} was detected as inappropriate.")
            return __blur_image(blob)
        else:
            print(f"The image {file_name} was detected as OK.")
    
    

    Go

    
    // GCSEvent is the payload of a GCS event.
    type GCSEvent struct {
    	Bucket string `json:"bucket"`
    	Name   string `json:"name"`
    }
    
    // BlurOffensiveImages blurs offensive images uploaded to GCS.
    func BlurOffensiveImages(ctx context.Context, e GCSEvent) error {
    	outputBucket := os.Getenv("BLURRED_BUCKET_NAME")
    	if outputBucket == "" {
    		return errors.New("BLURRED_BUCKET_NAME must be set")
    	}
    
    	img := vision.NewImageFromURI(fmt.Sprintf("gs://%s/%s", e.Bucket, e.Name))
    
    	resp, err := visionClient.DetectSafeSearch(ctx, img, nil)
    	if err != nil {
    		return fmt.Errorf("AnnotateImage: %v", err)
    	}
    
    	if resp.GetAdult() == visionpb.Likelihood_VERY_LIKELY ||
    		resp.GetViolence() == visionpb.Likelihood_VERY_LIKELY {
    		return blur(ctx, e.Bucket, outputBucket, e.Name)
    	}
    	log.Printf("The image %q was detected as OK.", e.Name)
    	return nil
    }
    

    Java

    // Blurs uploaded images that are flagged as Adult or Violence.
    public static void blurOffensiveImages(JsonObject data) {
      String fileName = data.get("name").getAsString();
      String bucketName = data.get("bucket").getAsString();
      BlobInfo blobInfo = BlobInfo.newBuilder(bucketName, fileName).build();
      // Construct URI to GCS bucket and file.
      String gcsPath = String.format("gs://%s/%s", bucketName, fileName);
      System.out.println(String.format("Analyzing %s", fileName));
    
      // Construct request.
      List<AnnotateImageRequest> requests = new ArrayList<>();
      ImageSource imgSource = ImageSource.newBuilder().setImageUri(gcsPath).build();
      Image img = Image.newBuilder().setSource(imgSource).build();
      Feature feature = Feature.newBuilder().setType(Type.SAFE_SEARCH_DETECTION).build();
      AnnotateImageRequest request =
          AnnotateImageRequest.newBuilder().addFeatures(feature).setImage(img).build();
      requests.add(request);
    
      // Send request to the Vision API.
      try (ImageAnnotatorClient client = ImageAnnotatorClient.create()) {
        BatchAnnotateImagesResponse response = client.batchAnnotateImages(requests);
        List<AnnotateImageResponse> responses = response.getResponsesList();
        for (AnnotateImageResponse res : responses) {
          if (res.hasError()) {
            System.out.println(String.format("Error: %s\n", res.getError().getMessage()));
            return;
          }
          // Get Safe Search Annotations
          SafeSearchAnnotation annotation = res.getSafeSearchAnnotation();
          if (annotation.getAdultValue() == 5 || annotation.getViolenceValue() == 5) {
            System.out.println(String.format("Detected %s as inappropriate.", fileName));
            blur(blobInfo);
          } else {
            System.out.println(String.format("Detected %s as OK.", fileName));
          }
        }
      } catch (Exception e) {
        System.out.println(String.format("Error with Vision API: %s", e.getMessage()));
      }
    }

  4. 从上面创建的 Cloud Storage 输入存储分区检索引用的图片,使用 ImageMagick 转换具有模糊效果的图片,并将结果上传到输出存储分区。

    Node.js

    // Blurs the given file using ImageMagick, and uploads it to another bucket.
    const blurImage = async (file, blurredBucketName) => {
      const tempLocalPath = `/tmp/${path.parse(file.name).base}`;
    
      // Download file from bucket.
      try {
        await file.download({destination: tempLocalPath});
    
        console.log(`Downloaded ${file.name} to ${tempLocalPath}.`);
      } catch (err) {
        throw new Error(`File download failed: ${err}`);
      }
    
      await new Promise((resolve, reject) => {
        gm(tempLocalPath)
          .blur(0, 16)
          .write(tempLocalPath, (err, stdout) => {
            if (err) {
              console.error('Failed to blur image.', err);
              reject(err);
            } else {
              console.log(`Blurred image: ${file.name}`);
              resolve(stdout);
            }
          });
      });
    
      // Upload result to a different bucket, to avoid re-triggering this function.
      const blurredBucket = storage.bucket(blurredBucketName);
    
      // Upload the Blurred image back into the bucket.
      const gcsPath = `gs://${blurredBucketName}/${file.name}`;
      try {
        await blurredBucket.upload(tempLocalPath, {destination: file.name});
        console.log(`Uploaded blurred image to: ${gcsPath}`);
      } catch (err) {
        throw new Error(`Unable to upload blurred image to ${gcsPath}: ${err}`);
      }
    
      // Delete the temporary file.
      const unlink = promisify(fs.unlink);
      return unlink(tempLocalPath);
    };

    Python

    # Blurs the given file using ImageMagick.
    def __blur_image(current_blob):
        file_name = current_blob.name
        _, temp_local_filename = tempfile.mkstemp()
    
        # Download file from bucket.
        current_blob.download_to_filename(temp_local_filename)
        print(f"Image {file_name} was downloaded to {temp_local_filename}.")
    
        # Blur the image using ImageMagick.
        with Image(filename=temp_local_filename) as image:
            image.resize(*image.size, blur=16, filter="hamming")
            image.save(filename=temp_local_filename)
    
        print(f"Image {file_name} was blurred.")
    
        # Upload result to a second bucket, to avoid re-triggering the function.
        # You could instead re-upload it to the same bucket + tell your function
        # to ignore files marked as blurred (e.g. those with a "blurred" prefix)
        blur_bucket_name = os.getenv("BLURRED_BUCKET_NAME")
        blur_bucket = storage_client.bucket(blur_bucket_name)
        new_blob = blur_bucket.blob(file_name)
        new_blob.upload_from_filename(temp_local_filename)
        print(f"Blurred image uploaded to: gs://{blur_bucket_name}/{file_name}")
    
        # Delete the temporary file.
        os.remove(temp_local_filename)
    
    

    Go

    
    // blur blurs the image stored at gs://inputBucket/name and stores the result in
    // gs://outputBucket/name.
    func blur(ctx context.Context, inputBucket, outputBucket, name string) error {
    	inputBlob := storageClient.Bucket(inputBucket).Object(name)
    	r, err := inputBlob.NewReader(ctx)
    	if err != nil {
    		return fmt.Errorf("NewReader: %v", err)
    	}
    
    	outputBlob := storageClient.Bucket(outputBucket).Object(name)
    	w := outputBlob.NewWriter(ctx)
    	defer w.Close()
    
    	// Use - as input and output to use stdin and stdout.
    	cmd := exec.Command("convert", "-", "-blur", "0x8", "-")
    	cmd.Stdin = r
    	cmd.Stdout = w
    
    	if err := cmd.Run(); err != nil {
    		return fmt.Errorf("cmd.Run: %v", err)
    	}
    
    	log.Printf("Blurred image uploaded to gs://%s/%s", outputBlob.BucketName(), outputBlob.ObjectName())
    
    	return nil
    }
    

    Java

      // Blurs the file described by blobInfo using ImageMagick,
      // and uploads it to the blurred bucket.
      public static void blur(BlobInfo blobInfo) throws IOException {
        String bucketName = blobInfo.getBucket();
        String fileName = blobInfo.getName();
        // Download image
        Blob blob = storage.get(BlobId.of(bucketName, fileName));
        Path download = Paths.get("/tmp/", fileName);
        blob.downloadTo(download);
    
        // Construct the command.
        List<String> args = new ArrayList<String>();
        args.add("convert");
        args.add(download.toString());
        args.add("-blur");
        args.add("0x8");
        Path upload = Paths.get("/tmp/", "blurred-" + fileName);
        args.add(upload.toString());
        try {
          ProcessBuilder pb = new ProcessBuilder(args);
          Process process = pb.start();
          process.waitFor();
        } catch (Exception e) {
          System.out.println(String.format("Error: %s", e.getMessage()));
        }
    
        // Upload image to blurred bucket.
        BlobId blurredBlobId = BlobId.of(BLURRED_BUCKET_NAME, fileName);
        BlobInfo blurredBlobInfo =
            BlobInfo.newBuilder(blurredBlobId).setContentType(blob.getContentType()).build();
        try {
          byte[] blurredFile = Files.readAllBytes(upload);
          Blob blurredBlob = storage.create(blurredBlobInfo, blurredFile);
          System.out.println(
              String.format("Blurred image uploaded to: gs://%s/%s", BLURRED_BUCKET_NAME, fileName));
        } catch (Exception e) {
          System.out.println(String.format("Error in upload: %s", e.getMessage()));
        }
    
        // Remove images from fileSystem
        Files.delete(download);
        Files.delete(upload);
      }
    }

将图片处理集成到 Pub/Sub 示例代码中

要修改现有服务以整合图片处理代码:

  1. 为您的服务添加新的依赖项,包括 Cloud Vision 和 Cloud Storage 客户端库:

    Node.js

    npm install --save gm @google-cloud/storage @google-cloud/vision

    Python

    添加必要的客户端库,让您的 requirements.txt 如下所示:
    Flask==2.1.0
    pytest==7.0.1; python_version > "3.0"
    # pin pytest to 4.6.11 for Python2.
    pytest==7.0.1; python_version < "3.0"
    gunicorn==20.1.0
    google-cloud-vision==3.1.0
    google-cloud-storage==2.1.0
    Wand==0.6.8
    

    Go

    Go 示例应用使用 go 模块,上述 imagemagick/imagemagick.go import 语句中添加的新依赖项会通过下一个需要它们的命令自动下载。

    Java

    pom.xml 中的 <dependencyManagement> 下添加以下依赖项:
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>3.2.1</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    pom.xml 中的 <dependencies> 下添加以下依赖项:
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <version>2.8.9</version>
      <scope>compile</scope>
    </dependency>
    <dependency>
      <groupId>org.json</groupId>
      <artifactId>json</artifactId>
      <version>20220320</version>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-vision</artifactId>
    </dependency>
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-starter-storage</artifactId>
    </dependency>
    <dependency>
      <groupId>commons-io</groupId>
      <artifactId>commons-io</artifactId>
      <version>2.11.0</version>
    </dependency>
    

  2. 通过修改 FROM 语句下的 Dockerfile,将 ImageMagick 系统软件包添加到您的容器中。如果使用的是“多阶段”Dockerfile,请将其置于最后一个阶段。

    Debian/Ubuntu
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
    RUN set -ex; \
      apt-get -y update; \
      apt-get -y install imagemagick; \
      rm -rf /var/lib/apt/lists/*
    
    Alpine
    
    # Install Imagemagick into the container image.
    # For more on system packages review the system packages tutorial.
    # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
    RUN apk add --no-cache imagemagick
    

    如需详细了解如何在 Cloud Run for Anthos 服务中使用系统软件包,请参阅使用系统软件包教程

  3. 将现有的 Pub/Sub 消息处理代码替换为全新模糊处理逻辑的函数调用。

    Node.js

    app.js 文件定义了 Express.js 应用,并准备了接收到的 Pub/Sub 消息以供使用。进行以下更改:

    • 添加代码以导入新的 image.js 文件
    • 从路由中移除现有的“Hello World”代码
    • 添加代码以进一步验证 Pub/Sub 消息
    • 添加代码以调用新的图片处理函数

      完成后,代码将如下所示:

    
    const express = require('express');
    const app = express();
    
    // This middleware is available in Express v4.16.0 onwards
    app.use(express.json());
    
    const image = require('./image');
    
    app.post('/', async (req, res) => {
      if (!req.body) {
        const msg = 'no Pub/Sub message received';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
      if (!req.body.message || !req.body.message.data) {
        const msg = 'invalid Pub/Sub message format';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Decode the Pub/Sub message.
      const pubSubMessage = req.body.message;
      let data;
      try {
        data = Buffer.from(pubSubMessage.data, 'base64').toString().trim();
        data = JSON.parse(data);
      } catch (err) {
        const msg =
          'Invalid Pub/Sub message: data property is not valid base64 encoded JSON';
        console.error(`error: ${msg}: ${err}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      // Validate the message is a Cloud Storage event.
      if (!data.name || !data.bucket) {
        const msg =
          'invalid Cloud Storage notification: expected name and bucket properties';
        console.error(`error: ${msg}`);
        res.status(400).send(`Bad Request: ${msg}`);
        return;
      }
    
      try {
        await image.blurOffensiveImages(data);
        res.status(204).send();
      } catch (err) {
        console.error(`error: Blurring image: ${err}`);
        res.status(500).send();
      }
    });

    Python

    main.py 文件定义了 Flask 应用,并准备了接收到的 Pub/Sub 消息以供使用。进行以下更改:

    • 添加代码以导入新的 image.py 文件
    • 从路由中移除现有的“Hello World”代码
    • 添加代码以进一步验证 Pub/Sub 消息
    • 添加代码以调用新的图片处理函数

      完成后,代码将如下所示:

    import base64
    import json
    import os
    
    from flask import Flask, request
    
    import image
    
    app = Flask(__name__)
    
    @app.route("/", methods=["POST"])
    def index():
        envelope = request.get_json()
        if not envelope:
            msg = "no Pub/Sub message received"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        if not isinstance(envelope, dict) or "message" not in envelope:
            msg = "invalid Pub/Sub message format"
            print(f"error: {msg}")
            return f"Bad Request: {msg}", 400
    
        # Decode the Pub/Sub message.
        pubsub_message = envelope["message"]
    
        if isinstance(pubsub_message, dict) and "data" in pubsub_message:
            try:
                data = json.loads(base64.b64decode(pubsub_message["data"]).decode())
    
            except Exception as e:
                msg = (
                    "Invalid Pub/Sub message: "
                    "data property is not valid base64 encoded JSON"
                )
                print(f"error: {e}")
                return f"Bad Request: {msg}", 400
    
            # Validate the message is a Cloud Storage event.
            if not data["name"] or not data["bucket"]:
                msg = (
                    "Invalid Cloud Storage notification: "
                    "expected name and bucket properties"
                )
                print(f"error: {msg}")
                return f"Bad Request: {msg}", 400
    
            try:
                image.blur_offensive_images(data)
                return ("", 204)
    
            except Exception as e:
                print(f"error: {e}")
                return ("", 500)
    
        return ("", 500)

    Go

    main.go 文件定义了 HTTP 服务,并准备了接收到的 Pub/Sub 消息以供使用。进行以下更改:

    • 添加代码以导入新的 imagemagick.go 文件
    • 从处理程序中移除现有的“Hello World”代码
    • 添加代码以进一步验证 Pub/Sub 消息
    • 添加代码以调用新的图片处理函数

    
    // Sample image-processing is a Cloud Run service which performs asynchronous processing on images.
    package main
    
    import (
    	"encoding/json"
    	"io/ioutil"
    	"log"
    	"net/http"
    	"os"
    
    	"github.com/GoogleCloudPlatform/golang-samples/run/image-processing/imagemagick"
    )
    
    func main() {
    	http.HandleFunc("/", HelloPubSub)
    	// Determine port for HTTP service.
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    	// Start HTTP server.
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // PubSubMessage is the payload of a Pub/Sub event.
    // See the documentation for more details:
    // https://cloud.google.com/pubsub/docs/reference/rest/v1/PubsubMessage
    type PubSubMessage struct {
    	Message struct {
    		Data []byte `json:"data,omitempty"`
    		ID   string `json:"id"`
    	} `json:"message"`
    	Subscription string `json:"subscription"`
    }
    
    // HelloPubSub receives and processes a Pub/Sub push message.
    func HelloPubSub(w http.ResponseWriter, r *http.Request) {
    	var m PubSubMessage
    	body, err := ioutil.ReadAll(r.Body)
    	if err != nil {
    		log.Printf("ioutil.ReadAll: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    	if err := json.Unmarshal(body, &m); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	var e imagemagick.GCSEvent
    	if err := json.Unmarshal(m.Message.Data, &e); err != nil {
    		log.Printf("json.Unmarshal: %v", err)
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if e.Name == "" || e.Bucket == "" {
    		log.Printf("invalid GCSEvent: expected name and bucket")
    		http.Error(w, "Bad Request", http.StatusBadRequest)
    		return
    	}
    
    	if err := imagemagick.BlurOffensiveImages(r.Context(), e); err != nil {
    		log.Printf("imagemagick.BlurOffensiveImages: %v", err)
    		http.Error(w, "Internal Server Error", http.StatusInternalServerError)
    	}
    }
    

    Java

    PubSubController.java 文件定义了处理 HTTP 请求的控制器,并准备了接收到的 Pub/Sub 消息以供使用。进行以下更改:

    • 添加新的导入
    • 从控制器中移除现有的“Hello World”代码
    • 添加代码以进一步验证 Pub/Sub 消息
    • 添加代码以调用新的图片处理函数

    import com.google.gson.JsonObject;
    import com.google.gson.JsonParser;
    import java.util.Base64;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    import org.springframework.web.bind.annotation.RestController;
    
    // PubsubController consumes a Pub/Sub message.
    @RestController
    public class PubSubController {
      @RequestMapping(value = "/", method = RequestMethod.POST)
      public ResponseEntity<String> receiveMessage(@RequestBody Body body) {
        // Get PubSub message from request body.
        Body.Message message = body.getMessage();
        if (message == null) {
          String msg = "Bad Request: invalid Pub/Sub message format";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Decode the Pub/Sub message.
        String pubSubMessage = message.getData();
        JsonObject data;
        try {
          String decodedMessage = new String(Base64.getDecoder().decode(pubSubMessage));
          data = JsonParser.parseString(decodedMessage).getAsJsonObject();
        } catch (Exception e) {
          String msg = "Error: Invalid Pub/Sub message: data property is not valid base64 encoded JSON";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        // Validate the message is a Cloud Storage event.
        if (data.get("name") == null || data.get("bucket") == null) {
          String msg = "Error: Invalid Cloud Storage notification: expected name and bucket properties";
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.BAD_REQUEST);
        }
    
        try {
          ImageMagick.blurOffensiveImages(data);
        } catch (Exception e) {
          String msg = String.format("Error: Blurring image: %s", e.getMessage());
          System.out.println(msg);
          return new ResponseEntity<>(msg, HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return new ResponseEntity<>(HttpStatus.OK);
      }
    }

下载完整示例

要检索完整的图片处理代码示例以供使用,请执行以下操作:

  1. 将示例应用代码库克隆到本地机器:

    Node.js

    git clone https://github.com/GoogleCloudPlatform/nodejs-docs-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

    Python

    git clone https://github.com/GoogleCloudPlatform/python-docs-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

    Go

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

    Java

    git clone https://github.com/GoogleCloudPlatform/java-docs-samples.git

    或者,您也可以下载该示例的 zip 文件并将其解压缩。

  2. 切换到包含 Cloud Run for Anthos 示例代码的目录:

    Node.js

    cd nodejs-docs-samples/run/image-processing/

    Python

    cd python-docs-samples/run/image-processing/

    Go

    cd golang-samples/run/image-processing/

    Java

    cd java-docs-samples/run/image-processing/

交付代码

交付代码包括三个步骤:使用 Cloud Build 构建容器映像、将容器映像上传到 Container Registry,以及将容器映像部署到 Cloud Run for Anthos。

要开发代码,请执行以下操作:

  1. 构建容器并将其发布到 Container Registry 上:

    Node.js

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    其中 PROJECT_ID 是您的 GCP 项目 ID,pubsub 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。映像存储在 Container Registry 中,并可根据需要重复使用。

    Python

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    其中 PROJECT_ID 是您的 GCP 项目 ID,pubsub 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。映像存储在 Container Registry 中,并可根据需要重复使用。

    Go

    gcloud builds submit --tag gcr.io/PROJECT_ID/pubsub

    其中 PROJECT_ID 是您的 GCP 项目 ID,pubsub 是您要为服务指定的名称。

    成功完成后,您将看到一条包含 ID、创建时间和映像名称的 SUCCESS 消息。映像存储在 Container Registry 中,并可根据需要重复使用。

    Java

    此示例使用 Jib 利用常见 Java 工具构建 Docker 映像。无需编写 Dockerfile 或安装 Docker,Jib 便可以优化容器构建。详细了解如何使用 Jib 构建 Java 容器

    1. 使用 Dockerfile,配置并构建已安装系统软件包的基本映像,以覆盖 Jib 的默认基本映像:

      # Use eclipse-temurin for base image.
      # It's important to use JDK 8u191 or above that has container support enabled.
      # https://hub.docker.com/_/eclipse-temurin/
      # https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
      FROM eclipse-temurin:17-jre
      
      # Install Imagemagick into the container image.
      # For more on system packages review the system packages tutorial.
      # https://cloud.google.com/run/docs/tutorials/system-packages#dockerfile
      RUN set -ex; \
        apt-get -y update; \
        apt-get -y install imagemagick; \
        rm -rf /var/lib/apt/lists/*

      gcloud builds submit --tag gcr.io/PROJECT_ID/imagemagick

      其中 PROJECT_ID 是您的 GCP 项目 ID。

    2. 使用 Jib 构建最终容器并在 Container Registry 上发布:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.2.1</version>
        <configuration>
          <from>
            <image>gcr.io/PROJECT_ID/imagemagick</image>
          </from>
          <to>
            <image>gcr.io/PROJECT_ID/pubsub</image>
          </to>
        </configuration>
      </plugin>
      
      mvn compile jib:build \
        -Dimage=gcr.io/PROJECT_ID/pubsub \
        -Djib.from.image=gcr.io/PROJECT_ID/imagemagick

      其中 PROJECT_ID 是您的 GCP 项目 ID。

  2. 利用您在 Pub/Sub 教程中用过的相同服务名称,运行以下命令来部署您的服务:

    Node.js

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Python

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Go

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME

    Java

    gcloud run deploy pubsub-tutorial --image gcr.io/PROJECT_ID/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M

    PROJECT_ID 替换为您的 GCP 项目 ID。pubsub 为容器名称,pubsub-tutorial 为服务名称。请注意,容器映像已部署到您之前在设置 gcloud 默认值中配置的服务和集群。

    BLURRED_BUCKET_NAME 替换为您之前创建来接收经过模糊处理的图片的 Cloud Storage 存储分区,以设置环境变量。

    等待部署完成,这可能需要半分钟左右的时间。 成功完成时,命令行会显示服务网址。

开启来自 Cloud Storage 的通知

配置 Cloud Storage,以便在上传或更改文件(称为对象)时将消息发布到 Pub/Sub 主题。将通知发送到之前创建的主题,以便上传任何新文件都会调用该服务。

gsutil notification create -t myRunTopic -f json gs://INPUT_BUCKET_NAME

gsutil 命令作为 Google Cloud CLI 的一部分安装。myRunTopic 是您在上一个教程中创建的主题。

INPUT_BUCKET_NAME 替换为您在创建存储分区时使用的名称。

如需详细了解存储分区通知,请阅读对象更改通知

测试

  1. 上传一张令人反感的图片,比如这张肉食僵尸图片:

    gsutil cp zombie.jpg gs://INPUT_BUCKET_NAME

    其中,INPUT_BUCKET_NAME 是您之前为了上传图片而创建的 Cloud Storage 存储分区。

  2. 导航到服务日志:

    1. 转到 Google Cloud 控制台中的 Cloud Run for Anthos 页面:

      转到 Cloud Run for Anthos

    2. 点击 pubsub-tutorial 服务。

    3. 选择日志标签页。您可能需要等待一些时间才能看到日志。 如果您没有立即看到日志,请稍等片刻再检查一次。

  3. 查找 Blurred image: zombie.png 消息。

  4. 您可以在之前创建的 BLURRED_BUCKET_NAME Cloud Storage 存储分区中查看经过模糊处理的图片:在 Google Cloud Console 中的 Cloud Storage 页面找到该存储分区

清理

如果您为本教程创建了一个新项目,请删除项目。 如果您使用的是现有项目,希望保留此项目且不保留本教程中添加的任何更改,请删除为教程创建的资源

删除项目

为了避免产生费用,最简单的方法是删除您为本教程创建的项目。

如需删除项目,请执行以下操作:

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除教程资源

  1. 删除您在本教程中部署的 Cloud Run for Anthos 服务:

    gcloud run services delete SERVICE-NAME

    其中,SERVICE-NAME 是您选择的服务名称。

    您还可以在 Google Cloud 控制台中删除 Cloud Run for Anthos 服务。

    转到 Cloud Run for Anthos

  2. 移除您在教程设置过程中添加的 gcloud 默认配置:

     gcloud config unset run/platform
     gcloud config unset run/cluster
     gcloud config unset run/cluster_location
    
  3. 移除项目配置:

     gcloud config unset project
    
  4. 删除在本教程中创建的其他 Google Cloud 资源:

后续步骤

  • 详细了解如何通过 Cloud Storage 使用 Cloud Run for Anthos 保留数据
  • 了解如何使用 Cloud Vision API 检测露骨内容之外的信息
  • 探索有关 Google Cloud 的参考架构、图表、教程和最佳做法。查看我们的 Cloud Architecture Center