Lambda(Python)でGoogle Driveへファイルアップロード
この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。
以下を見てからこの記事をみるといい感じです。
イントロダクション
S3ってファイルを保存するだけならいいのですが、そのファイルを人が参照するのには向いてないですよね。画像にしてもWebブラウザ上でそのまま見ることができず、一度ダウンロードしてから見る必要がある。ちょっと手間なんですよね。
その点、Google DriveはPCにしろスマホにしろ、ブラウザやアプリでいい感じに画像を参照することができますのでいい感じです。
コンテンツ
Google Developers Console プロジェクトの作成
GoogleのDevelopers Consoleのにアクセスします。
https://console.developers.google.com
Google Drive APIの有効化
コンソールの「APIとサービス」画面上部にある「+APIとサービスを有効化」を押下し、Google Driveを有効化します。
認証情報の作成
Google Drive APIの有効化が完了したら、コンソールの「APIとサービス」画面左ペインの「認証情報」から、認証情報を作成します。
認証情報のページ上部にある「+認証情報を作成」を押下すると表示されるドロップダウンメニューより、「サービスアカウント」を選択します。
サービスアカウント名を入力して作成してください。
なお、サービスアカウント名の下にあるサービスアカウントIDは大切な情報なので漏洩しないよう管理してください。
サービスアカウントの確認
サービスアカウントの作成が完了したら、コンソールの「IAM と 管理」画面左ペイン「サービスアカウント」から、作成したサービスアカウントの詳細を参照します。
サービスアカウント秘密鍵の作成
サービスアカウントの詳細から、「編集」そして「+キーを作成」ボタンを押下し、秘密鍵をJSON形式で作成、JSONファイルをダウンロードします。
Google Driveにフォルダを作成し、サービスアカウントと共有する
Google Driveにファイルアップロード用のフォルダを作成します。ここではsample-drive
というフォルダ名としました。そしてそのフォルダを先ほど作成したサービスアカウント(メールアドレス)と共有します。
このフォルダのURLの後ろの文字列は、後でプログラムから利用します。(下のキャプチャの隠してる部分)
さてこれでGoogle側の設定は完了です。
続いて、プログラムからGoogle APIでファイルをアップロードしましょう。
Lambda(Python)からGoogle APIをサービスアカウントでリクエストする
まずは、先ほどダウンロードしたサービスアカウントの秘密鍵JSONファイルを、適当な名前にリネームしてlambda_function.pyと同じ場所においておきます。ここでは「service-account-key.json」という名前としました。
続いて、必要なライブラリをインストールします。
$ pip install google-api-python-client -t .
$ pip install oauth2client -t .
で、必要なものをインポートします。
:
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from oauth2client.service_account import ServiceAccountCredentials
:
そして、以下のような実装でファイルをアップロードします。
:
def uploadFileToGoogleDrive(fileName, localFilePath):
try:
ext = os.path.splitext(localFilePath.lower())[1][1:]
if ext == "jpg":
ext = "jpeg"
mimeType = "image/" + ext
service = getGoogleService()
file_metadata = {"name": fileName, "mimeType": mimeType, "parents": ["*********************************"] }
media = MediaFileUpload(localFilePath, mimetype=mimeType, resumable=True)
file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
except Exception as e:
logger.exception(e)
def getGoogleService():
scope = ['https://www.googleapis.com/auth/drive.file']
keyFile = 'service-account-key.json'
credentials = ServiceAccountCredentials.from_json_keyfile_name(keyFile, scopes=scope)
return build("drive", "v3", credentials=credentials, cache_discovery=False)
"parents": ["*********************************"]この部分はGoogle Driveに作成したフォルダのURLの後ろ側の文字列に置き換えてください。
最後に念の為プログラム全体も載せておきますね。
# coding: UTF-8
import boto3
import os
import json
from urllib.parse import unquote_plus
import numpy as np
import cv2
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3 = boto3.client("s3")
rekognition = boto3.client('rekognition')
from gql import gql, Client
from gql.transport.requests import RequestsHTTPTransport
ENDPOINT = "https://**************************.appsync-api.ap-northeast-1.amazonaws.com/graphql"
API_KEY = "da2-**************************"
_headers = {
"Content-Type": "application/graphql",
"x-api-key": API_KEY,
}
_transport = RequestsHTTPTransport(
headers = _headers,
url = ENDPOINT,
use_json = True,
)
_client = Client(
transport = _transport,
fetch_schema_from_transport = True,
)
from googleapiclient.discovery import build
from googleapiclient.http import MediaFileUpload
from oauth2client.service_account import ServiceAccountCredentials
def lambda_handler(event, context):
bucket = event["Records"][0]["s3"]["bucket"]["name"]
key = unquote_plus(event["Records"][0]["s3"]["object"]["key"], encoding="utf-8")
logger.info("Function Start (deploy from S3) : Bucket={0}, Key={1}" .format(bucket, key))
fileName = os.path.basename(key)
dirPath = os.path.dirname(key)
dirName = os.path.basename(dirPath)
orgFilePath = "/tmp/" + fileName
if (not key.startswith("public") or key.startswith("public/processed/")):
logger.info("don't process.")
return
apiCreateTable(dirName, key)
keyOut = key.replace("public", "public/processed", 1)
dirPathOut = os.path.dirname(keyOut)
try:
s3.download_file(Bucket=bucket, Key=key, Filename=orgFilePath)
orgImage = cv2.imread(orgFilePath)
grayImage = cv2.cvtColor(orgImage, cv2.COLOR_RGB2GRAY)
processedFileName = "gray-" + fileName
processedFilePath = "/tmp/" + processedFileName
uploadImage(grayImage, processedFilePath, bucket, os.path.join(dirPathOut, processedFileName), dirName, False)
uploadFileToGoogleDrive(key, orgFilePath)
detectFaces(bucket, key, fileName, orgImage, dirName, dirPathOut)
except Exception as e:
logger.exception(e)
raise e
finally:
if os.path.exists(orgFilePath):
os.remove(orgFilePath)
def uploadImage(image, localFilePath, bucket, s3Key, group, isUploadGoogleDrive):
logger.info("start uploadImage({0}, {1}, {2}, {3})".format(localFilePath, bucket, s3Key, group))
try:
cv2.imwrite(localFilePath, image)
s3.upload_file(Filename=localFilePath, Bucket=bucket, Key=s3Key)
apiCreateTable(group, s3Key)
if isUploadGoogleDrive:
uploadFileToGoogleDrive(s3Key, localFilePath)
except Exception as e:
logger.exception(e)
raise e
finally:
if os.path.exists(localFilePath):
os.remove(localFilePath)
def apiCreateTable(group, path):
logger.info("start apiCreateTable({0}, {1})".format(group, path))
try:
query = gql("""
mutation create {{
createSampleAppsyncTable(input:{{
group: \"{0}\"
path: \"{1}\"
}}){{
group path
}}
}}
""".format(group, path))
_client.execute(query)
except Exception as e:
logger.exception(e)
raise e
def detectFaces(bucket, key, fileName, image, group, dirPathOut):
logger.info("start detectFaces ({0}, {1}, {2}, {3}, {4})".format(bucket, key, fileName, group, dirPathOut))
try:
response = rekognition.detect_faces(
Image={
"S3Object": {
"Bucket": bucket,
"Name": key,
}
},
Attributes=[
"ALL",
]
)
name, ext = os.path.splitext(fileName)
jsonFileName = name + ".json"
localPathJSON = "/tmp/" + jsonFileName
with open(localPathJSON, 'w') as f:
json.dump(response, f, ensure_ascii=False)
s3.upload_file(Filename=localPathJSON, Bucket=bucket, Key=os.path.join(dirPathOut, jsonFileName))
if os.path.exists(localPathJSON):
os.remove(localPathJSON)
imgHeight = image.shape[0]
imgWidth = image.shape[1]
index = 0
for faceDetail in response["FaceDetails"]:
index += 1
faceFileName = "face_{0:03d}".format(index) + ext
box = faceDetail["BoundingBox"]
x = max(int(imgWidth * box["Left"]), 0)
y = max(int(imgHeight * box["Top"]), 0)
w = int(imgWidth * box["Width"])
h = int(imgHeight * box["Height"])
logger.info("BoundingBox({0},{1},{2},{3})".format(x, y, w, h))
faceImage = image[y:min(y+h, imgHeight-1), x:min(x+w, imgWidth)]
localFaceFilePath = os.path.join("/tmp/", faceFileName)
uploadImage(faceImage, localFaceFilePath, bucket, os.path.join(dirPathOut, faceFileName), group, False)
cv2.rectangle(image, (x, y), (x+w, y+h), (0, 0, 255), 3)
processedFileName = "faces-" + fileName
processedFilePath = "/tmp/" + processedFileName
uploadImage(image, processedFilePath, bucket, os.path.join(dirPathOut, processedFileName), group, True)
except Exception as e:
logger.exception(e)
raise e
def uploadFileToGoogleDrive(fileName, localFilePath):
try:
ext = os.path.splitext(localFilePath.lower())[1][1:]
if ext == "jpg":
ext = "jpeg"
mimeType = "image/" + ext
service = getGoogleService()
file_metadata = {"name": fileName, "mimeType": mimeType, "parents": ["*********************************"] }
media = MediaFileUpload(localFilePath, mimetype=mimeType, resumable=True)
file = service.files().create(body=file_metadata, media_body=media, fields='id').execute()
except Exception as e:
logger.exception(e)
def getGoogleService():
scope = ['https://www.googleapis.com/auth/drive.file']
keyFile = 'service-account-key.json'
credentials = ServiceAccountCredentials.from_json_keyfile_name(keyFile, scopes=scope)
return build("drive", "v3", credentials=credentials, cache_discovery=False)
動作確認
Webアプリから画像をアップロードすると、LambdaのPythonが動いて画像処理をしますが、そのついでにGoogle Driveにもオリジナル画像と顔にROIをマーキングした画像の2つをアップロードしています。
S3と違ってそのまま絵として見れて嬉しいですね。
あとがき
Googleのクラウドサービスもそのうち一通り触ってみたいですね。Google好きですので。2020年の目標の1つです。
ちなみにワタシの身の回りのGoogle製品は、Pixel 3a, Chromebook, Google Home Mini, Chromecast, Google Wifiなど、それなりに多いです。
サービスは、Gmail, Calendar, Drive, Photoなどはもちろん、Google FitをMi Bandとともに日常的に利用してますし、Google Mapはローカルガイド気取りで投稿して楽しんでいます。
Google Oneも2TBにアップグレード(年額¥13,000)しています。
OK Google, だいすきだよ!