Python
OpenCV
scikit-learn
異常検知
colaboratory
BrainPadDay 11

機械学習を使ってゲーム動画からプレイシーンだけを抽出出来るか検証する。ちなみにだいたい出来た。

この記事は BrainPad AdventCalendar 2018 11日目の記事です。

時間がない(興味がない)人のための3行まとめ

  1. Youtubeにとりためているサッカーゲーム動画のうちプレイ部分だけを取り出して分析したい欲望に駆られる
  2. Colaboratory上で動画を画像に分解してOne-classSVMでプレイ部分以外を異常とみなして不要な画像を切り取る
  3. 意外とうまく識別出来ており、シンプルな機械学習アルゴリズムでもプレイ部分の識別程度のタスクなら簡単に出来ることがわかった

導入(どうでもいいことしか書いてないから読む必要なし)

サッカーゲームあるじゃないですか。:soccer:

FIFAってあるじゃないですか、協会じゃなくてゲームのほう。:video_game:

あれをずっとやってるんですよね。

しかもPS4にはプレイ動画のシェア機能があるので、3年くらい前からひたすらYoutubeに動画をあげ続けてるんですよ。

いつか動画を分析する日が来るかなと思って。。。

で、今回AdventCalendarを担当することになったのにかこつけて分析してしまおうかなと。

色々分析したいことはあるんですけど、動画は毎回15分くらいあってどうでもいいシーンから重要なシーンまで1試合分まるごと入っているので戦術の良し悪しなど高度な分析はすぐに出来そうにありません。

試合において重要なシーンとそうでないシーンがある程度分離できてないと細かい分析をしようにもノイズが大きすぎて手がつけられないわけです。

重要なシーンを示すハイライトはゲームのシステムとして提供されているんですけど

  • ハイライトのキャメラ設定が実際のプレイで設定しているキャメラ設定と異なる
  • ゴールシーンやチャンスシーンを自動で抜粋するので、アシストパスくらいから動画がclipされている。よってアシスト前の起点となるプレイは確認出来ない
  • 中盤の重要なプレイや機転の効いたディフェンスはハイライトしてくれない

などなど、微妙にプレイを分析するには痒いところに手が届かないわけです。(別に分析目的でハイライト機能があるわけではないのでFIFAは悪くない)

ということで

ゲームが用意しているハイライトよりは分析に向いているハイライト動画を作るために、プレイしているシーンとミニハイライトシーンや中断シーンを分離することから開始したいと思います。

*以下、敬体で書くのがメンドイので常体で書く

問題設定

FIFAのプレイ動画を大きく分けると大体3つのシーンがあると言えそう。
1. インプレイ:選手を実際に操作している全体の95%以上を占めるシーン
2. アウトオブプレイ:ゴールセレブレーションや選手交代などの1-5%を占めるシーン
3. 設定画面:選手交代を指示したり試合結果を確認するシーン(動画によっては存在しない)

今回はインプレイを正解の画像とした時にアウトオブプレイや設定画面の画像が出てきたら異常な状態と見做してカットできるかを検証してみたい。

ゲームをやったことがない人にはそれぞれの状態がイメージしづらいので画像で例示しておく。

1.インプレイ

普通に自分でコントローラを使って選手を操作できる間はインプレイと見做すことが出来そう。
画像で言うとこんな感じの場面
*赤と白のユニフォーム(Arsenalのホームユニフォーム)が私のチーム

fifa_w31_002070.png

赤い矢印が付いているのが今自分が操作している選手

2.アウトオブプレイ

得点が入った後のセレブレーションシーンやボールが外に出た時に選手にキャメラが寄るシーンなんかがアウトオブプレイと見做せるはず。
ちょっとボールが外に出たくらいでは画面は切り替わらないのでスローイング前の場面もアウトオブプレイに含めたいけど難しいかも

画像で言うとこんな感じの場面
fifa_w31_003530.png
得点を決められたシーンなので、見るたび腹たつよね。ナニ喜んどんねん!!:angry:
こういうシーンはバシバシカットしていかないとね!!

fifa_w31_003870.png
クルトワめっちゃ悔しがってるやん。可哀想だからこういうシーンもカットや!!

3.設定画面

プレイ動画とは直接関係ないけどハーフタイムの画面や試合終了時の設定画面も動画に入り込んでいることがあるので、これも一応切り分けたい。

実際にYoutubeに動画をあげると、この設定画面がサムネイルに選ばれていることが多いのでおそらくYoutubeは動画の全シーンのうち最も色合いや色の構成が他と異なる部分をサムネイルとして自動で選んでいると思われる。

↓こういう画像が出たら設定画面
fifa_w31_008560.png

動画をFrame単位の画像に分解する

では、具体的にプレイシーンを特定するために動画を処理していく

動画のままでは扱いづらいので動画をOpenCVを使ってFrame単位に分解していく

なお、完全に趣味の分析なのでお金は掛けたくない。そこで、ColaboratoryとGoogleDriveを使ってタダでデータの分析することにした。

ちなみにColaboratoryとGoogleDrive間のデータのやりとりは以前書いた記事にまとめたので、詳しく知りたければそちらをどうぞ。

Colaboratory上に動画をDownloadしてくる
# Install the PyDrive wrapper & import libraries.
# This only needs to be done once per notebook.
!pip install -U -q PyDrive
from pydrive.auth import GoogleAuth
from pydrive.drive import GoogleDrive
from google.colab import auth
from oauth2client.client import GoogleCredentials

# Authenticate and create the PyDrive client.
# This only needs to be done once per notebook.
auth.authenticate_user()
gauth = GoogleAuth()
gauth.credentials = GoogleCredentials.get_application_default()
drive = GoogleDrive(gauth)

# 作業用のディレクトリをColaboratory上に作成
tmp_directory = 'work'
if not os.path.exists(tmp_directory):
    os.mkdir(tmp_directory)

# 事前にGoogleDriveに格納しておいたプレイ動画のGoogleDriveでのIDを指定しColaboratoryにDownloadしてくる
play_movie_id = 'XXXXXXXX'
file_name = 'play_movie_fifa.mp4'
cf = drive.CreateFile({'id': play_movie_id})
cf.GetContentFile(file_name)
# rootにデータがあるのは気持ち悪いのでworkに移動させる
shutil.move(file_name, tmp_directory)        

動画を持ってきたら、次は画像に分解していく。
[参考] Pythonでの動画の取り扱い(OpenCVで再生とキャプチャ生成)

動画を画像に分解
# アウトプットとなる画像のプレフィックス
output_prefix_name = 'fifa_cap_'

# 画像の保存先のディレクトリを作成
dir_cap = 'fifa_cap'
if not os.path.exists(dir_cap):
    os.mkdir(dir_cap)

# 動画のパスを指定
file_path = os.sep.join(['work','play_movie_fifa.mp4'])

# OpenCVでビデオキャプチャとして動画を取り込む
video = cv2.VideoCapture(file_path)
#フレーム数を取得
frame_count = int(video.get(7))
# >24076

for i in range(frame_count):
    _, frame = video.read()
    # 実際のフレーム全てを画像にすると莫大な数になるので1/2に間引く
    if i %  ==0:
        file_name = output_prefix_name + '{:0=6}'.format(i) + '.png'
        filepath = os.sep.join([dir_name , filename ])
        cv2.imwrite(filepath, frame)

これでworkディレクトリにFrame単位に分解された(1/2に間引いているが)画像が格納された

1/2にFrameを間引いた後の検証用動画

ちなみに、約2万あるFrameを半分間引いて1万Frameにした後、動画を再作成したものが以下。半分間引いているので、2倍速みたいに見える。

↓youtubeのリンクに飛びます(ちょっと調べた感じだとQiitaにインライン機能はないらしい。あればインライン機能に差し替えます)
*音なし(広告なし)
プレイ動画(2倍速)

この動画を検証の対象として話を進めていく

正解画像を学習させる

最近の潮流だと、画像認識の問題といえばCNNやVAEのような深層学習を用いた手法で物体検出や異常検出をするのが主流になりつつある。。
せっかくColaboratory使っているのでGPUをガシガシ使って画像認識に取り組もうと思ったけれど、ふと社内でまことしやかに囁かれる噂が頭をよぎった。。。

「その問題、別に深層学習でやらなくてもよくない?」

「一般的な機械学習の方法でやったら、ほぼ同じ精度で1/10の時間で処理できた」

などなど

確かに。。。一理ある。

ということで

そのうち、深層学習を用いて同じ問題に取り組むとして、今回は一般的な機械学習の方法で3つのシーンを識別できるかをまず確認して、今後この結果をベンチマークに深層学習の効果を検証していくことにする。

何より私としては、インプレイの画像が識別できるのであれば別に手法はなんでもいい訳だし

1.画像をベクトル化する

%matplotlib inline

from PIL import Image
from sklearn.decomposition import PCA
from sklearn.svm import OneClassSVM

import numpy as np
import pandas as pd 

# 画像を格納したディレクトリ内の画像の一覧を取得
list_img = os.listdir(dir_name)
list_img_sorted = sorted(list_img)

# 1画像1レコードになるように640×360のカラー画像を1/8にリサイズして10800次元のベクトルとする
# 80 * 45* 3 の次元数
flat_size = 10800
array_mv = []
for img in list_img_sorted:
    file_path = '{}/{}'.format(dir_name,img)
    im = Image.open(file_path)
    # (640, 360)を1/8のサイズにリサイズ
    img_resize = im.resize((80, 45))
    array_resize  = np.asarray(img_resize) / 255.
    array_wide = array_resize.reshape(1, flat_size)
    array_mv.extend(array_wide)
array_mv = np.array(array_mv)

これで1画像1レコードの行列が出来上がった。

2.One-ClassSVMで異常度を算出

去年のAdventCalndarでも同じようなことやったが全体のうち何%かの画像を異常なデータとみなして検出する。

インプレイの画像は全体の95%近くを占めるため、アウトオブプレイなど画面を占める色構成が通常と大きく異なる画像は異常と見なされる可能性が高い。

なお、10800次元もあると過学習しがちなので、PCAで50次元に圧縮してから学習する
この次元数を下げすぎると精度が下がるので、時間があればチューニングしたいところ

# 圧縮する次元数を指定
n_comp = 50
pca = PCA(n_components=n_comp)

# 50次元に圧縮
pca_res = pca.fit(array_mv).transform(array_mv)
# 画像の名称をIndexに付与
df_pca = pd.DataFrame(pca_res, index=list_img_sorted)

# 5%異常値が含まれると設定する
ocsvm = OneClassSVM(nu=0.05)
# 異常検出モデリング
ocsvm.fit(X=df_pca)
# 学習したデータに対してスコアを付与
df_result = pd.DataFrame(ocsvm.decision_function(df_pca).ravel(),index=list_img_sorted,columns=['score'])

さて、一旦学習した動画に対してそのまま異常度を付与したけれど、どんな画像が異常だと見なされているだろうか?

3.異常とみなした画像を見てみる

試しに異常度が最も大きい画像を特定して図示してみる

[異常度最大の画像]
image.png
おぉーー!!ちゃんとアウトオブプレイが抽出されてる!!

他にも↓のように観客席の画像なんかも異常度の高い画像として識別出来ている
image.png
やったぜ!!:thumbsup_tone2::thumbsup_tone2::thumbsup_tone2:

異常度の高い画像をトリムしてインプレイ動画を作成する

なんとなくいい感じにインプレイじゃない部分を取り出すことに成功しているっぽいので、実際に異常と判定された画像だけ集めた「ハイライトシーン」と正常と見なされた画像だけを集めた「インプレイシーン」の動画を作成してみた

機械学習で作成したハイライトシーン

↓youtubeのリンクに飛びます
*音無し(もちろん広告もなし)
機械学習で異常とみなした動画

サムネイルの時点で明らかにそれっぽい画像が抽出されているところからしてもかなりうまくインプレイ以外の画像を抽出出来ていることがわかる

詳細シーン解説

開始 3-20秒:モドリッチのゴールシーン(まさしくこういうシーンを切り取りたかった!!)
image.png

開始 20-21秒:モドリッチのゴールスコアがピッチに表示されたシーン(結構微妙な画像だけど識別している。すげー!!)
image.png

開始 22-23秒:コーナーキックシーン&クルトワのワンショットシーン(コーナーも識別できるんかい)
image.png
image.png

開始 25-28秒:前半終了時の設定画面(やっぱり設定画面もちゃんと異常判定出来てるのねん。素晴らしいのねん。
image.png

開始30−33秒:敵チームがメッシを投入(ペレもロナウドもいて、さらにメッシまで投入してくるとは鬼畜の極み。でも勝ってるからえぇんや)
image.png

開始 35-50秒:長い長いガメイロのセレブレーションシーン(自分のゴールが決まった時は○ボタンを連打しないのでハイライトが長くなる傾向がある)
image.png

残り10秒:コーナーのシーンや交代のシーンが高速で詰め込まれている(コーナーキックはインプレイと言えなくもないが一箇所に赤いユニフォームと青いユニフォームが集まるのは結構異常な状態と言えるので抽出されてもおかしくはない)
image.png

機械学習で作成したインプレイシーン

次にインプレイの動画(ちょっと長い)
*音無し(もちろん広告もなし)
機械学習で異常とみなした動画

インプレイ動画は分かりにくいけどボールが外に出たタイミングやゴールセレブレーションが始まりそうになると綺麗にカットしている

ただ、1:52-59秒あたりにゴールシーンが微妙に含まれているなど完璧とまでは言えない模様。
異常度の閾値を変えればさらによくなる可能性はある。

ま、これだけ綺麗に抽出してくれたら十分だけど。

まとめ

  • サッカーゲームのように全体の色味が大きく変動しないような動画は、ハイライトシーンやゴールシーンのRGB値が大きく変化するのでそれらを検出するのは従来の機械学習のアルゴリズムでも容易い
  • 一方、画像(動画)を取り扱う場合には縦×横×RGBの次元分行列が発生するので、貧弱なメモリではそもそもデータを取り扱うことが出来なくなるので、どこかで従来型のやり方に限界は来そう
  • 1万画像をPCAかけてOne-ClassSVMして異常な画像をtrimした動画を作成するまでにかかった実行時間は5分程度なので、気軽に取り組める問題なのは間違いなさそう。(これだけ学習をすぐに終わらせられるなら動画ごとに学習モデルを作成しても問題なさそうなので汎化性能については二の次で良いかも)

おしまい