From 2260f0ddb2e1978bde7a3ce538e9e2a5fe8b4f25 Mon Sep 17 00:00:00 2001 From: mhby1g21 <mhby1g21@soton.ac.uk> Date: Tue, 29 Oct 2024 16:13:00 +0000 Subject: [PATCH] refactored shifter tab --- scripts/debug_tool/GUI_debug.py | 2 + scripts/debug_tool/tabs/shifter_tab.py | 271 +++++++-------------- scripts/debug_tool/utils/file_handlers.py | 85 ++++++- scripts/debug_tool/utils/image_handlers.py | 47 +++- scripts/debug_tool/utils/qt_widgets.py | 45 +++- 5 files changed, 248 insertions(+), 202 deletions(-) diff --git a/scripts/debug_tool/GUI_debug.py b/scripts/debug_tool/GUI_debug.py index dbacfdf..e599bc9 100644 --- a/scripts/debug_tool/GUI_debug.py +++ b/scripts/debug_tool/GUI_debug.py @@ -3,6 +3,7 @@ import sys import os from tabs.config_tab import ConfigTab +from tabs.shifter_tab import ShifterTab from utils.config_reader import ConfigReader class ModuleDebugGUI(QMainWindow): @@ -34,6 +35,7 @@ class ModuleDebugGUI(QMainWindow): # Initialize tabs self.tabs.addTab(ConfigTab(self.config_reader), "Configuration") + self.tabs.addTab(ShifterTab(self.config_reader), "Image Shifter") def main(): app = QApplication(sys.argv) diff --git a/scripts/debug_tool/tabs/shifter_tab.py b/scripts/debug_tool/tabs/shifter_tab.py index 0ec621d..d532b5d 100644 --- a/scripts/debug_tool/tabs/shifter_tab.py +++ b/scripts/debug_tool/tabs/shifter_tab.py @@ -1,226 +1,121 @@ -import tkinter as tk -from tkinter import ttk, messagebox, filedialog +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QGroupBox, QMessageBox) +from PyQt6.QtCore import Qt import os -from PIL import Image, ImageTk -import subprocess -class ShifterTab: - def __init__(self, notebook, config_reader): - self.tab = ttk.Frame(notebook) - notebook.add(self.tab, text='Image Shifter') - +from utils.qt_widgets import (create_group_with_text, create_button_layout, + create_info_group, create_preview_group) +from utils.file_handlers import select_file, run_command +from utils.image_handlers import update_preview + +class ShifterTab(QWidget): + def __init__(self, config_reader): + super().__init__() self.config_reader = config_reader self.input_file_path = None self.shifted_image_path = os.path.join( self.config_reader.directories['scriptDir'], "shifted_t.png" ) - self.setup_ui() def setup_ui(self): - # Split into left and right frames - left_frame = ttk.Frame(self.tab) - left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5) - - right_frame = ttk.Frame(self.tab) - right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=5, pady=5) - - self.setup_control_panel(left_frame) - self.setup_preview_panel(right_frame) + main_layout = QHBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + + # Left panel (controls) + left_layout = QVBoxLayout() + self.setup_control_panel(left_layout) + main_layout.addLayout(left_layout) + + # Right panel (preview) + right_layout = QVBoxLayout() + self.setup_preview_panel(right_layout) + main_layout.addLayout(right_layout) - def setup_control_panel(self, parent): - # Control section - control_frame = ttk.LabelFrame(parent, text="Controls", padding="5") - control_frame.pack(fill=tk.X, pady=5) - - # Input file selection - input_frame = ttk.Frame(control_frame) - input_frame.pack(fill=tk.X, pady=5) - - ttk.Label(input_frame, text="Input file:").pack(side=tk.LEFT, padx=5) - self.input_label = ttk.Label(input_frame, text="No file selected") - self.input_label.pack(side=tk.LEFT, padx=5) - - ttk.Button( - input_frame, - text="Select Input File", - command=self.select_input_file - ).pack(side=tk.RIGHT, padx=5) - - # Output file display - output_frame = ttk.Frame(control_frame) - output_frame.pack(fill=tk.X, pady=5) - - ttk.Label(output_frame, text="Output will be saved as:").pack(side=tk.LEFT, padx=5) - self.output_label = ttk.Label( - output_frame, - text=self.shifted_image_path - ) - self.output_label.pack(side=tk.LEFT, padx=5) - - # Environment info - env_frame = ttk.Frame(control_frame) - env_frame.pack(fill=tk.X, pady=5) - - ttk.Label(env_frame, text="Using conda environment:").pack(side=tk.LEFT, padx=5) - self.env_label = ttk.Label( - env_frame, - text=self.config_reader.config.get('materialEnv', 'Not found') - ) - self.env_label.pack(side=tk.LEFT, padx=5) + def setup_control_panel(self, layout): + # Info display + info_rows = [ + ("Input file:", "No file selected"), + ("Output file:", self.shifted_image_path), + ("Environment:", self.config_reader.config.get('materialEnv', 'Not found')) + ] + info_group, self.info_labels = create_info_group("Controls", info_rows) + layout.addWidget(info_group) # Command preview - cmd_frame = ttk.LabelFrame(parent, text="Command Preview", padding="5") - cmd_frame.pack(fill=tk.X, pady=5) - - self.cmd_text = tk.Text(cmd_frame, height=3, wrap=tk.WORD) - self.cmd_text.pack(fill=tk.X) - self.update_command_preview() - - # Control buttons - button_frame = ttk.Frame(control_frame) - button_frame.pack(fill=tk.X, pady=5) - - ttk.Button( - button_frame, - text="Run Shifter", - command=self.run_shifter - ).pack(side=tk.LEFT, padx=5) + cmd_group, self.cmd_text = create_group_with_text("Command Preview", 80) + layout.addWidget(cmd_group) - ttk.Button( - button_frame, - text="Clear Status", - command=self.clear_status - ).pack(side=tk.RIGHT, padx=5) + # Buttons + buttons = [ + ("Select Input", self.handle_file_select, 'left'), + ("Run Shifter", self.run_shifter, 'left'), + ("Clear Status", lambda: self.status_text.clear(), 'right') + ] + layout.addLayout(create_button_layout(*buttons)) - # Status section - status_frame = ttk.LabelFrame(parent, text="Status", padding="5") - status_frame.pack(fill=tk.BOTH, expand=True, pady=5) + # Status display + status_group, self.status_text = create_group_with_text("Status", 150) + layout.addWidget(status_group) - # Add scrollbar to status - status_scroll = ttk.Scrollbar(status_frame) - status_scroll.pack(side=tk.RIGHT, fill=tk.Y) - - self.status_text = tk.Text( - status_frame, - height=10, - wrap=tk.WORD, - yscrollcommand=status_scroll.set - ) - self.status_text.pack(fill=tk.BOTH, expand=True) - status_scroll.config(command=self.status_text.yview) + self.update_command_preview() - def setup_preview_panel(self, parent): - preview_frame = ttk.LabelFrame(parent, text="Image Preview", padding="5") - preview_frame.pack(fill=tk.BOTH, expand=True) + def setup_preview_panel(self, layout): + preview_layout = QVBoxLayout() - # Input image preview - input_preview_frame = ttk.LabelFrame(preview_frame, text="Input Image") - input_preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5) + # Split into two preview groups + preview_group = QGroupBox("Image Previews") + previews_layout = QHBoxLayout(preview_group) - self.input_preview = ttk.Label(input_preview_frame) - self.input_preview.pack(padx=5, pady=5) + input_group, self.input_preview = create_preview_group("Input Image") + output_group, self.output_preview = create_preview_group("Shifted Image") - # Output image preview - output_preview_frame = ttk.LabelFrame(preview_frame, text="Shifted Image") - output_preview_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5) + previews_layout.addWidget(input_group) + previews_layout.addWidget(output_group) + layout.addWidget(preview_group) + + def handle_file_select(self): + file_path = select_file( + self, + "Select Input Image", + "Images (*.png *.jpg *.jpeg)" + ) - self.output_preview = ttk.Label(output_preview_frame) - self.output_preview.pack(padx=5, pady=5) + if file_path: + self.input_file_path = file_path + self.info_labels["Input file:"].setText(os.path.basename(file_path)) + self.update_status(f"Selected input file: {file_path}") + self.update_command_preview() + update_preview(self.input_preview, file_path, error_callback=self.update_status) def update_command_preview(self): if not self.input_file_path: - self.cmd_text.delete(1.0, tk.END) - self.cmd_text.insert(tk.END, "Select an input file to see the command") + self.cmd_text.setText("Select an input file to see the command") return cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python "{os.path.join(self.config_reader.directories['scriptDir'], "shifter.py")}" "{self.input_file_path}" "{self.shifted_image_path}" && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"''' - self.cmd_text.delete(1.0, tk.END) - self.cmd_text.insert(tk.END, cmd) - - def select_input_file(self): - filepath = filedialog.askopenfilename( - filetypes=[("Image files", "*.png *.jpg *.jpeg")] - ) - if filepath: - self.input_file_path = os.path.normpath(filepath) - self.input_label.config(text=os.path.basename(filepath)) - self.update_status(f"Selected input file: {filepath}") - self.update_command_preview() - self.update_image_preview() + self.cmd_text.setText(cmd) def update_status(self, message): - self.status_text.insert(tk.END, f"{message}\n") - self.status_text.see(tk.END) - - def clear_status(self): - self.status_text.delete(1.0, tk.END) + self.status_text.append(message) + scrollbar = self.status_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) def run_shifter(self): if not self.input_file_path: - messagebox.showwarning("Warning", "Please select an input file first") + QMessageBox.warning(self, "Warning", "Please select an input file first") return self.update_status("\nRunning image shifter...") + success, _ = run_command( + self, + self.cmd_text.toPlainText(), + self.update_status + ) - try: - # Construct the command - cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python "{os.path.join(self.config_reader.directories['scriptDir'], "shifter.py")}" "{self.input_file_path}" "{self.shifted_image_path}" && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"''' - - # Run the command - self.update_status(f"Executing command:\n{cmd}\n") - - process = subprocess.Popen( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - shell=True, - text=True - ) - stdout, stderr = process.communicate() - - if process.returncode == 0: - self.update_status("Image shifter completed successfully") - if stdout: - self.update_status("Output:\n" + stdout) - - if os.path.exists(self.shifted_image_path): - self.update_status(f"Shifted image saved to: {self.shifted_image_path}") - self.update_image_preview() - else: - self.update_status("Warning: Output file not found!") - else: - self.update_status("Image shifter failed with error:\n" + stderr) - messagebox.showerror("Error", f"Command failed:\n\n{stderr}") - - except Exception as e: - error_msg = f"Failed to run image shifter: {str(e)}" - self.update_status(error_msg) - messagebox.showerror("Error", error_msg) - - def update_image_preview(self): - preview_size = (300, 300) # Adjust size as needed - - # Update input image preview if exists - if self.input_file_path and os.path.exists(self.input_file_path): - try: - input_img = Image.open(self.input_file_path) - input_img.thumbnail(preview_size) - input_photo = ImageTk.PhotoImage(input_img) - self.input_preview.configure(image=input_photo) - self.input_preview.image = input_photo # Keep a reference - except Exception as e: - self.update_status(f"Failed to load input preview: {str(e)}") - - # Update shifted image preview if exists - if os.path.exists(self.shifted_image_path): - try: - output_img = Image.open(self.shifted_image_path) - output_img.thumbnail(preview_size) - output_photo = ImageTk.PhotoImage(output_img) - self.output_preview.configure(image=output_photo) - self.output_preview.image = output_photo # Keep a reference - except Exception as e: - self.update_status(f"Failed to load shifted image preview: {str(e)}") \ No newline at end of file + if success and os.path.exists(self.shifted_image_path): + self.update_status(f"Shifted image saved to: {self.shifted_image_path}") + update_preview(self.output_preview, self.shifted_image_path, + error_callback=self.update_status) \ No newline at end of file diff --git a/scripts/debug_tool/utils/file_handlers.py b/scripts/debug_tool/utils/file_handlers.py index 8d79ed1..9746b2f 100644 --- a/scripts/debug_tool/utils/file_handlers.py +++ b/scripts/debug_tool/utils/file_handlers.py @@ -1,19 +1,27 @@ # utils/file_handlers.py -from PyQt6.QtWidgets import QFileDialog +from PyQt6.QtWidgets import QFileDialog, QMessageBox +import subprocess +import os -def select_file(parent, title="Select File", file_types="All Files (*)", initial_dir=""): +def select_file(parent, title="Select File", file_types="All Files (*)", initial_dir=None): """ Opens a file selection dialog. Args: - parent: Parent widget + parent: Parent widget (should have config_reader attribute) title: Dialog window title file_types: File filter (e.g., "Images (*.png *.jpg);;All Files (*)") - initial_dir: Starting directory for the dialog + initial_dir: Starting directory for the dialog. If None, uses default Data directory Returns: Selected file path or empty string if cancelled """ + # Get default directory if not specified + if initial_dir is None and hasattr(parent, 'config_reader'): + initial_dir = os.path.join(parent.config_reader.directories['edgeNetDir'], 'Data') + elif initial_dir is None and hasattr(parent, 'parent') and hasattr(parent.parent(), 'config_reader'): + initial_dir = os.path.join(parent.parent().config_reader.directories['edgeNetDir'], 'Data') + file_path, _ = QFileDialog.getOpenFileName( parent, title, @@ -22,20 +30,26 @@ def select_file(parent, title="Select File", file_types="All Files (*)", initial ) return file_path -def save_file(parent, title="Save File", file_types="All Files (*)", initial_dir="", suggested_name=""): +def save_file(parent, title="Save File", file_types="All Files (*)", initial_dir=None, suggested_name=""): """ Opens a save file dialog. Args: - parent: Parent widget + parent: Parent widget (should have config_reader attribute) title: Dialog window title file_types: File filter - initial_dir: Starting directory + initial_dir: Starting directory. If None, uses default Data directory suggested_name: Default filename Returns: Selected save path or empty string if cancelled """ + # Get default directory if not specified + if initial_dir is None and hasattr(parent, 'config_reader'): + initial_dir = os.path.join(parent.config_reader.directories['edgeNetDir'], 'Data') + elif initial_dir is None and hasattr(parent, 'parent') and hasattr(parent.parent(), 'config_reader'): + initial_dir = os.path.join(parent.parent().config_reader.directories['edgeNetDir'], 'Data') + file_path, _ = QFileDialog.getSaveFileName( parent, title, @@ -44,21 +58,72 @@ def save_file(parent, title="Save File", file_types="All Files (*)", initial_dir ) return file_path -def select_directory(parent, title="Select Directory", initial_dir=""): +def select_directory(parent, title="Select Directory", initial_dir=None): """ Opens a directory selection dialog. Args: - parent: Parent widget + parent: Parent widget (should have config_reader attribute) title: Dialog window title - initial_dir: Starting directory + initial_dir: Starting directory. If None, uses default Data directory Returns: Selected directory path or empty string if cancelled """ + # Get default directory if not specified + if initial_dir is None and hasattr(parent, 'config_reader'): + initial_dir = os.path.join(parent.config_reader.directories['edgeNetDir'], 'Data') + elif initial_dir is None and hasattr(parent, 'parent') and hasattr(parent.parent(), 'config_reader'): + initial_dir = os.path.join(parent.parent().config_reader.directories['edgeNetDir'], 'Data') + return QFileDialog.getExistingDirectory( parent, title, initial_dir, QFileDialog.Option.ShowDirsOnly ) + +def run_command(parent, cmd, status_callback=None): + """ + Runs a command and handles its output. + + Args: + parent: Parent widget + cmd: Command to execute + status_callback: Optional callback for status updates + + Returns: + tuple: (success, message) + """ + try: + if status_callback: + status_callback(f"Executing command:\n{cmd}\n") + + process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + shell=True, + text=True + ) + stdout, stderr = process.communicate() + + if process.returncode == 0: + if status_callback: + status_callback("Command completed successfully") + if stdout: + status_callback("Output:\n" + stdout) + return True, stdout + else: + error_msg = f"Command failed:\n\n{stderr}" + if status_callback: + status_callback("Command failed with error:\n" + stderr) + QMessageBox.critical(parent, "Error", error_msg) + return False, stderr + + except Exception as e: + error_msg = f"Failed to execute command: {str(e)}" + if status_callback: + status_callback(error_msg) + QMessageBox.critical(parent, "Error", error_msg) + return False, error_msg \ No newline at end of file diff --git a/scripts/debug_tool/utils/image_handlers.py b/scripts/debug_tool/utils/image_handlers.py index 9303376..497d1ea 100644 --- a/scripts/debug_tool/utils/image_handlers.py +++ b/scripts/debug_tool/utils/image_handlers.py @@ -1,8 +1,19 @@ +# utils/image_handlers.py from PyQt6.QtGui import QPixmap, QImage -import numpy as np +from PyQt6.QtCore import Qt +from PyQt6.QtWidgets import QLabel import cv2 +import os def convert_cv_to_pixmap(cv_img): + """ + Converts OpenCV image to QPixmap. + + Args: + cv_img: OpenCV image as numpy array + Returns: + QPixmap image + """ height, width = cv_img.shape[:2] if len(cv_img.shape) == 3: bytes_per_line = 3 * width @@ -13,6 +24,15 @@ def convert_cv_to_pixmap(cv_img): return QPixmap.fromImage(qt_img) def load_and_resize_image(image_path, max_size=800): + """ + Loads and resizes an image while maintaining aspect ratio. + + Args: + image_path: Path to the image file + max_size: Maximum size for the larger dimension + Returns: + Resized image as numpy array + """ try: img = cv2.imread(image_path) if img is None: @@ -31,4 +51,27 @@ def load_and_resize_image(image_path, max_size=800): return img except Exception as e: - raise Exception(f"Error loading image: {str(e)}") \ No newline at end of file + raise Exception(f"Error loading image: {str(e)}") + +def update_preview(preview_label, image_path, max_size=300, error_callback=None): + """ + Updates a QLabel with an image preview. + + Args: + preview_label: QLabel widget to update + image_path: Path to the image file + max_size: Maximum size for preview + error_callback: Optional callback function for error handling + """ + if image_path and os.path.exists(image_path): + try: + img = load_and_resize_image(image_path, max_size) + pixmap = convert_cv_to_pixmap(img) + preview_label.setPixmap(pixmap) + except Exception as e: + if error_callback: + error_callback(f"Failed to load preview: {str(e)}") + else: + preview_label.clear() + if error_callback: + error_callback(f"Image not found: {image_path}") \ No newline at end of file diff --git a/scripts/debug_tool/utils/qt_widgets.py b/scripts/debug_tool/utils/qt_widgets.py index 59c5284..85d49c4 100644 --- a/scripts/debug_tool/utils/qt_widgets.py +++ b/scripts/debug_tool/utils/qt_widgets.py @@ -1,6 +1,7 @@ # utils/qt_widgets.py from PyQt6.QtWidgets import (QTextEdit, QGroupBox, QVBoxLayout, - QHBoxLayout, QPushButton) + QHBoxLayout, QPushButton, QLabel) +from PyQt6.QtCore import Qt def create_group_with_text(title, height=100): """ @@ -58,4 +59,44 @@ def create_status_group(): Returns: tuple: (QGroupBox, QTextEdit) """ - return create_group_with_text("Status", 80) \ No newline at end of file + return create_group_with_text("Status", 80) + +def create_info_group(title, rows): + """ + Creates a QGroupBox with rows of label pairs. + + Args: + title: Group box title + rows: List of tuples (label_text, value_text) + Returns: + tuple: (QGroupBox, dict of value QLabels) + """ + group = QGroupBox(title) + layout = QVBoxLayout(group) + labels = {} + + for label_text, value_text in rows: + row = QHBoxLayout() + row.addWidget(QLabel(label_text)) + value_label = QLabel(value_text) + labels[label_text] = value_label + row.addWidget(value_label, stretch=1) + layout.addLayout(row) + + return group, labels + +def create_preview_group(title): + """ + Creates an image preview group. + + Args: + title: Group box title + Returns: + tuple: (QGroupBox, QLabel) + """ + group = QGroupBox(title) + layout = QVBoxLayout(group) + preview = QLabel() + preview.setAlignment(Qt.AlignmentFlag.AlignCenter) + layout.addWidget(preview) + return group, preview \ No newline at end of file -- GitLab