diff --git a/rope/Dicts.py b/rope/Dicts.py index 483099c9..0aa7ff37 100644 --- a/rope/Dicts.py +++ b/rope/Dicts.py @@ -249,6 +249,9 @@ 'VirtualCameraSwitchState': False, 'VirtualCameraSwitchInfoText': 'VIRTUAL CAMERA:\nFeed the swapped video output to virtual camera for using in external applications', +'ResolutionOverrideSwitchState': False, +'ResolutionOverrideSwitchInfoText': 'OVERRIDE RESOLUTION:\nPlayback and recording will be downsampled to the specified height while maintaining aspect ratio. May help performance. Does not upsample.', + 'RestoreEyesSwitchInfoText': 'RESTORE EYES: \nRestore eyes from the original face', 'RestoreEyesSwitchState': False, 'RestoreMouthSwitchInfoText': 'RESTORE MOUTH: \nRestore mouth from the original face', @@ -610,6 +613,11 @@ 'OrientSliderInfoText': 'ORIENTATION ANGLE:\nSet this to the angle of the input face angle to help with laying down/upside down/etc. Angles are read clockwise.', 'OrientSliderMax': 270, 'OrientSliderMin': 0, +'HeightOverrideSliderAmount': 480, +'HeightOverrideSliderInc': 60, +'HeightOverrideSliderInfoText': 'HEIGHT:\nSpecifies a height at which the video should be downsampled in playback and recording.', +'HeightOverrideSliderMax': 2160, +'HeightOverrideSliderMin': 120, 'RestorerSliderAmount': 100, 'RestorerSliderInc': 5, 'RestorerSliderInfoText': 'RESTORER AMOUNT:\nBlends the Restored results back into the original swap.', @@ -791,6 +799,17 @@ 'SwapperTypeTextSelMode': '128', 'SwapperTypeTextSelModes': ['128', '256', '512'], +# Frame skip +'FrameSkipModeTextSelInfoText': 'FRAME SKIP MODE:\nAllow video playback to skip frames. Does not affect recording.\nMANUAL: Set a constant skip rate.\nAUTO: Allow Rope to set the number of frames to skip based on the video playback speed, trying to maintain the original timing.', +'FrameSkipModeTextSelMode': 'none', +'FrameSkipModeTextSelModes': ['none', 'manual', 'auto'], + +'FramesToSkipInfoText': 'FRAMES TO SKIP (MANUAL): How many frames to explicitly skip when set to "manual". Has no effect otherwise.', +'FramesToSkipAmount': 1, +'FramesToSkipInc': 1, +'FramesToSkipMin': 1, +'FramesToSkipMax': 10, +# # Text Entry 'CLIPTextEntry': '', 'CLIPTextEntryInfoText': 'TEXT MASKING ENTRY:\nTo use, type a word(s) in the box separated by commas and press .', diff --git a/rope/GUI.py b/rope/GUI.py index 4e6c4827..c799c6a6 100644 --- a/rope/GUI.py +++ b/rope/GUI.py @@ -33,6 +33,7 @@ from rope.Dicts import CAMERA_BACKENDS from rope.FaceLandmarks import FaceLandmarks from rope.FaceEditor import FaceEditor +from rope.Hovertip import RopeHovertip import gc class GUI(tk.Tk): @@ -204,6 +205,13 @@ def load_shortcuts_from_json(): self.bind('', self.handle_key_press) self.bind("", lambda event: self.focus_set()) + + @staticmethod + def bind_scroll_events(widget, callback): + widget.bind("",lambda event: callback(event, delta=-int(event.delta / 120))) # Windows + widget.bind("", lambda event: callback(event, delta=-1)) # Unix + widget.bind("", lambda event: callback(event, delta=1)) # Unix + def handle_key_press(self, event): if isinstance(self.focus_get(), tk.Entry): return @@ -798,8 +806,8 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed # Input Videos Canvas self.target_media_canvas = tk.Canvas(self.layer['InputVideoFrame'], style.canvas_frame_label_3, height=100, width=195) self.target_media_canvas.grid(row=1, column=0, sticky='NEWS', padx=10, pady=10) - self.target_media_canvas.bind("", self.target_videos_mouse_wheel) self.target_media_canvas.create_text(8, 20, anchor='w', fill='grey25', font=("Arial italic", 20), text=" Input Videos") + self.bind_scroll_events(self.target_media_canvas, self.target_videos_mouse_wheel) # Scroll Canvas scroll_canvas = tk.Canvas(self.layer['InputVideoFrame'], style.canvas_frame_label_3, bd=0, ) @@ -821,7 +829,9 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed # Scroll Canvas self.source_faces_canvas = tk.Canvas(self.layer['InputVideoFrame'], style.canvas_frame_label_3, height = 100, width=195) self.source_faces_canvas.grid(row=1, column=2, sticky='NEWS', padx=10, pady=10) - self.source_faces_canvas.bind("", self.source_faces_mouse_wheel) + + self.bind_scroll_events(self.source_faces_canvas, self.source_faces_mouse_wheel) + self.source_faces_canvas.create_text(8, 20, anchor='w', fill='grey25', font=("Arial italic", 20), text=" Input Faces") scroll_canvas = tk.Canvas(self.layer['InputVideoFrame'], style.canvas_frame_label_3, bd=0, ) @@ -881,7 +891,7 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed # Preview Window self.video = tk.Label(self.layer['preview_column'], bg='black') self.video.grid(row=1, column=0, sticky='NEWS', padx=0, pady=0) - self.video.bind("", self.iterate_through_merged_embeddings) + self.bind_scroll_events(self.video, self.iterate_through_merged_embeddings) self.video.bind("", lambda event: self.toggle_play_video()) # Videos @@ -977,7 +987,7 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed # Scroll Canvas self.found_faces_canvas = tk.Canvas(ff_frame, style.canvas_frame_label_3, height = 100 ) self.found_faces_canvas.grid( row = 0, column = 1, sticky='NEWS') - self.found_faces_canvas.bind("", self.target_faces_mouse_wheel) + self.bind_scroll_events(self.found_faces_canvas, self.target_faces_mouse_wheel) self.found_faces_canvas.create_text(8, 45, anchor='w', fill='grey25', font=("Arial italic", 20), text=" Found Faces") self.static_widget['23'] = GE.Separator_y(ff_frame, 111, 0) @@ -1006,7 +1016,7 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed self.merged_faces_canvas = tk.Canvas(mf_frame, style.canvas_frame_label_3, height = 100) self.merged_faces_canvas.grid( row = 0, column = 1, sticky='NEWS') self.merged_faces_canvas.grid_rowconfigure(0, weight=1) - self.merged_faces_canvas.bind("", lambda event: self.merged_faces_canvas.xview_scroll(-int(event.delta/120.0), "units")) + self.bind_scroll_events(self.merged_faces_canvas, lambda event, delta: self.merged_faces_canvas.xview_scroll(delta, "units")) self.merged_faces_canvas.create_text(8, 45, anchor='w', fill='grey25', font=("Arial italic", 20), text=" Merged Faces") self.static_widget['24'] = GE.Separator_y(mf_frame, 111, 0) @@ -1042,10 +1052,11 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed self.widget['DefaultParamsButton'] = GE.Button(frame, 'DefaultParamsButton', 2, self.parameter_io, 'default', 'control', x=0 , y=8, width=100) self.layer['parameters_canvas'] = tk.Canvas(self.layer['parameter_frame'], style.canvas_frame_label_3, bd=0, width=width) - self.layer['parameters_canvas'].grid(row=1, column=0, sticky='NEWS', pady=0, padx=0) + self.parameters_canvas = self.layer['parameters_canvas'] + self.parameters_canvas.grid(row=1, column=0, sticky='NEWS', pady=0, padx=0) # Face Editor - tabview_main = ctk.CTkTabview(self.layer['parameters_canvas'], width=398, height=2050, corner_radius=6, border_width=1, + tabview_main = ctk.CTkTabview(self.parameters_canvas, width=398, height=2050, corner_radius=6, border_width=1, fg_color=style.main, border_color=style.main3, segmented_button_selected_hover_color='#b1b1b2', segmented_button_unselected_hover_color=style.main, @@ -1057,7 +1068,7 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed tabview_main.pack(fill='both', expand=True) # Utilizza pack per gestire il layout all'interno del Canvas # Inserisci il CTkTabview nel Canvas usando create_window - self.layer['parameters_canvas'].create_window(0, 0, window=tabview_main, anchor='nw') + self.parameters_canvas.create_window(0, 0, window=tabview_main, anchor='nw') # Aggiungi Tabs al CTkTabview tab_face_swapper = tabview_main.add("Face Swapper ") @@ -1070,11 +1081,14 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed self.layer['parameters_face_editor_frame'].grid(row=0, column=0, sticky='NEWS', pady=0, padx=0) self.layer['parameter_scroll_canvas'] = tk.Canvas(self.layer['parameter_frame'], style.canvas_frame_label_3, bd=0, ) - self.layer['parameter_scroll_canvas'].grid(row=1, column=1, sticky='NEWS', pady=0) - self.layer['parameter_scroll_canvas'].grid_rowconfigure(0, weight=1) - self.layer['parameter_scroll_canvas'].grid_columnconfigure(0, weight=1) + parameter_scroll_canvas = self.layer['parameter_scroll_canvas'] + parameter_scroll_canvas.grid(row=1, column=1, sticky='NEWS', pady=0) + parameter_scroll_canvas.grid_rowconfigure(0, weight=1) + parameter_scroll_canvas.grid_columnconfigure(0, weight=1) - self.static_widget['parameters_scrollbar'] = GE.Scrollbar_y(self.layer['parameter_scroll_canvas'], self.layer['parameters_canvas']) + self.static_widget['parameters_scrollbar'] = GE.Scrollbar_y(parameter_scroll_canvas, self.parameters_canvas) + + self.bind_scroll_events(parameter_scroll_canvas, self.parameters_mouse_wheel) self.static_widget['30'] = GE.Separator_x(parameters_control_frame, 0, 41) @@ -1116,6 +1130,18 @@ def apply_params_visibility_configuration(params_visibility=None, params_face_ed row = row + 1 self.widget['VirtualCameraSwitch'] = GE.Switch2(self.layer['parameters_frame'], 'VirtualCameraSwitch', 'Send Frames to Virtual Camera', 3, self.toggle_virtualcam, 'control', 398, 20, row, 0, padx, pady) + # Resolution override + row = row + 1 + self.widget['ResolutionOverrideSwitch'] = GE.Switch2(self.layer['parameters_frame'], 'ResolutionOverrideSwitch', 'Override Resolution', 3, self.update_data, 'parameter', 398, 20, row, 0, padx, pady) + row = row + 1 + self.widget['HeightOverrideSlider'] = GE.Slider2(self.layer['parameters_frame'], 'HeightOverrideSlider', 'Height', 3, self.update_data, 'parameter', 398, 20, row, 0, padx, pady, 0.72) + + # Frame Skip + row = row + 1 + self.widget['FrameSkipModeTextSel'] = GE.TextSelection(self.layer['parameters_frame'], 'FrameSkipModeTextSel', 'Frame Skip Mode', 3, self.update_data, 'parameter', 'parameter', 398, 20, row, 0, padx, pady, 0.72) + row = row + 1 + self.widget['FramesToSkip'] = GE.Slider2(self.layer['parameters_frame'], 'FramesToSkip', 'Frames to skip', 3, self.update_data, 'parameter', 398, 20, row, 0, padx, pady, 0.72) + # Restore row = row + 1 self.widget['RestorerSwitch'] = GE.Switch2(self.layer['parameters_frame'], 'RestorerSwitch', 'Restorer', 3, self.update_data, 'parameter', 398, 20, row, 0, padx, pady) @@ -1669,27 +1695,32 @@ def update_param_visibility(self, name, visible): def callback(self, url): webbrowser.open_new_tab(url) - def target_faces_mouse_wheel(self, event): - self.found_faces_canvas.xview_scroll(1*int(event.delta/120.0), "units") + def target_faces_mouse_wheel(self, event, delta = 0): + self.found_faces_canvas.xview_scroll(delta, "units") - def source_faces_mouse_wheel(self, event): - self.source_faces_canvas.yview_scroll(-int(event.delta/120.0), "units") + def source_faces_mouse_wheel(self, event, delta=0): + self.source_faces_canvas.yview_scroll(delta, "units") # Center of visible canvas as a percentage of the entire canvas center = (self.source_faces_canvas.yview()[1]-self.source_faces_canvas.yview()[0])/2 center = center+self.source_faces_canvas.yview()[0] self.static_widget['input_faces_scrollbar'].set(center) - def target_videos_mouse_wheel(self, event): - self.target_media_canvas.yview_scroll(-int(event.delta/120.0), "units") + def target_videos_mouse_wheel(self, event, delta = 0): + self.target_media_canvas.yview_scroll(delta, "units") # Center of visible canvas as a percentage of the entire canvas center = (self.target_media_canvas.yview()[1]-self.target_media_canvas.yview()[0])/2 center = center+self.target_media_canvas.yview()[0] self.static_widget['input_videos_scrollbar'].set(center) - def parameters_mouse_wheel(self, event): - self.canvas.yview_scroll(1*int(event.delta/120.0), "units") + def parameters_mouse_wheel(self, event, delta = 0): + self.parameters_canvas.yview_scroll(delta, "units") + + # Center of visible canvas as a percentage of the entire canvas + center = (self.parameters_canvas.yview()[1]-self.parameters_canvas.yview()[0])/2 + center = center+self.parameters_canvas.yview()[0] + self.static_widget['parameters_scrollbar'].set(center) # focus_get() # def preview_control(self, event): @@ -1770,6 +1801,7 @@ def preview_control(self, event): if event == ' ': self.toggle_play_video() elif event == 'w': + self.add_action("play_video", "stop") frame += 1 if frame > video_length: frame = video_length @@ -1777,6 +1809,7 @@ def preview_control(self, event): self.add_action("get_requested_video_frame", frame) # self.parameter_update_from_marker(frame) elif event == 's': + self.add_action("play_video", "stop") frame -= 1 if frame < 0: frame = 0 @@ -1784,6 +1817,7 @@ def preview_control(self, event): self.add_action("get_requested_video_frame", frame) # self.parameter_update_from_marker(frame) elif event == 'd': + self.add_action("play_video", "stop") frame += 30 if frame > video_length: frame = video_length @@ -1791,6 +1825,7 @@ def preview_control(self, event): self.add_action("get_requested_video_frame", frame) # self.parameter_update_from_marker(frame) elif event == 'a': + self.add_action("play_video", "stop") frame -= 30 if frame < 0: frame = 0 @@ -1798,6 +1833,7 @@ def preview_control(self, event): self.add_action("get_requested_video_frame", frame) # self.parameter_update_from_marker(frame) elif event == 'q': + self.add_action("play_video", "stop") frame = 0 self.video_slider.set(frame) self.add_action("get_requested_video_frame", frame) @@ -2022,7 +2058,7 @@ def load_dfl_input_models(self): new_source_face["TKButton"] = tk.Button(self.merged_faces_canvas, style.media_button_off_3, image=self.blank, text=button_text, height=14, width=text_width, compound='left', anchor='w') new_source_face["TKButton"].bind("", lambda event, arg=j: self.select_input_faces(event, arg)) - new_source_face["TKButton"].bind("", lambda event: self.merged_faces_canvas.xview_scroll(-int(event.delta/120.0), "units")) + self.bind_scroll_events(new_source_face["TKButton"], lambda event, delta: self.merged_faces_canvas.xview_scroll(delta, "units")) new_source_face['TextWidth'] = text_width x_width = 20 if len(self.source_faces)>0: @@ -2071,7 +2107,7 @@ def load_input_faces(self): new_source_face["TKButton"] = tk.Button(self.merged_faces_canvas, style.media_button_off_3, image=self.blank, text=temp0[j][0], height=14, width=text_width, compound='left', anchor='w') new_source_face["TKButton"].bind("", lambda event, arg=j: self.select_input_faces(event, arg)) - new_source_face["TKButton"].bind("", lambda event: self.merged_faces_canvas.xview_scroll(-int(event.delta/120.0), "units")) + self.bind_scroll_events(new_source_face["TKButton"], lambda event, delta: self.merged_faces_canvas.xview_scroll(delta, "units")) new_source_face['TextWidth'] = text_width x_width = 20 if len(self.source_faces)>0: @@ -2094,6 +2130,8 @@ def load_input_faces(self): directory = self.json_dict["source faces"] filenames = [os.path.join(dirpath,f) for (dirpath, dirnames, filenames) in os.walk(directory) for f in filenames] + filenames = sorted(filenames, key=str.lower) + # torch.cuda.memory._record_memory_history(True, trace_alloc_max_entries=100000, trace_alloc_record_context=True) i=0 for file in filenames: # Does not include full path @@ -2138,21 +2176,21 @@ def load_input_faces(self): else: face_emb, cropped_image = self.models.run_recognize(img, kpss_5, self.parameters["SimilarityTypeTextSel"], self.parameters['FaceSwapperModelTextSel']) crop = cv2.cvtColor(cropped_image.cpu().numpy(), cv2.COLOR_BGR2RGB) - crop = cv2.resize(crop, (85, 85)) + crop = cv2.resize(crop, (50, 50)) new_source_face = self.source_face.copy() self.source_faces.append(new_source_face) self.source_faces[-1]["Image"] = ImageTk.PhotoImage(image=Image.fromarray(crop)) self.source_faces[-1]["Embedding"] = face_emb - self.source_faces[-1]["TKButton"] = tk.Button(self.source_faces_canvas, style.media_button_off_3, image=self.source_faces[-1]["Image"], height=90, width=90) + self.source_faces[-1]["TKButton"] = tk.Button(self.source_faces_canvas, style.media_button_off_3, image=self.source_faces[-1]["Image"], height=55, width=55) self.source_faces[-1]["ButtonState"] = False self.source_faces[-1]["file"] = file self.source_faces[-1]["TKButton"].bind("", lambda event, arg=len(self.source_faces)-1: self.select_input_faces(event, arg)) - self.source_faces[-1]["TKButton"].bind("", self.source_faces_mouse_wheel) + self.bind_scroll_events(self.source_faces[-1]["TKButton"], self.source_faces_mouse_wheel) - self.source_faces_canvas.create_window((i % 2) * 100, (i // 2) * 100, window=self.source_faces[-1]["TKButton"], anchor='nw') + self.source_faces_canvas.create_window((i % 3) * 65, (i // 3) * 65, window=self.source_faces[-1]["TKButton"], anchor='nw') self.static_widget['input_faces_scrollbar'].resize_scrollbar(None) i = i + 1 @@ -2209,7 +2247,7 @@ def find_faces(self): last_index = len(self.target_faces)-1 self.target_faces[last_index]["TKButton"] = tk.Button(self.found_faces_canvas, style.media_button_off_3, height = 86, width = 86) - self.target_faces[last_index]["TKButton"].bind("", self.target_faces_mouse_wheel) + self.bind_scroll_events(self.target_faces[last_index]["TKButton"], self.target_faces_mouse_wheel) self.target_faces[last_index]["ButtonState"] = False self.target_faces[last_index]["Image"] = ImageTk.PhotoImage(image=Image.fromarray(crop)) self.target_faces[last_index]["Embedding"] = face[1] @@ -2389,6 +2427,8 @@ def populate_target_videos(self): directory = self.json_dict["source videos"] filenames = [os.path.join(dirpath,f) for (dirpath, dirnames, filenames) in os.walk(directory) for f in filenames] + filenames = sorted(filenames, key=str.lower) + images = [] self.target_media = [] self.target_media_buttons = [] @@ -2454,7 +2494,7 @@ def populate_target_videos(self): rgb_video = Image.fromarray(images[i][0]) self.target_media.append(ImageTk.PhotoImage(image=rgb_video)) self.target_media_buttons[i].config( image = self.target_media[i], command=lambda i=i: self.load_target(i, images[i][1], self.widget['PreviewModeTextSel'].get())) - self.target_media_buttons[i].bind("", self.target_videos_mouse_wheel) + self.bind_scroll_events(self.target_media_buttons[i], self.target_videos_mouse_wheel) self.target_media_canvas.create_window(0, i*dely, window = self.target_media_buttons[i], anchor='nw') #self.target_media_canvas.configure(scrollregion = self.target_media_canvas.bbox("all")) @@ -2467,10 +2507,11 @@ def populate_target_videos(self): self.target_media.append(ImageTk.PhotoImage(image=Image.fromarray(videos[i][0]))) filename = os.path.basename(videos[i][1]) + hovertip = RopeHovertip(self.target_media_buttons[i], filename, x_offset=190) if len(filename)>32: filename = filename[:29]+'...' - self.target_media_buttons[i].bind("", self.target_videos_mouse_wheel) + self.bind_scroll_events(self.target_media_buttons[i], self.target_videos_mouse_wheel) self.target_media_buttons[i].config(image = self.target_media[i], text=filename, compound='top', anchor='n',command=lambda i=i: self.load_target(i, videos[i][1], self.widget['PreviewModeTextSel'].get())) self.target_media_canvas.create_window(0, i*dely, window = self.target_media_buttons[i], anchor='nw') @@ -2490,6 +2531,7 @@ def auto_swap(self): pass def toggle_auto_swap(self): + print("toggle auto swap") self.widget['AutoSwapButton'].toggle_button() def load_target(self, button, media_file, media_type): @@ -2872,13 +2914,13 @@ def delete_merged_embedding(self): #add multi select self.load_input_faces() - def iterate_through_merged_embeddings(self, event): - if event.delta>0: + def iterate_through_merged_embeddings(self, event, delta): + if delta>0: for i in range(len(self.source_faces)): if self.source_faces[i]["ButtonState"] and i0: self.select_input_faces('none', i-1) diff --git a/rope/GUIElements.py b/rope/GUIElements.py index 94acd3f9..430b384f 100644 --- a/rope/GUIElements.py +++ b/rope/GUIElements.py @@ -1,6 +1,7 @@ import tkinter as tk from tkinter import font from PIL import Image, ImageTk +import time from rope.Dicts import DEFAULT_DATA import rope.Styles as style import customtkinter as ctk @@ -496,6 +497,12 @@ def __init__(self, parent, widget, temp_toggle_swapper, temp_toggle_enhancer, te self.slider_right = [] self.fps = 0 + self.last_update_time = time.time() + self.fps_history = [] + self.fps_history_limit = 30 + self.fps_string = tk.StringVar() + self.fps_string.set("Avg FPS: 0.0/0.0") + self.time_elapsed_string = tk.StringVar() self.time_elapsed_string.set("00:00:00") @@ -509,12 +516,17 @@ def __init__(self, parent, widget, temp_toggle_swapper, temp_toggle_enhancer, te self.slider.bind('', lambda e: self.update_timeline_handle(e, True)) self.slider.bind('', lambda e: self.update_timeline_handle(e, True)) self.slider.bind('', lambda e: self.update_timeline_handle(e, True)) + self.slider.bind('', lambda e: self.update_timeline_handle(e, True)) + self.slider.bind('', lambda e: self.update_timeline_handle(e, True)) # Add the Entry to the frame self.entry_width = 40 self.entry = tk.Entry(self.parent, style.entry_3, textvariable=self.entry_string) self.entry.bind('', lambda event: self.entry_input(event)) + # Add the Average FPS entry + self.fps_entry = tk.Entry(self.parent, style.entry_3, textvariable=self.fps_string, width=24) + # Add the Time Entry to the frame self.time_width = 40 self.time_entry = tk.Entry(self.parent, style.entry_3, textvariable=self.time_elapsed_string, width=8) @@ -526,6 +538,7 @@ def draw_timeline(self): # Configure widths and placements self.slider.configure(width=self.frame_length) self.entry.place(x=self.parent.winfo_width()-self.counter_width, y=0) + self.fps_entry.place(x=8, y=25) self.time_entry.place(x=(self.parent.winfo_width()-self.counter_width-self.time_width-40) / 2, y=25) # Draw the slider @@ -562,21 +575,44 @@ def update_timeline_handle(self, event, also_update_entry=False): requested = True if isinstance(event, float): + + # Record fps calculcation to history + delta_time = time.time() - self.last_update_time + if delta_time > 0.0: + self.fps_history.append(1 / delta_time) + + # Maintain sample size + if len(self.fps_history) > self.fps_history_limit: + self.fps_history.pop(0) + + # Update last_update_time + self.last_update_time = time.time() + + # Update timeline position position = event requested = False else: - if event.type == '38': # mousewheel + if event.type == '38': # windows mousewheel + self.add_action("play_video", "stop") position = self.last_position+int(event.delta/120.0) - elif event.type == '4': # l-button press - x_coord = float(event.x) - position = self.coord2pos(x_coord) + elif event.type == '4': # l-button press or Unix mousewheel - # Turn off swapping, enhancer, face editor - self.temp_toggle_swapper('off') - self.temp_toggle_enhancer('off') - self.temp_toggle_faces_editor('off') - self.add_action("play_video", "stop") + if event.num == 4: # Mouse wheel up (Unix) + self.add_action("play_video", "stop") + position = self.last_position + self.fps + elif event.num == 5: # Mouse wheel down (Unix) + self.add_action("play_video", "stop") + position = self.last_position - self.fps + else: + x_coord = float(event.x) + position = self.coord2pos(x_coord) + + # Turn off swapping, enhancer, face editor + self.temp_toggle_swapper('off') + self.temp_toggle_enhancer('off') + self.temp_toggle_faces_editor('off') + self.add_action("play_video", "stop") elif event.type == '5': # l-button release x_coord = float(event.x) @@ -613,6 +649,10 @@ def update_timeline_handle(self, event, also_update_entry=False): self.entry_string.set(str(position)) self.update_time_elapsed(position) + if self.fps_history: # Check if history is not empty + average_fps = sum(self.fps_history) / len(self.fps_history) + self.fps_string.set(f"Avg FPS: {average_fps:.1f} / {self.fps}") + def entry_input(self, event): # event.char self.entry.update() diff --git a/rope/Hovertip.py b/rope/Hovertip.py new file mode 100644 index 00000000..1eadeaf7 --- /dev/null +++ b/rope/Hovertip.py @@ -0,0 +1,46 @@ +import tkinter as tk +from idlelib.tooltip import Hovertip + +class RopeHovertip(Hovertip): + def __init__(self, widget, text, bg_color='white', font_size=14, delay=1, x_offset=0, y_offset=0, *args, **kwargs): + super().__init__(widget, text, *args, **kwargs) + + self.widget = widget + self.bg_color = bg_color + self.font_size = font_size + self.delay = delay + self.x_offset = x_offset + self.y_offset = y_offset + self.tooltip_active = False + + # Customize tooltip appearance after it's created + self.tooltip = self._create_tooltip(text) + + # Bind events + widget.bind("", self.show_tooltip) + widget.bind("", self.hide_tooltip) + + def _create_tooltip(self, text): + tooltip = tk.Toplevel(self.widget) + tooltip.wm_overrideredirect(True) + tooltip.withdraw() # Hide it initially + label = tk.Label(tooltip, text=text, justify='left', bg=self.bg_color, font=("Helvetica", self.font_size)) + label.pack() + return tooltip + + def show_tooltip(self, event): + if not self.tooltip_active: + self.tooltip_active = True + # Schedule the tooltip to appear after the specified delay + self.tooltip.after(self.delay, self._display_tooltip) + + def _display_tooltip(self): + if self.tooltip_active: + x = self.widget.winfo_rootx() + self.x_offset + y = self.widget.winfo_rooty() + self.y_offset + self.tooltip.geometry(f"+{x}+{y}") + self.tooltip.deiconify() + + def hide_tooltip(self, event): + self.tooltip_active = False + self.tooltip.withdraw() diff --git a/rope/VideoManager.py b/rope/VideoManager.py index b2bd8b83..b181983c 100644 --- a/rope/VideoManager.py +++ b/rope/VideoManager.py @@ -81,6 +81,12 @@ def __init__(self, models ): self.fps = 1.0 self.temp_file = [] + # Frame skipping + self.auto_frame_skip = 0 + self.frames_over_auto_frame_skip_threshold = 0 + self.frames_under_auto_frame_skip_threshold = 0 + self.auto_frame_skip_tolerance = 15 + self.start_time = [] self.record = False self.output = [] @@ -108,7 +114,7 @@ def __init__(self, models ): "FrameNumber": [], "ProcessedFrame": [], "Status": 'clear', - "ThreadTime": [] + "ThreadTime": 0.0 } self.process_qs = [] self.rec_q = { @@ -190,6 +196,11 @@ def load_target_video( self, file ): self.is_image_loaded = False if not self.webcam_selected(file): self.video_frame_total = int(self.capture.get(cv2.CAP_PROP_FRAME_COUNT)) + + self.auto_frame_skip = 0 + self.frames_over_auto_frame_skip_threshold = 0 + self.frames_under_auto_frame_skip_threshold = 0 + else: self.video_frame_total = 99999999 self.play = False @@ -483,6 +494,69 @@ def terminate_audio_process_tree(self): self.audio_sp = None + def get_current_frame_skip_value(self): + + if 'FrameSkipModeTextSel' not in self.parameters or 'FramesToSkip' not in self.parameters: + return 0 + + if self.parameters['FrameSkipModeTextSel'] == "manual": + return self.parameters['FramesToSkip'] + + if self.parameters['FrameSkipModeTextSel'] == "auto": + return self.auto_frame_skip + + return 0 + + def get_effective_fps_target(self): + if self.record: + return self.fps + + return self.fps / (self.get_current_frame_skip_value() + 1) + + def evaluate_auto_frame_skip(self, actual_delta_time, target_delta_time): + + if self.parameters['FrameSkipModeTextSel'] == "auto": + + delta_difference = actual_delta_time - target_delta_time + + print(f"actual_delta_time: {actual_delta_time}") + print(f"target_delta_time: {target_delta_time}") + print(f"delta_difference: {delta_difference}") + + auto_frame_skip_deviation_threshold = 1.0 / self.fps + + # Increase auto skip + if delta_difference > auto_frame_skip_deviation_threshold: + + self.frames_over_auto_frame_skip_threshold += 1 + self.frames_under_auto_frame_skip_threshold = 0 + + print(f"count_over_threshold: {self.frames_over_auto_frame_skip_threshold}") + + if self.frames_over_auto_frame_skip_threshold > self.auto_frame_skip_tolerance: + self.auto_frame_skip += 1 + self.frames_over_auto_frame_skip_threshold = 0 + + # Decrease auto skip + elif self.auto_frame_skip > 0 and delta_difference < auto_frame_skip_deviation_threshold: + + self.frames_under_auto_frame_skip_threshold += 1 + self.frames_over_auto_frame_skip_threshold = 0 + print(f"count_under_threshold: {self.frames_under_auto_frame_skip_threshold}") + + if self.frames_under_auto_frame_skip_threshold > self.auto_frame_skip_tolerance: + self.auto_frame_skip = max(0, self.auto_frame_skip - 1) + self.frames_under_auto_frame_skip_threshold = 0 + + # Reset auto skip frame counts + else: + self.frames_over_auto_frame_skip_threshold = 0 + self.frames_under_auto_frame_skip_threshold = 0 + + print("counts reset") + + print(f"auto_frame_skip: {self.auto_frame_skip}") + # @profile def process(self): process_qs_len = range(len(self.process_qs)) @@ -491,6 +565,14 @@ def process(self): if self.play == True and self.is_video_loaded == True: for item in self.process_qs: if item['Status'] == 'clear' and self.current_frame < self.video_frame_total: + + skip_frame = not self.record and self.current_frame % (self.get_current_frame_skip_value() + 1) != 0 + if skip_frame: + with lock: + self.capture.grab() # Advance frame without decoding the image + self.current_frame += 1 + continue + item['Thread'] = threading.Thread(target=self.thread_video_read, args = [self.current_frame]).start() item['FrameNumber'] = self.current_frame item['Status'] = 'started' @@ -505,18 +587,21 @@ def process(self): # Always be emptying the queues time_diff = time.time() - self.frame_timer - if not self.record and time_diff >= 1.0/float(self.fps) and self.play: + target_delta_time = 1.0 / float(self.get_effective_fps_target()) + if self.play and not self.record and time_diff >= target_delta_time: index, min_frame = self.find_lowest_frame(self.process_qs) if index != -1: if self.process_qs[index]['Status'] == 'finished': - temp = [self.process_qs[index]['ProcessedFrame'], self.process_qs[index]['FrameNumber']] + processed_frame_number = self.process_qs[index]['FrameNumber'] + + temp = [self.process_qs[index]['ProcessedFrame'], processed_frame_number] self.frame_q.append(temp) # Report fps, other data self.fps_average.append(1.0/time_diff) - avg_fps = self.fps / self.fps_average[-1] if self.fps_average else 10 + avg_fps = self.get_effective_fps_target() / self.fps_average[-1] if self.fps_average else 10 # self.send_to_virtual_camera(temp[0], 15) if self.control['VirtualCameraSwitch'] and self.virtcam: @@ -526,19 +611,26 @@ def process(self): self.virtcam.sleep_until_next_frame() except Exception as e: print(e) - if len(self.fps_average) >= floor(self.fps): + if len(self.fps_average) >= floor(self.get_effective_fps_target()): fps = round(np.average(self.fps_average), 2) msg = "%s fps, %s process time" % (fps, round(self.process_qs[index]['ThreadTime'], 4)) self.fps_average = [] - if self.process_qs[index]['FrameNumber'] >= self.video_frame_total-1 or self.process_qs[index]['FrameNumber'] == self.stop_marker: + if processed_frame_number >= self.video_frame_total-1 or processed_frame_number == self.stop_marker: + print("stop video") self.play_video('stop') + actual_thread_delta_time = self.process_qs[index]['ThreadTime'] + self.process_qs[index]['Status'] = 'clear' self.process_qs[index]['Thread'] = [] self.process_qs[index]['FrameNumber'] = [] - self.process_qs[index]['ThreadTime'] = [] - self.frame_timer += 1.0/self.fps + self.process_qs[index]['ThreadTime'] = 0.0 + + self.frame_timer += target_delta_time + + self.evaluate_auto_frame_skip(actual_thread_delta_time, target_delta_time) + if not self.webcam_selected(self.video_file): if self.record: @@ -607,10 +699,28 @@ def process(self): # @profile def thread_video_read(self, frame_number): + with lock: success, target_image = self.capture.read() if success: + + if self.parameters['ResolutionOverrideSwitch']: + + max_height = self.parameters['HeightOverrideSlider'] + + # Get the original dimensions + height, width = target_image.shape[:2] + + # Check if the frame height is greater than the maximum height + if height > max_height: + # Calculate the scaling factor + scale = max_height / height + new_width = int(width * scale) + new_height = max_height + # Resize the frame + target_image = cv2.resize(target_image, (new_width, new_height)) + target_image = cv2.cvtColor(target_image, cv2.COLOR_BGR2RGB) if not self.control['SwapFacesButton'] and not self.control['EditFacesButton']: temp = [target_image, frame_number] @@ -1692,7 +1802,7 @@ def apply_face_parser(self, img, parameters): 13: parameters['LowerLipParserSlider'], #Lower Lip 14: parameters['NeckParserSlider'], #Neck } - + # Pre-calculated kernel for dilation (3x3 kernel to reduce iterations) kernel = torch.ones((1, 1, 3, 3), dtype=torch.float32, device=self.models.device) # Kernel 3x3 @@ -1884,7 +1994,7 @@ def apply_fake_diff(self, swapped_face, original_face, DiffAmount): def soft_oval_mask(self, height, width, center, radius_x, radius_y, feather_radius=None): """ - Create a soft oval mask with feathering effect using integer operations. + Create a soft oval mask with a feathering effect using integer operations. Args: height (int): Height of the mask. @@ -1897,16 +2007,26 @@ def soft_oval_mask(self, height, width, center, radius_x, radius_y, feather_radi Returns: torch.Tensor: Soft oval mask tensor of shape (H, W). """ + + # Clamp input values to ensure they are valid + + height = max(1, height) # Ensure height is at least 1 + width = max(1, width) # Ensure width is at least 1 + center_x = max(0, min(center[0], width - 1)) + center_y = max(0, min(center[1], height - 1)) + + # Set feather_radius if not provided if feather_radius is None: feather_radius = max(radius_x, radius_y) // 2 # Integer division + feather_radius = max(1, feather_radius) # Ensure feather_radius is at least 1 - # Calculating the normalized distance from the center + # Create meshgrid y, x = torch.meshgrid(torch.arange(height), torch.arange(width), indexing='ij') - # Calculating the normalized distance from the center - normalized_distance = torch.sqrt(((x - center[0]) / radius_x) ** 2 + ((y - center[1]) / radius_y) ** 2) + # Calculate normalized distance from the center + normalized_distance = torch.sqrt(((x - center_x) / radius_x) ** 2 + ((y - center_y) / radius_y) ** 2) - # Creating the oval mask with a feathering effect + # Create the oval mask with a feathering effect mask = torch.clamp((1 - normalized_distance) * (radius_x / feather_radius), 0, 1) return mask