目的
pythonのtkinter用いてGUIを操作し、動画にエフェクトをかけて再生します。
実行環境
Python version: 3.9.13
OS: Windows 10
GUIイメージ
動画ファイルを読み込んで、サイズを枠に合わせて縮小し、左のキャンバス上に映し出します。再生、停止、最初に戻る、閉じるボタンを作っています。エフェクトは、ttk.Comboboxを使って選択します。
プログラム
動画ファイルを読み込むと、threadingがstartします。threadingは、main_thread_func関数内のwhileをループしています。ボタンを押すと、フラグが立ち、フレームを読み出し動画を再生します。Comboboxで設定したエフェクトは、effect_image
の関数で処理されます。
next_frame
では、カラーの順番をpilに合わせる(BGRからRGBに変換) =>読み出している1フレームの画像をcanvasのサイズに合わせる=>エフェクト =>GUIのcanvas内画像を差し替えをしています。
import sys
import time
import tkinter as tk
from tkinter import ttk
from tkinter import filedialog
from tkinter.filedialog import askopenfile
import cv2
from PIL import Image, ImageFilter, ImageOps, ImageTk, ImageEnhance
import threading
class Set_gui:
def __init__(self, main_window):
# Variable setting
self.file_filter = [("Movie file", ".mp4")]
self.set_movie = True
self.thread_set = False
self.start_movie = False
self.video_frame = None
self.video_cap = None
self.video_reset = False
self.run_one_frame = False
self.function_btn = [
"Default",
"Gray_scale",
"Binarization",
"Sepia",
"Jagged_mosaic",
"Soft_mosaic",
"Quantize",
"Invert",
"Mirror",
"Flip",
"Posterize",
"Solarize",
"Equalize",
"Counter",
"Emboss",
"Find_emboss",
]
# Main window
self.main_window = main_window
self.main_window.geometry("1400x800")
self.main_window.title("Movie Editor v0.10")
# Sub window
self.canvas_frame = tk.Frame(self.main_window, height=450, width=400)
self.path_frame = tk.Frame(self.main_window, height=100, width=400)
self.func_frame = tk.Frame(self.main_window, height=100, width=400)
self.opr_frame = tk.Frame(self.main_window, height=100, width=400)
# Widgetsmith
self.canvas_frame.place(relx=0.05, rely=0.05)
self.path_frame.place(relx=0.60, rely=0.2)
self.func_frame.place(relx=0.60, rely=0.4)
self.opr_frame.place(relx=0.60, rely=0.5)
# 1.1 canvas_frame (label)
self.title_label_grid(self.canvas_frame, "Movie", 0, 0)
# 1.2 canvas_frame (canvas)
self.canvas = tk.Canvas(self.canvas_frame, width=700, height=500, bg="#A9A9A9")
self.canvas.grid(row=1, column=0)
# 2 path_frame
self.btn_grid(self.path_frame, "Start", self.on_click_path, 0, 0, tk.W)
self.path_stvar = tk.StringVar()
self.path_entry_grid(self.path_frame, self.path_stvar, 1, 0)
self.label_text = tk.StringVar(value="FPS:")
self.label_grid(self.path_frame, self.label_text, 2, 0)
# 3 func_frame
self.func_combobox = ttk.Combobox(
self.func_frame,
text="combo_file",
state="readonly",
value=self.function_btn,
width=30,
)
self.func_combobox.set(self.function_btn[0])
self.func_combobox.bind("<<ComboboxSelected>>", self.effect_event)
self.func_combobox.pack()
# 4 opr_frame
self.btn_grid(self.opr_frame, "Start", self.on_click_start, 1, 0, tk.SE)
self.btn_grid(self.opr_frame, "Stop", self.on_click_stop, 1, 1, tk.SE)
self.btn_grid(self.opr_frame, "Reset", self.on_click_reset, 1, 2, tk.SE)
self.btn_grid(self.opr_frame, "Exit", self.on_click_close, 3, 2, tk.SE)
def title_label_grid(self, set_frame, title, r_num, c_num):
label = tk.Label(set_frame, text=title, bg="white", relief=tk.RIDGE)
return label.grid(row=r_num, column=c_num, sticky=tk.W + tk.E)
def label_grid(self, set_frame, title, r_num, c_num):
label = tk.Label(set_frame, textvariable=title)
return label.grid(row=r_num, column=c_num, sticky=tk.W, padx=10, pady=10)
def path_entry_grid(self, set_frame, stver, r_num, c_num):
path_entry = tk.Entry(set_frame, textvariable=stver, width=70)
return path_entry.grid(row=r_num, column=c_num, sticky=tk.EW, padx=10)
def btn_grid(self, set_frame, btn_name, act_command, r_num, c_num, stk):
button = tk.Button(set_frame, text=btn_name, width=10, command=act_command)
return button.grid(row=r_num, column=c_num, sticky=stk, padx=10, pady=10)
def func_pack(self, set_frame, btn_name, act_command, r_num, c_num, stk):
button = tk.Button(set_frame, text=btn_name, width=10, command=act_command)
return button.grid(row=r_num, column=c_num, sticky=stk, padx=10, pady=10)
def effect_event(self, arg):
self.func_no = self.func_combobox.current()
self.func_name = self.func_combobox.get()
def on_click_path(self):
self.movie_path = self.get_path()
self.path_stvar.set(self.movie_path)
self.run_one_frame = True
# Movie standby.
self.thread_set = True
self.thread_main = threading.Thread(target=self.main_thread_func)
self.thread_main.start()
def on_click_start(self):
self.start_movie = True
def on_click_stop(self):
self.start_movie = False
def on_click_reset(self):
self.start_movie = False
self.video_reset = True
#
def on_click_close(self):
self.start_movie = False
self.set_movie = False
# Block the calling thread until the thread represented by this instance end.
if self.thread_set == True:
self.thread_main.join()
self.video_cap.release()
self.main_window.destroy()
def main_thread_func(self):
self.video_cap = cv2.VideoCapture(self.movie_path)
self.total_frame_count = int(self.video_cap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = int(self.video_cap.get(cv2.CAP_PROP_FPS))
self.label_text.set("FPS:" + str(fps))
FPS = 1 / (fps * 1)
while self.set_movie:
if self.start_movie:
ret, self.video_frame = self.video_cap.read()
if ret:
self.next_frame(self.video_frame)
time.sleep(FPS)
if self.set_movie == False:
break
else:
self.start_movie = False
elif self.video_reset:
self.video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
ret, self.video_frame = self.video_cap.read()
self.next_frame(self.video_frame)
self.video_reset = False
elif self.run_one_frame:
ret, self.video_frame = self.video_cap.read()
self.next_frame(self.video_frame)
self.run_one_frame = False
def next_frame(self, frame):
# convert color order from BGR to RGB
pil = self.cvtopli_color_convert(frame)
self.effect_img = self.resize_image(pil)
# To escape error. when close,not to run ImageTk.PhotoImage.
if self.set_movie == False:
return
self.converted_img = self.effect_image(self.effect_img)
self.canvas_create = self.resizeimg_canvas(self.converted_img, self.canvas)
self.canvas.photo = ImageTk.PhotoImage(self.converted_img)
return self.canvas.itemconfig(self.canvas_create, image=self.canvas.photo)
def adjust_scale_value(self, value):
if self.video_frame is None:
print("None")
else:
cap_scale = self.cap_scale.get()
total_frame_count = int(self.video_cap.get(cv2.CAP_PROP_FRAME_COUNT))
read_cap_num = total_frame_count * (1 + cap_scale / 100)
self.video_cap.set(cv2.CAP_PROP_POS_FRAMES, read_cap_num)
self.run_one_frame()
def cvtopli_color_convert(self, video):
rgb = cv2.cvtColor(video, cv2.COLOR_BGR2RGB)
return Image.fromarray(rgb)
# Model
def resize_image(self, img):
w = img.width
h = img.height
if w > h:
resize_img = img.resize((int(w * (700 / w)), int(h * (700 / w))))
else:
resize_img = img.resize((int(w * (500 / h)), int(h * (500 / h))))
return resize_img
def resizeimg_canvas(self, resized_img, canvas):
w = resized_img.width
h = resized_img.height
w_offset = 350 - (w / 2)
h_offset = 250 - (h / 2)
self.pil_img = ImageTk.PhotoImage(resized_img)
canvas.delete("can_pic")
if w > h:
resized_img_canvas = canvas.create_image(
0, h_offset, anchor="nw", image=self.pil_img, tag="can_pic"
)
else:
resized_img_canvas = canvas.create_image(
w_offset, 0, anchor="nw", image=self.pil_img, tag="can_pic"
)
return resized_img_canvas
def get_path(self):
return filedialog.askopenfilename(
title="Please select image file", filetypes=self.file_filter
)
def effect_image(self, effect_img):
func_name = self.func_combobox.get()
convert_gray_img = self.retouch_gray_scale(effect_img)
if func_name == "Gray_scale":
converted_img = convert_gray_img
elif func_name == "Binarization":
converted_img = self.retouch_binarization(convert_gray_img)
elif func_name == "Sepia":
converted_img = self.retouch_sepia(convert_gray_img)
elif func_name == "Default":
converted_img = effect_img
elif func_name == "Jagged_mosaic":
converted_img = self.retouch_jagged_mosaic(effect_img)
elif func_name == "Soft_mosaic":
converted_img = self.retouch_soft_mosaic(effect_img)
elif func_name == "Quantize":
converted_img = self.retouch_alpha_blend(effect_img)
elif func_name == "Invert":
converted_img = self.retouch_invert(effect_img)
elif func_name == "Mirror":
converted_img = self.retouch_mirror(effect_img)
elif func_name == "Flip":
converted_img = self.retouch_flip(effect_img)
elif func_name == "Posterize":
converted_img = self.retouch_posterize(effect_img)
elif func_name == "Solarize":
converted_img = self.retouch_solarize(effect_img)
elif func_name == "Equalize":
converted_img = self.retouch_equalize(effect_img)
elif func_name == "Counter":
converted_img = self.retouch_counter(effect_img)
elif func_name == "Emboss":
converted_img = self.retouch_emboss(effect_img)
elif func_name == "Find_emboss":
converted_img = self.retouch_findemboss(effect_img)
return converted_img
# effect image model
def retouch_gray_scale(self, ef_img):
return ef_img.convert("L")
def retouch_binarization(self, ef_img):
return ef_img.point(lambda x: 0 if x < 230 else x)
def retouch_sepia(self, ef_img):
converted_img = Image.merge(
"RGB",
(
ef_img.point(lambda x: x * 240 / 255),
ef_img.point(lambda x: x * 200 / 255),
ef_img.point(lambda x: x * 145 / 255),
),
)
return converted_img
def retouch_jagged_mosaic(self, ef_img):
return ef_img.resize([x // 8 for x in ef_img.size]).resize(ef_img.size)
def retouch_soft_mosaic(self, ef_img):
gimg = ef_img.filter(ImageFilter.GaussianBlur(4))
return gimg.resize([x // 8 for x in ef_img.size]).resize(ef_img.size)
def retouch_alpha_blend(self, ef_img):
return ef_img.quantize(4)
def retouch_invert(self, ef_img):
return ImageOps.invert(ef_img.convert("RGB"))
def retouch_mirror(self, ef_img):
return ImageOps.mirror(ef_img.convert("RGB"))
def retouch_flip(self, ef_img):
return ImageOps.flip(ef_img.convert("RGB"))
def retouch_posterize(self, ef_img):
return ImageOps.posterize(ef_img.convert("RGB"), 2)
def retouch_solarize(self, ef_img):
return ImageOps.solarize(ef_img.convert("RGB"), 128)
def retouch_equalize(self, ef_img):
return ImageOps.equalize(ef_img.convert("RGB"))
def retouch_counter(self, ef_img):
return ef_img.filter(ImageFilter.CONTOUR)
def retouch_emboss(self, ef_img):
return ef_img.filter(ImageFilter.EMBOSS)
def retouch_findemboss(self, ef_img):
return ef_img.filter(ImageFilter.FIND_EDGES)
def main():
# Tk MainWindow
main_window = tk.Tk()
# Viewクラス生成
Set_gui(main_window)
# フレームループ処理
main_window.mainloop()
if __name__ == "__main__":
main()
フローチャート
main_thread_func
の関数内で行なっている操作をフローチャートにしてみました。set movie、start movieがTureになると、動画を再生することができます。フレームレートを調整するため、1フレームの画像を表示し、待機し、次のフレームの読み出しをします。1フレームの表示では、画像をキャンバスに合わせ、エフェクトをかける操作をしています。そうすることで、動画にエフェクトをかけています。また、リセットを押したり、動画を読み込んだ最初の時は、キャンバス上に画像を表示させるために1フレームの読み出しを行い、フラグを切り替えています。
動画のエフェクト
以前投稿した記事を参考に下記のように関数を設定し、エフェクトをかけています。
def | 概要 |
---|---|
retouch_gray_scale | グレイスケールに変換する(Imageモジュール) |
retouch_binarization | 2値化(Imageモジュール) |
retouch_sepia | セピアに変換する(Imageモジュール) |
retouch_jagged_mosaic | ギザギザモザイク(Imageモジュール) |
retouch_soft_mosaic | やわらかいモザイク(Imageモジュール) |
retouch_alpha_blend | アルファブレンド(Imageモジュール) |
retouch_invert | ネガポジ反転(ImageOpsモジュール) |
retouch_mirror | 左右反転(ImageOpsモジュール) |
retouch_flip | 上下反転(ImageOpsモジュール) |
retouch_posterize | ポスタライズ(ImageOpsモジュール) |
retouch_solarize | ソーラライズ(ImageOpsモジュール) |
retouch_equalize | イコライズ(ImageOpsモジュール) |
retouch_counter | counter表示(ImageFilterモジュール) |
retouch_emboss | emboss表示(ImageFilterモジュール) |
retouch_findemboss | findemboss表示(ImageFilterモジュール) |
FPS
FPS(Frames Per Second)は、1秒あたりのフレーム数(=フレームレート)で、動画の滑らかさを決める要素になります。
テレビやDVDなどは、標準の25~30FPS、一時停止など確認を重視したい場合は、1~5FPSで使用されているそうです。