From 34d72aafada7dc2957688080d8bdb7155c720700 Mon Sep 17 00:00:00 2001
From: mhby1g21 <mhby1g21@soton.ac.uk>
Date: Tue, 29 Oct 2024 16:44:58 +0000
Subject: [PATCH] refactored material tab (much more managable file size/lines
 now)

---
 scripts/debug_tool/GUI_debug.py            |   2 +
 scripts/debug_tool/tabs/material_tab.py    | 812 ++++++---------------
 scripts/debug_tool/utils/image_handlers.py |  30 +-
 scripts/debug_tool/utils/qt_widgets.py     |  85 ++-
 4 files changed, 328 insertions(+), 601 deletions(-)

diff --git a/scripts/debug_tool/GUI_debug.py b/scripts/debug_tool/GUI_debug.py
index 670e565..a4c39fb 100644
--- a/scripts/debug_tool/GUI_debug.py
+++ b/scripts/debug_tool/GUI_debug.py
@@ -5,6 +5,7 @@ import os
 from tabs.config_tab import ConfigTab
 from tabs.shifter_tab import ShifterTab
 from tabs.depth_tab import DepthTab
+from tabs.material_tab import MaterialTab
 from utils.config_reader import ConfigReader
 
 class ModuleDebugGUI(QMainWindow):
@@ -38,6 +39,7 @@ class ModuleDebugGUI(QMainWindow):
         self.tabs.addTab(ConfigTab(self.config_reader), "Configuration")
         self.tabs.addTab(ShifterTab(self.config_reader), "Image Shifter")
         self.tabs.addTab(DepthTab(self.config_reader), "MonoDepth Estimation")
+        self.tabs.addTab(MaterialTab(self.config_reader), "Material Recognition")
 
 def main():
     app = QApplication(sys.argv)
diff --git a/scripts/debug_tool/tabs/material_tab.py b/scripts/debug_tool/tabs/material_tab.py
index c21c376..b38111d 100644
--- a/scripts/debug_tool/tabs/material_tab.py
+++ b/scripts/debug_tool/tabs/material_tab.py
@@ -1,15 +1,20 @@
-import tkinter as tk
-from tkinter import ttk, messagebox, filedialog
+# tabs/material_tab.py
+from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, 
+                           QMessageBox, QTabWidget)
+from PyQt6.QtCore import Qt
 import os
-from PIL import Image, ImageTk
-import subprocess
-import shutil
 
-class MaterialTab:
-    def __init__(self, notebook, config_reader):
-        self.tab = ttk.Frame(notebook)
-        notebook.add(self.tab, text='Material Recognition')
-        
+from utils.qt_widgets import (create_group_with_text, create_button_layout, 
+                            create_info_group, create_preview_group, 
+                            create_status_label, create_preview_grid,
+                            update_status_indicator, get_status_text)
+from utils.file_handlers import select_file, clean_directory, run_command
+from utils.image_handlers import (update_preview, update_face_previews, 
+                                clear_previews)
+
+class MaterialTab(QWidget):
+    def __init__(self, config_reader):
+        super().__init__()
         self.config_reader = config_reader
         self.input_file_path = None
         
@@ -17,641 +22,250 @@ class MaterialTab:
         self.material_recog_dir = self.config_reader.directories['materialRecogDir']
         self.checkpoint_file = self.config_reader.file_paths['checkpointFile']
         self.cubemap_dir = os.path.join(self.material_recog_dir, "cubemap_faces")
+        self.material_output_dir = os.path.join(self.material_recog_dir, "output", "cubemap_faces")
         
         self.setup_ui()
+        self.verify_checkpoint()
 
     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)
-        
-        # Checkpoint file verification
-        checkpoint_frame = ttk.Frame(control_frame)
-        checkpoint_frame.pack(fill=tk.X, pady=5)
-        
-        ttk.Label(checkpoint_frame, text="Checkpoint file:").pack(side=tk.LEFT, padx=5)
-        self.checkpoint_label = ttk.Label(
-            checkpoint_frame,
-            text="✓ Found" if os.path.exists(self.checkpoint_file) else "✗ Missing",
-            foreground="green" if os.path.exists(self.checkpoint_file) else "red"
-        )
-        self.checkpoint_label.pack(side=tk.LEFT, padx=5)
+    def setup_control_panel(self, layout):
+        # Info display
+        info_rows = [
+            ("Input file:", "No file selected"),
+            ("Checkpoint:", "✓ Found" if os.path.exists(self.checkpoint_file) else "✗ Missing")
+        ]
+        info_group, self.info_labels = create_info_group("Controls", info_rows)
+        layout.addWidget(info_group)
         
         # Progress indicators
-        self.progress_frame = ttk.LabelFrame(control_frame, text="Progress", padding="5")
-        self.progress_frame.pack(fill=tk.X, pady=5)
-        
-        # Split progress
-        split_frame = ttk.Frame(self.progress_frame)
-        split_frame.pack(fill=tk.X, pady=2)
-        ttk.Label(split_frame, text="Split 360:").pack(side=tk.LEFT, padx=5)
-        self.split_status = ttk.Label(split_frame, text="Not started")
-        self.split_status.pack(side=tk.LEFT, padx=5)
-        
-        # Material recognition progress
-        recog_frame = ttk.Frame(self.progress_frame)
-        recog_frame.pack(fill=tk.X, pady=2)
-        ttk.Label(recog_frame, text="Material Recognition:").pack(side=tk.LEFT, padx=5)
-        self.recog_status = ttk.Label(recog_frame, text="Not started")
-        self.recog_status.pack(side=tk.LEFT, padx=5)
-        
-        # Combine progress
-        combine_frame = ttk.Frame(self.progress_frame)
-        combine_frame.pack(fill=tk.X, pady=2)
-        ttk.Label(combine_frame, text="Combine Output:").pack(side=tk.LEFT, padx=5)
-        self.combine_status = ttk.Label(combine_frame, text="Not started")
-        self.combine_status.pack(side=tk.LEFT, padx=5)
-        
-        # Control buttons
-        button_frame = ttk.Frame(control_frame)
-        button_frame.pack(fill=tk.X, pady=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Clean Working Dir",
-            command=self.clean_working_dir
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Select Input",
-            command=self.select_input_file
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Run Split 360",
-            command=self.run_split_360
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Run Material Recognition",
-            command=self.run_material_recognition
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Run Combine",
-            command=self.run_combine
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Run All Steps",
-            command=self.run_all_steps
-        ).pack(side=tk.LEFT, padx=5)
-        
-        ttk.Button(
-            button_frame,
-            text="Clear Status",
-            command=self.clear_status
-        ).pack(side=tk.RIGHT, padx=5)
-        
-        # Status section
-        status_frame = ttk.LabelFrame(parent, text="Status", padding="5")
-        status_frame.pack(fill=tk.BOTH, expand=True, pady=5)
-        
-        # 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)
+        progress_group = QGroupBox("Progress")
+        progress_layout = QVBoxLayout(progress_group)
+        
+        self.split_status = create_status_label("Split 360:", "Not started")
+        self.recognition_status = create_status_label("Material Recognition:", "Not started")
+        self.combine_status = create_status_label("Combine Output:", "Not started")
+        
+        progress_layout.addLayout(self.split_status)
+        progress_layout.addLayout(self.recognition_status)
+        progress_layout.addLayout(self.combine_status)
+        layout.addWidget(progress_group)
+        
+        # Buttons
+        buttons = [
+            ("Clean Working Dir", self.clean_working_dir, 'left'),
+            ("Select Input", self.select_input, 'left'),
+            ("Run Split 360", self.run_split_360, 'left'),
+            ("Run Recognition", self.run_material_recognition, 'left'),
+            ("Run Combine", self.run_combine, 'left'),
+            ("Run All Steps", self.run_all_steps, 'left'),
+            ("Clear Status", lambda: self.status_text.clear(), 'right')
+        ]
+        layout.addLayout(create_button_layout(*buttons))
+        
+        # Status display
+        status_group, self.status_text = create_group_with_text("Status", 150)
+        layout.addWidget(status_group)
 
-    def setup_preview_panel(self, parent):
-        notebook = ttk.Notebook(parent)
-        notebook.pack(fill=tk.BOTH, expand=True)
+    def setup_preview_panel(self, layout):
+        preview_tabs = QTabWidget()
         
         # Input/Output preview tab
-        io_frame = ttk.Frame(notebook)
-        notebook.add(io_frame, text='Input/Output')
+        io_tab = QWidget()
+        io_layout = QVBoxLayout(io_tab)
         
-        # Input preview
-        input_frame = ttk.LabelFrame(io_frame, text="Input Image")
-        input_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
+        input_group, self.input_preview = create_preview_group("Input Image")
+        output_group, self.output_preview = create_preview_group("Material Map")
         
-        self.input_preview = ttk.Label(input_frame)
-        self.input_preview.pack(padx=5, pady=5)
+        io_layout.addWidget(input_group)
+        io_layout.addWidget(output_group)
+        preview_tabs.addTab(io_tab, "Input/Output")
         
-        # Material map preview
-        output_frame = ttk.LabelFrame(io_frame, text="Material Map")
-        output_frame.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)
+        # RGB faces preview tab
+        rgb_tab = QWidget()
+        rgb_layout = QVBoxLayout(rgb_tab)
         
-        self.output_preview = ttk.Label(output_frame)
-        self.output_preview.pack(padx=5, pady=5)
-        
-        # RGB Cube faces preview tab
-        rgb_faces_frame = ttk.Frame(notebook)
-        notebook.add(rgb_faces_frame, text='RGB Cube Faces')
-        
-        # Create grid for RGB cube faces
         self.rgb_face_previews = {}
-        face_names = ['front', 'back', 'left', 'right', 'top', 'bottom']
-        row = 0
-        col = 0
-        for face in face_names:
-            frame = ttk.LabelFrame(rgb_faces_frame, text=face)
-            frame.grid(row=row, column=col, padx=5, pady=5, sticky='nsew')
-            
-            label = ttk.Label(frame)
-            label.pack(padx=5, pady=5)
-            self.rgb_face_previews[face] = label
-            
-            col += 1
-            if col > 2:  # 3 columns
-                col = 0
-                row += 1
-
-        # Configure grid weights for RGB faces
-        rgb_faces_frame.grid_columnconfigure(0, weight=1)
-        rgb_faces_frame.grid_columnconfigure(1, weight=1)
-        rgb_faces_frame.grid_columnconfigure(2, weight=1)
-
-        # Material Cube faces preview tab
-        material_faces_frame = ttk.Frame(notebook)
-        notebook.add(material_faces_frame, text='Material Cube Faces')
+        faces_layout = create_preview_grid(
+            ['front', 'back', 'left', 'right', 'top', 'bottom'],
+            self.rgb_face_previews
+        )
+        rgb_layout.addLayout(faces_layout)
+        preview_tabs.addTab(rgb_tab, "RGB Cube Faces")
+        
+        # Material faces preview tab
+        material_tab = QWidget()
+        material_layout = QVBoxLayout(material_tab)
         
-        # Create grid for material cube faces
         self.material_face_previews = {}
-        row = 0
-        col = 0
-        face_names = ['frontrgb', 'backrgb', 'leftrgb', 'rightrgb', 'toprgb', 'bottomrgb']
-        for face in face_names:
-            frame = ttk.LabelFrame(material_faces_frame, text=face)
-            frame.grid(row=row, column=col, padx=5, pady=5, sticky='nsew')
-            
-            label = ttk.Label(frame)
-            label.pack(padx=5, pady=5)
-            self.material_face_previews[face] = label
-            
-            col += 1
-            if col > 2:  # 3 columns
-                col = 0
-                row += 1
+        material_faces_layout = create_preview_grid(
+            ['front', 'back', 'left', 'right', 'top', 'bottom'],
+            self.material_face_previews
+        )
+        material_layout.addLayout(material_faces_layout)
+        preview_tabs.addTab(material_tab, "Material Cube Faces")
+        
+        layout.addWidget(preview_tabs)
 
-        # Configure grid weights for material faces
-        material_faces_frame.grid_columnconfigure(0, weight=1)
-        material_faces_frame.grid_columnconfigure(1, weight=1)
-        material_faces_frame.grid_columnconfigure(2, weight=1)
+    def select_input(self):
+        file_path = select_file(
+            self,
+            "Select Input Image",
+            "Images (*.png *.jpg *.jpeg)"
+        )
+        
+        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}")
+            update_preview(self.input_preview, file_path, 
+                         error_callback=self.update_status)
+            self.reset_status_indicators()
 
     def clean_working_dir(self):
-        """Clean all working directories"""
-        try:
-            directories_to_clean = [
-                # Main cubemap_faces directory
-                self.cubemap_dir,
-                # Output cubemap_faces directory for material maps
-                os.path.join(self.material_recog_dir, "output", "cubemap_faces"),
-            ]
-            
-            for directory in directories_to_clean:
-                if os.path.exists(directory):
-                    self.update_status(f"Cleaning directory: {directory}")
-                    shutil.rmtree(directory)
-                # Recreate directories
-                os.makedirs(directory, exist_ok=True)
-            
-            self.update_status("All working directories cleaned")
-            
-            # Reset status indicators
-            self.split_status.config(text="Not started")
-            self.recog_status.config(text="Not started")
-            self.combine_status.config(text="Not started")
-            
-            # Clear all previews
-            self.clear_previews()
+        dirs_to_clean = [self.cubemap_dir, self.material_output_dir]
+        
+        for directory in dirs_to_clean:
+            if not clean_directory(directory, self.update_status):
+                QMessageBox.critical(self, "Error", f"Failed to clean directory: {directory}")
+                return
             
-        except Exception as e:
-            error_msg = f"Error cleaning directories: {str(e)}"
-            self.update_status(error_msg)
-            messagebox.showerror("Error", error_msg)
-
-    def clear_previews(self):
-        """Clear all image previews"""
-        self.input_preview.configure(image='')
-        self.output_preview.configure(image='')
-        for face_preview in self.rgb_face_previews.values():
-            face_preview.configure(image='')
-        for face_preview in self.material_face_previews.values():
-            face_preview.configure(image='')
-    
-    def select_input_file(self):
-        filepath = filedialog.askopenfilename(
-            filetypes=[("Image files", "*.png *.jpg *.jpeg")]
+        clear_previews(
+            self.input_preview, 
+            self.output_preview,
+            self.rgb_face_previews,
+            self.material_face_previews
         )
-        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_input_preview()
-            
-            # Reset status indicators
-            self.split_status.config(text="Not started")
-            self.recog_status.config(text="Not started")
-            self.combine_status.config(text="Not started")
+        self.reset_status_indicators()
 
     def run_split_360(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
             
-        try:
-            self.update_status("Running 360 image splitting...")
-            self.split_status.config(text="Running...", foreground="orange")
-            
-            # Change to material recognition directory
-            original_dir = os.getcwd()
-            os.chdir(self.material_recog_dir)
-            
-            # Run split_img.py
-            cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python split_img.py "{self.input_file_path}" && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"'''
-            
-            process = subprocess.Popen(
-                cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                shell=True,
-                text=True
-            )
-            stdout, stderr = process.communicate()
-            
-            # Change back to original directory
-            os.chdir(original_dir)
-            
-            if process.returncode == 0:
-                self.update_status("Split 360 completed successfully")
-                self.split_status.config(text="✓ Complete", foreground="green")
-                self.update_face_previews()
-            else:
-                self.update_status(f"Split 360 failed:\n{stderr}")
-                self.split_status.config(text="✗ Failed", foreground="red")
-                raise Exception(stderr)
-                
-        except Exception as e:
-            self.update_status(f"Error in split 360: {str(e)}")
-            self.split_status.config(text="✗ Failed", foreground="red")
-            messagebox.showerror("Error", f"Split 360 failed: {str(e)}")
-
-    def run_command(self, cmd, description):
-        """Run a single command and return its output"""
-        self.update_status(f"Executing {description}:\n{cmd}")
-        
-        process = subprocess.Popen(
-            cmd,
-            stdout=subprocess.PIPE,
-            stderr=subprocess.PIPE,
-            shell=True,
-            text=True
-        )
-        stdout, stderr = process.communicate()
+        self.update_status("Running 360 image splitting...")
+        update_status_indicator(self.split_status, "Running")
         
-        if process.returncode != 0:
-            self.update_status(f"Error in {description}:\n{stderr}")
-            raise Exception(f"{description} failed: {stderr}")
-            
-        if stdout:
-            self.update_status(f"{description} output:\n{stdout}")
-            
-        return process.returncode == 0
+        original_dir = os.getcwd()
+        os.chdir(self.material_recog_dir)
+        
+        cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python split_img.py "{self.input_file_path}" && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"'''
+        
+        success, _ = run_command(self, cmd, self.update_status)
+        
+        os.chdir(original_dir)
+        
+        if success:
+            update_status_indicator(self.split_status, "Complete")
+            self.update_face_previews('rgb')
+        else:
+            update_status_indicator(self.split_status, "Failed")
 
     def run_material_recognition(self):
         if not os.path.exists(self.cubemap_dir):
-            messagebox.showwarning("Warning", "Please run Split 360 first")
+            QMessageBox.warning(self, "Warning", "Please run Split 360 first")
             return
             
-        try:
-            self.update_status("Running material recognition...")
-            self.recog_status.config(text="Running...", foreground="orange")
-            
-            # Save current directory
-            original_dir = os.getcwd()
-            
-            # Change to material recognition directory
-            os.chdir(self.material_recog_dir)
-            
-            # Combine conda activation and Python command into a single command using cmd /c
-            material_cmd = (
-                f'cmd /c ""{self.config_reader.config["condaDir"]}\\Scripts\\activate.bat" {self.config_reader.config["materialEnv"]} && '
-                f'python train_sota.py --data-root "./datasets" '
-                f'--batch-size 1 --tag dpglt --gpus 1 --num-nodes 1 '
-                f'--epochs 200 --mode 95 --seed 42 '
-                f'--test "{self.checkpoint_file}" '
-                f'--infer "{self.material_recog_dir}/cubemap_faces/"'
-            )
-            
-            self.update_status(f"Executing command:\n{material_cmd}")
-            
-            process = subprocess.Popen(
-                material_cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                shell=True,
-                text=True
-            )
-            stdout, stderr = process.communicate()
-            
-            if stdout:
-                self.update_status("\nOutput:\n" + stdout)
-            if stderr:
-                self.update_status("\nError output:\n" + stderr)
-            
-            # Check error level
-            if process.returncode != 0:
-                self.update_status("Error: Material recognition failed. Please check the output above for more details.")
-                raise Exception(stderr if stderr else "Material recognition failed")
-            
-            self.update_status("Material recognition completed successfully")
-            self.recog_status.config(text="✓ Complete", foreground="green")
-            
-        except Exception as e:
-            self.update_status(f"Error in material recognition: {str(e)}")
-            self.recog_status.config(text="✗ Failed", foreground="red")
-            messagebox.showerror("Error", f"Material recognition failed: {str(e)}")
-            
-        finally:
-            self.update_material_face_previews()
-            if original_dir:
-                os.chdir(original_dir)
-                         
-    def verify_input_directory(self):
-        """Verify that the input directory has the correct files"""
-        required_files = ['front.png', 'back.png', 'left.png', 'right.png', 'top.png', 'bottom.png']
-        missing_files = []
-        
-        for file in required_files:
-            if not os.path.exists(os.path.join(self.cubemap_dir, file)):
-                missing_files.append(file)
-                
-        if missing_files:
-            self.update_status("Missing required files:")
-            for file in missing_files:
-                self.update_status(f"- {file}")
-            return False
-            
-        self.update_status("All required input files present")
-        return True
-
-    def verify_working_directory(self):
-        """Verify that we're in the correct directory with required files"""
-        required_paths = [
-            "./datasets",
-            self.checkpoint_file,
-            "train_sota.py"
-        ]
+        self.update_status("Running material recognition...")
+        update_status_indicator(self.recognition_status, "Running")
         
-        missing_paths = []
-        for path in required_paths:
-            if not os.path.exists(path):
-                missing_paths.append(path)
-                
-        if missing_paths:
-            self.update_status("Missing required paths:")
-            for path in missing_paths:
-                self.update_status(f"- {path}")
-            return False
-            
-        self.update_status("All required paths present")
-        return True
-    
-    # Add a method to verify conda environment
-    def verify_conda_env(self):
-        """Verify that the conda environment exists"""
-        try:
-            cmd = f'call "{self.config_reader.config["condaDir"]}\\condabin\\conda.bat" env list'
-            process = subprocess.Popen(
-                cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                shell=True,
-                text=True
-            )
-            stdout, stderr = process.communicate()
-            
-            if process.returncode == 0:
-                env_name = self.config_reader.config["materialEnv"]
-                if env_name in stdout:
-                    self.update_status(f"Found conda environment: {env_name}")
-                    return True
-                else:
-                    self.update_status(f"Conda environment not found: {env_name}")
-                    return False
-            else:
-                self.update_status(f"Failed to list conda environments: {stderr}")
-                return False
-        except Exception as e:
-            self.update_status(f"Error checking conda environment: {str(e)}")
-            return False
+        original_dir = os.getcwd()
+        os.chdir(self.material_recog_dir)
         
+        cmd = (
+            f'cmd /c ""{self.config_reader.config["condaDir"]}\\Scripts\\activate.bat" {self.config_reader.config["materialEnv"]} && '
+            f'python train_sota.py --data-root "./datasets" '
+            f'--batch-size 1 --tag dpglt --gpus 1 --num-nodes 1 '
+            f'--epochs 200 --mode 95 --seed 42 '
+            f'--test "{self.checkpoint_file}" '
+            f'--infer "{self.material_recog_dir}/cubemap_faces/"'
+        )
+        
+        success, _ = run_command(self, cmd, self.update_status)
+        
+        os.chdir(original_dir)
+        
+        if success:
+            update_status_indicator(self.recognition_status, "Complete")
+            self.update_face_previews('material')
+        else:
+            update_status_indicator(self.recognition_status, "Failed")
+
     def run_combine(self):
-        try:
-            self.update_status("Running combine step...")
-            self.combine_status.config(text="Running...", foreground="orange")
-            
-            # Change to material recognition directory
-            original_dir = os.getcwd()
-            os.chdir(self.material_recog_dir)
-            
-            # Run combine script
-            cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python combine_img.py && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"'''
-            
-            process = subprocess.Popen(
-                cmd,
-                stdout=subprocess.PIPE,
-                stderr=subprocess.PIPE,
-                shell=True,
-                text=True
+        self.update_status("Running combine step...")
+        update_status_indicator(self.combine_status, "Running")
+        
+        original_dir = os.getcwd()
+        os.chdir(self.material_recog_dir)
+        
+        cmd = f'''call "{self.config_reader.config["condaDir"]}\\condabin\\activate.bat" {self.config_reader.config["materialEnv"]} && python combine_img.py && call "{self.config_reader.config["condaDir"]}\\condabin\\deactivate.bat"'''
+        
+        success, _ = run_command(self, cmd, self.update_status)
+        
+        os.chdir(original_dir)
+        
+        if success:
+            update_status_indicator(self.combine_status, "Complete")
+            output_path = os.path.join(
+                self.config_reader.directories['edgeNetDir'],
+                'Data', 'Input', 'material.png'
             )
-            stdout, stderr = process.communicate()
-            
-            # Change back to original directory
-            os.chdir(original_dir)
-            
-            if process.returncode == 0:
-                self.update_status("Combine step completed successfully")
-                self.combine_status.config(text="✓ Complete", foreground="green")
-                self.update_output_preview()
-            else:
-                self.update_status(f"Combine step failed:\n{stderr}")
-                self.combine_status.config(text="✗ Failed", foreground="red")
-                raise Exception(stderr)
-                
-        except Exception as e:
-            self.update_status(f"Error in combine step: {str(e)}")
-            self.combine_status.config(text="✗ Failed", foreground="red")
-            messagebox.showerror("Error", f"Combine step failed: {str(e)}")
+            update_preview(self.output_preview, output_path, 
+                         error_callback=self.update_status)
+        else:
+            update_status_indicator(self.combine_status, "Failed")
 
     def run_all_steps(self):
-        """Run all steps in sequence"""
         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
             
-        try:
-            # Run all steps
-            self.run_split_360()
-            if self.split_status.cget("text") == "✓ Complete":
-                self.run_material_recognition()
-                if self.recog_status.cget("text") == "✓ Complete":
-                    self.run_combine()
-                    
-        except Exception as e:
-            self.update_status(f"Error in pipeline: {str(e)}")
-            messagebox.showerror("Error", f"Pipeline failed: {str(e)}")
+        self.run_split_360()
+        if get_status_text(self.split_status) == "Complete":
+            self.run_material_recognition()
+            if get_status_text(self.recognition_status) == "Complete":
+                self.run_combine()
 
     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 update_input_preview(self):
-        """Update the input image preview"""
-        if self.input_file_path and os.path.exists(self.input_file_path):
-            try:
-                preview_size = (300, 150)  # Adjust size as needed for 360 image
-                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
-            except Exception as e:
-                self.update_status(f"Failed to load input preview: {str(e)}")
+    def reset_status_indicators(self):
+        for status in [self.split_status, self.recognition_status, self.combine_status]:
+            update_status_indicator(status, "Not started")
 
-    def update_rgb_face_previews(self):
-        """Update the RGB cube face previews"""
-        preview_size = (150, 150)
-        face_names = {
-            'front': 'front',
-            'back': 'back',
-            'left': 'left',
-            'right': 'right',
-            'top': 'top',
-            'bottom': 'bottom'
-        }
-        
-        for face, filename in face_names.items():
-            face_path = os.path.join(self.cubemap_dir, f"{filename}.png")
-            if os.path.exists(face_path):
-                try:
-                    face_img = Image.open(face_path)
-                    face_img.thumbnail(preview_size)
-                    face_photo = ImageTk.PhotoImage(face_img)
-                    self.rgb_face_previews[face].configure(image=face_photo)
-                    self.rgb_face_previews[face].image = face_photo
-                except Exception as e:
-                    self.update_status(f"Failed to load RGB {face} preview: {str(e)}")
-
-    def update_material_face_previews(self):
-        """Update the material map cube face previews"""
-        preview_size = (150, 150)
-        material_dir = os.path.join(self.material_recog_dir, "output", "cubemap_faces")
-        face_names = {
-            'frontrgb': 'frontrgb',
-            'backrgb': 'backrgb',
-            'leftrgb': 'leftrgb',
-            'rightrgb': 'rightrgb',
-            'toprgb': 'toprgb',
-            'bottomrgb': 'bottomrgb'
-        }
-        
-        for face, filename in face_names.items():
-            face_path = os.path.join(material_dir, f"{filename}.png")
-            if os.path.exists(face_path):
-                try:
-                    face_img = Image.open(face_path)
-                    face_img.thumbnail(preview_size)
-                    face_photo = ImageTk.PhotoImage(face_img)
-                    self.material_face_previews[face].configure(image=face_photo)
-                    self.material_face_previews[face].image = face_photo
-                except Exception as e:
-                    self.update_status(f"Failed to load material {face} preview: {str(e)}")
-
-    def update_face_previews(self):
-        """Update both RGB and material cube face previews"""
-        self.update_rgb_face_previews()
-        self.update_material_face_previews()
-        """Update the material map cube face previews"""
-        preview_size = (150, 150)
-        material_dir = os.path.join(self.material_recog_dir, "output", "cubemap_faces")
-        face_names = {
-            'front': 'front',
-            'back': 'back',
-            'left': 'left',
-            'right': 'right',
-            'top': 'top',
-            'bottom': 'bottom'
-        }
-        
-        for face, filename in face_names.items():
-            face_path = os.path.join(material_dir, f"{filename}.png")
-            if os.path.exists(face_path):
-                try:
-                    face_img = Image.open(face_path)
-                    face_img.thumbnail(preview_size)
-                    face_photo = ImageTk.PhotoImage(face_img)
-                    self.material_face_previews[face].configure(image=face_photo)
-                    self.material_face_previews[face].image = face_photo
-                except Exception as e:
-                    self.update_status(f"Failed to load material {face} preview: {str(e)}")
-
-    def update_output_preview(self):
-        """Update the material map preview"""
-        try:
-            # First check in EdgeNet Input directory
-            output_path = os.path.join(
-                self.config_reader.directories['edgeNetDir'],
-                'Data',
-                'Input',
-                'material.png'
+    def update_face_previews(self, preview_type='rgb'):
+        if preview_type == 'rgb':
+            update_face_previews(
+                self.rgb_face_previews,
+                self.cubemap_dir,
+                '.png',
+                self.update_status
+            )
+        else:
+            update_face_previews(
+                self.material_face_previews,
+                self.material_output_dir,
+                'rgb.png',
+                self.update_status
             )
-            
-            if not os.path.exists(output_path):
-                self.update_status("Material map not found in expected location")
-                return
-                
-            preview_size = (300, 150)  # Adjust size for 360 image
-            output_img = Image.open(output_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
-            
-        except Exception as e:
-            self.update_status(f"Failed to load material map preview: {str(e)}")
 
     def verify_checkpoint(self):
-        """Verify that the checkpoint file exists"""
-        if not os.path.exists(self.checkpoint_file):
-            self.update_status("Warning: Checkpoint file not found!")
-            self.checkpoint_label.config(text="✗ Missing", foreground="red")
-            return False
-            
-        self.checkpoint_label.config(text="✓ Found", foreground="green")
-        return True
\ No newline at end of file
+        exists = os.path.exists(self.checkpoint_file)
+        self.info_labels["Checkpoint:"].setText("✓ Found" if exists else "✗ Missing")
+        self.info_labels["Checkpoint:"].setStyleSheet(
+            "color: green" if exists else "color: red")
+        return exists
\ 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 497d1ea..e1cdcdd 100644
--- a/scripts/debug_tool/utils/image_handlers.py
+++ b/scripts/debug_tool/utils/image_handlers.py
@@ -74,4 +74,32 @@ def update_preview(preview_label, image_path, max_size=300, error_callback=None)
     else:
         preview_label.clear()
         if error_callback:
-            error_callback(f"Image not found: {image_path}")
\ No newline at end of file
+            error_callback(f"Image not found: {image_path}")
+            
+def update_face_previews(preview_dict, src_dir, suffix='', error_callback=None):
+    """
+    Updates a dictionary of face preview labels with images from directory.
+    
+    Args:
+        preview_dict: Dictionary of face preview labels
+        src_dir: Directory containing face images
+        suffix: Suffix for face image filenames
+        error_callback: Optional callback function for error handling
+    """
+    for face, preview in preview_dict.items():
+        face_path = os.path.join(src_dir, face + suffix)
+        update_preview(preview, face_path, error_callback=error_callback)
+
+def clear_previews(*preview_widgets):
+    """
+    Clears multiple preview widgets.
+    
+    Args:
+        preview_widgets: List of preview widgets to clear
+    """
+    for widget in preview_widgets:
+        if isinstance(widget, dict):
+            for preview in widget.values():
+                preview.clear()
+        else:
+            widget.clear()     
\ 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 85d49c4..6003e78 100644
--- a/scripts/debug_tool/utils/qt_widgets.py
+++ b/scripts/debug_tool/utils/qt_widgets.py
@@ -99,4 +99,87 @@ def create_preview_group(title):
     preview = QLabel()
     preview.setAlignment(Qt.AlignmentFlag.AlignCenter)
     layout.addWidget(preview)
-    return group, preview
\ No newline at end of file
+    return group, preview
+
+def create_status_label(label_text, initial_status="Not started"):
+    """
+    Creates a status indicator layout with label and status.
+    
+    Args:
+        label_text: Text for the label
+        initial_status: Initial status text 
+    Returns:    
+        QHBoxLayout layout with label and status
+    """
+    layout = QHBoxLayout()
+    label = QLabel(label_text)
+    status = QLabel(initial_status)
+    layout.addWidget(label)
+    layout.addWidget(status, stretch=1)
+    return layout
+
+def create_preview_grid(face_names, preview_dict, cols=3):
+    """
+    Creates a grid layout of preview groups for cubemap faces.
+    
+    Args:
+        face_names: List of face names
+        preview_dict: Dictionary to store preview widgets
+        cols: Number of columns
+    Returns:
+        QVBoxLayout layout with preview groups in grids for cubemap faces
+    """
+    grid = QVBoxLayout()
+    row_layout = QHBoxLayout()
+    count = 0
+    
+    for face in face_names:
+        group, preview = create_preview_group(face)
+        preview_dict[face] = preview
+        row_layout.addWidget(group)
+        count += 1
+        
+        if count % cols == 0:
+            grid.addLayout(row_layout)
+            row_layout = QHBoxLayout()
+    
+    if count % cols != 0:
+        grid.addLayout(row_layout)
+    
+    return grid
+
+def update_status_indicator(status_layout, state):
+    """
+    Updates a status indicator with new state and color.
+    
+    Args:
+        status_layout: QHBoxLayout layout with label and status
+        state: New status text
+    """
+    label = status_layout.itemAt(1).widget()
+    states = {
+        "Running": ("Running...", "orange"),
+        "Complete": ("✓ Complete", "green"),
+        "Failed": ("✗ Failed", "red"),
+        "Not started": ("Not started", "black")
+    }
+    
+    if state in states:
+        text, color = states[state]
+        label.setText(text)
+        label.setStyleSheet(f"color: {color}")
+    else:
+        label.setText(state)
+        label.setStyleSheet("")
+
+def get_status_text(status_layout):
+    """
+    Gets the current status text without markers.
+    
+    Args:
+        status_layout: QHBoxLayout layout with label and status
+    Returns:
+        str: Current status text
+    """
+    text = status_layout.itemAt(1).widget().text()
+    return text.replace("✓ ", "").replace("✗ ", "")
\ No newline at end of file
-- 
GitLab