diff --git a/scripts/Converter.py b/scripts/Converter.py new file mode 100644 index 0000000..5aac8bf --- /dev/null +++ b/scripts/Converter.py @@ -0,0 +1,326 @@ +import customtkinter as Ctk +from tkinter import filedialog +import scripts.get_sys_info as system_code +import cv2 +import threading + + +EPSILON = 1e-9 # choose an appropriate epsilon value + + +class SharedCounter: + def __init__(self): + self._value = 0 + self.lock = threading.Lock() + + @property + def value(self): + with self.lock: + return self._value + + @value.setter + def value(self, new_value): + with self.lock: + self._value = new_value + +class WorkerThread(threading.Thread): + def __init__(self, thread_id, thread_count, last_frame, delay, shared_counter, is_running, video_path, output_path, img_name): + super(WorkerThread, self).__init__() + self.thread_id = thread_id + self.thread_count = thread_count + self.last_frame = last_frame + self.delay = delay + self.shared_counter = shared_counter + self.is_running = is_running + self.video_path = video_path + self.output_path = output_path + self.img_name = img_name + if len(self.img_name) == 0: + self.img_name = "Img_seq" + + + def run(self): + video = cv2.VideoCapture(self.video_path) + if not video.isOpened(): + print(f"Error opening video file {self.video_path}") + return + + for i in range(self.thread_id, self.last_frame, self.thread_count): + video.set(cv2.CAP_PROP_POS_FRAMES, i) # Seek to the correct frame. + + ret, frame = video.read() + # Check if the frame was read successfully + if not ret: + break + # Save the frame as an image + cv2.imwrite(f'{self.output_path}/{self.img_name}_{i+1}.png', frame) + if not self.is_running.value: + break + with self.shared_counter.lock: # Safely increment the shared counter + self.shared_counter._value += 1 + # Release the video file + video.release() + + + + +class Converter(Ctk.CTkFrame): + def __init__(self,master, **kwargs): + super().__init__(master, **kwargs) + # get the threads counts: + # create objects for the classes + self.shared_counter = SharedCounter() + self.is_running = SharedCounter() + #system_code.load_json_file() + self.thread_count = system_code.used_threads + self.continue_offset = 0 + self.my_font = Ctk.CTkFont(family="Berlin Sans FB", size=22) + self.font_entry = Ctk.CTkFont(family="Berlin Sans FB", size=18) + stop_btn_txt = ("Stop Convert", "Continue") + self.test_var = 0 + self.input_path = None + self.output_path = None + self.img_name = None + self.total_frames = None + self.step_size = None + # the layout + # input layout + self.input_entry = Ctk.CTkEntry(self, placeholder_text="input path", width=350, font=self.my_font) + self.input_btn = Ctk.CTkButton(self, text="Browse Input", width=100, command=self.get_video_path, font=self.my_font) + + # output layout + self.output_entry = Ctk.CTkEntry(self, placeholder_text="output path", width=350, font=self.my_font) + self.output_btn = Ctk.CTkButton(self, text="Browse Output", width=100, command=self.get_folder_path, font=self.my_font) + + # button row + img name selection + self.img_naming = Ctk.CTkEntry(self, placeholder_text="Img_seq", width=150, font=self.my_font) + # start Button + self.start_btn = Ctk.CTkButton(self, text="Start Convert", width=100, command=self.start_threads, font=self.my_font) + # stop Button + self.stop_btn = Ctk.CTkButton(self, text=stop_btn_txt[0], width=50, command=self.stop_continue_threads, font=self.my_font) + self.stop_btn.configure(text='Stop', state=Ctk.DISABLED) + self.show_progress() + self.progress_bar.set(0) + self.align() + + def enable_keybinding(self): + self.master.bind("", self.on_a_press, add="+") + + def disable_keybinding(self): + self.master.unbind("") + + def on_a_press(self, event): + # Check if the frame is visible by querying its manager info + if self.winfo_manager(): + self.start_threads() + + + def get_frame_rate_and_last_frame(self, video_path): + """ + Determines the frame rate of a video file and returns the index of the last frame using OpenCV. + + :param video_path: The path to the video file. + :return: A tuple containing the frame rate and the index of the last frame of the video. + """ + # Open the video file using OpenCV's VideoCapture class + video = cv2.VideoCapture(video_path) + + # Check if the video was opened successfully + if not video.isOpened(): + raise ValueError(f"Failed to open video file at path: {video_path}") + + # Get the total number of frames and the frame rate of the video + total_frames = int(video.get(cv2.CAP_PROP_FRAME_COUNT)) + frame_rate = video.get(cv2.CAP_PROP_FPS) + + # Check if the total number of frames is positive + if total_frames <= 0: + raise ValueError(f"Invalid total number of frames: {total_frames}") + + # Check if the frame rate is positive + if frame_rate <= 0: + raise ValueError(f"Invalid frame rate: {frame_rate}") + + # Index of the last frame + last_frame_index = total_frames - 1 + + # Release the video file + video.release() + + return last_frame_index + + + def show_progress(self): + # Progress + # progressbar + self.progress_bar = Ctk.CTkProgressBar(self,orientation="horizontal", width=500, height=30) + # progressinfo + self.progress_info = Ctk.CTkLabel(self, text="", width=1, font=self.my_font) + # button row + self.progress_bar.place( + relx=0.5, + rely=0.6, + anchor="center", + relwidth=0.8, + relheight=0.03 + ) # Position the converter button to the left + self.progress_info.place( + relx=0.5, + rely=0.7, + anchor="center", + relwidth=0.35, + relheight=0.06 + ) # Position the converter button to the left + + def update_progress(self, total_frames, last_counter_value): + current_counter_value = self.shared_counter.value + + if current_counter_value == total_frames: + # Disable the stop button and display "Finished! :)" + self.progress_bar.set(1) + self.stop_btn.configure(state="disabled") + self.start_btn.configure(state="enabled") + self.input_btn.configure(state="enabled") + self.output_btn.configure(state="enabled") + self.progress_info.configure(text="Finished! :)") + self.is_running.value = False + self.stop_threads() + elif current_counter_value > last_counter_value: + jump = current_counter_value / self.total_frames + self.progress_bar.set(jump) + self.progress_info.configure(text=f"{int(self.progress_bar.get()*100)} %") + self.after(1, lambda: self.update_progress(total_frames, current_counter_value)) + else: + # If the counter value did not change, schedule the next update + self.after(1, lambda: self.update_progress(total_frames, current_counter_value)) + + def get_video_path(self): + video_path = filedialog.askopenfilename(filetypes=[('Video files', '*.mp4 *.avi *.mov')]) + self.input_entry.delete(0, Ctk.END) # Delete any existing text in the Entry widget + self.input_entry.insert(0, video_path) # Insert the new text + + def get_folder_path(self): + file_path = filedialog.askdirectory() + self.output_entry.delete(0, Ctk.END) # Delete any existing text in the Entry widget + self.output_entry.insert(0, file_path) # Insert the new text + + def start_threads(self): + self.input_btn.configure(state="disabled") + self.output_btn.configure(state="disabled") + self.start_btn.configure(state="disabled") + self.input_path = self.input_entry.get() + self.output_path = self.output_entry.get() + self.img_name = self.img_naming.get() + self.total_frames = self.get_frame_rate_and_last_frame(self.input_path) + self.step_size = 1 / self.total_frames * 100 /2 + self.progress_bar.configure(determinate_speed=self.step_size) + self.progress_bar.set(0) + + self.is_running.value = True + self.continue_offset = 0 # Reset the continue offset to start from the beginning + self.shared_counter.value = 0 # Reset the counter + self.threads = [] + for i in range(0,self.thread_count): + thread = WorkerThread(i, self.thread_count, self.total_frames, 100, self.shared_counter, self.is_running, self.input_path, self.output_path, self.img_name) + self.threads.append(thread) + thread.start() + self.update_progress(self.total_frames, 0) + self.start_btn.configure(state=Ctk.DISABLED) + self.stop_btn.configure(text='Stop', state=Ctk.NORMAL) + + def stop_threads(self): + self.is_running.value = False + for thread in self.threads: + thread.join() + self.continue_offset = self.shared_counter.value # Save the current counter value for continue + if self.shared_counter.value != self.total_frames: + self.stop_btn.configure(text='Continue', state=Ctk.NORMAL) + # Close all OpenCV windows + cv2.destroyAllWindows() + + + def stop_continue_threads(self): + self.start_btn.configure(state=Ctk.NORMAL) + self.input_btn.configure(state=Ctk.NORMAL) + self.output_btn.configure(state=Ctk.NORMAL) + if self.is_running.value: + self.stop_threads() + else: + self.continue_threads() + + def continue_threads(self): + self.is_running.value = True + self.threads = [] + + for i in range(self.thread_count): + thread = WorkerThread(i, self.thread_count, self.total_frames, 100, self.shared_counter, self.is_running, self.input_path, self.output_path, self.img_name) + self.threads.append(thread) + thread.start() + + self.after(100, self.update_progress(self.total_frames, 0)) + self.stop_btn.config(text='Stop', state="normal") + + def calculate_step_size(self, frame_count): + """This function calculates the the step size for the + progressbar. The Progressbar does 0.02 steps normally.""" + percent = frame_count / 100 # find out how many frames are 1% + percent = 1 / percent # find out how many % one frame is + percent = percent * 0.5 # half that, because tkinter makes 0.02 with each step + return percent + + def align(self): + self.enable_keybinding() + # output placing + self.input_entry.place( + relx=0.10, + rely=0.3, + anchor="w", + relwidth=0.5, + relheight=0.06 + ) # Position the converter button to the left + self.input_btn.place( + relx=0.90, + rely=0.3, + anchor="e", + relwidth=0.2, + relheight=0.06 + ) # Position the converter button to the left + + # output placing + self.output_entry.place( + relx=0.10, + rely=0.4, + anchor="w", + relwidth=0.5, + relheight=0.06 + ) # Position the converter button to the left + self.output_btn.place( + relx=0.90, + rely=0.4, + anchor="e", + relwidth=0.2, + relheight=0.06 + ) # Position the converter button to the left + self.img_naming.place( + relx=0.1, + rely=0.5, + anchor="w", + relwidth=0.2, + relheight=0.06 + ) # Position the converter button to the left + # button row + self.start_btn.place( + relx=0.325, + rely=0.5, + anchor="w", + relwidth=0.15, + relheight=0.06 + ) # Position the converter button to the left + self.stop_btn.place( + relx=0.6, + rely=0.5, + anchor="e", + relwidth=0.1, + relheight=0.06 + ) # Position the converter button to the left + \ No newline at end of file