教學課程:處理 Cloud Storage 中的圖片


本教學課程將示範如何使用 Cloud Run、Cloud Vision API 和 ImageMagick,偵測並模糊處理上傳至 Cloud Storage 值區的令人反感圖片。本教學課程是根據「搭配 Cloud Run 使用 Pub/Sub」教學課程所撰寫。

本教學課程將逐步說明如何修改現有的範例應用程式。您也可以視需要下載完成的範例

目標

  • 撰寫、建構及部署非同步資料處理服務至 Cloud Run。
  • 將檔案上傳至 Cloud Storage 來叫用服務,建立 Pub/Sub 訊息。
  • 使用 Cloud Vision API 來偵測暴力或成人內容。
  • 使用 ImageMagick 來模糊處理令人反感的圖片。
  • 上傳食人殭屍的圖片來測試服務。

費用

在本文件中,您會使用 Google Cloud的下列計費元件:

您可以使用 Pricing Calculator 根據預測用量產生預估費用。 新 Google Cloud 使用者可能符合申請免費試用的資格。

事前準備

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Go to project selector

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

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

    Go to project selector

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

  6. Enable the Artifact Registry, Cloud Build, Pub/Sub, Cloud Run, Cloud Storage and Cloud Vision APIs.

    Enable the APIs

  7. 安裝並初始化 gcloud CLI
  8. 更新元件:
    gcloud components update
  9. 按照使用 Pub/Sub 教學課程的說明,設定 Pub/Sub 主題、安全的推送訂閱項目,以及初始 Cloud Run 服務,以便處理訊息
  10. 必要的角色

    如要取得完成本教學課程所需的權限,請要求管理員為您授予專案的下列 IAM 角色:

    如要進一步瞭解如何授予角色,請參閱「管理專案、資料夾和機構的存取權」。

    您或許還可透過自訂角色或其他預先定義的角色取得必要權限。

設定 gcloud 預設值

如要針對 Cloud Run 服務設定 gcloud 的預設值:

  1. 設定您的預設專案:

    gcloud config set project PROJECT_ID

    PROJECT_ID 改為您為本教學課程建立的專案名稱。

  2. 為所選地區設定 gcloud:

    gcloud config set run/region REGION

    REGION 改為您所選擇的支援 Cloud Run 地區

Cloud Run 位置

Cloud Run 具有「地區性」,這表示執行 Cloud Run 服務的基礎架構位於特定地區,並由 Google 代管,可為該地區內所有區域提供備援功能。

選擇 Cloud Run 服務的執行地區時,請將延遲時間、可用性或耐用性需求做為主要考量。一般而言,您可以選擇最靠近使用者的地區,但您應考量 Cloud Run 服務所使用的其他 Google Cloud產品位置。使用分散在不同位置的 Google Cloud 產品,可能會影響服務的延遲時間和費用。

Cloud Run 可在下列地區使用:

採用級別 1 定價

採用級別 2 定價

如果您已建立 Cloud Run 服務,即可在 Google Cloud 控制台的 Cloud Run 資訊主頁中查看地區。

瞭解運算順序

本教學課程中的資料流程如下:

  1. 使用者將圖片上傳至 Cloud Storage 值區。
  2. Cloud Storage 會將新檔案的訊息發布至 Pub/Sub。
  3. Pub/Sub 會將訊息推送至 Cloud Run 服務。
  4. Cloud Run 服務會擷取 Pub/Sub 訊息中參照的圖片檔案。
  5. Cloud Run 服務會使用 Cloud Vision API 分析圖片。
  6. 如果偵測到暴力或成人內容,Cloud Run 服務會使用 ImageMagick 模糊處理圖片。
  7. Cloud Run 服務會將經過模糊處理的圖片上傳至其他 Cloud Storage 值區,以供使用。

讀者可以自行決定是否要使用模糊處理過的圖片。

建立 Artifact Registry 標準存放區

建立 Artifact Registry 標準存放區來儲存容器映像檔:

gcloud artifacts repositories create REPOSITORY \
    --repository-format=docker \
    --location=REGION

取代:

  • REPOSITORY 與存放區的專屬名稱。
  • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。

設定 Cloud Storage 值區

gcloud

  1. 建立 Cloud Storage 值區來上傳圖片,其中 INPUT_BUCKET_NAME 是不重複的值區名稱:

    gcloud storage buckets create gs://INPUT_BUCKET_NAME

    Cloud Run 服務只會從這個值區讀取資料。

  2. 建立第二個 Cloud Storage 值區來接收模糊處理過的圖片,其中 BLURRED_BUCKET_NAME 是不重複的值區名稱:

    gcloud storage buckets create gs://BLURRED_BUCKET_NAME

    Cloud Run 服務會將模糊處理過的圖片上傳至這個值區。使用不同的值區可避免已處理的圖片重新觸發服務。

    根據預設,Cloud Run 修訂版本會以 Compute Engine 預設服務帳戶執行。

    如果您使用的是使用者管理的服務帳戶,請務必指派必要的 IAM 角色,讓該帳戶具備 storage.objects.get 權限,以便從 INPUT_BUCKET_NAME 讀取資料,以及 storage.objects.create 權限,以便將資料上傳至 BLURRED_BUCKET_NAME

Terraform

如要瞭解如何套用或移除 Terraform 設定,請參閱「基本 Terraform 指令」。

建立兩個 Cloud Storage 值區:一個用於上傳原始圖片,另一個用於 Cloud Run 服務上傳模糊圖片。

如要建立兩個名稱不重複的 Cloud Storage 值區,請在現有的 main.tf 檔案中新增下列內容:

resource "random_id" "bucket_suffix" {
  byte_length = 8
}

resource "google_storage_bucket" "imageproc_input" {
  name     = "input-bucket-${random_id.bucket_suffix.hex}"
  location = "us-central1"
}

output "input_bucket_name" {
  value = google_storage_bucket.imageproc_input.name
}

resource "google_storage_bucket" "imageproc_output" {
  name     = "output-bucket-${random_id.bucket_suffix.hex}"
  location = "us-central1"
}

output "blurred_bucket_name" {
  value = google_storage_bucket.imageproc_output.name
}

根據預設,Cloud Run 修訂版本會以 Compute Engine 預設服務帳戶執行。

如果您使用的是使用者管理的服務帳戶,請務必指派必要的 IAM 角色,讓該帳戶具備從 google_storage_bucket.imageproc_input 讀取的 storage.objects.get 權限,以及上傳至 google_storage_bucket.imageproc_outputstorage.objects.create 權限。

在下列步驟中,您將建立並部署一項服務,處理上傳至 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"
    	"cloud.google.com/go/vision/v2/apiv1/visionpb"
    )
    
    // 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

    def blur_offensive_images(data):
        """Blurs uploaded images that are flagged as Adult or Violence.
    
        Args:
            data: Pub/Sub message 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: %w", 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

    def __blur_image(current_blob):
        """Blurs the given file using ImageMagick.
    
        Args:
            current_blob: a Cloud Storage 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: %w", 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: %w", 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<>();
        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 gm @google-cloud/storage @google-cloud/vision

    Python

    新增必要的用戶端程式庫,讓 requirements.txt 的樣式如下所示:
    Flask==3.0.3
    google-cloud-storage==2.12.0
    google-cloud-vision==3.8.1
    gunicorn==23.0.0
    Wand==0.6.13
    Werkzeug==3.0.3
    

    Go

    Go 範例應用程式使用 Go 模組,因此在 imagemagick/imagemagick.go 匯入陳述式中新增的依附元件,會在下一個需要這些依附元件的指令自動下載。

    Java

    pom.xml<dependencyManagement> 下方新增下列依附元件:
    <dependency>
      <groupId>com.google.cloud</groupId>
      <artifactId>spring-cloud-gcp-dependencies</artifactId>
      <version>4.9.2</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
    
    pom.xml<dependencies> 下方新增下列依附元件:
    <dependency>
      <groupId>com.google.code.gson</groupId>
      <artifactId>gson</artifactId>
      <scope>compile</scope>
    </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>
    

  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 服務中使用系統套件,請參閱使用系統套件教學課程

  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():
        """Receive and parse Pub/Sub messages containing Cloud Storage event data."""
        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"
    	"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 := io.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 範例程式碼的目錄:

    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 建構容器映像檔、將容器映像檔上傳到 Artifact Registry,然後將容器映像檔部署到 Cloud Run。

如要推送程式碼:

  1. 建構容器並發布至 Artifact Registry:

    Node.js

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    其中 pubsub 是服務名稱。

    取代:

    • PROJECT_ID 改成您的 Google Cloud 專案 ID
    • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
    • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔會儲存在 Artifact Registry 中,日後如有需要,可以重複使用。

    Python

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    其中 pubsub 是服務名稱。

    取代:

    • PROJECT_ID 改成您的 Google Cloud 專案 ID
    • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
    • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔會儲存在 Artifact Registry 中,日後如有需要,可以重複使用。

    Go

    gcloud builds submit --tag REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub

    其中 pubsub 是服務名稱。

    取代:

    • PROJECT_ID 改成您的 Google Cloud 專案 ID
    • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
    • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。

    若成功執行,您會看到包含 ID、建立時間和映像檔名稱的「SUCCESS」(成功) 訊息。映像檔會儲存在 Artifact Registry 中,日後如有需要,可以重複使用。

    Java

    這個範例使用 Jib 建構 Docker 映像檔,並使用常見的 Java 工具。Jib 可在不需要 Dockerfile 或安裝 Docker 的情況下,最佳化容器建構作業。進一步瞭解如何使用 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.0.15_6-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 REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/imagemagick

      取代:

      • PROJECT_ID 改成您的 Google Cloud 專案 ID
      • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
      • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。
    2. 使用 gcloud 憑證輔助程式授權 Docker 推送至 Artifact Registry。

      gcloud auth configure-docker

    3. 使用 Jib 建構最終容器,並發布至 Artifact Registry:

      <plugin>
        <groupId>com.google.cloud.tools</groupId>
        <artifactId>jib-maven-plugin</artifactId>
        <version>3.4.0</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=REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/pubsub \
        -Djib.from.image=REGION-docker.pkg.dev/PROJECT_ID
      /REPOSITORY/imagemagick

      取代:

      • PROJECT_ID 改成您的 Google Cloud 專案 ID
      • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
      • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。

  2. 執行下列指令來部署服務,並使用您在使用 Pub/Sub 教學課程中使用的服務名稱:

    Node.js

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Python

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Go

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --no-allow-unauthenticated

    Java

    gcloud run deploy pubsub-tutorial --image REGION-docker.pkg.dev/PROJECT_ID
    /REPOSITORY/pubsub --set-env-vars=BLURRED_BUCKET_NAME=BLURRED_BUCKET_NAME --memory 512M --no-allow-unauthenticated

    其中 pubsub 是容器名稱,pubsub-tutorial 是服務名稱。請注意,容器映像檔是部署到您之前在「設定 gcloud 預設值」中設定的服務和地區 (Cloud Run)。取代:

    • PROJECT_ID 改成您的 Google Cloud 專案 ID
    • REPOSITORY 替換為 Artifact Registry 存放區的名稱。
    • REGION 與要用於 Artifact Registry 存放區的 Google Cloud 區域。
    • BLURRED_BUCKET_NAME 與您先前建立的 Cloud Storage 值區,以便接收經過模糊處理的圖片,並設定環境變數。

    --no-allow-unauthenticated 標記可限制未經驗證的服務存取權。不公開服務可讓您依靠 Cloud Run 的 Pub/Sub 自動整合來驗證要求。如要進一步瞭解如何進行設定,請參閱「與 Pub/Sub 整合」一節。如要進一步瞭解以 IAM 為基礎的驗證,請參閱「管理存取權」一文。

    請等待部署完成,這可能需要半分鐘的時間。成功完成後,指令列會顯示服務網址。

開啟 Cloud Storage 通知

設定 Cloud Storage,以便在檔案 (稱為物件) 上傳或變更時,將訊息發布至 Pub/Sub 主題。將通知傳送至先前建立的主題,這樣任何新的檔案上傳作業都會叫用該服務。

gcloud

gcloud storage service-agent --project=PROJECT_ID
gcloud storage buckets notifications create gs://INPUT_BUCKET_NAME --topic=myRunTopic --payload-format=json

myRunTopic 是您在先前教學課程中建立的主題。

INPUT_BUCKET_NAME 替換為您在建立資料夾時使用的名稱。

如要進一步瞭解儲存空間值區通知,請參閱「物件變更通知」。

Terraform

如要瞭解如何套用或移除 Terraform 設定,請參閱「基本 Terraform 指令」。

如要啟用通知功能,專案中必須有專屬的 Cloud Storage 服務帳戶,且該帳戶必須具備 Pub/Sub 主題的 IAM 權限 pubsub.publisher。如要授予這項權限並建立 Cloud Storage 通知,請在現有的 main.tf 檔案中加入以下內容:

data "google_storage_project_service_account" "gcs_account" {}

resource "google_pubsub_topic_iam_binding" "binding" {
  topic   = google_pubsub_topic.default.name
  role    = "roles/pubsub.publisher"
  members = ["serviceAccount:${data.google_storage_project_service_account.gcs_account.email_address}"]
}

resource "google_storage_notification" "notification" {
  bucket         = google_storage_bucket.imageproc_input.name
  payload_format = "JSON_API_V1"
  topic          = google_pubsub_topic.default.id
  depends_on     = [google_pubsub_topic_iam_binding.binding]
}

立即體驗

  1. 上傳令人反感的圖片,例如這張食肉殭屍圖片:

    curl -o zombie.jpg https://cdn.pixabay.com/photo/2015/09/21/14/24/zombie-949916_960_720.jpg
    gcloud storage cp zombie.jpg gs://INPUT_BUCKET_NAME

    其中 INPUT_BUCKET_NAME 是您先前建立用於上傳圖片的 Cloud Storage 值區。

  2. 前往服務記錄:

    1. 前往 Google Cloud 控制台的 Cloud Run 頁面
    2. 按一下 pubsub-tutorial 服務。
    3. 選取「Logs」分頁標籤。記錄需要一些時間才會出現。如果您沒有立即看到記錄,請稍候片刻再查看一次。
  3. 尋找 Blurred image: zombie.png 訊息。

  4. 您可以在先前建立的 BLURRED_BUCKET_NAME Cloud Storage 值區中查看模糊處理的圖片:在 Cloud Storage 頁面中找到該值區 Google Cloud

清除所用資源

如果您是為了這個教學課程建立新專案,請刪除專案。如果您使用現有專案,且希望保留該專案而不採用本教學課程中的變更,請刪除為教學課程建立的資源

刪除專案

如要避免付費,最簡單的方法就是刪除您為了本教學課程所建立的專案。

如要刪除專案:

  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 服務:

    gcloud run services delete SERVICE-NAME

    其中 SERVICE-NAME 是您選擇的服務名稱。

    您也可以從 Google Cloud 控制台刪除 Cloud Run 服務。

  2. 移除您在教學課程設定期間新增的 gcloud 預設區域設定:

     gcloud config unset run/region
    
  3. 移除專案設定:

     gcloud config unset project
    
  4. 刪除本教學課程中建立的其他 Google Cloud 資源:

後續步驟

  • 進一步瞭解如何使用 Cloud Storage 搭配 Cloud Run 儲存資料。
  • 瞭解如何使用 Cloud Vision API 偵測煽情露骨內容以外的內容。
  • 探索 Google Cloud 的參考架構、圖表和最佳做法。歡迎瀏覽我們的雲端架構中心