import cv2 import os 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 sys import webbrowser import screeninfo import feedparser import subprocess import platform import urllib.request import re from bs4 import BeautifulSoup import threading import configparser DEBUG = True STARTING = True INVALID_INI = False # Debug function def debug(state, message): if DEBUG: allowed_states = ['LOG EVENT', 'DEBUG', 'ERROR', 'INFO'] if state in allowed_states: debug_message = f"[{state}] {message}" print(f"{debug_message}") debugfile_path = os.path.join(os.getcwd(), 'debugfile.txt') with open(debugfile_path, 'a') as debugfile: log_debug_message = f"{datetime.now().strftime('%m-%d_%H:%M:%S')} | [{state}] {message}\n" debugfile.write(log_debug_message) else: raise ValueError(f"Invalid state '{state}'. Please choose from {', '.join(allowed_states)}.") def fetch_ini(): global INVALID_INI try: with open(config_file, 'w') as file: config_url = 'https://git.rolfsvaag.no/frarol96/Video2Crops/raw/branch/main/config.ini' with urllib.request.urlopen(config_url) as config_template: config_template = config_template.read().decode("utf-8") file.write(config_template) tk.messagebox.showinfo( "Reset config file", f"Config file has been reset" ) debug('DEBUG', f"Config file was reset to default") INVALID_INI = False except: print(f"[ERROR] Unable to fetch the missing config.ini file!\nCheck directory permissions and network connectivity!") exit # Setup config file config_file = 'config.ini' config = configparser.ConfigParser() if not os.path.exists(config_file): fetch_ini() config.read(config_file) def checkinifile(): # Check for the presence of sections and keys expected_sections = ['settings', 'debug'] expected_settings_keys = { 'frame_step': int, 'contrast_threshold': float, 'duplicate_threshold': int, 'save_output': bool, 'show_playback': bool, 'roi_size': int, 'window_size': str } expected_debug_keys = { 'debug_output': bool } CHECK_SECTIONS = True CHECK_KEYS = True CHECK_VALUES = False global INVALID_INI # Check for the presence of sections if CHECK_SECTIONS: for section in expected_sections: if section not in config.sections(): INVALID_INI = True print(f"Missing section: [{section}]") # Check for the presence of keys and their types in the 'settings' section if 'settings' in config.sections(): for key, expected_type in expected_settings_keys.items(): if key not in config['settings']: if CHECK_KEYS: INVALID_INI = True print(f"Missing key '{key}' in [settings] section") else: value = config.get('settings', key) if not isinstance(value, expected_type): if CHECK_VALUES: INVALID_INI = True print(f"Value of '{key}' in [settings] section is not of expected type {expected_type}") # Check for the presence of keys and their types in the 'debug' section if 'debug' in config.sections(): for key, expected_type in expected_debug_keys.items(): if key not in config['debug']: if CHECK_KEYS: INVALID_INI = True print(f"Missing key '{key}' in [debug] section") else: value = config.get('debug', key) if not isinstance(value, expected_type): if CHECK_VALUES: INVALID_INI = True print(f"Value of '{key}' in [debug] section is not of expected type {expected_type}") if INVALID_INI: fetch_ini() checkinifile() debug('INFO', "Video2Crops starting ...") # START INITIATING PROGRAM VARIABLES FROM HERE # Constants v2cv = [1, 2, 0] v2cversion1 = v2cv[0] v2cversion2 = v2cv[1] v2cversion3 = v2cv[2] v2cversion = f"{v2cversion1}.{v2cversion2}.{v2cversion3}" processing = False processing_button_state = "start" # Load ini settings frame_step = config.get('settings', 'frame_step') contrast_threshold = config.get('settings', 'contrast_threshold') duplicate_threshold = config.get('settings', 'duplicate_threshold') SAVE = config.get('settings', 'save_output') SHOW = config.get('settings', 'show_playback') roi_size = config.get('settings', 'roi_size') tksize = config.get('settings', 'window_size') DEBUG = config.get('debug', 'debug_output') def write2ini(section, variable, value): config.set(section, str(variable), str(value)) # Save the changes to the INI file with open(config_file, 'w') as configfile: config.write(configfile) # Define the RSS feed URL rss_feed_url = "https://git.rolfsvaag.no/frarol96/Video2Crops/releases.rss" # Create the root window root = TkinterDnD.Tk() root.title("Video2Crops") root.geometry(tksize) # Function to clear the queue def clear_queue(): selected_files_listbox.delete(0, tk.END) debug('DEBUG', 'Cleared work queue') 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") def extract_version(title): # Use regular expressions to extract the version number from the title match = re.search(r'(\d+\.\d+\.\d+)', title) if match: return match.group(1) return None def extract_changelog(description): # Parse the HTML content using BeautifulSoup soup = BeautifulSoup(description, 'html.parser') # Find all changelog sections changelog_sections = soup.find_all('h3', id='user-content-changelog') changelogs = [] for changelog_section in changelog_sections: # Extract the changelog text as plain text changelog_text = changelog_section.find_next('ul').get_text() changelogs.append(changelog_text.strip()) # Join the changelogs into a single string changelog_str = '\n'.join(changelogs) return changelog_str if changelog_str else None # 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 > (v2cversion1, v2cversion2, v2cversion3): # 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): 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) if 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 return frame_is_duplicate, mse, ref_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 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) # Store the first frame as reference for the duplicate check ret, reference_frame = cap.read() 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 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 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(): file_paths = filedialog.askopenfilenames(filetypes=[("Video Files", "*.mp4 *.avi *.mov")]) if file_paths: for file_path in file_paths: selected_files_listbox.insert(tk.END, file_path) debug('DEBUG', f"Queued video file: {file_path}") # Create a button to remove the selected file from the Listbox def remove_file(): selected_index = selected_files_listbox.curselection() if selected_index: selected_files_listbox.delete(selected_index) debug('DEBUG', f"Removed file from queue: {selected_index}") # Create a function to clear the queue def clear_queue(): selected_files_listbox.delete(0, tk.END) 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 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, processing_button_state if processing_button_state == "start": 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 processing_thread.start() processing_button.config(text="Cancel") processing_button_state = "cancel" elif processing_button_state == "cancel": if processing: processing = False if processing_thread.is_alive(): processing_thread.join() processing_button.config(text="Process Files") processing_button_state = "start" debug('DEBUG', f"Processing/Cancel button state switch finished") # 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(): if not processing: new_frame_step = simpledialog.askinteger("Frame Step", "Enter the new frame step:") if new_frame_step is not None: frame_step_var.set(str(new_frame_step)) global 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(): if not processing: new_contrast_threshold = simpledialog.askfloat("Contrast Threshold", "Enter the new value threshold:") 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(): if not processing: new_duplicate_threshold = simpledialog.askfloat("Duplicate Threshold", "Enter the new value threshold:") 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 toggle_save(): if not processing: global SAVE SAVE = not SAVE write2ini('settings', 'save_output', SAVE) debug('DEBUG', f"Saving output: {SAVE}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") def toggle_show(): if not processing: global SHOW SHOW = not SHOW write2ini('settings', 'show_playback', SHOW) debug('DEBUG', f"Displaying output: {SHOW}") else: messagebox.showinfo("Info", "Video2Crops is currently processing files!") def apply_roi_size(): if not processing: new_roi_size = simpledialog.askinteger("ROI Size", "Enter the new ROI size:") 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) 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) 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("https://git.rolfsvaag.no/frarol96/Video2Crops/wiki")) 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) # 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) 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()