こんにちは。@wezardnet です。今回は、前回の Cloud Spanner の続きを書こうと思ったのですが、あまりにも課金が痛いので Google Cloud Next '17 で発表された Google Cloud Video Intelligence API を試してみようと思います。
#1. Cloud Video Intelligence API について
発表後に即時に触って記事をアップされている先駆者がおりますので、ここでは割愛させていただきます。以下をお読みいただければ概要は掴めるかと思います
API でサポートしている動画形式は、「.MOV」、「.MPEG4」、「.MP4」、「.AVI」とのことです。
#2. API を使うのための準備
この API は現在プライベートベータ版のため、利用するにはサインアップが必要です。フォームに必要事項を書いて送信すると、数日で「Invitation to join cloud-videointel-trusted-testers」という件名のメールが送られてきます。フォームで入力した Google Cloud Platform(GCP) のプロジェクト、あるいはアカウントに対して API が使えるようになります。毎度のことですが Google Developer Console の API Manager で Google Video Intelligence API を有効にします。
#3. 使用した動画ファイルについて
今回使用した動画は、著作権などの問題もあるので、私が勤務する会社の Facebook 新卒採用ページで一般公開されている会社紹介のモノを使うことにしました
https://www.facebook.com/NJC.recruit/videos/1490243897677240/
元ネタとなる動画ファイルは Google Cloud Storage(GCS) にあらかじめ格納しておく必要があります。また、分析結果は JSON ファイルで GCS に格納されます。つまり、動画分析は非同期で行われることになります。
この動画を下記のサービスを利用して mp4 形式にして GCS に格納しました。
#4. コードを書く
私事ですが、サーバーサイドでは Go 言語をメインにしていくため GAE/Go を使うことにします。
動画を API に食わせて分析するまでの流れは、、、
1. GCS に動画ファイルを格納(アップロード)する → 今回は手動でアップロードしておきます
2. Cloud Video Intelligence API をコールする
3. 分析結果の JSON ファイルが GCS に格納される → GCS API を使って取得します
という感じになります。
また Cloud Video Intelligence API を使うための OAuth スコープは以下になります。認証/認可まわりはめんどうなので App Engine のサービスアカウントを使うと楽ちんです
https://www.googleapis.com/auth/cloud-platform
過去に GData API で辛い目にあった事があるので API はできるだけ REST をナマで使うようにしてます。というわけで API をコールする部分のコードは次のようになります。コードは見づらくなるので一部省略してます。
必要なパッケージをインポートします。
import (
"encoding/json"
"io/ioutil"
"net/http"
"github.com/bitly/go-simplejson"
"golang.org/x/oauth2/google"
"google.golang.org/appengine"
"google.golang.org/appengine/log"
"google.golang.org/appengine/urlfetch"
)
次にリクエストボディの JSON を組み立てます。
type m map[string]interface{}
type a []interface{}
values := m {
"inputUri": "gs://{Youre Bucket ID}/{Youre Object ID}",
"features": a {
"FEATURE_UNSPECIFIED",
"LABEL_DETECTION",
"FACE_DETECTION",
"SHOT_CHANGE_DETECTION",
},
"outputUri": "gs://{Youre Bucket ID}/{Youre Object ID}",
"locationId": "asia-east1",
}
payload, _ := json.MarshalIndent(values, "", " ")
フィールドは次の意味を持ちます。
フィールド | 説明 |
---|---|
inputUri | 元ネタの動画ファイルを格納した GCS の URI を指定する(ワイルドカード可) |
features | 動画注釈(アノテーション)を指定する |
outputUri | 分析結果ファイル(JSON)を出力する GCS の URI を指定する |
locationId | 動画分析を行うリージョンを指定する(us-east1、us-west1、europe-west1、asia-east1 から選択) |
動画注釈(アノテーション)は以下を指定することができます(複数可)。
アノテーション | 説明 |
---|---|
FEATURE_UNSPECIFIED | 不特定 |
LABEL_DETECTION | ラベル検出(犬や花などをオブジェクトを検出する) |
FACE_DETECTION | 人の顔を検出する |
SHOT_CHANGE_DETECTION | ショット変化を検出する |
組み立てたリクエストボディを API にセットしてリクエストを出します。
ctx := appengine.NewContext(r)
client := &http.Client{
Transport: &oauth2.Transport{
Source: google.AppEngineTokenSource(ctx, "https://www.googleapis.com/auth/cloud-platform"),
Base: &urlfetch.Transport{Context: ctx},
},
Timeout: 30 * time.Second,
}
req, err := http.NewRequest(
"POST",
"https://videointelligence.googleapis.com/v1beta1/videos:annotate",
bytes.NewReader(payload),
)
if err != nil {
log.Errorf(ctx, "err = %s", err.Error())
}
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
log.Errorf(ctx, "err = %s", err.Error())
}
body, _ := ioutil.ReadAll(resp.Body)
data, err := simplejson.NewJson(body)
if err != nil {
log.Errorf(ctx, "err = %s", err.Error())
}
log.Infof(ctx, "data = %s", data)
動画分析は非同期で行われるので、コールした後のレスポンスは即時に返ってきます。
公式ドキュメントでは「name」、「metadata」、「done」など、いろいろフィールドが返ってくるようなことが書いてあるのですが、私が試した時は次のように「name」のみでした
{
name: asia-east1.70379040653900xxxxx
}
公式ドキュメントによると、レスポンスフィールドは次のように書かれています。
フィールド | 説明 |
---|---|
name | サーバーが割り当てた一意の名前 |
metadata | 操作に関連付けられたサービス固有のメタデータ |
done | 値が false の場合は操作がまだ進行中であることを意味する |
#5. 分析結果
結果はリクエストボディで指定した GCS のバケットに出力されます。今回使用した動画は 3 分程度のモノで結果が出力されるまで 2 分ぐらいでした。遅いか速いかは意見が別れるところだとは思いますが、私は速い方だと思いました。
分析結果が記述された JSON の一部は以下のような感じです。めちゃくちゃ長いので一部省略してます。
{
"annotation_results": [ {
"input_uri": "/{Youre Bucket ID}/{Youre Object ID}",
"label_annotations": [ {
"description": "Animation",
"language_code": "en-us",
"locations": [ {
"segment": {
"start_time_offset": 66690,
"end_time_offset": 2268996
},
"confidence": 0.83687836,
"level": "SHOT_LEVEL"
}, {
"segment": {
"start_time_offset": 2302277,
"end_time_offset": 8108147
},
"confidence": 0.71536106,
"level": "SHOT_LEVEL"
} ]
}, {
"description": "Architecture",
"language_code": "en-us",
"locations": [ {
"segment": {
"start_time_offset": 165965806,
"end_time_offset": 169602779
},
"confidence": 0.92555267,
"level": "SHOT_LEVEL"
} ]
}, {
"description": "Black and white",
(中略)
"face_annotations": [ {
"thumbnail": "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",
"segments": [ {
"start_time_offset": 62929502,
"end_time_offset": 65765683
}, {
"start_time_offset": 65799092,
"end_time_offset": 69369247
}, {
"start_time_offset": 69402656,
"end_time_offset": 71771751
}, {
"start_time_offset": 71805032,
"end_time_offset": 73740322
}, {
"start_time_offset": 73773731,
"end_time_offset": 75542104
}, {
"start_time_offset": 75575513,
"end_time_offset": 77744282
}, {
"start_time_offset": 77777691,
"end_time_offset": 85452033
}, {
"start_time_offset": 169636188,
"end_time_offset": 172005155
}, {
"start_time_offset": 172038564,
"end_time_offset": 176176032
} ],
"locations": [ {
"bounding_box": {
"left": 185,
"right": 246,
"bottom": 30,
"top": 101
},
"time_offset": 62929502
}, {
"bounding_box": {
"left": 180,
"right": 244,
"bottom": 29,
"top": 104
},
"time_offset": 63930492
}, {
"bounding_box": {
(中略)
"shot_annotations": [ {
"start_time_offset": 66690,
"end_time_offset": 2268996
}, {
"start_time_offset": 2302277,
"end_time_offset": 8108147
}, {
"start_time_offset": 8141428,
"end_time_offset": 9809830
(中略)
"safe_search_annotations": [ ]
} ]
}
5.1. ラベルアノテーション(label_annotations)
ラベルアノテーションでは、ビデオセグメント単位に何が映っているかを示すラベル(description)、そのラベルの信頼性、正確性を表す数値(confidence)、開始(start_time_offset)/終了(end_time_offset)のオフセット(単位はマイクロ秒)が出力されます。
5.2. フェイスアノテーション(face_annotations)
フェイスアノテーションは、人の顔のサムネイル(base64でエンコードされた文字列)を表す(thumbnail)、顔の位置を示す(locations)、ビデオ時間のオフセット(timeOffset)が出力されます。オフセットの単位はマイクロ秒になります。
5.3. ショットアノテーション(shot_annotations)
ショットアノテーションは、ショットの切り替えを検出し、開始(start_time_offset)/終了(end_time_offset)のオフセットだけが出力されます。単位はマイクロ秒になります。
#6. 課金について
アルファ期間中 Cloud Video Intelligence API の機能のいずれかを使用して費用が発生することはありません
ということですが GCS の料金は発生します
#7. 制限について
コンテンツの制限とリクエストの制限があります。
##7.1. コンテンツの制限
種類 | 使用制限 |
---|---|
ビデオサイズ | 10GB |
##7.2. リクエストの制限
公式ドキュメントにはアルファ期間中とあるので、今後は変わる可能性があります。
種類 | 使用制限 |
---|---|
リクエストごとの動画 | 10 |
100秒あたりのリクエスト | 10 |
#8. 使ってみた所感
分析結果の JSON ファイルを読み込んで、次のような感じで表示してみました。
まぁ、手抜きと言われれば返す言葉はありませんが、ビデオセグメント単位に分析結果を並べて表示したりする UI を作るのがめんどうだったりで、この動画になにが映っているかをタグクラウドで表現することにし、人物についてはサムネイルを並べて表示させるだけとしました。
今のところ思いつくユースケースは、動画になにが映っているかでファイルを仕分けたり(ファイルにタグ付けやフォルダ振り分けなど)、、、監視カメラの映像をチェックしたりするぐらいでしょうかww
当たり前ですけど、人は検出できますが ”誰” かを特定することはできません。なので、登場人物でのアプローチは TensorFlow と組み合わせて独自の機械学習が必要になりそうです。
また、分析結果は GCS に吐き出されるので Object Change Notification(OCN) で結果ファイルが出力されたことを検知して何かしらの処理を走らせるのが良いでしょう。