每天一度:研究室 1 - 儲存及分析相片

1. 總覽

在第一個程式碼研究室中,您將上傳值區中的圖片。這會產生由函式處理的檔案建立事件。這個函式會呼叫 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 未使用的字元字串。您隨時可以更新這項資訊。
  • 所有 Google Cloud 專案的專案 ID 均不得重複,且設定後即無法變更。Cloud 控制台會自動產生一個不重複的字串。但通常是在乎它何在在大部分的程式碼研究室中,您必須參照專案 ID (通常為 PROJECT_ID)。如果您對產生的 ID 不滿意,可以隨機產生一個 ID。此外,您也可以自行嘗試,看看系統是否提供該付款方式。在完成這個步驟後就無法變更,而且在專案期間仍會保持有效。
  • 資訊中的第三個值是專案編號,部分 API 會使用這個編號。如要進一步瞭解這三個值,請參閱說明文件
  1. 接下來,您需要在 Cloud 控制台中啟用計費功能,才能使用 Cloud 資源/API。執行這個程式碼研究室並不會產生任何費用,如果有的話。如要關閉資源,以免系統產生本教學課程結束後產生的費用,您可以刪除自己建立的資源,或刪除整個專案。Google Cloud 的新使用者符合 $300 美元免費試用計畫的資格。

啟動 Cloud Shell

雖然 Google Cloud 可以從筆記型電腦遠端操作,但在本程式碼研究室中,您將使用 Google Cloud Shell,這是一種在 Cloud 中執行的指令列環境。

Google Cloud 控制台,按一下右上方的工具列上的 Cloud Shell 圖示:

55efc1aaa7a4d3ad.png

佈建並連線至環境的作業只需幾分鐘的時間。完成後,您應該會看到類似下方的內容:

7ffe5cbb04455448.png

這部虛擬機器都裝載了您需要的所有開發工具。提供永久的 5 GB 主目錄,而且在 Google Cloud 中運作,大幅提高網路效能和驗證能力。本程式碼研究室的所有工作都可以在瀏覽器中完成。不必安裝任何程式。

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 指令列工具進行操作。

來自「漢堡」(ICON) 選單,前往 Storage 頁面。

1930e055d138150a.png

為值區命名

按一下「CREATE BUCKET」按鈕。

34147939358517f8.png

按一下「CONTINUE」。

選擇位置

197817f20be07678.png

在您選擇的區域建立多區域值區 (這個頁面 Europe)。

按一下「CONTINUE」。

選擇預設儲存空間級別

53cd91441c8caf0e.png

請為資料選擇 Standard 儲存空間級別。

按一下「CONTINUE」。

設定存取權控管

8c2b3b459d934a51.png

由於您將使用可公開存取的圖片,因此您希望這個值區中儲存的所有圖片都具備相同的統一存取權控管機制。

選擇 Uniform 存取權控管選項。

按一下「CONTINUE」。

設定保護/加密功能

d931c24c3e705a68.png

保留預設值 (Google-managed key),因為您不會用到自己的加密金鑰)。

按一下 CREATE,最後完成值區建立作業。

將 allUsers 新增為儲存空間檢視者

前往「Permissions」分頁:

d0ecfdcff730ea51.png

allUsers 成員新增至值區,角色為 Storage > Storage Object Viewer,如下所示:

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 瀏覽器後,值區會在清單中看到值區的「Public」(公開) 字樣存取 (包括警告標誌,提醒您任何人有權存取該值區的內容)。

89e7a4d2c80a0319.png

您的值區已準備好接收圖片。

按一下值區名稱,即可查看值區的詳細資料。

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

按一下 Create function

選擇名稱 (例如picture-uploaded) 和地區 (請記得與值區的區域選項一致):

4bb222633e6f278.png

函式分為兩種:

  • 可透過網址 (例如網頁 API) 叫用的 HTTP 函式。
  • 可由某些事件觸發的背景函式。

您想要建立會在新檔案上傳至 Cloud Storage 值區時觸發的背景函式:

d9a12fcf58f4813c.png

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

b30c8859b07dc4cb.png

選取先前建立的值區,這樣 Cloud Functions 就會在這個特定值區中建立 / 更新檔案時接收通知:

cb15a1f4c7a1ca5f.png

按一下 Select 來選擇您先前建立的值區,然後點選「Save

c1933777fac32c6a.png

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

83d757e6c38e10.png

按一下 Next 後,您可以調整「Runtime」、「Source code」和「進入點」

保留這個函式的 Inline editor

7dccb5a3fa66363d.png

選取其中一個 Node.js 執行階段:

21defc3b0accd5b4.png

原始碼包含 index.js JavaScript 檔案,以及提供各種中繼資料和依附元件的 package.json 檔案。

保留預設的程式碼片段,內容會記錄已上傳圖片的檔案名稱:

465aca96eb8ca5f9.png

目前,請將要執行的函式名稱保留至 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 資料庫,這是運作快速、全代管的無伺服器雲端原生 NoSQL 文件資料庫。前往 Cloud 控制台的 Firestore 部分,準備資料庫:

9e4708d2257de058.png

提供兩種選項:Native modeDatastore mode。使用原生模式,提供離線支援和即時同步處理等額外功能。

按一下 SELECT NATIVE MODE

9449ace8cc84de43.png

選擇一個多區域 (這裡位於歐洲,但最好至少應與函式和 Storage 值區位於相同區域)。

按一下 CREATE DATABASE 按鈕。

資料庫建立完成後,您應該會看到以下內容:

56265949a124819e.png

按一下「+ START COLLECTION」按鈕,建立新的珍藏內容

為集合「pictures」命名。

75806ee24c4e13a7.png

不必建立文件,當新圖片儲存在 Cloud Storage 並由 Vision API 分析時,您即可透過程式輔助方式新增圖片。

按一下「Save」。

Firestore 會在新建立的集合中建立第一個預設文件,但其中不含任何實用資訊,您可以放心刪除該文件:

5c2f1e17ea47f48f.png

透過程式在集合中建立的文件會含有 4 個欄位:

  • name (字串):上傳圖片的檔案名稱,也是文件金鑰
  • labels (字串陣列):Vision API 可識別項目的標籤
  • color (字串):主色 (即#ab12ef)
  • 建立 (日期):此圖片中繼資料儲存時間的時間戳記
  • 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、儲存空間和 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 函式的值區名稱。

事件酬載如下所示,供您參考:

{
  "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 (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.png

點選「DEPLOY」按鈕,系統就會部署新版本,而您也可以查看進度:

4e0ac812a9124e7c.png

13. 再次測試函式

函式部署成功後,請將圖片張貼至 Cloud Storage,看看是否叫用我們的函式、Vision API 傳回的內容,以及中繼資料是否儲存在 Firestore 中。

回到 Cloud Storage,然後點選我們在研究室一開始建立的值區:

d44c1584122311c7.png

進入值區詳細資料頁面後,點選 Upload files 按鈕上傳圖片。

26bb31d35fb6aa3d.png

來自「漢堡」(ICON) 選單,前往 Logging > Logs Explorer。

Log Fields 選取器中,選取 Cloud Function 即可查看函式專屬的記錄。向下捲動記錄欄位,甚至能選取特定函式,以更精細的方式查看函式相關記錄檔。選取 picture-uploaded 函式。

b651dca7e25d5b11.png

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

d22a7f24954e4f63.png

記錄會指出函式執行的開始與結束時間。兩者之間,可以透過 console.log() 陳述式查看函式中所放入的記錄檔。例如:

  • 觸發函式的事件詳細資料
  • Vision API 呼叫的原始結果。
  • 在上傳圖片中找到的標籤
  • 主要顏色資訊
  • 相片是否安全
  • 最終的圖片中繼資料則儲存在 Firestore 中。

9ff7956a215c15da.png

又一次的「漢堡」(±) 選單,前往 Firestore 區段。在 Data 子區段 (預設顯示) 中,您應該會看到 pictures 集合,其中還有一張新文件,對應到您剛上傳的相片:

a6137ab9687da370.png

14. 清除 (選用)

如果不想繼續參加本系列的其他研究室課程,您可以清理資源來節省成本,並成為良好的雲端公民。您可以按照下列步驟個別清除資源。

刪除值區:

gsutil rb gs://${BUCKET_PICTURES}

刪除函式:

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

如要刪除 Firestore 集合,請選取集合中的「刪除集合」:

410b551c3264f70a.png

或者,您也可以刪除整個專案:

gcloud projects delete ${GOOGLE_CLOUD_PROJECT} 

15. 恭喜!

恭喜!您已成功實作專案的第一個金鑰服務!

涵蓋內容

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

後續步驟