Advertisement
creativesamurai1982

pyGUI_CineFrameSaver

Apr 10th, 2025
378
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 26.76 KB | Photo | 0 0
  1. import cv2
  2. import numpy as np
  3. import os
  4. import threading
  5. import time
  6. import tkinter as tk
  7. from tkinter import ttk, Frame, Label, Canvas, Spinbox, messagebox, filedialog, Scrollbar
  8. from PIL import Image, ImageTk
  9. import imghdr
  10.  
  11. # =============================================================================
  12. # Global Settings and Variables
  13. # =============================================================================
  14. DEBUG_MODE = True  # Toggle debugging messages
  15.  
  16. def debug_print(message):
  17.     """Print debugging messages if DEBUG_MODE is enabled."""
  18.     if DEBUG_MODE:
  19.         print(message)
  20.  
  21. debug_print("πŸ” DEBUG ENABLED")
  22.  
  23. # Global state variables and configuration
  24. image_display = None         # Tkinter image for current preview display
  25. processed_image = None       # Processed image used for display
  26. latest_image_path = None     # Latest loaded image (currently unused)
  27. lock = threading.Lock()      # For thread safety
  28. debounce_time = 0            # Timestamp for debouncing slider changes
  29. display_mode = None          # Tkinter StringVar for display mode selection
  30. threshold_value = 205        # Default threshold for image processing
  31.  
  32. frame_counter = 1            # Frame counter (used for unique filenames)
  33. processed_count = 0          # Total count of processed strips
  34. image_queue = []             # List of image file paths to process
  35. update_pending = False       # Flag indicating if preview update is underway
  36. update_requested = False     # Flag to queue a preview update if one is already running
  37. stop_requested = False       # Flag to signal a processing stop
  38.  
  39. # Adjustable cropping parameters
  40. frame_x_offset = 50          # Horizontal offset for cropped frames
  41. frame_y_offset = 250         # Vertical offset for cropped frames
  42. frame_width_max = 850        # Maximum width for cropped frames
  43. frame_height_max = 1700       # Maximum height for cropped frames
  44.  
  45. # Folder paths (to be set in the GUI)
  46. input_folder = ""
  47. output_folder = ""
  48.  
  49. # Global variables for dialogs and overlays
  50. loading_window = None        # The modal processing dialog
  51. loading_label = None         # The overlay label for preview updates
  52.  
  53. # =============================================================================
  54. # Image Processing Functions
  55. # =============================================================================
  56. def load_image(image_path):
  57.     """
  58.    Loads and validates an image from the given path.
  59.    Returns:
  60.        numpy.ndarray: Loaded image, or None if loading fails.
  61.    """
  62.     try:
  63.         debug_print(f"πŸ” Attempting to load image: {image_path}")
  64.         formatted_path = os.path.normpath(image_path)
  65.         debug_print(f"πŸ”„ Reformatted path: {formatted_path}")
  66.        
  67.         if not os.path.exists(formatted_path):
  68.             debug_print(f"❌ File does not exist: {formatted_path}")
  69.             return None
  70.        
  71.         time.sleep(1)  # Pause to ensure file writing is complete
  72.        
  73.         file_type = imghdr.what(formatted_path)
  74.         if file_type not in ['jpeg', 'png']:
  75.             debug_print(f"❌ Unsupported file type: {file_type}")
  76.             return None
  77.        
  78.         debug_print(f"βœ… Validated file type: {file_type}")
  79.         image = cv2.imread(formatted_path)
  80.         if image is None:
  81.             debug_print(f"❌ Failed to load image: {formatted_path}")
  82.         else:
  83.             debug_print(f"βœ… Image loaded with shape: {image.shape}")
  84.         return image
  85.  
  86.     except cv2.error as e:
  87.         debug_print(f"❌ OpenCV error in load_image: {e}")
  88.         return None
  89.     except Exception as e:
  90.         debug_print(f"❌ Unexpected error in load_image: {e}")
  91.         return None
  92.  
  93. def detect_and_crop_frames(image_path):
  94.     """
  95.    Detects sprocket holes and calculates cropping areas.
  96.    Skips the first cropped frame (which is assumed to be unwanted).
  97.    Returns:
  98.        list: Cropped frames as numpy arrays.
  99.    """
  100.     try:
  101.         sprocket_count = 0
  102.         cropped_frames = []
  103.  
  104.         original_image = load_image(image_path)
  105.         if original_image is None:
  106.             debug_print(f"❌ Error loading image: {image_path}")
  107.             return []
  108.  
  109.         rotated_image = cv2.rotate(original_image, cv2.ROTATE_180)
  110.         gray = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
  111.         _, thresh = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
  112.         cnts, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  113.         debug_print(f"πŸ›  Found {len(cnts)} contours.")
  114.  
  115.         # Sort contours by x-coordinate (right to left), and for matching x-values, sort by y (top to bottom)
  116.         cnts = sorted(cnts, key=lambda x: (cv2.boundingRect(x)[0], -cv2.boundingRect(x)[1]), reverse=True)
  117.        
  118.         # **Validation debug print** - Check the order of sorted contours
  119.         for idx, c in enumerate(cnts):
  120.             x, y, w, h = cv2.boundingRect(c)
  121.             debug_print(f"πŸ” Contour {idx}: x={x}, y={y}, w={w}, h={h}")
  122.            
  123.  
  124.         for c in cnts:
  125.             x, y, w, h = cv2.boundingRect(c)
  126.             aspect_ratio = w / float(h)
  127.             if 0.2 < aspect_ratio < 1.0 and w < 300 and h > 50:
  128.                 sprocket_count += 1
  129.                 debug_print(f"πŸ”΄ Sprocket #{sprocket_count}: x={x}, y={y}, w={w}, h={h}")
  130.                 frame_x = max(x + frame_x_offset, 0)
  131.                 frame_y = max(y - frame_y_offset, 0)
  132.                 frame_w = min(frame_width_max, rotated_image.shape[1] - frame_x)
  133.                 frame_h = min(frame_height_max, rotated_image.shape[0] - frame_y)
  134.                 debug_print(f"πŸ“Œ Cropping frame: x={frame_x}, y={frame_y}, w={frame_w}, h={frame_h}")
  135.                 frame = rotated_image[frame_y:frame_y + frame_h, frame_x:frame_x + frame_w]
  136.                 debug_print(f"Appending frame: x={frame_x}, y={frame_y}, w={frame_w}, h={frame_h}")
  137.                 if frame.size > 0:
  138.                     cropped_frames.append(frame)
  139.                 else:
  140.                     debug_print("⚠️ Skipped empty frame.")
  141.  
  142.         debug_print(f"βœ… Sprocket count: {sprocket_count}, frames before skip: {len(cropped_frames)}")
  143.         if cropped_frames:
  144.             debug_print("πŸ”§ Skipping first cropped frame (likely unwanted).")
  145.             cropped_frames = cropped_frames[1:]
  146.         debug_print(f"βœ… Final cropped frames count: {len(cropped_frames)}")
  147.         return cropped_frames
  148.  
  149.     except cv2.error as e:
  150.         debug_print(f"❌ OpenCV error in detect_and_crop_frames: {e}")
  151.         return []
  152.     except Exception as e:
  153.         debug_print(f"❌ Unexpected error in detect_and_crop_frames: {e}")
  154.         return []
  155.  
  156. # =============================================================================
  157. # GUI Update Functions
  158. # =============================================================================
  159. def update_preview():
  160.     """
  161.    Updates the preview image on the canvas.
  162.    Loads the first image from the input folder and applies rotation, thresholding,
  163.    and optional overlay drawing.
  164.    """
  165.     global update_pending, update_requested, image_display, processed_image
  166.     if update_pending:
  167.         return
  168.     update_pending = True
  169.  
  170.     # Show the preview loading overlay
  171.     loading_label.grid(row=0, column=0, sticky="nsew")
  172.     root.update_idletasks()  # Ensure UI is updated immediately
  173.  
  174.     try:
  175.         # Use the first file in the input folder for preview
  176.         if input_folder and os.listdir(input_folder):
  177.             sample_file = os.path.join(input_folder, sorted(os.listdir(input_folder))[0])
  178.         else:
  179.             debug_print("⚠️ No image available for preview.")
  180.             return
  181.  
  182.         debug_print(f"πŸ›  Loading image for preview: {sample_file}")
  183.         original_image = load_image(sample_file)
  184.         if original_image is None:
  185.             debug_print(f"❌ Error loading preview image: {sample_file}")
  186.             return
  187.  
  188.         rotated_image = cv2.rotate(original_image, cv2.ROTATE_180)
  189.         debug_print(f"🟒 Display mode: {display_mode.get()}")
  190.  
  191.         # Apply threshold for contour detection
  192.         gray = cv2.cvtColor(rotated_image, cv2.COLOR_BGR2GRAY)
  193.         _, thresh = cv2.threshold(gray, threshold_value, 255, cv2.THRESH_BINARY)
  194.         cnts, _ = cv2.findContours(thresh, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
  195.         debug_print(f"πŸ›  Detected {len(cnts)} contours.")
  196.  
  197.         # Choose the base image for preview depending on display mode
  198.         if display_mode.get() == "Threshold Image":
  199.             base_image = cv2.cvtColor(thresh, cv2.COLOR_GRAY2BGR)
  200.             debug_print("πŸ–€ Using Threshold Image mode.")
  201.         else:
  202.             base_image = rotated_image
  203.             debug_print("πŸ–Ό Using Original Image mode.")
  204.  
  205.         # If overlay is enabled, draw the detection overlay
  206.         if overlay_enabled.get():
  207.             debug_print("🟒 Overlay enabled.")
  208.             overlay = base_image.copy()
  209.             sprocket_count = 0
  210.             for c in cnts:
  211.                 x, y, w, h = cv2.boundingRect(c)
  212.                 aspect_ratio = w / float(h)
  213.                 if 0.2 < aspect_ratio < 1.0 and w < 300 and h > 50:
  214.                     sprocket_count += 1
  215.                     debug_print(f"πŸ”΄ Sprocket: x={x}, y={y}, w={w}, h={h} (Count: {sprocket_count})")
  216.                     frame_x = max(x + frame_x_offset, 0)
  217.                     frame_y = max(y - frame_y_offset, 0)
  218.                     frame_w = min(frame_width_max, rotated_image.shape[1] - frame_x)
  219.                     frame_h = min(frame_height_max, rotated_image.shape[0] - frame_y)
  220.                     box_color = (0, 255, 0) if sprocket_count % 2 == 0 else (255, 0, 0)
  221.                     box_overlay = overlay.copy()
  222.                     cv2.rectangle(box_overlay, (frame_x, frame_y),
  223.                                   (frame_x + frame_w, frame_y + frame_h),
  224.                                   box_color, -1)
  225.                     cv2.addWeighted(box_overlay, 0.3, overlay, 0.7, 0, overlay)
  226.             valid_frames = sprocket_count - 1 if sprocket_count > 0 else 0
  227.             frame_count_label.config(
  228.                 text=f"[ {valid_frames} (+1 rubbish) ] Frame(s) Detected, [ {sprocket_count} ] Sprocket(s) Detected"
  229.             )
  230.             base_image = overlay
  231.         else:
  232.             debug_print("πŸ”΄ Overlay disabled.")
  233.  
  234.         # Resize for preview (scale factor 0.2)
  235.         scale_factor = 0.2
  236.         preview_resized = cv2.resize(base_image,
  237.                                      (int(base_image.shape[1] * scale_factor),
  238.                                       int(base_image.shape[0] * scale_factor)))
  239.         debug_print(f"πŸ–Ό Resized preview dimensions: {preview_resized.shape}")
  240.         processed_image = cv2.cvtColor(preview_resized, cv2.COLOR_BGR2RGB)
  241.         image_display = ImageTk.PhotoImage(image=Image.fromarray(processed_image))
  242.         preview_canvas.create_image(0, 0, anchor='nw', image=image_display)
  243.         preview_canvas.config(scrollregion=preview_canvas.bbox('all'))
  244.         debug_print("πŸ“Œ Preview updated.")
  245.     except Exception as e:
  246.         debug_print(f"❌ Error in update_preview: {e}")
  247.     finally:
  248.         update_pending = False
  249.         loading_label.grid_forget()  # Hide the loading overlay
  250.         if update_requested:
  251.             update_requested = False
  252.             threading.Thread(target=update_preview, daemon=True).start()
  253.  
  254. def update_threshold(value):
  255.     """
  256.    Updates the threshold value used for processing, using a 0.3-second debounce.
  257.    """
  258.     global threshold_value, debounce_time, threshold_label, update_pending, update_requested
  259.     current_time = time.time()
  260.     if current_time - debounce_time < 0.3:
  261.         return
  262.     debounce_time = current_time
  263.     with lock:
  264.         threshold_value = int(value)
  265.     debug_print(f"πŸ”§ Threshold updated to: {threshold_value}")
  266.     if threshold_label is not None:
  267.         threshold_label.config(text=f"Threshold: {threshold_value}")
  268.     if update_pending:
  269.         update_requested = True
  270.     else:
  271.         threading.Thread(target=update_preview, daemon=True).start()
  272.  
  273. def reset_gui():
  274.     """
  275.    Resets the GUI state for a new run.
  276.    Clears the preview and resets frame and sprocket counts.
  277.    """
  278.     global processed_count, frame_counter
  279.     processed_count = 0
  280.     frame_counter = 1
  281.     frame_count_label.config(text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected")
  282.     preview_canvas.delete("all")
  283.     debug_print("πŸ”„ GUI reset for a new run.")
  284.  
  285. def select_input_folder():
  286.     """
  287.    Opens a dialog to select the input folder.
  288.    Updates the input folder label and refreshes the preview.
  289.    """
  290.     global input_folder
  291.     selected = filedialog.askdirectory(title="Select Input Folder")
  292.     if selected:
  293.         input_folder = selected
  294.         input_folder_label.config(text="Input: " + os.path.basename(input_folder))
  295.     else:
  296.         input_folder_label.config(text="Input: Not Selected")
  297.     threading.Thread(target=update_preview, daemon=True).start()
  298.  
  299. def select_output_folder():
  300.     """
  301.    Opens a dialog to select the output folder and updates the folder label.
  302.    """
  303.     global output_folder
  304.     selected = filedialog.askdirectory(title="Select Output Folder")
  305.     if selected:
  306.         output_folder = selected
  307.         output_folder_label.config(text="Output: " + os.path.basename(output_folder))
  308.     else:
  309.         output_folder_label.config(text="Output: Not Selected")
  310.  
  311. # =============================================================================
  312. # Loading Dialog / Progress Indicator Functions
  313. # =============================================================================
  314. def show_loading_dialog():
  315.     """
  316.    Displays a modal dialog with an indeterminate progress bar and a
  317.    'Stop Processing' button. Resets the stop flag.
  318.    """
  319.     global loading_window, stop_requested
  320.     stop_requested = False  # Reset for a new processing run
  321.     loading_window = tk.Toplevel(root)
  322.     loading_window.title("Processing...")
  323.     tk.Label(loading_window, text="Processing, please wait...").grid(row=0, column=0, padx=10, pady=10)
  324.     progress = ttk.Progressbar(loading_window, mode='indeterminate', length=200)
  325.     progress.grid(row=1, column=0, padx=10, pady=10)
  326.     progress.start()
  327.     ttk.Button(loading_window, text="Stop Processing", command=stop_processing).grid(row=2, column=0, pady=10)
  328.     loading_window.grab_set()  # Make the dialog modal
  329.  
  330. def stop_processing():
  331.     """
  332.    Signals the processing loop to stop, resets counters and clears the preview,
  333.    then hides the loading dialog.
  334.    """
  335.     global stop_requested, processed_count, frame_counter
  336.     stop_requested = True
  337.     processed_count = 0
  338.     hide_loading_dialog()
  339.     preview_canvas.delete("all")
  340.     frame_count_label.config(text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected")
  341.     debug_print("⏹️ Processing stopped and state reset.")
  342.  
  343. def hide_loading_dialog():
  344.     """Closes the loading dialog if it exists."""
  345.     global loading_window
  346.     if loading_window is not None:
  347.         loading_window.destroy()
  348.         loading_window = None
  349.  
  350. def processing_complete_dialog(total_strips, total_frames):
  351.     """
  352.    Hides the loading dialog and shows a message box indicating
  353.    that processing is complete.
  354.    """
  355.     hide_loading_dialog()
  356.     messagebox.showinfo("Processing Complete", f"Processed {total_strips} strips.\nTotal frames saved: {total_frames}")
  357.  
  358. # =============================================================================
  359. # Image Saving Function
  360. # =============================================================================
  361. def save_detected_frames(detected_frames, strip_filename):
  362.     """
  363.    Saves each detected frame after rotating 90Β° counterclockwise and flipping vertically.
  364.    **Now ensures frames are saved in the correct order by reversing the list before saving.**
  365.    Returns:
  366.        int: Number of frames saved.
  367.    """
  368.     global frame_counter
  369.     frames_saved = 0
  370.  
  371.     # βœ… Reverse the order of detected frames before saving
  372.     detected_frames.reverse()
  373.  
  374.     try:
  375.         for idx, frame in enumerate(detected_frames):
  376.             if frame is None or not isinstance(frame, np.ndarray):
  377.                 debug_print(f"❌ Invalid frame at index {idx}; skipping.")
  378.                 continue
  379.  
  380.             rotated_frame = cv2.rotate(frame, cv2.ROTATE_90_COUNTERCLOCKWISE)
  381.             flipped_frame = cv2.flip(rotated_frame, 0)
  382.             filename = os.path.join(output_folder, f"frame_{frame_counter:06d}.jpg")
  383.             cv2.imwrite(filename, flipped_frame, [cv2.IMWRITE_JPEG_QUALITY, 100])
  384.  
  385.             debug_print(f"βœ… Frame saved: {filename}")
  386.             frame_counter += 1
  387.             frames_saved += 1
  388.  
  389.         print(f"Strip {strip_filename}: Frames saved: {frames_saved}")
  390.         return frames_saved
  391.  
  392.     except cv2.error as e:
  393.         debug_print(f"❌ OpenCV error when saving frames: {e}")
  394.         return frames_saved
  395.     except OSError as e:
  396.         debug_print(f"❌ OS error when saving frames: {e}")
  397.         return frames_saved
  398.     except Exception as e:
  399.         debug_print(f"❌ Unexpected error when saving frames: {e}")
  400.         return frames_saved
  401.  
  402. # =============================================================================
  403. # Process Image Queue Function
  404. # =============================================================================
  405. def process_image_queue():
  406.     """
  407.    Processes all images in the input folder by detecting and cropping frames.
  408.    Introduces a waiting period before starting to allow cancellation.
  409.    If uninterrupted, shows a processing complete dialog.
  410.    """
  411.     global image_queue, processed_count, frame_counter, stop_requested
  412.  
  413.     if not input_folder or not output_folder:
  414.         messagebox.showerror("Error", "Please select both input and output folders before starting the process.")
  415.         hide_loading_dialog()
  416.         return
  417.  
  418.     image_queue = sorted(
  419.         [os.path.join(input_folder, f)
  420.          for f in os.listdir(input_folder)
  421.          if f.lower().endswith(('.jpg', '.png', '.jpeg'))]
  422.     )
  423.     if not image_queue:
  424.         messagebox.showerror("Error", "No valid images found in the input folder.")
  425.         hide_loading_dialog()
  426.         return
  427.  
  428.     if stop_requested:
  429.         debug_print("πŸ›‘ Processing stopped before starting the loop.")
  430.         hide_loading_dialog()
  431.         return
  432.  
  433.     debug_print("⏳ Waiting for user input before starting...")
  434.     for _ in range(20):  # 20 iterations x 0.1 s delay = 2 seconds
  435.         if stop_requested:
  436.             debug_print("πŸ›‘ Processing stopped before it began.")
  437.             hide_loading_dialog()
  438.             return
  439.         time.sleep(0.1)
  440.  
  441.     total_strips = 0
  442.     for image_path in image_queue:
  443.         if stop_requested:
  444.             debug_print("πŸ›‘ Processing stopped by user.")
  445.             break
  446.         total_strips += 1
  447.         debug_print(f"πŸ” Processing strip: {image_path}")
  448.         try:
  449.             cropped_frames = detect_and_crop_frames(image_path)
  450.             if not cropped_frames:
  451.                 debug_print(f"⚠️ No frames detected in {image_path}. Skipping.")
  452.                 continue
  453.             for idx, frame in enumerate(cropped_frames):
  454.                 debug_print(f"Frame order before saving: index={idx}, size={frame.shape}")
  455.             frames_saved = save_detected_frames(cropped_frames, os.path.basename(image_path))
  456.             processed_count += 1
  457.         except Exception as e:
  458.             debug_print(f"❌ Error processing {image_path}: {e}")
  459.  
  460.     debug_print("βœ… Processing loop completed.")
  461.     if not stop_requested:
  462.         root.after(0, lambda: processing_complete_dialog(total_strips, frame_counter - 1))
  463.  
  464. # =============================================================================
  465. # Tkinter GUI Setup
  466. # =============================================================================
  467. root = tk.Tk()
  468. root.title("CineFrame Cutter")
  469. root.grid_rowconfigure(0, weight=1)
  470. root.grid_columnconfigure(0, weight=1)
  471.  
  472. main_frame = Frame(root)
  473. main_frame.grid(row=0, column=0, sticky='nsew')
  474. main_frame.grid_rowconfigure(0, weight=1)
  475. main_frame.grid_columnconfigure(0, weight=1)
  476.  
  477. # Input/Output Folder Selection Panel
  478. io_frame = Frame(main_frame, relief=tk.RIDGE, borderwidth=2)
  479. io_frame.grid(row=0, column=0, sticky='ew', padx=5, pady=5)
  480. io_frame.columnconfigure(1, weight=1)
  481. input_folder_label = Label(io_frame, text="Input: Not Selected")
  482. input_folder_label.grid(row=0, column=0, padx=5, pady=5)
  483. ttk.Button(io_frame, text="Browse Input", command=select_input_folder).grid(row=0, column=1, padx=5, pady=5)
  484. output_folder_label = Label(io_frame, text="Output: Not Selected")
  485. output_folder_label.grid(row=1, column=0, padx=5, pady=5)
  486. ttk.Button(io_frame, text="Browse Output", command=select_output_folder).grid(row=1, column=1, padx=5, pady=5)
  487.  
  488. # Image Preview Frame (600x450)
  489. preview_frame = Frame(main_frame, relief=tk.SUNKEN, borderwidth=2)
  490. preview_frame.grid(row=1, column=0, sticky='nsew', padx=5, pady=5)
  491. preview_frame.grid_rowconfigure(0, weight=1)
  492. preview_frame.grid_columnconfigure(0, weight=1)
  493. preview_canvas = Canvas(preview_frame, bg='white', width=600, height=450)
  494. preview_canvas.grid(row=0, column=0, sticky='nsew')
  495. h_scroll = Scrollbar(preview_frame, orient=tk.HORIZONTAL, command=preview_canvas.xview)
  496. h_scroll.grid(row=1, column=0, sticky='ew')
  497. v_scroll = Scrollbar(preview_frame, orient=tk.VERTICAL, command=preview_canvas.yview)
  498. v_scroll.grid(row=0, column=1, sticky='ns')
  499. preview_canvas.configure(xscrollcommand=h_scroll.set, yscrollcommand=v_scroll.set)
  500. loading_label = Label(preview_frame, text="Loading preview...", font=("Arial", 14), fg="red", bg="white")
  501. loading_label.grid(row=0, column=0, sticky="nsew")
  502. loading_label.grid_forget()  # Hide the loading overlay by default
  503.  
  504. # Frame and Sprocket Count Label
  505. frame_count_label = Label(main_frame, text="[ 0 ] Frame(s) Detected, [ 0 ] Sprocket(s) Detected", font=("Arial", 12))
  506. frame_count_label.grid(row=2, column=0, padx=5, pady=5)
  507.  
  508. # Controls Panel (Threshold, Display Mode, Overlay, Frame Area Adjusters)
  509. controls_frame = Frame(main_frame, relief=tk.GROOVE, borderwidth=2)
  510. controls_frame.grid(row=3, column=0, sticky='ew', padx=5, pady=5)
  511. controls_frame.columnconfigure(0, weight=1)
  512. threshold_label = Label(controls_frame, text=f"Threshold: {threshold_value}", font=("Arial", 10))
  513. threshold_label.grid(row=0, column=0, padx=5, pady=5, sticky='w')
  514. threshold_slider = ttk.Scale(controls_frame, from_=0, to=255, orient='horizontal',
  515.                              command=lambda v: update_threshold(int(float(v))))
  516. threshold_slider.set(threshold_value)
  517. threshold_slider.grid(row=0, column=1, padx=5, pady=5, sticky='ew')
  518. controls_frame.columnconfigure(1, weight=1)
  519. Label(controls_frame, text="Display Mode:").grid(row=1, column=0, padx=5, pady=5, sticky='w')
  520. display_mode = tk.StringVar(value="Threshold Image")
  521. display_dropdown = ttk.OptionMenu(controls_frame, display_mode, "Threshold Image", "Threshold Image", "Original Image")
  522. display_dropdown.grid(row=1, column=1, padx=5, pady=5, sticky='ew')
  523. display_mode.trace_add("write", lambda *args: threading.Thread(target=update_preview, daemon=True).start())
  524. overlay_enabled = tk.BooleanVar(value=True)
  525. overlay_checkbox = ttk.Checkbutton(controls_frame, text="Enable Overlay", variable=overlay_enabled,
  526.                                    command=lambda: threading.Thread(target=update_preview, daemon=True).start())
  527. overlay_checkbox.grid(row=2, column=0, padx=5, pady=5, sticky='w')
  528.  
  529. # Frame Area Adjustment Controls
  530. area_frame = Frame(controls_frame, relief=tk.RIDGE, borderwidth=1)
  531. area_frame.grid(row=3, column=0, columnspan=2, sticky='ew', padx=5, pady=5)
  532. Label(area_frame, text="Frame X Offset:").grid(row=0, column=0, padx=3, pady=3, sticky='w')
  533. x_offset_spin = Spinbox(area_frame, from_=0, to=500, width=5)
  534. x_offset_spin.delete(0, tk.END)
  535. x_offset_spin.insert(0, frame_x_offset)
  536. x_offset_spin.grid(row=0, column=1, padx=3, pady=3)
  537. Label(area_frame, text="Frame Y Offset:").grid(row=0, column=2, padx=3, pady=3, sticky='w')
  538. y_offset_spin = Spinbox(area_frame, from_=0, to=500, width=5)
  539. y_offset_spin.delete(0, tk.END)
  540. y_offset_spin.insert(0, frame_y_offset)
  541. y_offset_spin.grid(row=0, column=3, padx=3, pady=3)
  542. Label(area_frame, text="Max Width:").grid(row=1, column=0, padx=3, pady=3, sticky='w')
  543. width_spin = Spinbox(area_frame, from_=100, to=2000, width=5)
  544. width_spin.delete(0, tk.END)
  545. width_spin.insert(0, frame_width_max)
  546. width_spin.grid(row=1, column=1, padx=3, pady=3)
  547. Label(area_frame, text="Max Height:").grid(row=1, column=2, padx=3, pady=3, sticky='w')
  548. height_spin = Spinbox(area_frame, from_=100, to=3000, width=5)
  549. height_spin.delete(0, tk.END)
  550. height_spin.insert(0, frame_height_max)
  551. height_spin.grid(row=1, column=3, padx=3, pady=3)
  552. def refresh_preview():
  553.     """
  554.    Updates global cropping parameters from the spinboxes,
  555.    then refreshes the preview.
  556.    """
  557.     global frame_x_offset, frame_y_offset, frame_width_max, frame_height_max
  558.     try:
  559.         frame_x_offset = int(x_offset_spin.get())
  560.         frame_y_offset = int(y_offset_spin.get())
  561.         frame_width_max = int(width_spin.get())
  562.         frame_height_max = int(height_spin.get())
  563.         debug_print(f"Updated frame area: x_offset={frame_x_offset}, y_offset={frame_y_offset}, "
  564.                     f"width_max={frame_width_max}, height_max={frame_height_max}")
  565.         threading.Thread(target=update_preview, daemon=True).start()
  566.     except Exception as e:
  567.         debug_print(f"Error refreshing preview: {e}")
  568. refresh_button = ttk.Button(area_frame, text="Apply Frame Size", command=refresh_preview)
  569. refresh_button.grid(row=2, column=0, columnspan=4, pady=5)
  570.  
  571. # Bottom Control Panel (Start, Quit, Reset Buttons)
  572. buttons_frame = Frame(main_frame)
  573. buttons_frame.grid(row=4, column=0, sticky='ew', padx=5, pady=5)
  574. buttons_frame.grid_columnconfigure(3, weight=1)  # Spacer column to right-align the Quit button
  575.  
  576. start_button = ttk.Button(
  577.     buttons_frame,
  578.     text="Start",
  579.     command=lambda: [show_loading_dialog(),
  580.                      threading.Thread(target=process_image_queue, daemon=True).start()]
  581. )
  582. start_button.grid(row=0, column=0, padx=5, pady=5)
  583.  
  584. quit_button = ttk.Button(buttons_frame, text="Quit", command=root.destroy)
  585. quit_button.grid(row=0, column=3, padx=5, pady=5, sticky='e')  # Placed in far-right column
  586.  
  587. def confirm_reset():
  588.     """Prompts the user to confirm before resetting the GUI."""
  589.     if messagebox.askyesno("Confirm Reset", "Are you sure you want to reset the GUI for a new run?"):
  590.         reset_gui()
  591. reset_button = ttk.Button(buttons_frame, text="Reset GUI", command=confirm_reset)
  592. reset_button.grid(row=0, column=2, padx=5, pady=5)
  593.  
  594. # =============================================================================
  595. # Main Event Loop
  596. # =============================================================================
  597. try:
  598.     root.mainloop()
  599. except KeyboardInterrupt:
  600.     debug_print("πŸ›‘ Keyboard Interrupt: Exiting.")
  601.  
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement