iT邦幫忙

2021 iThome 鐵人賽

1
AI & Data

觀賞魚辨識的YOLO全餐系列 第 33

Day 33 - 實作 S3 驅動 Lambda 函數進行鏡像

Day 33 - 實作 S3 驅動 Lambda 函數進行鏡像

AWS 有個教學課程,教學課程:使用 Amazon S3 觸發條件建立縮圖影像,今天我們就以這個教程為基礎,並結合Day 32 - 透過手機呼叫 Amazon API Gateway 上傳圖片到 S3這篇文章,讓使用者可以上傳一個圖片後,就完成圖片鏡像的動作。

以上這個實驗需要的 AWS 服務有

  • Amazon API Gateway:提供上傳圖片用的 API
  • AWS Lambda: 執行將圖片執行鏡像處理的運算。
  • Amazon S3: 提供兩個儲存貯體,一個作為上傳圖片,一個作為鏡像圖片的存放。
  • Identity and Access Management (IAM): 提供兩個角色,一個是授權給 API Gateway 上傳圖片的許可權限;另一個是授權給 AWS Lambda 讀取圖片與寫入處理後的圖片的許可權限。
  • CloudWatch:除錯、監控之用。

下圖中顯示,透過具有 CORS 設定的 API Gateway 取得角色 A 的授權許可,將圖片寫入到儲存貯體 A,寫入的動作會觸發 Lambda 函數,於是 Lambda 函數就會讀取儲存貯體 A 的圖片,進行鏡像運算後,寫入儲存貯體 B,此時它是取得角色 B 的許可授權,而因為 S3 儲存貯體 B 具有對外公開的讀取權限,所以網際網路中的用戶就可以直接讀取鏡像圖片。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510mxg8S3KuoJ.png
圖 1、實作 S3 驅動 Lambda 函數進行鏡像架構圖

上傳圖片的 API Gateway 已於 Day 32 - 透過手機呼叫 Amazon API Gateway 上傳圖片到 S3 這篇文章中完成實作,接著我們需要完成的就是建立一個產生鏡像圖片的 Lambda 函數,步驟如下:

  1. 本機端建立縮圖程式
    • 建立虛擬環境並安裝需要套件
    • 撰寫程式並運行
  2. 建立 IAM Role and Policy
  3. 建立 Lambda layer
  4. 建立 Lambda 函數
  5. 建立 S3 bucket 事件通知

建立縮圖程式

以下命令用來建立虛擬環境 pil,並激活虛擬環境,接著將安裝套件用的 pip3 更新到最新版本,並安裝所需套件 pillow。

python3 -m venv pil
. pil/bin/activate
cd pil
pip3 install --upgrade pip
pip3 install Pillow
tree -L 2

撰寫需要的程式 mirror.py,會將指定的圖片呈現鏡像,左右顛倒,需要先放一張圖片 00-frame-0054.jpg 到目錄中,文件夾結構如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295103Bplpz67On.png
圖 2、虛擬環境 pil 的文件夾結構

_mirror.py

from PIL import Image, ImageOps
import PIL.Image

def resize_image(image_path, resized_path):
  with Image.open(image_path) as image:
      im_mirror = ImageOps.mirror(image)
      im_mirror.save(resized_path)
      
OriginImg = '00-frame-0054.jpg'
ResizeImg = '00-frame-0054_mirror.jpg'

resize_image(OriginImg,ResizeImg)

運行以下命令後,可以得到一張呈現鏡像,左右顛倒的圖片,如下圖所示,

python3 mirror.py

https://ithelp.ithome.com.tw/upload/images/20211007/20129510Kp7yPB4Urd.png
圖 3、呈現鏡像,左右顛倒的圖片

建立 IAM 角色與政策

需要建立一個角色需要由 Lambda 函數來執行,且具有讀取儲存貯體 A 與寫入儲存貯體 B 的許可授權。進入 IAM 管理控制台,選擇新增角色,接下來如下圖所示,選擇 Lambda 的使用案例後點擊 下一個:許可 按鈕。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510WYK3SUlBvJ.png
圖 4、建立一個角色選擇 Lambda 的使用案例

在搜尋文字框中輸入 basic 找到 AWSLambdaBasicExecutionRole 進行連接,這將允許這個角色有寫入 CloudWatch 記錄檔的全縣,方便程式除錯之用,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295108dyqJGu4Jf.png
圖 5、連接基礎的 CloudWatch 除錯用的許可政策

最後確定先前的設定後並輸入角色名稱後,就可以建立角色,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510q7otPjt1ZV.png
圖 6、檢閱設定並建立角色

編輯一個新的政策,內容如下圖所示,給定讀取 (GetObject) 儲存貯體 A 與寫入物件 (PutObject) 與權限 (PutObjectAcl) 到儲存貯體 B。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510UXadxnJZpr.png
圖 7、新增政策

接著到角色設定畫面,將新建政策連接到角色上,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510ndf6jHSGos.png
圖 8、將新增的政策連接到先前的角色

建立 Lambda layer

因為接下來的 Lambda 函數會用到 pillow 函式庫,所以必須要這個函式庫的套件一起打包到 Lambda 函式中,使用的方法有兩種,一種是跟主程式打包在一起,另一種則是以分層 (Layer) 的方式,獨立打包成一層,在 Lambda 函數中再添加需要的函式庫層,我們採用第二種方法。

根據 AWS 的官方教程 建立和共用 Lambda 層,所建立的 pillow 函式庫會出現 cannot import name '_imaging' from 'PIL' 的錯誤訊息,後來看了很多討論後,發現應該是 pillow 套件會用到已經編譯好的函式庫 (cpython) ,以致於會出現這樣的錯誤,解決方法就是直接去官方網站下載套件安裝包,如下圖所示。在 python 套件的官方網站 https://pypi.org/ 中找到 pillow 專案所在,找尋適合的安裝包。比方說,如果你的 Python 版本是 3.8,pillow 版本是 8.3.2,那就可以找 Pillow-8.3.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl 這個檔案下載,manylinux_2_5 只的是它適用於多種版本linux且核心為2.5, x86_64 則是適用的 CPU 類型,切勿以自己電腦的環境來看,因為這是被放在 lambda 的虛擬環境中的,而 lambda 的虛擬環境大多是以 linux 容器為主,當然也有 arm 架構跟 x86 架構,所以要根據建立函式庫層的設定來挑選合適的安裝包。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510AefaHRB9IK.png
圖 9、至 Python 套件管理官方網站找尋安裝包

下載後直接用 zip 工具解壓縮,會得到 3 個文件夾,將這三個文件夾放在 python 的文件夾中,在進行壓縮即可,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510rppvMB4fC2.png
圖 10、壓縮至 python 文件夾中

進入 AWS Lambda 管理控制台,選擇建立 Layer,輸入 Layer 名稱,並上傳先前建立的壓縮檔,在選擇相容架構 x86_64,而相容的執行時間指的是希望可以在哪些版本的 python 中執行,確定後按下 建立 鍵即完成建立 Layer,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510pGhX9fL0Mv.png
圖 11、建立 Layer 設定畫面

建立 Lambda 函數

進入 AWS Lambda 管理控制台,選擇建立 Lambda 函數,設定內容如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295100P9eNUstUv.png
圖 12、建立 Lambda 函數設定畫面

建立 Lambda 函數後,選擇進入 resizeFunc 函數的設定畫面,在畫面的最底端,為本函數新建 Layer,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295102jDzwdPjO3.png
圖 13、為 Lambda 函數新增 Layer

進入新增 Layer 畫面後,選擇層來源為 自訂 Layer,接著選擇先前建立的 pillow8_3_2 函式庫層,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510ZYlTRkYN4R.png
圖 14、選擇 pillow8_3_2 函式庫層

建立新測試事件,事件範本選擇 hello-world,事件名稱輸入 mirror,內容所下圖所示,這是用來模擬當 S3 觸發 Lambda 函數後所傳過來的參數內容,記得將 [INPUT_BUCKET] 改成實際的輸入儲存貯體名稱,而[INPUT_OBJECT]要確保有這個檔案。

{
  "Records": [
    {
      "s3": {
        "bucket": {
          "name": "[INPUT_BUCKET]",
          "arn": "arn:aws:s3:::[INPUT_BUCKET]"
        },
        "object": {
          "key": "[INPUT_OBJECT]"
        }
      }
    }
  ]
}

https://ithelp.ithome.com.tw/upload/images/20211007/20129510wGO97OS6x8.png
圖 15、設定 Lambda 函數測試事件

而處理完後的鏡像圖片所在的儲存貯體名稱,則是設定在 Lambda 函數組態中的環境變量,變量金鑰為 putbucket ,值是根據自己的實際設定來給定,設定畫面如下所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510Cj8tv2lqaG.png
圖 16、設定 Lambda 函數組態中的環境變量

以下為 Lambda 函數的代碼部分,會根據測試事件與環境參數的參數來進行讀取。

lambda_function.py

import boto3
import os
import sys
import uuid
from urllib.parse import unquote_plus
from PIL import Image, ImageOps
import PIL.Image

s3_client = boto3.client('s3')

def mirror_image(image_path, mirror_path):
  with Image.open(image_path) as image:
    im_mirror = ImageOps.mirror(image)
    im_mirror.save(mirror_path)
    print('mirror the image {} to {}'.format(image_path, mirror_path))

def lambda_handler(event, context):
  for record in event['Records']:
	  inputbucket = record['s3']['bucket']['name']
	  outputbucket = os.environ['putbucket']
	  key = unquote_plus(record['s3']['object']['key'])
	  tmpkey = key.replace('/', '')
	  download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
	  upload_path = '/tmp/mirror-{}'.format(tmpkey)
	  s3_client.download_file(inputbucket, key, download_path)
	  mirror_image(download_path, upload_path)
	  s3_client.upload_file(upload_path, outputbucket, key,ExtraArgs={'ACL': 'public-read','ContentType':'image/jpeg'})

https://ithelp.ithome.com.tw/upload/images/20211007/20129510cmuKt2E48V.png
圖 17、Lambda 函數測試結果

建立 S3 bucket事件通知

進入 S3 管理控制台畫面,選擇儲存貯體 A (上傳時的儲存貯體),選擇 屬性 頁籤,找到 事件通知 這個屬性,點擊 建立事件通知 按鈕來進入建立事件通知畫面,完成以下配置:

  • 事件名稱: mirrorEvent
  • 事件類型: 所有物件建立事件 (s3:ObjectCreated:): 勾選
  • 目的地: Lambda 函數
  • 指定 Lambda 函數: 從 Lambda 函數 中選擇: resizeFunc

https://ithelp.ithome.com.tw/upload/images/20211007/20129510AkhG0a3Pzn.png
圖 18、建立事件通知設定畫面

進入儲存貯體 A 物件 頁籤畫面,手動上傳一個檔案,測試先前建立的事件通知是否正常運行,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510811Ohq7ZXy.png
圖 19、手動上傳一個檔案

切換到儲存貯體 B 物件 頁籤畫面,確認鏡像圖片檔案是否寫入,如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510BfHzK5jS3N.png
圖 20、確認鏡像圖片檔案是否寫入儲存貯體 B

打開 CloudWatch 管理控制台,找到 CloudWatch 日誌中的 Lambda 函數日誌,檢查是否有運行,結果如下圖所示。

https://ithelp.ithome.com.tw/upload/images/20211007/201295107PtQONwqr7.png
圖 21、檢查 CloudWatch 日誌中的 Lambda 函數日誌

最後就是整合 API Gateway,在本地端撰寫一個網頁,可以上傳本地檔案到儲存貯體 A ,因而觸發 Lambda 函數後,生成一個鏡像圖片,放在儲存貯體 B ,因為鏡像圖片的屬性是公開存取,所以可以直接用網頁的 <img> 標籤讀取,下圖中的下方圖片就是位於儲存貯體 B 的檔案。

https://ithelp.ithome.com.tw/upload/images/20211007/20129510EArLk6ywka.png
圖 22、呈現鏡像,左右顛倒的圖片

參考網頁程式如下所示。

uploadtoS3.html

<!DOCTYPE html>
<html>
  <head>
  	<meta charset="utf-8"/>
    <title>Upload file to S3</title>
    <script src="https://unpkg.com/vue"></script>
    <script src="https://unpkg.com/axios@0.2.1/dist/axios.min.js"></script>
  </head>
  <body>
    <div id="app">
      <h1>S3 Uploader Test</h1>
  
      <div v-if="!image">
        <h2>Select an image</h2>
        <input type="file" @change="onFileChange">
      </div>
      <div v-else>
        <img :src="image" />
        <button v-if="!uploadURL" @click="removeImage">Remove image</button>
        <button v-if="!uploadURL" @click="uploadImage">Upload image</button>
      </div>
      <h2 v-if="uploadURL">Success! Image uploaded to bucket.<br/>
      	<img :src="returnImage" />
      </h2>
    </div>
  
    <script>
      const MAX_IMAGE_SIZE = 10000000

      /* ENTER YOUR ENDPOINT HERE */

      const API_ENDPOINT = '[API_ENDPOINT]' // e.g. https://ab1234ab123.execute-api.us-east-1.amazonaws.com/uploads
      const RET_ENDPOINT = '[RET_ENDPOINT]' // e.g. 'https://bucketb.s3.ap-southeast-1.amazonaws.com/'
			
      uploadFile=''
      new Vue({
        el: "#app",
        data: {
          image: '',
          uploadURL: '',
          returnImage: ''
        },
        methods: {
          onFileChange (e) {
            let files = e.target.files || e.dataTransfer.files
            if (!files.length) return
            for( attr in files[0])
            	console.log(attr)
            console.log(files[0].name)
            uploadFile = files[0].name
            this.createImage(files[0])
          },
          createImage (file) {
            // var image = new Image()
            let reader = new FileReader()
            reader.onload = (e) => {
              console.log('length: ', e.target.result.includes('data:image/jpeg'))
              if (!e.target.result.includes('data:image/jpeg')) {
                return alert('Wrong file type - JPG only.')
              }
              if (e.target.result.length > MAX_IMAGE_SIZE) {
                return alert('Image is loo large.')
              }
              this.image = e.target.result
            }
            reader.readAsDataURL(file)
          },
          removeImage: function (e) {
            console.log('Remove clicked')
            this.image = ''
          },
          uploadImage: async function (e) {
            console.log('Upload clicked')
            console.log('Uploading: ', uploadFile) 
            let binary = atob(this.image.split(',')[1])
            let array = []
            for (var i = 0; i < binary.length; i++) {
              array.push(binary.charCodeAt(i))
            }
            let blobData = new Blob([new Uint8Array(array)], {type: 'image/jpeg'})
            this.uploadURL = API_ENDPOINT + uploadFile
            console.log('Uploading to: ', this.uploadURL)
            const result = await fetch(this.uploadURL, {
              method: 'PUT',
              body: blobData
            })
            console.log('Result: ', result)
            this.returnImage = RET_ENDPOINT + uploadFile
          }
        }
      })
    </script>
    <style type="text/css">
      body {
        background: #20262E;
        padding: 20px;
        font-family: sans-serif;
      }
      #app {
        background: #fff;
        border-radius: 4px;
        padding: 20px;
        transition: all 0.2s;
        text-align: center;
      }
      #logo {
        width: 100px;
      }
      h2 {
        font-weight: bold;
        margin-bottom: 15px;
      }
      h1, h2 {
        font-weight: normal;
        margin-bottom: 15px;
      }
      a {
        color: #42b983;
      }
      img {
        width: 30%;
        margin: auto;
        display: block;
        margin-bottom: 10px;
      }
    </style>
  </body>
</html>

參考資料


上一篇
Day 32 - 透過手機呼叫 Amazon API Gateway 上傳圖片到 S3
下一篇
Day 34 - 實作 S3 驅動 Lambda 函數進行 YOLO 物件辨識
系列文
觀賞魚辨識的YOLO全餐38

尚未有邦友留言

立即登入留言