Help us understand the problem. What is going on with this article?

Lambda(Python)でGoogle Driveへファイルアップロード

Lambda(Python)でGoogle Driveへファイルアップロード

この記事はサーバーレスWebアプリ Mosaicを開発して得た知見を振り返り定着させるためのハンズオン記事の1つです。

以下を見てからこの記事をみるといい感じです。
* Lambda(Python) + Rekognition で顔検出

イントロダクション

S3ってファイルを保存するだけならいいのですが、そのファイルを人が参照するのには向いてないですよね。画像にしてもWebブラウザ上でそのまま見ることができず、一度ダウンロードしてから見る必要がある。ちょっと手間なんですよね。
その点、Google DriveはPCにしろスマホにしろ、ブラウザやアプリでいい感じに画像を参照することができますのでいい感じです。

コンテンツ

Google Developers Console プロジェクトの作成

GoogleのDevelopers Consoleのにアクセスします。
https://console.developers.google.com

プロジェクトが無い場合は作成してください。
Screenshot 2020-01-06 at 22.43.06.png

Google Drive APIの有効化

コンソールの「APIとサービス」画面上部にある「+APIとサービスを有効化」を押下し、Google Driveを有効化します。
Screenshot 2020-01-06 at 22.45.12.png
Screenshot 2020-01-06 at 22.47.53.png
Screenshot 2020-01-06 at 22.48.10.png

認証情報の作成

Google Drive APIの有効化が完了したら、コンソールの「APIとサービス」画面左ペインの「認証情報」から、認証情報を作成します。
認証情報のページ上部にある「+認証情報を作成」を押下すると表示されるドロップダウンメニューより、「サービスアカウント」を選択します。
Screenshot 2020-01-06 at 22.56.00.png

サービスアカウント名を入力して作成してください。
なお、サービスアカウント名の下にあるサービスアカウントIDは大切な情報なので漏洩しないよう管理してください。
ink (1).png

サービスアカウントの確認

サービスアカウントの作成が完了したら、コンソールの「IAM と 管理」画面左ペイン「サービスアカウント」から、作成したサービスアカウントの詳細を参照します。
ink (2).png

サービスアカウント秘密鍵の作成

サービスアカウントの詳細から、「編集」そして「+キーを作成」ボタンを押下し、秘密鍵をJSON形式で作成、JSONファイルをダウンロードします。
ink (4).png

Google Driveにフォルダを作成し、サービスアカウントと共有する

Google Driveにファイルアップロード用のフォルダを作成します。ここではsample-driveというフォルダ名としました。そしてそのフォルダを先ほど作成したサービスアカウント(メールアドレス)と共有します。
Screenshot 2020-01-06 at 23.11.04.png
ink (3).png

このフォルダのURLの後ろの文字列は、後でプログラムから利用します。(下のキャプチャの隠してる部分)
ink (5).png

さてこれで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 .

で、必要なものをインポートします。

lambda_function.py
  :
from googleapiclient.discovery import build 
from googleapiclient.http import MediaFileUpload 
from oauth2client.service_account import ServiceAccountCredentials 
  :

そして、以下のような実装でファイルをアップロードします。

lambda_function.py
  :
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の後ろ側の文字列に置き換えてください。

最後に念の為プログラム全体も載せておきますね。

lambda_function.py
# 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と違ってそのまま絵として見れて嬉しいですね。
Screenshot 2020-01-08 at 22.51.30.png

あとがき

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, だいすきだよ!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away