VMAFスコアを可視化する

こんにちは。バックエンドエンジニアの阿部です。Goが好きです。

今回のブログは、動画品質の指標の1つとなるVMAFスコアというものを可視化した話です。

※VMAFスコアの取得はffmpegコマンドを利用しますが、今回はffmepgの細かい内容については説明はしません。

VMAFとは

動画配信では、元の動画素材をそのまま配信しようとするとその容量が大きすぎるため圧縮を施したり、最適な動画形式への変更を行うなどのエンコード作業が必要不可欠となりますが、その画質やビットレートなどをどこに着地させて品質を担保するかは、長年の経験や勘、主観など人的要素での決定が大きかったのではないでしょうか。

VMAF(Video Multi-Method Assessment Fusion)は簡単に言うと、そのような定性的な判断から定量的な判断が可能となるスコアを算出してくれるライブラリです。元素材とエンコード後の動画データを1フレームずつ比較し、その劣化具合を数値として出力します。これにより、VMAFスコアの低い作品をシステマチックに抽出することが可能となります。さらに、その分析を行うことでエンコード作業の最適化を行え、動画品質の向上につなげるようなPDCAサイクルの促進が期待できます。

そもそもはNETFLIX社が自社用で開発した動画品質評価ライブラリですが、それをOSSとして提供してくれています。

github.com

アーキテクチャ

今回構築した可視化システムのアーキテクチャです。弊社ではシステムのプラットフォームには主にGoogle Cloudを利用しており、以下のようにシンプルな構成で構築できました。

アーキテクチャ

データのパイプラインは以下の通りです。

  1. エンコードサーバーでエンコード完了後、ffmpegコマンドを実行してVMAFスコアを取得する。取得したスコアはファイル出力し、圧縮してCloud Storageにアップロードする。
  2. Cloud Functionsの関数は、Cloud Storageへのファイナライズでトリガーし、1.でアップロードされたファイルの内容をBigQueryにロードする。
  3. BigQueryに蓄積したデータはBIツールで可視化する。

Google CloudではETL処理にはDataflowの利用も考えられますが、弊社では使い慣れたGoで実装した関数をCloud FunctionsにデプロイしてETLを構築することが多いです。今回は特に必要ありませんでしたがデータの加工や制御などを柔軟に行えることや、Goに慣れているエンジニアが多いことが理由となります。

以下はCloud Functionsにデプロイしている関数です(多少マスクしています)。プラス独自のlogger.goを実装しているのと、本来、getBigQuerySchema関数にはBigQueryのテーブルのスキーマ情報を記載するのでもう少しボリュームは大きくはなりますが、ロジックの実装量自体はそれほど大きくありません。

package function

import (
    "context"
    "fmt"
    "log"
    "os"

    "masked-path/logger" # パスはマスク
    "cloud.google.com/go/bigquery"
    "cloud.google.com/go/logging"

    "github.com/GoogleCloudPlatform/functions-framework-go/functions"
    "github.com/cloudevents/sdk-go/v2/event"
)

type GCS struct {
    Bucket string `json:"bucket"`
    Name   string `json:"name"`
}

func init() {
    // Register a CloudEvent function with the Functions Framework
    functions.CloudEvent("importVMAF", importVMAF)
}

// importVMAF はCloudEvent オブジェクトの受理と処理を行う
func importVMAF(ctx context.Context, e event.Event) error {
    // ロガー設定
    if err := logger.NewLogger(ctx, os.Getenv("GCP_PROJECT_ID"), os.Getenv("LOG_ID")); err != nil {
        log.Fatal(err)
    }
    logger.Log("========== Start VMAF Importer ==========", logging.Info)

    // BigQueryのクライアント設定
    client, err := bigquery.NewClient(ctx, os.Getenv("GCP_PROJECT_ID"))
    if err != nil {
        logger.Log(fmt.Sprintf("bigquery.NewClient: %v", err), logging.Error)
        return err
    }
    defer client.Close()

    // GCS構造体にCloudEventデータをunmarshal
    var gcs GCS
    if err := e.DataAs(&gcs); err != nil {
        logger.Log(fmt.Sprintf("e.DataAs: %v", err), logging.Error)
        return err
    }
    gsutilURI := fmt.Sprintf("gs://%s/%s", gcs.Bucket, gcs.Name)

    // ローダーの作成
    gcsReference := bigquery.NewGCSReference(gsutilURI)
    gcsReference.SourceFormat = bigquery.JSON
    gcsReference.Compression = bigquery.Gzip
    gcsReference.Schema = getBigQuerySchema()
    loader := client.Dataset(os.Getenv("DATASET_ID")).Table(os.Getenv("BIGQUERY_TABLE_NAME")).LoaderFrom(gcsReference)
    // 追加モードでロードするように指定
    loader.WriteDisposition = bigquery.WriteAppend

    // ジョブ実行
    job, err := loader.Run(ctx)
    if err != nil {
        logger.Log(fmt.Sprintf("loader.Run: %v", err), logging.Error)
        return err
    }

    // ジョブ待ち
    status, err := job.Wait(ctx)
    if err != nil {
        logger.Log(fmt.Sprintf("job.Wait: %v", err), logging.Error)
        return err
    }
    // ジョブのエラーを確認
    if err := status.Err(); err != nil {
        logger.Log(fmt.Sprintf("status.Err: %v", err), logging.Error)
        return err
    }
    logger.Log("Data loaded successfully", logging.Info)

    logger.Log("========== End VMAF Importer ==========", logging.Info)
    return nil
}

// getBigQuerySchema はBigQueryのスキーマ情報を返却する
func getBigQuerySchema() []*bigquery.FieldSchema {
    return bigquery.Schema{
        {
            Name: "Column1",
            Type: bigquery.StringFieldType,
        },
        {
            Name: "Column2",
            Type: bigquery.IntegerFieldType,
        },
    }
}

BIツールの選定と問題点

BigQueryから簡単に設定が出来る点、弊社ではGoogle Workspaceを利用しており以前から社内での利用事例が多い点から、当初はLooker Studioを採用していましたが、Looker Studioではやりたいことが実現できないことが判明しました。その問題点とは、折れ線グラフのX軸の要素数の最大が5000である点です。 VMAFから出力された1フレームずつのスコアを折れ線グラフで表現したかったのですが、例えば29.97fpsの2時間の動画の全フレーム215,784枚の情報を1viewで表示することは不可能となります。

以下はLooker Studioを用いた場合の例となります。全フレーム171,929枚を1度で表示できていません。フレーム番号の範囲を指定して表示させることも可能ですが、分析作業を考えるとあまり現実的な解決方法とは言えません。

Looker Studioでは折れ線グラフで全フレームを表示することは不可能

BIツールの変更

結論から言えば、Grafanaを利用することで上記問題を解決することができました。GrafanaはGCE上に構築しています。

以下はLooker Studioで表示している同データをGrafanaで見た場合の折れ線グラフになりますが、全フレーム171,929枚のVMAFスコアを表示できています。グラフをマウスオーバーすることによりそのフレーム番号とVMAFスコアも表示されるので、分析作業が楽に行えます。

Grafanaの折れ線グラフ

最終的なアーキテクチャはこうなりました。

最終的なアーキテクチャ

最後に

今回はVMAFスコアにフォーカスを当ててGoogle Cloud + Grafanaで画質を可視化した話をしましたが、画質以外にも音量に関するLoudness値などの指標の可視化も同時に行っています。弊社では日々数百本の動画をエンコードしていますが、その品質を定量的にまたシステマチックにチェックできる仕組みを構築できたことは、今後の動画品質向上に大いに役立つものになると思っています。

これと同時に分析作業も進めており、VMAFスコアが下がるフレームの傾向や要因の分析、解決策の策定とその実施なども行っています。

皆様に高品質の動画を楽しんでいただけるよう、これからも日々動画品質の向上に努めていきます。