キャプチャした試合から撃墜の瞬間だけを切り取ってクリップを自動で作成する #スマブラAdventCalendar2019

この記事は、#スマブラAdventCalendar2019の19日目の記事です。

まずはじめに

この記事を開いてくださり、ありがとうございます。好きなオルタはBLACK ROSES赤Bの高速オルタ、豚平です。

今回の記事の執筆にあたり、こんなものを作りました。


自動抽出された撃墜集

やたらスピード感のある撃墜集をご覧になったと思います。

元動画はこちら。


フレンド戦_191006_02

スマブラAdventCalendarの発起人であり友人の寝椅子くんたちとの試合をキャプチャしたものです。

今回、対戦中の動画からバーストシーンを自動で切り取るプログラムを作成しました。

25分程度の動画から、バーストシーンを特定して切り出し、(スペックによりますが)合成までおよそ3分で出来上がります。 バーストの何秒前から切り取るか、何秒後まで切り取るかも設定可能です。まるで衝撃があったときに保存するドライブレコーダー

動機

  • ハイライト動画をできるだけ楽に作りたい

  • 来るスマブラビッグデータ時代(?)に備え、データ収集のスキームを作りたい

  • コードベースでの、プリミティブな動画編集方法の体得 本当はffmpegAPIを叩いて編集をしてみたかったのですが、時間が足りず、今回はffmpegの実行ファイルに頼りました。 ffmpegのビルドに悪戦苦闘した部分がこの記事の大部分を占める予定でしたが、これはまた別の記事にまとめます。(ビルド自体は成功しました)

以下、技術的な説明になるので、とりあえず使ってみたい方は使ってみるへどうぞ。

今回のソースコードこちら。そのまま実行できるように、ビルド済の実行ファイルも残してあります。コンパイル用のコマンドも.batで残してあります。実装の詳細については、こっちを見たほうがわかりやすいかと。

アプローチ

このプログラムは、3つのモジュールで構成されています。

  1. ソースの入出力 後続の処理がしやすくなるように、入出力を制御します。 今回は、「動画ファイル」をソースとします。

  2. 編集点検索 入力されたソースから、編集すべき箇所がどこなのかを検索します。 今回は、「バーストの瞬間」を検索するモジュールを作成します。

  3. 具体的な編集 得られた編集点から、ソースに対してどのようなアクションを行うのか定義します。今回は、「編集点の前後数秒を切り取り、つなぎ合わせる」編集を行います。

動画を扱う準備

FFmpegの実行ファイルの準備

CLIで動画をいじるならこいつ。

ここから(LinkingはstaticでOKです)ダウンロードして、ffmpeg.exeを作業用のディレクトリに置きます。

これで準備OK。

編集用コマンドの発行と実行を行うクラスの作成

入力ファイルのパス(必須)と、切り取り時間のオフセット(オプション)を引数に、FFmpeg.exeに実行してもらうコマンドを準備、実行まで行うEditerクラスを定義します。詳しくはコードを参照してみてください。

void Editer::exec_trimming()
{
    deque<string> command_list;
    deque<string> file_list;
    int index = 1;

    if(edit_points.empty())
    {
        throw string("Error:Edit points is empty");
    }

    for(auto p : edit_points)
    {
        stringstream ss;
        ss << index++;
        string dst = "output/cut" + ss.str() + ".mp4";

        string cmd = "ffmpeg -ss " + get_offset_as_string(p) + " -i " + src + " -y -t " + get_duration_as_string() + dst;
        cout << cmd << endl;
        command_list.push_back(cmd);

        string file;
        file = "file " + dst;
        file_list.push_back(file);
    }

    ofstream fout;
    fout.open("edit_list.txt");
    for(auto f : file_list)
    {
        fout << f << endl;
    }
    fout.close();

    for(auto c : command_list)
    {
        const char* cmd_as_char = c.c_str();
        std::system(cmd_as_char);
    }

    exec_concat();

}

こんな感じで、edit_pointsに含まれる編集点(単位は秒)の数だけ、FFmpegに切り取りをお願いするコマンドを作成していき、まとめて実行します。

最後にexec_concat();で、切り取った動画をつなぎ合わせ、"00_output.mp4"として出力します。

Editerクラスの使い方はこんな感じです。main関数内を抜粋。

...
    try
    {
        Editer e;
        if (argc == 2)
        {
            e = Editer(argv[1]);
        }
        else if(argc == 4)
        {
            e = Editer(argv[1], atof(argv[2]), atof(argv[3]));
        }
        else
        {
            throw string("Usage:client.exe [input video filename] [offset_front(default=5.0) offset_back(default=2.0)]");
        }
        
        
        deque<double> edit_points;
        search_edit_points(e.get_input_filename() ,&edit_points);
        e.set_edit_points(edit_points);
        e.exec_trimming();

        cout << "INFO:Edit Finished" << endl;
    }
...

edit_pointsへの編集点の登録はsearch.cpp内の関数search_edit_points()で行います。詳しくは次の章で。

編集点検索

OpenCVの準備

画像処理するならやっぱりこいつ。

ここからダウンロードして、任意のフォルダに展開します。

必要なのは、includeフォルダ、opencv_world412.lib、opencv_world412.dll、opencv_videoio_ffmpeg412_64.dllです。libファイルはlibフォルダに、.dllファイルは実行ファイルと同じディレクトリに置きます。(.dllへのPATHを通してもOKです)

今回は説明の簡略化のために必要なライブラリやヘッダファイルをすべてリポジトリに入れてます。(ライセンス表記は書いたから大丈夫なはず。。。)

テンプレートマッチング

認識方法についてはあまり複雑なことはやらずに、テンプレートマッチングで行きます。

今作は1on1だと残りストックが画面上に大きく出てくれるので、大変やりやすい。

・・・とりあえずという気持ちと、実装に充てられる時間の少なさでハードコーディングモリモリの大分お行儀の悪いコードになってしまいました。いずれリファクタリングします。

テンプレートとして用意する画像はこれ。

f:id:tonpeidon:20191217213521p:plain

自キャラの紫パルテナです。色も見ます。

このアイコンはそれっぽいワードで検索したらでてきます。リポジトリには検証用の紫パルテナとデフォルトネスの画像しかつけていませんので、あしからず。

検索用のコードがこちらです。

#include "searcher.h"

void search_edit_points(string src, deque<double> *edit_points)
{
    //open
    VideoCapture cap(src);
    if(!cap.isOpened())
    {
        throw "Error:File can not open:" + src;
    }
    double framerate = cap.get(CAP_PROP_FPS);
    cout << "INFO:Frame rate is " << framerate << " fps" << endl;
    Mat frame;
    Mat resized_frame;

    Mat icon = imread("icon.png");
    resize(icon, icon, Size(20, 20));

    int number_of_frame = 0;
    int last_detected = -framerate * 3;

    cout << "INFO:SEARCH START. It may take time." << endl;
    while (cap.read(frame))
    {
        resize(frame, resized_frame, Size(640, 360));
        Mat cropped_frame(resized_frame, Rect(75, 170, 500, 30));
        Mat matching_result;
        matchTemplate(cropped_frame, icon, matching_result, TM_CCOEFF_NORMED);

        Mat match_mask = Mat::zeros(matching_result.size(), CV_8UC1);
        match_mask.setTo(1, matching_result > 0.60);
        int stock = sum(match_mask)[0];
        if (stock > 0 && (number_of_frame - last_detected > framerate * 3))
        {
            double timestamp = number_of_frame / framerate;
            edit_points->push_back(timestamp);
            cout << "INFO:BURST DETECTED AT " << timestamp << " sec." << endl;
            last_detected = number_of_frame;
        }
        ++number_of_frame;
    }
    cout << "INFO:SEARCH COMPLETED" << endl;
}

・・・うーんきれいじゃない。時間ができたらきれいにしたい。。。

撃墜時にストックが表示される中央部分に絞ってマッチングを行っています。

意図しない連続検出を避けるために、一度ストックの変化を検出できたら以降3秒は検出しないようにしています。

edit_pointsに編集点のリストが出来上がるので、Editerのメンバ変数に渡してあげます。

        e.set_edit_points(edit_points);

これで編集点が登録されました。あとは前述のとおりEditerのexec_trimming()を呼べば編集が実行される仕組みです。

使ってみる

例えば、下の元動画にたいして


フレンド戦_191006_03

client.exe test.mp4

と実行すると、以下のような動画が出来上がります。(デフォルトでは、バースト前5秒、バースト後2秒です。)


抽出結果 1

最初にご覧いただいた動画は以下のようにオフセットを明示的に指定して、バースト前後の時間を短くしています。小数点以下の指定にも対応です。

client.exe test_long.mp4 1.0 1.0

オフセットを長くすれば立ち回りを勉強するときに使えるし、短くすれば疾走感のある動画が出来上がります。

注目したいキャラを変更したいときには、自分の使っているキャラのアイコンを用意して、icon.pngを書き換えてください。

対戦動画がフルスクリーンでない、L字の配信枠がついているような動画は現状うまく認識できないので、ご了承ください。

対戦情報を乗っけるオーバーレイぐらいだったらOKだと思います(未確認)。

???

「豚平さん、スマブラSPって何スト制でよく遊ばれてるっけ」

「ええと・・・3ストックだね」

「このプログラムで1試合当たりにとれる最大のバーストシーンの数は?」

「4つだね」

「もひとつ質問いいかな」

「”GAME SETの瞬間”、どこへ行った?」

「・・・君のような勘のいいガキは嫌いだよ」

ッアー

終わりに

作成した検出器と編集内容を複雑なものに入れ替えれば、今後以下のような動画も生成できるでしょう。

  • 100%以上ダメージが溜まっていてガケに捕まったとき、スローにして再生する

  • 動画のクローラと組み合わせて、特定の選手の撃墜集を作る

  • シールドが割れる直前に、"To be continued..."の差し込みを入れる

アドベントカレンダーらしく、ちょっと技術的なことするか!と思い立ったはいいけども、心の怪盗業務が忙しくて結局ギリギリになってしまいました。何とか形になってよかった。。。

今回はコード量も比較的少なめだし、Visual Studio2019をインストールしていればリポジトリをCloneしてそのまま開発ができるようになっています。時間の都合でガバガバになってしまった撃墜認識を(今回実装しきれなかったGAME SETの瞬間の検出も)、ぜひ強い方、改善してください。待ってます🙏

github.com

プログラムに関する改善提案歓迎です。「このキャラ試したけど動かなかった!」みたいな報告をたくさんいただければ、腰を上げて改善に取り組もうと思います。シモンのNBマサカリは投げないでください。

最後までご覧いただき、ありがとうございました。よきスマブラ動画ライフを。