Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
16
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Firebase Cloud FunctionsとGoogle Cloud Vision APIで画像に自動タグ付け

Firebase Cloud Functionsでサムネイル作成に続き、Google Cloud Vision APIを使って、Firebase Cloud Storageにアップロードした画像への自動タグ付けを行ってみたいと思います。

Google Cloud Vision APIのコール部分は、@google-cloud/visionを使いました。
FirebaseとGCPはアカウント含めいい感じに統合されている(というよりバックエンドは一緒)ので、Vision APIとの連携時にもプロジェクトIDやAPIキーの指定などを行わずに簡単に使用することができました。

事前準備

今回のサンプルは、Firebase CLI(v3.16.0)で作成し、TypeScriptで実装していきます。

Cloud Storageにアップロードした画像の公開URL(署名付きURL)の取得を行う際に、サービスアカウントキーと呼ばれる秘密鍵が必要になります。
次の手順で取得したサービスアカウントキーをfunctionsディレクトリ以下にコピーしておきます。
今回は、service-account-credentials.jsonというファイル名にリネームしています。

サービスアカウントキーの取得

  1. Firebaseのコンソール右上にある歯車から「プロジェクトの設定」をクリック
  2. プロジェクト設定ページにあるサービスアカウントタブを開き、「新しい秘密鍵の生成」をクリック

サンプルコード

「バケットへのファイル追加をフック→サムネイル生成」までの流れは、Firebase Cloud Functionsでサムネイル作成と同様になります。

index.ts
import * as functions from 'firebase-functions';
import * as admin from 'firebase-admin';
admin.initializeApp(functions.config().firebase);
const gcs = require('@google-cloud/storage')({
  keyFilename: 'service-account-credentials.json',
});
import * as vision from '@google-cloud/vision';
import * as sharp from 'sharp';
import * as path from 'path';
import * as os from 'os';

const THUMB_MAX_WIDTH = 200;
const THUMB_PREFIX = 'thumb_';

// sharpで最大幅のみを指定してリサイズ
const resizeImage = (tmpFilePath: string, destFilePath: string, width: number): Promise<any> => {
  return new Promise((resolve, reject) => {
    sharp(tmpFilePath)
      .resize(width, null)
      .toFile(destFilePath, (err, info) => {
        if (!err) {
          resolve();
        } else {
          reject(err);
        }
      });
  });
};

// パブリック参照できるURLを取得
const getDownloadURL = (bucket: any, filePath: string): Promise<string> => {
  const config = {
    action: 'read',
    expires: '03-01-2500'
  };
  const file = bucket.file(filePath);
  return file.getSignedUrl(config);
}


export const detectImage = functions.storage.object().onChange(event => {
  // eventからファイル情報取得
  const filePath = event.data.name;
  const contentType = event.data.contentType;
  const fileDir = path.dirname(filePath);
  const fileName = path.basename(filePath);
  const thumbFilePath = path.normalize(path.join(fileDir, `${THUMB_PREFIX}${fileName}`));
  const tempLocalFile = path.join(os.tmpdir(), filePath);
  const tempLocalThumbFile = path.join(os.tmpdir(), thumbFilePath);

  // 画像以外だったら何もしない
  if (!contentType.startsWith('image/')) {
    console.log('This is not an image.');
    return null;
  }

  // 既にサムネイル画像だったら何もしない
  if (fileName.startsWith(THUMB_PREFIX)) {
    console.log('Already a Thumbnail.');
    return null;
  }

  // 削除時だったら何もしない
  if (event.data.resourceState === 'not_exists') {
    console.log('This is a deletion event.');
    return null;
  }

  const bucket = gcs.bucket(event.data.bucket);
  const file = bucket.file(filePath);
  const metadata = { contentType: contentType };

  const client = new vision.ImageAnnotatorClient();

  (async () => {
    // バケットにアップロードされたファイルを仮想マシンのテンプディレクトリにダウンロード
    await file.download({ destination: tempLocalFile });

    // リサイズ
    await resizeImage(tempLocalFile, tempLocalThumbFile, THUMB_MAX_WIDTH);

    // リサイズされたサムネイルをバケットにアップロード
    await bucket.upload(tempLocalThumbFile, { destination: thumbFilePath, metadata: metadata });

    // Vision APIでラベル(タグ)検出
    const detectionRes = await client.labelDetection(tempLocalFile);
    const labels = await detectionRes[0].labelAnnotations.map(label => label.description) || [];

    // パブリック参照できるURLを取得
    const downloadUrl =  await getDownloadURL(bucket, filePath); 
    const thumbnailUrl = await getDownloadURL(bucket, thumbFilePath); 

    // Realtime Databaseに画像情報を保存
    const imageObj = {
      path: filePath,
      thumbnail: thumbFilePath,
      download_url: downloadUrl[0],
      thumbnail_url: thumbnailUrl[0],
      tags: labels,
      timestamp: new Date().toISOString()
    };
    await admin.database().ref('/images').push(imageObj);    

  })()
  .then(() => console.log('Generate Thumbnail and Detect Image Success!'))
  .catch(err => console.log(err));
});
  • getDownloadURL()

    • 画像の公開URL(署名付きURL)の取得を行っています。getSignedUrl()に許可するアクションと署名の有効期限を設定します。
  • client.labelDetection()

    • @google-cloud/visionで用意されているAPIを使って、Cloud Vision APIの呼び出しを行っています。
  • admin.database()...

    • 最後にRealtimeDatabaseに画像の各種詳細情報を保存します。

依存関係

package.jsonは次の通りです。

package.json
{
  "name": "functions",
  "scripts": {
    "build": "./node_modules/.bin/tslint -p tslint.json && ./node_modules/.bin/tsc",
    "serve": "npm run build && firebase serve --only functions",
    "shell": "npm run build && firebase experimental:functions:shell",
    "start": "npm run shell",
    "deploy": "firebase deploy --only functions",
    "logs": "firebase functions:log"
  },
  "main": "lib/index.js",
  "dependencies": {
    "@google-cloud/storage": "^1.5.1",
    "@google-cloud/vision": "^0.14.0",
    "firebase-admin": "~5.4.2",
    "firebase-functions": "^0.7.1",
    "sharp": "^0.18.4"
  },
  "devDependencies": {
    "@types/sharp": "^0.17.6",
    "tslint": "^5.8.0",
    "typescript": "^2.5.3"
  },
  "private": true
}

動作確認

まずは、Firebase CLIでFunctionをデプロイ。

$ firebase deploy --only functions

コンソールからCloud Storageに直接画像をアップロードすると、しばらくしてthumb_ファイルが作成作成されます。

スクリーンショット 2017-12-30 0.10.58.png

また、それと同時にDatabaseには今回保存した画像のURLとタグ情報が保存されています。

スクリーンショット 2017-12-30 0.14.11(2).png

Google Cloud Vision APIからはラベル情報が英語で返却されますが、日本語のタグ情報が欲しい場合は、更にGoogle Cloud Translation APIで翻訳する方法もあるかと思います。

注意

GCPのコンソールからGoogle Cloud Vision APIの利用を有効化する際には、無料枠での利用であってもクレジットカードの登録を行い、課金を有効にする必要があります。
また、上記操作を行うと、Firebaseの課金体系も自動的に切り替わるようです。

画像の通り、無料のSparkプランから従量制のBlazeプランへの移行されていました。

スクリーンショット 2017-12-29 14.52.22.png

この後、Sparkプランにダウングレードした所、APIの呼び出しに失敗したので、どうやら無料枠での利用はできなそうです...
Cloud Functionsから他のGoogleサービスのアウトバウンド通信は制限されていないはずなので、FirebaseとGCPの課金状態が連携しているということなのでしょう。(あとでちゃんと調べたい)

プロジェクトをGoogle Cloud Platformで拡張できます。

とある通り、Google Cloud Platformで提供されているAPIを利用する場合は、Blazeプラン以上が必要になるようです。

所感

既存機能の組み合わせですが、数十行のコードとデプロイのコマンドだけで、ここまでできしまうのはいいですね。
また、GCPのリソース内で完結すると、APIキーの管理などを気にせず使えるのも楽です。
次は、Firebase Cloud Functionsと全文検索と組み合わせあたりを試してみたいです。

参考

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
16
Help us understand the problem. What are the problem?