【PythonでGUIを作りたい】Tkinterで作る画像編集ソフト#7:動画編集の内部設計を追加で考える
こんにちは。すうちです。
今週3連休だったので初日に「あれもやりたい」「これもやりたい」と計画立てたりしましたが、ふたを開けてみると無常にも時間は過ぎていき、現時点で目標の半分も消化してない状態で今を迎えています。
最近は質より量ではなく、質を重視して今できることに集中するでも良いのかなと考えることもあります。
余談は置いといて本題です。Tkinterで作る画像編集ソフトの連載も7回目となりました。
前回の記事はこちらです。
今週上手くいけば実装とテストまで全て終わるかと思っていましたが、じつは動画編集の実装過程で考慮必要なことが複数みつかり予想より時間かかっています(まだ実装もテストも途中で終わってません)。
そんな訳で!?今回は追加検討した設計の話を中心に書きたいと思います。
◆はじめに
作りたい機能のイメージ
初期検討のメモです。
メモを元に、前回はGUIのイベントをどう実現するか?内部の設計を考えましたが、その内容だけだと細かい所を具体的にどうするか決まってなかった事もあり実装がなかなか進まない状況でした。
動画の場合、複数の画像を扱うので、編集時にどう実現するかもう少し踏み込んで考えるべきだったと思います(今更ですが反省点です)。
以下、現時点の状況と追加検討した内容です。
◆現在の状況
・動画再生/停止
前回の設計方針に基づき、tkinterのafterメソッドで周期的に動画のフレーム画像を表示するまでを実装済です(動画再生/停止は可能)。
ただ、詳細は後述しますが再生速度が遅い(期待のフレームレートで動画表示できない)不具合が残っています。
・フレーム(特定位置)の切り出し
切出し位置のフレーム先頭と終端を2面のキャンバスに表示させて、双方のフレーム位置を取得する所まで実装済です。
細かいテストはまだこれからですが、今の所大きな問題はないです。
・フレームのキャプチャ(画像保存)
現在表示中のフレーム画像をファイルに保存する機能です。こちらも実装は完了して実際に画像保存できる所まで確認済です。
・編集機能(回転/反転/クリップ)
前回の設計方針に沿って、一通り実装までは終えた状況です。
回転、反転、クリップそれぞれ単独動作は問題なく動きますが、編集の組み合わせ条件で後述の動画ファイル保存に失敗することがあります。
・編集した動画の保存
こちらも実装して切出したフレーム範囲に編集かけて動画ファイルに保存できることは確認しています。
ただし、ある条件下でファイル保存に失敗するので原因調査中です。
なんなら毎回100%失敗してくれた方が原因は追いやすかったりしますが、たまに失敗する場合は、発生条件を見つけないとなので対策に時間かかったりします。
◆追加設計(前回の検討漏れ)
編集(回転/反転/クリップ)組合せ
静止画編集の時は、回転、反転、クリップ毎に編集画像を更新して表示させていました。
前回の時点では、編集自体は静止画の処理を流用しようと考えていて、確かに1枚のフレーム画像に対してはこれで上手くいきます。
ただし、動画の連続フレームに適用する場合、少々問題がありました。
動画作成(保存)前に編集の組合せ条件を決める
動画保存時の基本的な実装は、最初にフレームレートや画像サイズをopencvのVideoWriterに設定して対象範囲のフレーム画像を読みつつ、随時編集かけた画像を保存していく流れになります。
例えば、編集処理の順序(コマンド)を全て保存しておいて、それらを読み出した対象フレームに適用すれば実現可能ですが、欲しいのは最終的に決定した編集の設定(回転方向、反転有無、クリップ位置)であって、途中経過の設定は不要なことに気づくと思います。以下、参考例です。
例:90°回転を4回、又は90°と270°回転を1回ずつ実行
この場合、実行結果は回転なしの条件と同じ。
例:上下反転2回、左右反転2回実行
同様に2回実行で結果反転なしの条件と同じ。
これらを解決するため、各回転、反転処理の実行回数をカウントして、それを一巡する数で割った余りから最終的な編集設定の組合せを決めることにしました。
コマンドリスト作成と動画ファイル作成処理
def set_command_list(self):
flip_UB_keys = ['None', 'flip-1']
flip_LR_keys = ['None', 'flip-2']
ROT_keys = ['None', 'rotate-1', 'rotate-2', 'rotate-3']
ub_idx = self.flip_UB_cnt%len(flip_UB_keys)
lr_idx = self.flip_LR_cnt%len(flip_LR_keys)
rot_idx = self.flip_ROT_cnt%len(ROT_keys)
self.edit_command_list = [flip_UB_keys[ub_idx]]
self.edit_command_list += [flip_LR_keys[lr_idx]]
self.edit_command_list += [ROT_keys[rot_idx]]
if self.CLIP_cnt > 0:
self.edit_command_list += ['clip_done']
上記で求めた編集コマンド(組合せ)を動画保存時に適用します。
video_format = cv2.VideoWriter_fourcc(*'mp4v')
self.video = cv2.VideoWriter(fpath, video_format, self.fps, (self.w, self.h))
self.cap.set(cv2.CAP_PROP_POS_FRAMES, fno_sp)
self.edit_num = fno_ep - fno_sp+1
for n in range(self.edit_num):
ret, frame = self.cap.read()
if ret:
img_cnv = self.edit_video(frame, self.edit_command_list, args)
self.video.write(img_cnv)
if n % 100 == 0:
print('{}/{}'.format(n, self.edit_num))
self.video.release()
再生状態の表示更新
動画再生とあわせて、経過時間などを更新したいと考えています。
Pythonはクラス間で通信などを実現する場合、例えばクラス処理をスレッドやプロセスにして、ソケット通信やデータキューを使う方法があります。
ただ、それなり通信のオーバヘッドもあり表示更新のためだけにそこまで大げさにする必要もないと思ったので、今回はViewクラスで定義した時間更新処理をコールバック関数としてModelクラスに渡して、規定のタイミングで実行する仕組みにしました(組込開発では割込処理でよく使う方法です)。
コールバック関数(再生状態更新)とフレーム表示処理
def update_playstat(self, status, time):
if status:
self.label_frame[self.canvas_idx]['text'] = 'Time:[{}]'.format(time)
def event_selectfile(self, event):
print(sys._getframe().f_code.co_name)
set_pos = self.combo_file.current()
callback = None
if self.select_tab == '[Video]':
callback = self.update_playstat
self.control.Set(self.select_tab, set_pos, callback)
def loop_video(self, loop=True):
ret, self.video_img = self.cap.read()
if ret:
self.draw_video(self.canvas_video, self.video_img)
self.play_status = True
self.cur_frame += 1
#コールバック実行(表示更新処理)
self.callback(self.play_status, self.cur_frame)
if loop:
self.vid = self.canvas_video.after(self.interval, self.loop_video)
その他(クラス間メソッド等)
その他、まだクラス間のメソッド(関数)が固まっていません。
以下、現在のクラス構成(参考)です。
今は仮置きでModelクラスに必要な動画処理を追加して、基本的に静止画と同等処理を公開する形で進めています。
主に固まってないのは、「動画と静止画の処理を分岐させる時にできるだけ上位のクラスが意識せずに内部で処理を分けること」ですが、これをタブを切替えた時点で決めてしまうか?毎回コマンド毎に指定するか?です。
今はタブ切替時に決めた方が構造はすっきりする気がするので、その方向で進めています。
◆主な不具合(バグ)状況
期待のフレームレートで表示できない
前述のように基本的な動画再生/停止は実装済で、再生自体はできてますが、期待するフレームレートの間隔で画像表示できてません。
例えば、24fpsは1秒/24回=42msとなり、42ms周期で表示する必要ありますが、現状はこれより長い間隔で表示されます(見た目は通常の再生速度よりスローな感じ)。
今回音声はサポート外なので、最初画像だけでは見落としていたのですが、試しに普段よく観る動画を再生した所、なんか表示遅いなと気づきました。
以下、調べた範囲で関係ありそうな箇所です。
1フレームの画像表示(処理時間)
平均36ms(20~75ms)※1倍速再生で計測
平均でみると36msでした。想定では1フレームの表示処理の時間は基本変わらないと思っていたのですが結構ばらつきありました。画像毎に処理時間が変わるのか時間測定の精度誤差なのかも検証必要そうですが、仮に平均値で試算しても想定より時間かかっていて
78ms(1フレーム更新周期試算)= 36ms(1フレーム処理時間)+42ms(表示更新間隔@24fps)
なので、そもそも24fpsの周期をこえていると分かりました。
今の実装は1フレーム画像表示後に次のフレーム表示を指示しているので処理順序の問題もありそうです。
なので、上記も含めて再生処理の仕組みを見直すこと(事前にデータを先読みしてバッファリングする等)も考えたいと思います。
tkinterのafterメソッド実行間隔(表示更新間隔)
1倍速再生(青):平均 102 ms
2倍速再生(橙):平均 53 ms
4倍速再生(灰):平均 42 ms
通常再生と倍速再生で比較しました。平均値でみると4倍速再生で通常再生の速度(例:24fps=42ms間隔)に近いことになります。
簡単に修正する場合は、fpsで計算した表示周期の値を小さい値に補正することが考えられますが、そもそもafterメソッドによる実行周期のばらつきも気になります。当初懸念していた実行精度の問題であれば、やはりスレッド実装にするか別方法も検討必要な気がします。
クリップ時の編集動画の保存
opencvのVideoWriterは初めに動画のサイズ(幅、高さ)や表示のフレームレートを指定しますが、入力したフレームと動画サイズ指定が一致しない場合、ファイル保存できないようです。
回転やクリップの場合、画像サイズ(幅、高さ)が変わるので、おそらく編集画像のサイズ計算がある条件で違っていると思いますが、原因特定に至ってません。
ひとつの可能性として、動画のリサイズにImageOps.padを使ってますが、動画サイズと表示キャンバスの値によって発生するPad処理が関係してると推測してます。仕様の理解不足もありそうなのでこの機会に調べてみます。
◆今後の予定
次回は、動画編集の実装やテスト結果について書く予定です(進捗次第で変わる可能性もあり)。
最後に
本日はここまでです。
今回の内容は結果だけ知りたい人には不要な情報と思います。正直記事にするかどうかも迷いましたが、こういう途中経過の話はほとんど見かけない気がするので書いてみました(現時点で全ての問題が解決した訳ではなく、まだ完成まで時間かかりそうなので現状を投稿した経緯もあります)。
引き続き足りない部分の実装と不具合対策を進めます。多分あと2,3回程と思いますがもう少しだけ続きます(週刊連載時のドラゴン〇ールみたいですが…)。もしご興味あれば最後までお付き合い頂ければと思います。
GUI構築の流れを大まかに把握したい方、Tkinterを使うことを検討されている方に、何か参考になれば幸いです。
最後まで読んで頂き、ありがとうございました。
ーーー
ソフトウェアを作る過程を体系的に学びたい方向けの本を書きました(連載記事に大幅に加筆・修正)
Kindle Unlimited 会員の方は無料で読めます。
#連載記事の一覧
ーーー