Edited at

【デレステ】OpenCVで腋を見せてもらう

まずはこの動画を見てくれ。

15分50秒、1秒たりとも目を離すことなく堪能してくれたであろうと思う。

訓練されたPなら全員の特定も余裕だろう。1

今回はこの動画作成に使った技術を紹介しよう。


使用した技術


目grep

技術もへったくれもなかった!

録画したPV動画をコマ送りしながら、同じくらいのシーンをスクリーンショット撮影する。

これを190回繰り返しました。

image_shudou.png


自動化しよう

まあなんだ、キャプチャしながら考えてはいたわけですが、こういうのはやっぱり自動化したほうがいいですよね。

やりたいことは動画から特定のシーンを抜き出すことです。

動画は手動録画したものなので録画開始時刻が微妙にずれているため、時間指定で抜き出すことはできません。

何らかのアルゴリズムで抽出タイミングを算出する必要があるでしょう。


実装方針

ここでは単純に、サンプル画像を1枚撮っておいて、『動画からサンプル画像と最も近いシーンを抜き出す』というアプローチでいきましょう。

まずはサンプル画像を用意。

最初の一枚くらいは仕方ないね。

eve.png


環境準備


Anaconda

Anaconda Promptを起動して、とりあえず全部アプデ。

conda update -n base conda

conda update --all
conda update python


OpenCV

Anaconda navigatorを起動する。

Environment → 『OpenCV』環境を作成

OpenCVおよびscikit-imageをインストールする。

OpenCVで調べたらlibopencvopencvpy-opencvの三つが出てきたんだけど、区別がつかないのでとりあえず全部インストールした。

install.png

あと、そのままだと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フレームずつ調べるという二段階の調査になっています。

さすがに最初から最後まで全フレームを調べるのは重すぎますからね。


実行

全員の処理に3時間弱かかりました。

image_opencv.png

キャプチャ画像のサムネイルを見るかぎりでは、非常に良さそうな結果に見えますね。

髪の毛とかあるからもっと残念なことになるかもしれないと危惧していたのですが、予想外に綺麗にいいかんじになってくれました。


完成したPV

OpenCVが出力した画像を使って作成したPVがこちらになります

残念ながら、カメラが下の方にずれているアイドルが散見されます。

腋ではなく、なにやら別のところを注視しているようにも見えてしまいますね。

しかし、全く関係ないシーンを検出するようなことはありませんでした。

最良とはとても言えないものの、まあそこそこな結果になったのではないでしょうか。


今後の展望

今回は『画像同士の近似度を測る』だけという単純なアプローチでした。

キャプチャの高さがずれているアイドルはおそらく背景に引っ張られているせいだと思うので、次は人物だけをクリッピングして比較するとか、あるいは機械学習やらなにやらを使って骨格の位置で比較する、みたいなことをやりたいですね。

あと、どちらかというとスクリーンショットのキャプチャより、その前のPV録画のほうが辛かったので、むしろそっちを自動化したいところです。

・アイドルを選択する

・シンデレラドリームに着替えてもらう

・PVを再生する

・PCのキャプチャソフトでPVを録画する

・録画されたファイルにアイドル名を付ける

・これを190回繰り返す

movie.png

ちなみにPV録画機材はRagno Grabber2です。

安いわりにけっこうな画質でなんでも録画できるのと、あとパススルーがあるので無遅延でTV出力できるのがとっても便利。


まとめ

サンタクロースちゃんと結婚したい。






  1. 私は20人くらいしかわからない。