import cv2, os, sys, re, threading, webbrowser, screeninfo import numpy as np import tkinter as tk from tkinterdnd2 import DND_FILES, TkinterDnD from tkinter import filedialog, simpledialog, messagebox, scrolledtext from datetime import datetime import feedparser import subprocess import platform import urllib.request from v2cmodules.toolbox import extract_version, debug, checkinifile, write2ini, extract_changelog, toggle_save, toggle_show from v2cmodules.common import * checkinifile() debug('INFO', "Video2Crops starting ...") # Create the root window root = TkinterDnD.Tk() root.title("Video2Crops") root.geometry(tksize) def open_changelog_window(): debug('DEBUG', 'Loading changelog window') changelog_window = tk.Toplevel(root) changelog_window.title("Video2Crops Changelog") try: debug('DEBUG', f"Reading the RSS feed from: {RSS_FEED_URL}") # Parse the RSS feed feed = feedparser.parse(RSS_FEED_URL) if not feed.entries: scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD) scrolled_text.pack(fill=tk.BOTH, expand=True) scrolled_text.insert(tk.END, "No updates available.") return # Create a single scrolled text widget to display all changelog entries scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD) scrolled_text.pack(fill=tk.BOTH, expand=True) debug('DEBUG', f"Done collecting RSS data") entry_num = 0 for entry in feed.entries: entry_num += 1 version_str = extract_version(entry.link) changelog_md = extract_changelog(entry.content[0].value) if version_str is not None and changelog_md is not None: version_info = f"# v{version_str}" print(version_str) if str(version_str) == str(V2CVERSION) and entry_num == 1: version_info += "(up to date)" elif str(version_str) == str(V2CVERSION): version_info += "(current)" elif entry_num == 1: version_info += "(latest)" version_info += "\n" date_pattern = r'(\w{3}, \d{2} \w{3} \d{4})' pub_date = re.search(date_pattern, entry.published) if pub_date: rel_date = pub_date.group(1) version_info += f"\n{rel_date}\n\n" # Append the version information and changelog entry to the scrolled text widget changelog_text = version_info + changelog_md + f"\n--------------------\n" scrolled_text.insert(tk.END, changelog_text) debug('DEBUG', f"Finished processing changelog") except Exception as e: scrolled_text = scrolledtext.ScrolledText(changelog_window, wrap=tk.WORD) scrolled_text.pack(fill=tk.BOTH, expand=True) scrolled_text.insert(tk.END, f"An error occurred: {str(e)}") debug('ERROR', f"An error occured while processing changelog:\n{str(e)}\n") # Function for updating Video2Crops def check_for_updates(): debug('DEBUG', f"Checking for updates") try: # Parse the RSS feed feed = feedparser.parse(RSS_FEED_URL) if not feed.entries and not STARTING: response = tk.messagebox.showinfo( "No updates found", f"Latest version: ({V2CVERSION})\n" f"Current version: ({V2CVERSION})\n" ) debug('DEBUG', f"No updates found") return # Get the latest release latest_release = feed.entries[0] latest_version_str = extract_version(latest_release.link) if latest_version_str is None and not STARTING: tk.messagebox.showinfo( "No update Available", f"Latest version: ({V2CVERSION})\n" f"Current version: ({V2CVERSION})\n" ) debug('DEBUG', f"No updates available") return # Extract version numbers from the latest release title latest_version = tuple(map(int, latest_version_str.split('.'))) # Compare the versions if latest_version > (V2CV[0], V2CV[1], V2CV[2]): # Prompt the user for an update response = tk.messagebox.askyesno( "Update Available", f"A newer version ({latest_version_str}) is available.\n" f"Current version: ({V2CVERSION})\n" "Do you want to update?" ) debug('DEBUG', f"Found new update, prompting user") if response: debug('DEBUG', f"User chose to update ...") # Determine the platform current_platform = platform.system().lower() if current_platform == "windows": debug('DEBUG', f"Detected Windows system, grabbing installer") # On Windows, download and run the installer installer_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases/download/v{}/Video2Crops-windows-installer.exe".format(latest_version_str) try: urllib.request.urlretrieve(installer_url, "Video2Crops-windows-installer.exe") subprocess.Popen("Video2Crops-windows-installer.exe", shell=True) debug('DEBUG', f"Downloaded new installer, running it ...") root.quit() except: tk.messagebox.showinfo( "Installer not found", f"The installer file was not found.\nPlease check your antivirus and network connectivity" ) debug('ERROR', f"Windows installer not found") elif current_platform == "linux": debug('DEBUG', f"Detected Linux system, grabbing executable") # On Linux, download and run the executable executable_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases/download/v{}/Video2Crops-linux".format(latest_version_str) try: urllib.request.urlretrieve(executable_url, "Video2Crops-linux") os.chmod("Video2Crops-linux", 0o755) subprocess.Popen("./Video2Crops-linux", shell=True) debug('DEBUG', f"Downloaded new executable, running it ...") root.quit() except: tk.messagebox.showinfo( "Executable not found", f"The executable file was not found.\nPlease check your antivirus and network connectivity" ) debug('ERROR', f"Linux executable not found") else: debug('DEBUG', f"Non-supported platform found: {current_platform}") tk.messagebox.showinfo( "Non-supported platform", f"Video2Crops detected non-supported platform: {current_platform}.\nCurrently, only Windows and Linux are supported.\n\nYou can build from source if you need to run Video2Crops on other platforms." ) else: debug('INFO', 'User closed prompt window') else: if not STARTING: tk.messagebox.showinfo( "No update Available", f"Latest version: ({V2CVERSION})\n" f"Current version: ({V2CVERSION})\n" ) debug('DEBUG', f"No update available") except Exception as e: tk.messagebox.showerror("Error", f"An error occurred: {str(e)}") debug('ERROR', f"An error occured while checking for updates:\n{str(e)}\n") # Function to create the output directory and return its path def create_output_directory(video_filename): debug('DEBUG', f"Creating output directory") base_directory = os.getcwd() # You can change this to specify a different base directory video_filename_base, _ = os.path.splitext(os.path.basename(video_filename)) output_folder_path = os.path.join(base_directory, video_filename_base) # Create the output directory if it doesn't exist if not os.path.exists(output_folder_path): os.makedirs(output_folder_path) debug('DEBUG', f"Processing file: {video_filename}") debug('DEBUG', f"Created output directory at: {output_folder_path}") return output_folder_path # Function to calculate ROI position based on alignment and size settings def calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size): y1 = (video_height - roi_size) // 2 x1 = (video_width - roi_size) // 2 if vertical_alignment == "Top": y1 = 0 elif vertical_alignment == "Center": y1 = (video_height - roi_size) // 2 elif vertical_alignment == "Bottom": y1 = video_height - roi_size if horizontal_alignment == "Left": x1 = 0 elif horizontal_alignment == "Center": x1 = (video_width - roi_size) // 2 elif horizontal_alignment == "Right": x1 = video_width - roi_size x2 = x1 + roi_size y2 = y1 + roi_size return x1, y1, x2, y2 # Create a function to process video files def process_video(video_filenames, main_window): debug('DEBUG', f"Starting video processing ...") global processing debug('DEBUG', f"Processing state: {processing}") def write_log(message): logfile_path = os.path.join(output_folder, 'logfile.txt') with open(logfile_path, 'a') as logfile: log_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} - {message}\n" logfile.write(log_message) debug('LOG EVENT', f"{log_message}") def calc_value(image): grayscale = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) return np.std(grayscale) def check_duplicate(ref_frame, frame): global first_frame min_possible_mse = 0 # Calculate the maximum possible MSE (when images are completely different) max_possible_mse = 255**2 * roi_size * roi_size if duplicate_threshold > 0 and not first_frame: frame_is_duplicate = True # Calculate the Absolute Difference between the frames frame_diff = cv2.absdiff(ref_frame, frame) # Calculate the Mean Squared Error (MSE) between the frames mse = np.mean(frame_diff) normalized_mse = 100 * (1 - (mse - min_possible_mse) / (max_possible_mse - min_possible_mse)) if normalized_mse > duplicate_threshold: # The frame is significantly different, store it ref_frame = frame.copy() # Process the frame here or store it for later processing frame_is_duplicate = False debug('DEBUG', f"Frame is duplicate: {frame_is_duplicate}, MSE: {mse}, Normalized MSE: {normalized_mse}") return frame_is_duplicate, mse, ref_frame else: if first_frame: debug('DEBUG', f"Frame is the first frame, storing as initial reference") first_frame = False else: debug('DEBUG', f"First Frame Duplicate check is disabled, ignoring!") return False, 0, frame corner1 = (-1, -1) corner2 = (-1, -1) drawing = False cropped_image = None for videofilename in video_filenames: debug('DEBUG', f"Processing video file: {videofilename}") if not processing: debug('DEBUG', f"Breaking loop due to processing = {processing}") break global first_frame first_frame = True main_window.title(f"Video Processing App") video_file_src = videofilename # Define videofilename_base based on the video file name videofilename_base, _ = os.path.splitext(os.path.basename(videofilename)) cap = cv2.VideoCapture(video_file_src, 0) if not cap.isOpened(): write_log(f"Error: Could not open video file: {video_file_src}") sys.exit() counter = 0 frame_cnt = 0 discarded_frames = 0 # Create the output folder early on output_folder = create_output_directory(videofilename) # Create the "Discarded Frames" subfolder within the output folder discarded_folder = os.path.join(output_folder, 'Discarded Frames') if not os.path.exists(discarded_folder) and SAVE: os.makedirs(discarded_folder) write_log(f"[DEBUG] Output Folder: {output_folder}\n[DEBUG] Discarded Folder: {discarded_folder}") # Initialize ROI parameters from preferences roi_size = int(roi_size_var.get()) vertical_alignment = vertical_alignment_var.get() horizontal_alignment = horizontal_alignment_var.get() debug('DEBUG', f"Displaying video player window: {SHOW}") if SHOW: vp_title = "Video2Crops Video Player" # Get the screen resolution screen = screeninfo.get_monitors()[0] screen_width, screen_height = screen.width, screen.height # Calculate the initial window size as a quarter of the screen resolution initial_width = screen_width // 2 initial_height = screen_height // 2 # Create the video player window cv2.namedWindow(vp_title, cv2.WINDOW_NORMAL) cv2.resizeWindow(vp_title, initial_width, initial_height) while True: ret, frame = cap.read() if not ret: break # Get the video width and height from the frame video_width = frame.shape[1] video_height = frame.shape[0] x1, y1, x2, y2 = calculate_roi(video_width, video_height, vertical_alignment, horizontal_alignment, roi_size) cropped_image_status = "Rejected" frame_copy = frame.copy() if drawing: cv2.rectangle(frame_copy, corner1, corner2, (0, 255, 0), 3) test = cv2.rectangle(frame.copy(), (x1 - 1, y2 - 1), (x2 + 1, y1 + 1), (255, 0, 0), 4) cropped_image = frame[y1:y2, x1:x2] if first_frame: reference_frame = cropped_image if SHOW: cv2.imshow(vp_title, test) key = cv2.waitKey(10) if key == ord('q') or (not processing): debug('DEBUG', f"Breaking loop due to cancel event") break if cropped_image is not None and frame_cnt % frame_step == 0: if SHOW: ci_title = "Video2Crops Cropped Image" cv2.namedWindow(ci_title, cv2.WINDOW_NORMAL) cv2.imshow(ci_title, cropped_image) frame_is_duplicate, frame_duplicate_mse, reference_frame = check_duplicate(reference_frame, cropped_image) frame_value = calc_value(cropped_image) if (frame_value >= contrast_threshold or contrast_threshold == 0) and not frame_is_duplicate: cropped_image_status = "Accepted" filename = videofilename_base + '_' + str(counter).zfill(4) + ".png" image_filename = os.path.join(output_folder, filename) if SAVE: cv2.imwrite(image_filename, cropped_image) cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status} | {frame_duplicate_mse}) - {frame_value} / {contrast_threshold} - image nr: {str(str(counter)).zfill(4)}" write_log(cropped_image_info) counter += 1 else: filename = videofilename_base + '_' + str(discarded_frames).zfill(4) + ".png" image_filename = os.path.join(discarded_folder, filename) if SAVE: cv2.imwrite(image_filename, cropped_image) cropped_image_info = f"Image on frame {frame_cnt} ({cropped_image_status} | {frame_duplicate_mse}) - {frame_value} / {contrast_threshold} - image nr: {str(str(counter)).zfill(4)}" write_log(cropped_image_info) discarded_frames += 1 frame_cnt += 1 if frame_cnt == 1: debug('DEBUG', f"Video size: {video_width}x{video_height}") debug('DEBUG', f"ROI location: {vertical_alignment}-{horizontal_alignment}") debug('DEBUG', f"ROI size: {roi_size}") debug('DEBUG', f"ROI position (left, top, right, bottom): {x1}, {y1}, {x2}, {y2}") cap.release() cv2.destroyAllWindows() debug('DEBUG', f"Destroyed all current CV2 windows") write_log(f"Video2Crops run complete!\nSaved {counter} frames.\nDiscarded {discarded_frames} frames.") processing = False # Create a function to add files to the selected files Listbox def add_files(): global processing_button selected_files = selected_files_listbox.get(0, tk.END) file_paths = filedialog.askopenfilenames(title='Select Video File(s)', filetypes=[("Video Files", FILE_TYPES)]) if file_paths: for file_path in file_paths: if file_path not in selected_files: selected_files_listbox.insert(tk.END, file_path) debug('DEBUG', f"Queued video file: {file_path}") else: debug('DEBUG', f"Attempted to queue already-queued file \'{file_path}\'") selected_files = selected_files_listbox.get(0, tk.END) if len(selected_files) > 0: processing_button.config(state='normal') # Create a button to remove the selected file from the Listbox def remove_file(): global processing_button selected_index = selected_files_listbox.curselection() if selected_index: selected_files_listbox.delete(selected_index) debug('DEBUG', f"Removed file from queue: {selected_index}") selected_files = selected_files_listbox.get(0, tk.END) if len(selected_files) <= 0: processing_button.config(state='disabled') # Create a function to clear the queue def clear_queue(): global processing_button selected_files_listbox.delete(0, tk.END) processing_button.config(state='disabled') debug('DEBUG', f"Cleared the queue") # Create a function to cancel processing def cancel_processing(): global processing debug('DEBUG', f"Attempting cancelation ...") if processing: processing = False if processing_thread.is_alive(): processing_thread.join() # Wait for the processing thread to finish cleanly messagebox.showinfo("Processing Canceled", "Video processing has been canceled.") elif not processing: processing = False messagebox.showinfo("Nothing to cancel", "Video2Crops isn't processing anything.") debug('DEBUG', f"Cancel complete. Processing state: {processing}") # Create a button to process the selected files def process_files(): debug('DEBUG', f"Starting file processing ...") global processing, processing_thread, selected_files if not processing: processing = True selected_files = selected_files_listbox.get(0, tk.END) processing_thread = threading.Thread(target=process_video, args=(selected_files, root)) processing_thread.daemon = True # Set the thread as a daemon so it terminates when the main program exits processing_thread.start() else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") debug('DEBUG', f"File processing done") # Define dynamic process/cancel button def process_cancel_toggle(): debug('DEBUG', f"Toggling process/cancel button state") global processing, processing_thread, selected_files if not processing: toggle_process_button(False) processing = True selected_files = selected_files_listbox.get(0, tk.END) processing_thread = threading.Thread(target=process_video, args=(selected_files, root)) processing_thread.daemon = True processing_thread.start() elif processing: toggle_process_button(True) processing = False if processing_thread.is_alive(): processing_thread.join() debug('DEBUG', f"Processing/Cancel button state switch finished") async def toggle_process_button(enable=bool): """True = Set state to 'Process Files' | False = Set state to 'Cancel'""" global processing_button if enable: processing_button.config(text="Process Files") else: processing_button.config(text="Cancel") # Create a function to open the preferences window def open_preferences(): debug('DEBUG', f"Opening preferences window") preferences_window = tk.Toplevel(root) preferences_window.title("Preferences") def change_frame_step(): global frame_step if not processing: new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:", initialvalue=frame_step, minvalue=1) if new_frame_step is not None: frame_step_var.set(str(new_frame_step)) frame_step = new_frame_step write2ini('settings', 'frame_step', new_frame_step) debug('DEBUG', f"Set frame stepping to {frame_step}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") def change_contrast_threshold(): global contrast_threshold if not processing: new_contrast_threshold = simpledialog.askfloat("Contrast Threshold", "Enter the new value threshold:", initialvalue=contrast_threshold, minvalue=0) if new_contrast_threshold is not None: contrast_threshold_var.set(str(new_contrast_threshold)) write2ini('settings', 'contrast_threshold', new_contrast_threshold) debug('DEBUG', f"Set contrast discard threshold to {new_contrast_threshold}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") def change_duplicate_threshold(): global duplicate_threshold if not processing: new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:", initialvalue=duplicate_threshold, minvalue=0, maxvalue=100) if new_duplicate_threshold is not None: duplicate_threshold_var.set(str(new_duplicate_threshold)) write2ini('settings', 'duplicate_threshold', new_duplicate_threshold) debug('DEBUG', f"Set duplicate discard threshold to {new_duplicate_threshold}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") def apply_roi_size(): global roi_size if not processing: new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:", initialvalue=roi_size, minvalue=1) if new_roi_size is not None: roi_size_var.set(str(new_roi_size)) write2ini('settings', 'roi_size', new_roi_size) debug('DEBUG', f"Set new ROI size to {new_roi_size}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") debug('DEBUG', f"Creating preference fields ...") # Create preference fields frame_step_label = tk.Label(preferences_window, text="Skip n frames:") frame_step_label.grid(row=0, column=0, padx=20, pady=5) frame_step_value = tk.Label(preferences_window, textvariable=frame_step_var) frame_step_value.grid(row=0, column=1, padx=20, pady=5) frame_step_button = tk.Button(preferences_window, text="Edit", command=change_frame_step) frame_step_button.grid(row=0, column=2, padx=20, pady=5) contrast_threshold_label = tk.Label(preferences_window, text="Contrast Threshold:") contrast_threshold_label.grid(row=1, column=0, padx=20, pady=5) if contrast_threshold == float(0): contrast_threshold_value = tk.Label(preferences_window, text="Disabled") else: contrast_threshold_value = tk.Label(preferences_window, textvariable=contrast_threshold_var) contrast_threshold_value.grid(row=1, column=1, padx=20, pady=5) contrast_threshold_button = tk.Button(preferences_window, text="Edit", command=change_contrast_threshold) contrast_threshold_button.grid(row=1, column=2, padx=20, pady=5) duplicate_threshold_label = tk.Label(preferences_window, text="Duplicate Threshold:") duplicate_threshold_label.grid(row=2, column=0, padx=20, pady=5) if duplicate_threshold == float(0): duplicate_threshold_value = tk.Label(preferences_window, text="Disabled") else: duplicate_threshold_value = tk.Label(preferences_window, textvariable=duplicate_threshold_var) duplicate_threshold_value.grid(row=2, column=1, padx=20, pady=5) duplicate_threshold_button = tk.Button(preferences_window, text="Edit", command=change_duplicate_threshold) duplicate_threshold_button.grid(row=2, column=2, padx=20, pady=5) save_checkbox = tk.Checkbutton(preferences_window, text="Save Output Images", variable=save_var, command=toggle_save) save_checkbox.grid(row=3, column=0, columnspan=3, padx=20, pady=5) show_checkbox = tk.Checkbutton(preferences_window, text="Display Playback", variable=show_var, command=toggle_show) show_checkbox.grid(row=4, column=0, columnspan=3, padx=20, pady=5) # Section divider divider_label = tk.Label(preferences_window, text="ROI Settings", font=("Helvetica", 12, "bold")) divider_label.grid(row=5, column=0, columnspan=3, padx=20, pady=(20, 10)) # Create ROI fields vertical_alignment_label = tk.Label(preferences_window, text="Vertical Alignment:") vertical_alignment_label.grid(row=6, column=0, padx=20, pady=5) vertical_alignment_var.set("Center") # Default alignment vertical_alignment_menu = tk.OptionMenu(preferences_window, vertical_alignment_var, "Top", "Center", "Bottom") vertical_alignment_menu.grid(row=6, column=1, padx=20, pady=5) horizontal_alignment_label = tk.Label(preferences_window, text="Horizontal Alignment:") horizontal_alignment_label.grid(row=7, column=0, padx=20, pady=5) horizontal_alignment_var.set("Center") # Default alignment horizontal_alignment_menu = tk.OptionMenu(preferences_window, horizontal_alignment_var, "Left", "Center", "Right") horizontal_alignment_menu.grid(row=7, column=1, padx=20, pady=5) roi_size_label = tk.Label(preferences_window, text="ROI Size:") roi_size_label.grid(row=8, column=0, padx=20, pady=5) roi_size_value = tk.Label(preferences_window, textvariable=roi_size_var) roi_size_value.grid(row=8, column=1, padx=20, pady=5) roi_size_button = tk.Button(preferences_window, text="Edit", command=apply_roi_size) roi_size_button.grid(row=8, column=2, padx=20, pady=5) debug('DEBUG', f"Done creating preference fields") debug('DEBUG', 'Functions defined') # Create a menu menu = tk.Menu(root) root.config(menu=menu) # Variables for preferences frame_step_var = tk.StringVar() contrast_threshold_var = tk.StringVar() duplicate_threshold_var = tk.StringVar() save_var = tk.BooleanVar() show_var = tk.BooleanVar() # Variables for ROI vertical_alignment_var = tk.StringVar() horizontal_alignment_var = tk.StringVar() roi_size_var = tk.StringVar() # Set initial values for preference variables frame_step_var.set(str(frame_step)) contrast_threshold_var.set(str(contrast_threshold)) duplicate_threshold_var.set(str(duplicate_threshold)) save_var.set(SAVE) show_var.set(SHOW) roi_size_var.set(roi_size) # Create a "Queue" submenu queue_menu = tk.Menu(menu, tearoff=0) menu.add_cascade(label="Queue", menu=queue_menu) queue_menu.add_command(label="Add file", command=add_files) queue_menu.add_command(label="Clear queue", command=clear_queue) queue_menu.add_separator() queue_menu.add_command(label="Cancel", state="disabled", command=cancel_processing) queue_menu.add_command(label="Cancel & Clear", state="disabled", command=lambda: [cancel_processing(), clear_queue()]) # Create a "Preferences" submenu menu.add_command(label="Preferences", command=open_preferences) # Create an "Info" submenu info_menu = tk.Menu(menu, tearoff=0, disabledforeground="#000") menu.add_cascade(label="Info", menu=info_menu) info_menu.add_command(label="Project Folder", command=lambda: webbrowser.open(os.getcwd())) info_menu.add_separator() info_menu.add_command(label="About", command=lambda: webbrowser.open(WIKI_URL)) info_menu.add_command(label=f"Check for updates", command=check_for_updates) info_menu.add_command(label=f"View Changelog", command=open_changelog_window) info_menu.add_separator() info_menu.add_command(state="disabled", label=f"Version: {str(V2CVERSION)}") # Create a label for selected files label = tk.Label(root, text="Selected Files:") label.pack(padx=20, pady=5) # Create a Listbox to display the selected files selected_files_listbox = tk.Listbox(root, selectmode=tk.SINGLE, exportselection=0) selected_files_listbox.pack(fill=tk.BOTH, expand=True, padx=20, pady=5) selected_files = selected_files_listbox.get(0, tk.END) # Create a button to select and add files to the Listbox select_files_button = tk.Button(root, text="Select Files", command=add_files) select_files_button.pack(padx=20, pady=5) # Create a button to remove the selected file from the Listbox remove_file_button = tk.Button(root, text="Remove File", command=remove_file) remove_file_button.pack(padx=20, pady=5) # Create a button to process the selected files processing_button = tk.Button(root, text="Process Files", command=process_cancel_toggle, state='disabled') processing_button.pack(padx=20, pady=5) debug('DEBUG', 'UI generated, running main loop') debug('INFO', '... Video2Crops initialized and ready!') check_for_updates() STARTING = False root.mainloop()