講義や講演のプレゼン動画をスライドごとにショット分割したくて作った簡単なスクリプトを記録しておく. スライド枚数はおおよそ発表分数程度を想定していて,スライド中に動画がバンバン含まれるようなものは想定外. k-means法は予め指定したクラスタ数(k個)にデータを分割できるので,動画フレームをk枚のスライドに分割するという作戦. k-means法とはたくさんのデータを似たもの通しくっつけてk個のグループに分けるアルゴリズム. 何個に分けていいか分からないときにつかうmeanshift法でもいいかも.
準備
ライブラリは画像を使うのでOpenCVとnumpyは当たり前として,k-means法についてはscikit-learnのcluster.KMeansを使用. フレームを無作為抽出してクラスタリングするためrandomも使います.
import cv2 import numpy as np import random from sklearn.cluster import KMeans
フレーム数と動画の時間を取得する.それを分数になおしてスライド枚数とする. また,動画のフレームサイズが大きすぎるとメモリを使いすぎるので 内容が判別できる程度に縮小したい. ここではフレームサイズ取得用に動画を1フレームだけ取り出しておく.
window_name = "video" cap = cv2.VideoCapture('test.mp4') n_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) #フレーム数 fps = cap.get(cv2.CAP_PROP_FPS) #1枚だけ取得 n_divisions = int(n_frames/fps/60) #クラスタ数 ret, frame = cap.read() #再生位置を初期フレームに戻す
ちなみに使用した動画はこちら.Youtubeは直接ダウンロード禁止なので,mp4ファイルを所有者から頂いた. 量子コンピュータの原理をペロッと味あわせてくれる面白い講義です.
無作為抽出したフレームをクラスタリング
全フレームをクラスタリングしようとすると, 動画の全画像をメモリ上に配置しないといけない(たぶん他に方法あると思うが)ので, ここでは無作為抽出したフレーム群を用いてクラスタに分ける. さらに画像は8分の1のサイズに縮小しておく. 無作為抽出するために全フレームの番号の入った配列frame_listをシャッフルしておく. random.seed(1)は結果を毎回同じにするため.
# フレーム番号をシャッフル frame_list = list(range(n_frames)) random.seed(1) random.shuffle(frame_list) # 解像度を下げる frame2 = cv2.pyrDown(frame) frame4 = cv2.pyrDown(frame2) frame8 = cv2.pyrDown(frame4) # 1次元ベクトル化した画像を積み上げるための変数Xを準備 shape = (0, np.ravel(frame8).shape[0]) X = np.empty(shape)
フレーム群の枚数は分けたいクラスタの数の20倍にしておく. これで十分かどうかは動画よる. for文で1フレームずつ動画から画像を無作為抽出して, 画面に表示しつつ,1次元ベクトル化して配列Xにスタックしていく. 最後のcv2.destroyWindow関数はJupyter Notebook用. Jupyter Notebookでは各セルの実行が終わってもwindowが残り続けるので,それを強制的に閉じるため.
# フレーム番号のリスト先頭からn_divisions枚だけ使用 for i in range(n_divisions*20): print("set %06d-th frame" % frame_list[i]) cap.set(cv2.CAP_PROP_POS_FRAMES, frame_list[i]) ret, frame = cap.read() if ret == False: print("could not get frame") continue frame2 = cv2.pyrDown(frame) frame4 = cv2.pyrDown(frame2) frame8 = cv2.pyrDown(frame4) cv2.imshow(window_name, frame8) key = cv2.waitKey(100) if key == 27: break # frame8をXの最下行に1次元配列として追加 X = np.insert(X, X.shape[0], np.ravel(frame8), axis=0) cv2.destroyWindow(window_name)
k-means法を実行してn_divisions個のクラスにクラスタリング.
# k-meansクラスタリングで学習 kmeans = KMeans(n_clusters=n_divisions, random_state=0).fit(X) centers = kmeans.cluster_centers_
これがクラスタ中心になっている画像.よく見ると複数の画像がアルファブレンドされたようになっている. 各画像の解像度が1/8になっているせいもある. 似た画像を平均してるのだからそうなって当然だけど,あまりボケボケになっていないということは,ちゃんと似た画像だけが1つのクラスタに分類されている証拠かな.
# クラスタ中心となる画像 rows = centers.shape[0]//4 for i in range(centers.shape[0]): ax = plt.subplot(4, rows+1, i+1) ax.axis('off') img = centers[i].reshape(frame8.shape).astype(np.uint8) img_bgr = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) plt.imshow(img_bgr)
全フレームをクラスタに分類
n_frames枚の画像フレーム全部をn_divisions個のクラスタのいずれかに分類する. ここでも全フレーム全部を一気にやってもいいのだけど,メモリを心配してfor文で一枚ずつ分類する. 分類されたクラスタのラベル番号を動画フレームの左上に表示している. scoreを用いて最もクラスタ中心に近い画像のインデックスを保存している.
modoids = np.ones(n_divisions).astype(np.int32)*-1 mindists = np.ones(n_divisions)*sys.maxsize cap.set(cv2.CAP_PROP_POS_FRAMES, 0) for i in range(n_frames): ret, frame = cap.read() if ret == False: print("could not get frame") continue frame2 = cv2.pyrDown(frame) frame4 = cv2.pyrDown(frame2) frame8 = cv2.pyrDown(frame4) # 一次元化して最も近いクラスタラベルを求める shape = (0, np.ravel(frame8).shape[0]) Y = np.empty(shape) Y = np.insert(Y, Y.shape[0], np.ravel(frame8), axis=0) label = kmeans.predict(Y) # -score(重心からの距離の自乗和)の最小値とそのときのラベル score = kmeans.score(Y) if(-score < mindists[label[0]]): mindists[label[0]] = -score modoids[label[0]] = i # フレーム番号,ラベル,スコアを表示 text = "%06d-th frame, label: %03d, score: %f" % (i, label[0],-score) cv2.putText(frame, text, (10, 20), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (255, 255, 255), 3, cv2.LINE_AA) cv2.putText(frame, text, (10, 20), cv2.FONT_HERSHEY_COMPLEX_SMALL, 1, (0, 0, 0), 1, cv2.LINE_AA) cv2.imshow(window_name, frame) key = cv2.waitKey(1) if key == 27: break cv2.destroyWindow(window_name)
クラスタ中心に最も近い画像を表示
さっきの画像とほとんど同じに見えるけど,よく見るとこちらはくっきりしている.
# modoidsを表示 rows = centers.shape[0]//4 for label in range(modoids.shape[0]): ax = plt.subplot(4, rows+1, label+1) ax.axis('off') cap.set(cv2.CAP_PROP_POS_FRAMES, modoids[label]) ret, frame = cap.read() if ret == False: print("could not get frame") continue frame2 = cv2.pyrDown(frame) frame4 = cv2.pyrDown(frame2) frame8 = cv2.pyrDown(frame4) img_bgr = cv2.cvtColor(frame4, cv2.COLOR_BGR2RGB) plt.imshow(img_bgr)
参考
- 大阪大学 基礎工学部 Web Lecture Series
https://www.es.osaka-u.ac.jp/ja/web-lecture-series/index.html - 量子コンピュータ入門 - 教授:藤井 啓祐
https://www.youtube.com/watch?v=tR1rhxY0-HU