【PythonでGUIを作りたい】Tkinterで作る画像編集ソフト#6:動画編集の内部設計を考える
こんにちは。すうちです。
週末はめずらしく特別な予定も入れず時間もあったので外に出かけてもよかったのですが、なんとなくそういう気持ちにもなれず選挙の投票以外は家でnoteやプログラムのコードを書いてました。
それでは本題です。Tkinterで作る画像編集ソフトの連載も6回目となりました。
前回の記事はこちらです。
今回はTkinterを使った動画編集の内部設計に関する話です。
◆はじめに
作りたい機能のイメージ
初期検討のメモです。
メモを元に、前回はユーザ視点からGUI操作や必要な部品を考えました。今回は配置したGUIのイベントをどう実現するか?内部の設計を考えたいと思います。
◆動画と静止画の違い
普段PCやスマホを使っている時に意識しないと思いますが、動画と静止画の違いを簡単に言うならば、扱う画像が1枚か複数枚かです。
静止画は幅(Width)、高さ(Height)の2次元データがRGB(3Channel)で構成されています。例えば、W:100 H:200の場合、100x200x3のデータになります。
動画の場合、ご存じの方も多いと思いますが、上記の構成が時間方向に(W,H,CH)xN個存在するので、それをフレームレートと呼ばれる更新タイミングにあわせて(紙芝居の絵のように)1枚ずつ表示すると、人間の目の錯覚で画像が動いて見える原理です。
後述の動画編集では、静止画の時に1枚の画像に対して行った処理を複数枚の画像(フレーム)に適用することになります。
◆GUI画面のイベント処理
以下、画面のレイアウトに沿って各イベントの設計方針です。
[1] 動画再生/停止
動画の場合、静止画と違って複数の画像を一定周期で表示しますが、この時ユーザ操作のイベントを処理する部分と動画を再生(複数フレームの連続表示)する部分は、通常スレッド(タスク)にして並列処理できるようにします。
例えば、単純に動画の画像を表示するだけであれば以下のようになりますが、
import cv2
cap = cv2.VideoCapture('./test.mp4')
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
for n in range(total_frames):
# Frame読み出し
ret, frame = cap.read()
if ret == True:
# BGRからRGBフォーマットへ変換
img_conv = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Pillowイメージへ変換
pil_img = Image.fromarray(img_conv)
# TKイメージへ変換
tk_img = ImageTk.PhotoImage(image=pil_img)
canvas.create_image(0, 0, image=tk_img, tag='Video')
cap.release()
この場合、動画再生中に操作ボタンを押してもGUIは反応できません。動画再生が終わるまで処理を抜けられない(時間かかる)ので、GUIに登録されたイベントを処理できないからです。
もちろんループ処理の中にイベント実行をチェックする処理を書けばできないこともないですが(昔マルチタスクがサポートされてない組込機器などではそのような実装もありました)、今はOSやプログラム言語で並列処理はサポートされてるのでそれを使用します。これによりコードをシンプルに書けるメリットもあります。
また動作再生に関わるファイルの読み込み、書き込み等はサンプルのようにopencv(画像処理ライブラリ)を使用予定です。
並列処理の実現(Threadかafterメソッドか?)
Pythonでは並列処理を実現する仕組みとして、ThreadingやTkinterのafterメソッドが用意されています。
Threadの実装例
import threading
# スレッド生成
th_video = threading.Thread(target=draw_video, args=())
# スレッド開始
th_video.start()
threading.Threadの引数targetはスレッドで実行する処理です。
afterの実装例
import tkinter as tk
canvas = tk.Canvas(tab2, height=10, width=20, bg='gray')
# 周期処理の実行開始
video_id = canvas.after(interval, draw_video)
# 周期処理の実行停止
canvas.after_cancel(video_id)
afterの引数intervalは何ms後にdraw_video(実行したい処理)を呼ぶかを指定。draw_video内で再びafterメソッドを呼べば周期的に処理を実行できます。
今回の動画再生はどちらでも実現可能と思いますが、まずはafterメソッドで実装進めます。
ただネットで調べるとafterメソッドは定期的に実行した場合の時間精度が良くない話も見かけるので、一旦実装とテストして問題あれば見直そうと思います。
[2] コマ送り(1フレームSTEP再生)
コマ送りは次のフレーム画像を読み出して表示すればOK(動画処理の1回表示で終了)なので、周期実行する1フレームの画像表示を流用します。
[3] 画面キャプチャ
キャプチャは、静止画編集の保存と同様に動画停止状態のフレーム画像を参照してPillowのsave()でファイルに保存します。
opencvを使う時の注意点は、opencvで読み込んだ画像はBGRフォーマット(通常のR:Red、G:Green、B:Blueの並びでなくBGRの順番)という事です。そのままでも保存できますが並びがRGBと違うため表示色がおかしくなります。
そのため、前述のサンプルはBGRからRGBへの変換処理を入れています。
# BGRからRGBフォーマットへ変換
img_conv = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Pillowイメージへ変換
pil_img = Image.fromarray(img_conv)
[4] 倍速再生
前述の動画のフレームレートは1秒間に何回表示するかの意味です。例えば20fpsは1秒/20回=50ms周期で表示すれば良いことになります。
この場合は2倍速は25ms。4倍速は12.5ms周期で表示するとその速度で再生できます。
動画ファイルのフレームレートはopencvで以下の方法で取得できるので、上記の方針で倍速の実装を進めます。
cap = cv2.VideoCapture('./test.mp4')
fps = int(cap.get(cv2.CAP_PROP_FPS))
[5] 再生位置の移動
Tkinterのスライダ(Scale)は、指定範囲(例:0〜100)において動かした位置の値を取得できます(バーを動かすと登録したイベント処理が呼ばれる)。
仮に動画再生に必要な総フレーム数をスライダの範囲に割り当てる(例:100分割する)場合、動かした位置から大まかなフレーム番号はわかります。
このフレーム番号をopencvに設定して該当のフレーム位置に変更します。
cap = cv2.VideoCapture('./test.mp4')
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_no)
また動画ファイルの総フレーム数は、以下の方法で取得できます。
cap = cv2.VideoCapture('./test.mp4')
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
[6] フレーム切出し/キャンバス切替
動画の特定範囲の画像フレームを切出す場合、最低必要な情報はそれらのフレームの開始/終了位置です。
前回検討した仕様は「キャンバス2面の片方に切り出す先頭フレーム、もう片方に終端フレームを表示する」でした。この時双方のフレーム番号から切り出す範囲はわかります。
また双方のフレーム表示画面(Canvas)の切替は、静止画編集のクリップ時に使用したマウスイベントでキャンバス表示を切替えます。
video_canvas1.bind('<ButtonPress-1>', event_mouse_select1)
video_canvas2.bind('<ButtonPress-1>', event_mouse_select2)
[7] 編集(回転/反転/クリップ)
編集対象のフレーム(1〜N個)は前述の方法で取得できそうです。
実際の動画編集(回転/反転/クリップ)は静止画の時に使った関数を流用するつもりです。
動画はフレーム画像(静止画)の集まりなので、各フレームを編集して保存すれば動画も同じことが実現できます。
[8] 編集ファイル保存(Save)
編集フレーム(1〜N個)をファイルに保存します。動画ファイルの作成はopencvのVideoWriterを使用します。
import cv2
video_format = cv2.VideoWriter_fourcc(*'mp4v')
video = cv2.VideoWriter('./output/test.mp4', video_format, fps, (w,h))
for frame in target_frames:
edit_image = EditVideo(frame)
video.write(edit_image)
video.release()
基本的に、これまで述べた設計方針をもとに実装を進めていきます。
今後の予定
次回は、動画編集の実装やテストについて書く予定です。
最後に
今回はここまでです。
仕事と違い個人で作るプログラムは(自由に作れるためか?)余計なことを考えず集中できるので、私の場合、気持ち的にリフレッシュになったりします。
動画編集の実装もだいぶ固まってきました。最初は連載を始めておきながら本当にできるか見えてなかった所もありますが、目標の範囲はなんとか行けそうな気がしてきました。あと1,2回ほど最後までお付き合い頂ければと思います。
GUI構築の流れを大まかに把握したい方、Tkinterを使うことを検討されている方に、何か参考になれば幸いです。
最後まで読んで頂き、ありがとうございました。
ーーー
ソフトウェアを作る過程を体系的に学びたい方向けの本を書きました(連載記事に大幅に加筆・修正)
Kindle Unlimited 会員の方は無料で読めます。
#連載記事の一覧
ーーー