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