まずはこの動画を見てくれ。
— ラナ・クアール (@rana_kualu) 2019年5月15日
15分50秒、1秒たりとも目を離すことなく堪能してくれたであろうと思う。
訓練されたPなら全員の特定も余裕だろう。1
今回はこの動画作成に使った技術を紹介しよう。
使用した技術
目grep
技術もへったくれもなかった!
録画したPV動画をコマ送りしながら、同じくらいのシーンをスクリーンショット撮影する。
自動化しよう
まあなんだ、キャプチャしながら考えてはいたわけですが、こういうのはやっぱり自動化したほうがいいですよね。
やりたいことは動画から特定のシーンを抜き出すことです。
動画は手動録画したものなので録画開始時刻が微妙にずれているため、時間指定で抜き出すことはできません。
何らかのアルゴリズムで抽出タイミングを算出する必要があるでしょう。
実装方針
ここでは単純に、サンプル画像を1枚撮っておいて、『動画からサンプル画像と最も近いシーンを抜き出す』というアプローチでいきましょう。
環境準備
Anaconda
Anaconda Promptを起動して、とりあえず全部アプデ。
conda update -n base conda
conda update --all
conda update python
OpenCV
Anaconda navigatorを起動する。
Environment → 『OpenCV』環境を作成
OpenCV
およびscikit-image
をインストールする。
OpenCV
で調べたらlibopencv
とopencv
とpy-opencv
の三つが出てきたんだけど、区別がつかないのでとりあえず全部インストールした。
あと、そのままだとmatplotlib._qhull
がねーぞとかいうエラーが出てきたけど、uninstall matplotlib && install matplotlib
としたらなおった。
よくわからない。
実装
ソース
import cv2
import os
import glob
def save_frame(video_path, frame_num, result_path):
"""
対象のフレームを画像に保存する。
Parameters
----------
video_path : string
動画フルパス
frame_num : int
フレーム数
result_path : string
保存先の画像フルパス
Returns
-------
ret : boolean
キャプチャできたらTrue。
"""
cap = cv2.VideoCapture(video_path)
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_num)
ret, frame = cap.read()
# cv2.imwriteは日本語使えないので、一度テンポラリに保存してから移動する
temporary_path = 'C:/path/to/dummy.png'
if ret:
cv2.imwrite(temporary_path, frame)
shutil.move(temporary_path, result_path)
return True
return False
def get_ssim_list(imageFileName, movieFileName, frameStart=0, frameFinish=9999, frameStep=1):
"""
動画のSSIMを求める
Parameters
----------
imageFileName : string
画像ファイルのフルパス
movieFileName : string
動画ファイルのフルパス
frameStart : int
調べる開始フレーム
frameFinish : int
調べる終了フレーム
frameStep : int
フレームを飛ばす数。1だととても遅い。
Returns
-------
ssimArray : array
{フレーム:SSIM}
"""
# 初期値
imageData = cv2.imread(imageFileName)
movieData = cv2.VideoCapture(movieFileName)
ssimArray = {}
frame = frameStart
while frame < frameFinish:
# 画像を取得
movieData.set(cv2.CAP_PROP_POS_FRAMES, frame)
ret, movieImageData = movieData.read()
# 取れなくなったら終わり
if not ret:
break
# 返り値に積む
ssimArray[frame] = compare_ssim(imageData, movieImageData, multichannel=True)
# 次フレーム
frame += frameStep
return ssimArray
# 比較元画像
defaultImage = 'C:/path/to/eve.png';
# 調査元動画ディレクトリ
movieDir = 'C:/path/to/movie/'
# 画像の出力先ディレクトリ
imageDir = 'C:/path/to/image/'
# 動画リストを取得
movieArray = glob.glob(movieDir + '*.mp4', recursive=True)
# 動画リストでループ
for movieFile in movieArray:
# 保存ファイル名を作成
a = os.path.basename(movieFile)
saveImageFile = imageDir + os.path.splitext(a)[0] + ".png"
# まず大まかに調べる
ssimList1 = get_ssim_list(defaultImage, movieFile, frameStart=0, frameFinish=600, frameStep=30)
nearFrame = max(ssimList1, key=ssimList1.get)
# 一番近いところの周囲をフレーム単位で調査
ssimList2 = get_ssim_list(defaultImage, movieFile, frameStart=max(0, nearFrame-28), frameFinish=nearFrame+28, frameStep=1)
nearestFrame = max(ssimList2, key=ssimList2.get)
# 保存する
save_frame(movieFile, nearestFrame, saveImageFile)
Pythonは素人なのでソースは適当です。
最初は大まかに1秒ごと調べて、最も近かったところの周囲を再度1フレームずつ調べるという二段階の調査になっています。
さすがに最初から最後まで全フレームを調べるのは重すぎますからね。
実行
キャプチャ画像のサムネイルを見るかぎりでは、非常に良さそうな結果に見えますね。
髪の毛とかあるからもっと残念なことになるかもしれないと危惧していたのですが、予想外に綺麗にいいかんじになってくれました。
完成したPV
OpenCVが出力した画像を使って作成したPVがこちらになります。
— ラナ・クアール (@rana_kualu) 2019年5月15日
残念ながら、カメラが下の方にずれているアイドルが散見されます。
腋ではなく、なにやら別のところを注視しているようにも見えてしまいますね。
しかし、全く関係ないシーンを検出するようなことはありませんでした。
最良とはとても言えないものの、まあそこそこな結果になったのではないでしょうか。
今後の展望
今回は『画像同士の近似度を測る』だけという単純なアプローチでした。
キャプチャの高さがずれているアイドルはおそらく背景に引っ張られているせいだと思うので、次は人物だけをクリッピングして比較するとか、あるいは機械学習やらなにやらを使って骨格の位置で比較する、みたいなことをやりたいですね。
あと、どちらかというとスクリーンショットのキャプチャより、その前のPV録画のほうが辛かったので、むしろそっちを自動化したいところです。
・アイドルを選択する
・シンデレラドリームに着替えてもらう
・PVを再生する
・PCのキャプチャソフトでPVを録画する
・録画されたファイルにアイドル名を付ける
・これを190回繰り返す
ちなみにPV録画機材はRagno Grabber2です。
安いわりにけっこうな画質でなんでも録画できるのと、あとパススルーがあるので無遅延でTV出力できるのがとっても便利。
まとめ
サンタクロースちゃんと結婚したい。
サンタクロースちゃんに全部
— ラナ・クアール (@rana_kualu) 2019年4月25日
#デレステ #第8回シンデレラガール総選挙 pic.twitter.com/YtNCPzRPCs
-
私は20人くらいしかわからない。 ↩