お気持ち表明のブログ

ツイッターで書けないことを書こうと思います

ギルティの体力とテンションゲージの時間変化をグラフにするやつを自分もやってみたいと思ったからプログラム初心者が挑戦している話。その1

乱文。めちゃ読みにくい。技術的にも初心者の文。

最初に

こんにちは。椎名りるあです。


皆さん、ギルティやってますか?私は亜熱帯の洗礼を受けていて勝てておりません。21段で懸命に戦っております。うん、適正段位だな!!


以前書いた「家庭用賞金首行くまでのお気持ち文」がいろいろな方に見て頂けたようで嬉しい限りです。ありがとうございます。

 

さて、みなさんは ARC REVOLUTION CUP 2017 in 闘神祭(以下あくれぼ2017) を覚えていらっしゃるでしょうか。この あくれぼ2017 ではこんなシステムが実装されていました。

 

sega.jp

(セガ製品情報サイト)

 

Battle timelineという機能です。この機能は要するに「対戦中の体力ゲージとテンションゲージの変化、バーストのタイミングを表示する機能」「実況解説の方がここがポイント!という場面をすぐにリプレイできる機能」を併せ持った素晴らしいシステムです。

f:id:ri_ru_ria:20201128202037p:plain
こんな感じで体力ゲージやテンションゲージの変化がグラフでわかる(左)リプレイを再生している様子(右)上記セガ製品情報サイトより引用

あくれぼ2017を見ていた私はこの機能を見て、

プレイヤーのゲージ管理を可視化できるなんてなんて面白い機能なんだ!

と興奮していました。最近この機能見ないですね…ぜひ実況解説に復活してほしいです。それとも自分が知らないだけですかね?

 

話は少し変わるのですが、私はこういう何らかの情報を可視化するツールが大好きで、アイトラッカーを買って対戦動画を撮ったり、友人に送りつけてその様子を見せてもらったりしています。なので今は手元にない

 

自分ではあまり意識・理解していないことを可視化して後から確認できるってすごい面白くないですか?

別の言い方をすると、相手を見て~とかゲージ管理が~とかを言われても自分じゃどうしたらいいかわかりにくいじゃないですか。少なくとも私はよくわかりません…それを可視化することで少しは 理解の助けになりそうじゃないですか?知らんけど。

実際アイトラッカーを友人につけてもらってギルティをプレイしてもらった時は、ジャムの低ダJPに対して対空を全くこっちを見ずに完璧に出していて笑いました。ほんとに見てなかったんかい!!



話がずれましたが、そんな可視化ツール大好きマンな私がこのBattle Timeline を見て、「いいなー自分もこういうのしてみたいなー」と思ったので、「よっしゃ作ってみるか!!」

 となったのでチャレンジしているという内容の記事になります。

ですので、備忘録的な役割が多分に含まれています。また、どこまでやるかも決まっていません。素人の遊びです。こういう記事にして自分の退路を断つことでエターならないようにする理由もある…あきっぽいんです。

 
結果だけ見たい人は結果まで飛んでください。
ジャンプ機能ってどうやってつけるの…





背景・仕様?

 

筆者の背景

私ですが、こういうソフトウェア的な開発?やアプリケーションの開発をしたことは一切ありません。なんならコンピュータ言語すらまともに扱ったことがないです。マジの素人です。なのでいろいろと拙いです。ユルシテ…

 

作製するものの仕様?

今回まずどのようなものを作製し、どこで妥協するのかを決定することから始めました。こういうのって仕様っていうんですか?全く知らない…ユルシテ…

決定した機能は以下です。

 

ゲージ関連

  • 体力ゲージとテンションゲージの時間変化を追う
  • バーストを吐いたタイミングを感知する

 

妥協ポイント

  • ゲージの多少の乱高下は許す
  • 動画はmp4を読み込む リアルタイム処理はしない
  • バーストタイミングは感知できなかったら諦める
  • どこからどこまでが1ラウンドなのかは感知しない
  • キャラの認識はあきらめる
  • 覚醒を使ったのか、ロマキャンを使ったのかは認識しない
  • できればなんでもいい

 

こんな感じです。全くの素人にとってはこれでもハードルが高い気がしていましたが、思いつく限りでこんな感じで決めました。

とりあえず体力ゲージとテンションゲージの大まかな変化が解ればOK!ってことです

 あと最後の、できればなんでもいい これが一番大事ですね。クオリティ?知らん知らん!まず作れ!素人なんだから!

環境

Python 3.8.5

Opencv 4.4.0

 

有識者に聞いたらOpencvでできるんじゃない?と言われたので、流行っているらしいPython+Opencvで実装することにしました。Opencvはライブラリ?なので単体じゃ使えないみたいですね!知らんけど!

どうやって導入したかですが、全部本に書いてあった通りにやっただけです。


 本だけは買っていた。3か月以上ほったらかしていたけど。お兄ちゃんはおしまい!はいいぞ。

 

アルゴリズム?と実装内容


さてどうやって実現するかですが、いろいろと調べたり聞いてみたところ、以下の手順を踏めればやれそうな気がしたので一旦これでやってみます。

  1. 動画を読み込む
  2. 動画から体力ゲージ、テンションゲージ全体を切り取る
  3. ゲージから色抽出し2値化。ゲージの欲しい部分だけを白色で抽出
  4. ノイズ除去
  5. 白色の長さを数える(単位ピクセル)
  6. 長さを配列に格納し、グラフ化

あれ?いけそうだぞ?ってなりませんか?素人なので僕はなりました。ノイズ除去がヤバそうですが。

一応今回はギルティ用のツールなので、入力の動画を解像度を決め切ってしまえば、2のゲージ切り取りはピクセルで指定できるので楽できそうかなぁとはこの時には考えていました。


 

1.動画を読み込む

 動画の読みこみと再生をするコマンドですが、ネットに記事がありました。便利な世の中だ。
note.nkmk.me

import cv2
import sys

file_path = 'data/temp/sample_video.mp4'
delay = 1
window_name = 'frame'

cap = cv2.VideoCapture(file_path)

if not cap.isOpened():
    sys.exit()

while True:
    ret, frame = cap.read()
    if ret:
        cv2.imshow(window_name, frame)
        if cv2.waitKey(delay) & 0xFF == ord('q'):
            break
    else:
        cap.set(cv2.CAP_PROP_POS_FRAMES, 0)

cv2.destroyWindow(window_name)

 
こう記述するらしい。細かいことは置いておいて、
「file_path」に読み込ませたい動画の絶対パスを入れればよいようです。
else以下で動画のフレームを0にもどしているので1回だけ再生するなら
「cap.set(cv2.CAP_PROP_POS_FRAMES, 0)」を「break」 に変えればよさそう。
さらに都合の?いいことにWhile True文の中で(たぶん)動画を1フレームずつ読み込んでいるのでこの中で処理をしてやればよいことになります。一歩前進!


こうなると絶対パスを動画を変えるたびに手打ちするのが嫌なのでGUI的にファイルを選んだらその動画の絶対パスを返してくれるような関数が欲しくなりますよね。
探したらありました。便利な(ry
元記事がどこか忘れたので自分が実装したコードを。

import tkinter
from tkinter import filedialog

  #ウインドウの作成
  root = tkinter.Tk()
  root.title("Python GUI")
  root.geometry("360x240")

  #入力欄の作成
  input_box = tkinter.Entry(width=40)
  input_box.place(x=10, y=100)

  #ラベルの作成
  input_label = tkinter.Label(text="結果")
  input_label.place(x=10, y=70)

  #ボタンの作成
  button = tkinter.Button(text="参照",command=file_select)
  button.place(x=10, y=130)

  #ウインドウの描画
     
  idir = 'C:\\python_test' #初期フォルダ
  filetype = [ ("mp4","*.mp4")] #拡張子の選択
  file_path = tkinter.filedialog.askopenfilename(filetypes = filetype, initialdir = idir)
  #input_box.insert(tkinter.END, file_path) #結果を表示
  root.destroy()
  root.mainloop() 
  
  return file_path

file_path を返してくれるようにしたのであとはこれを引き渡すだけです。これで動画読み込み部分がとりあえず完成しました。



2.動画から体力ゲージ、テンションゲージ全体を切り取る

次は動画から体力ゲージとテンションゲージを切り取る作業です。今回読み込むファイルはツイッターで見るような画質を想定して、640×360として考えているので、一意に決まります。
動画の切り取りは、以下のようなコードでできるらしいです。
qiita.com

import cv2

  #画像入力
    im = cv2.imread('Lena.bmp',0)

    #新しい配列に入力画像の一部を代入
    dst = im[200:400,70:270]

    #書き出し
    cv2.imwrite('cut.bmp',dst)

自分の必要なところだけ抜き出しましたが、python+opencvって画像が配列なんですね。
なので読み込んだ画像の番号を指定してあげるだけでよいようです。なんて楽なんだ… ここでは im[]←この部分で指定。
書き出しは動画なので上記の再生で使った
cv2.imshow(window_name, frame)
の引数に新しい配列を入れればよいみたいですね。
今回は体力ゲージとテンションゲージが2プレイヤー分あるので4つ新しく配列を用意してあげるだけでいいと。なるほどなー。

これで、動画から体力ゲージ、テンションゲージ全体を切り取る作業は終わりです。





3.ゲージから色抽出し2値化。ゲージの欲しい部分だけを白色で抽出

これが1番肝のような気がします。自分では最初気が付かなかったのですが、ギルティのゲージって体力ゲージもテンションゲージもグラデーションが流れている?んですよね。
なのでゲージ側を白くするのはきっと得策ではないので、ゲージが無い方(黒くなってる方)を抽出することにしました。

抽出する前に動画の色形式?をRGB形式からHSV形式に変更します。こっちのほうが良い気がしたので。

コードはこちら。

cv2.cvtColor(input_image, cv2.COLOR_BGR2HSV)

input_imageに動画の配列を入れればいいみたいです。便利。

その後、動画の色を抽出します。
qiita.com

コードはこちら

import numpy as np
bgrLower = np.array([0, 0, 130])    # 抽出する色の下限(BGR)
bgrUpper = np.array([120,120, 255])    # 抽出する色の上限(BGR)
img_mask = cv2.inRange(img, bgrLower, bgrUpper) 

bgrって書いてますが、inRange関数は配列の値の下限と上限の間を探すだけなので元の画像がHSVならHSVでやってくれるみたいです。たぶん。

ここでうまいこと値を選べばいいわけです。そこが1番難しいやんけ!!

入力がimgで、抽出後の結果はimg_maskに保管されるわけですね。

なにはともあれこれで動画の読み込みから2値化ができたわけです。その結果の1部がこちら。

これで2値化も完了です。




4.ノイズ除去

ノイズ除去ですが、よくわからないので使ってみた関数を並べます。
labs.eecs.tottori-u.ac.jp
labs.eecs.tottori-u.ac.jp


メディアンフィルタ

median = cv2.medianBlur(img,5)

画像の中央値を探すらしい。カーネルのサイズごとに計算するので大きいと効きすぎてちゃんと認識できなくなった。でも効果はアリ。

・ガウシアンフィルタ

blur = cv2.GaussianBlur(img,(5,5),0)

ガウシアンで画像をぼかす。ぼかすので今回の目的には合わなかった模様。

・オープニング

opening = cv2.morphologyEx(img, cv2.MORPH_OPEN, kernel)

黒の領域を増やしてから元に戻す。ごま塩上の白色のノイズが消せる。イイ感じ。

これらをうまいこと利用しました。

FFTフィルタ(ローパス)
画像には直接このフィルタをかけていませんが、結果のグラフにはかけています。定義だけ示しておきます。

def fft_filter(data,s_freq,low_filter):#FFTでローパスフィルタ
  sample_freq =fftpack.fftfreq(len(data),d = s_freq)
  sig_fft=fftpack.fft(data)
  #pidxs = np.where(sample_freq > 0) #sample_freqが正の値をとる時のインデックスを取り出す.
  #freqs, power = sample_freq[pidxs], np.abs(sig_fft)[pidxs] #freqsに周波数, powerに強さを入れる
  #plt.plot(freqs, power) #図示
  sig_fft[np.abs(sample_freq) > low_filter] = 0  # ノイズを消してから
  main_sig =np.real( fftpack.ifft(sig_fft))  # 逆フーリエ変換
  return main_sig

引数の1番目がデータで、3番目がカットオフ周波数です。

5.白色の長さを数える(単位ピクセル)

これなんですが、ネットでイイ感じの記事を見つけました。便利な(ry
qiita.com

この記事では2値化した画像の輪郭を抽出し、その輪郭に接するような長方形を作っています。
これをゲージの輪郭にも採用し、長方形の幅を取ればいいはす。

コードはこんな感じ

contours, hierarchy = cv2.findContours(thresh, cv2.RETR_CCOMP, cv2.CHAIN_APPROX_SIMPLE)
contours.sort(key=lambda x: cv2.contourArea(x), reverse=True)
epsilon = 0.1*cv2.arcLength(cntours,True)
approx = cv2.approxPolyDP(cnt_hp1,epsilon,True)
x,y,w,h = cv2.boundingRect(approx)

1行目で輪郭を作成
2行目で面積の大きい順に輪郭をソート
3,4行目で輪郭を近似し、ノイズを除去?
5行目で外接する長方形を作成します

これでwにゲージの長さが代入されたはずです。
あとはこれを配列に追加してあげればいいはず!

6.長さを配列に格納し、グラフ化

www.python-beginners.com

これはあまり説明が要らないと思うので割愛。



結果

長くなりましたが、現在(プログラミング開始して4日目)の結果がこちらになります。

グラフだけ大きくしたものがこちら。全く同じものではないですが。

f:id:ri_ru_ria:20201129002700p:plain
ゲージの時間変化グラフ上が1Pで下が2P

今後

まだグラフにノイズがあるのでこれを何とかしたいですね。
また、バーストのタイミングを検出したいです。


初心者なのでわからないことばかりですが頑張ってみたいと思います。
こんな方法もあるよ!というようなアイディア?があれば教えていただけると泣いて喜びます。