diff --git a/.gitignore b/.gitignore index 8e1c9ff719ae01a8e81bc7dfdfc5bc50f90a8997..6b5efc082fa8b5f261078601f901ca8a784b7092 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ scripts/360monodepthexecution/rgb.jpg scripts/shifted_t.png -scripts/config.ini \ No newline at end of file +scripts/config.ini +*.pyc \ No newline at end of file diff --git a/Images/GUI.png b/Images/GUI.png new file mode 100644 index 0000000000000000000000000000000000000000..cec8260f51fdddbe1468f9abcbd5681fc095f889 Binary files /dev/null and b/Images/GUI.png differ diff --git a/Images/GUI_debug.jpg b/Images/GUI_debug.jpg new file mode 100644 index 0000000000000000000000000000000000000000..84cf1c61e1ec96929a1e62035cb4beb5c3da0e36 Binary files /dev/null and b/Images/GUI_debug.jpg differ diff --git a/Images/Pipeline-Overview.png b/Images/Pipeline-Overview.png new file mode 100644 index 0000000000000000000000000000000000000000..6bc8a50b5d4226980c091eb7e9ac60ce73ba4d53 Binary files /dev/null and b/Images/Pipeline-Overview.png differ diff --git a/README.md b/README.md index c5437e52429f5f8cbe2751371f0946ace41a04e9..dd50a29fe0671ff674af6cf18f5431fb4b5e71e3 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ The repository is structured as follows: - `Dynamic-Backward-Attention_Transformer/`: Submodule for material recognition using Dynamic Backward Attention Transformer - `RIR_Analysis`: Python notebook for sine sweep and deconvolution by Mona - `scripts/`: Automation and integration scripts + - `360monodepthexecution/`: Powershell automation scripts for docker (360monodepth) + - `debug_tool/`: Debug tools to run modules one by one - `Unity/`: - `AV-VR/`: Main Unity project folder, extending GDP work - `S3A/`: Dr. Hansung's original Unity project for reference (Steam Audio integration, sound source positioning) @@ -25,6 +27,7 @@ The repository is structured as follows: - `scripts/config.ini`: Modify the value in this file to fit system - `scripts/GUI.py`: Main script to be run after following the setup instructions +- `scripts/debug_tool/GUI_debug.py`: Main script for debugging the module one by one - `AVVR-Papers/report.pdf`: 23/24 GDP's report - `Manual.docx` / `Manual.pdf`: User manual provided by the GDP group - `Intern-logs/Internship-Report.pdf`: 10-week internship technical report @@ -99,7 +102,7 @@ python GUI.py 6. GUI choices -![GUI](Intern-Logs/Readme_Images/GUI.png) +![GUI](Images/GUI.png) - Tick Create depth map, tick include Top for mesh (.obj) with ceiling. - Choose image from different scenes folder (KT, ST, UL, MR, LR) in edgenet360/Data - The pipeline should run for about 5-15 minutes depending on the system spec. @@ -107,9 +110,14 @@ python GUI.py Refer to Manual.pdf for detailed prerequisites and setup instructions for the ML pipeline if needed and Unity VR rendering. +7. Debug GUI to run modules one by one for troubleshooting + +![Debug GUI](Images/GUI_debug.jpg) +- Save time by not needing to run all modules sequentially to isolate error faster. +- Easier to understand the underlying input and output of each modules. ## Pipeline Overview -![image](Intern-Logs/Readme_Images/Pipeline-Overview.png) +![Pipeline Overview](Images/Pipeline-Overview.png) ## Video Demonstration diff --git a/scripts/debug_tool/GUI_debug.py b/scripts/debug_tool/GUI_debug.py new file mode 100644 index 0000000000000000000000000000000000000000..1ce869060e8f77cfaeba0c421f9912e24c4b7967 --- /dev/null +++ b/scripts/debug_tool/GUI_debug.py @@ -0,0 +1,54 @@ +from PyQt6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QTabWidget +from PyQt6.QtCore import Qt +import sys +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 tabs.edge_net_tab import EdgeNetTab +from utils.config_reader import ConfigReader + +class ModuleDebugGUI(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Pipeline Debug Tool") + self.setGeometry(100, 100, 1600, 800) + + # Initialize paths + self.DEBUG_DIR = os.path.dirname(os.path.abspath(__file__)) # debug_tool directory + self.SCRIPT_DIR = os.path.dirname(self.DEBUG_DIR) # scripts directory + self.ROOT_DIR = os.path.dirname(self.SCRIPT_DIR) + + # Read configuration + self.config_reader = ConfigReader(self.DEBUG_DIR, self.ROOT_DIR) + + # Setup UI + self.setup_ui() + + def setup_ui(self): + # Create main widget and layout + main_widget = QWidget() + self.setCentralWidget(main_widget) + layout = QVBoxLayout(main_widget) + + # Create tab widget + self.tabs = QTabWidget() + layout.addWidget(self.tabs) + + # Initialize tabs + 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") + self.tabs.addTab(EdgeNetTab(self.config_reader), "EdgeNet Execution") + +def main(): + app = QApplication(sys.argv) + window = ModuleDebugGUI() + window.show() + sys.exit(app.exec()) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/debug_tool/__init__.py b/scripts/debug_tool/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/debug_tool/tabs/__init__.py b/scripts/debug_tool/tabs/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/debug_tool/tabs/config_tab.py b/scripts/debug_tool/tabs/config_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..17d9ad1f4605686a8f8648f177a55d33c31f2480 --- /dev/null +++ b/scripts/debug_tool/tabs/config_tab.py @@ -0,0 +1,122 @@ +# tabs/config_tab.py +from PyQt6.QtWidgets import QWidget, QVBoxLayout, QMessageBox +from PyQt6.QtGui import QTextCharFormat, QColor, QTextCursor +from utils.qt_widgets import create_group_with_text, create_button_layout +import os + +class ConfigTab(QWidget): + def __init__(self, config_reader): + super().__init__() + self.config_reader = config_reader + self.setup_ui() + + def setup_ui(self): + main_layout = QVBoxLayout(self) + main_layout.setContentsMargins(10, 10, 10, 10) + + # Create groups and text areas using the utility function + config_group, self.config_text = create_group_with_text("Config Values", 100) + dir_group, self.dir_text = create_group_with_text("Directory Paths", 120) + file_group, self.file_text = create_group_with_text("File Paths", 80) + verify_group, self.verify_text = create_group_with_text("Path Verification", 120) + + # Add groups to layout + main_layout.addWidget(config_group) + main_layout.addWidget(dir_group) + main_layout.addWidget(file_group) + main_layout.addWidget(verify_group) + + # Create buttons using the utility function + buttons = [ + ("Refresh Config", self.refresh_all, 'left'), + ("Verify Paths", self.verify_paths, 'left'), + ("Save Debug Info", self.save_debug_info, 'right') + ] + button_layout = create_button_layout(*buttons) + main_layout.addLayout(button_layout) + + # Initial display + self.refresh_all() + + def refresh_all(self): + self.display_config() + self.display_directories() + self.display_files() + self.verify_paths() + + def display_config(self): + self.config_text.clear() + for key, value in self.config_reader.config.items(): + self.config_text.append(f"{key} = {value}") + + def display_directories(self): + self.dir_text.clear() + for key, path in self.config_reader.directories.items(): + self.dir_text.append(f"{key}: {path}") + + def display_files(self): + self.file_text.clear() + for key, path in self.config_reader.file_paths.items(): + self.file_text.append(f"{key}: {path}") + + def verify_paths(self): + self.verify_text.clear() + + # Create formats for colored text + green_format = QTextCharFormat() + green_format.setForeground(QColor("green")) + red_format = QTextCharFormat() + red_format.setForeground(QColor("red")) + + self.verify_text.append("Directory Verification:") + for key, path in self.config_reader.directories.items(): + exists = os.path.exists(path) + status = "✓ exists" if exists else "✗ missing" + cursor = self.verify_text.textCursor() + cursor.movePosition(QTextCursor.MoveOperation.End) + self.verify_text.setTextCursor(cursor) + self.verify_text.insertPlainText(f"{key}: ") + self.verify_text.setCurrentCharFormat(green_format if exists else red_format) + self.verify_text.insertPlainText(f"{status}\n") + + self.verify_text.setCurrentCharFormat(QTextCharFormat()) + self.verify_text.append("\nFile Verification:") + for key, path in self.config_reader.file_paths.items(): + exists = os.path.exists(path) + status = "✓ exists" if exists else "✗ missing" + cursor = self.verify_text.textCursor() + cursor.movePosition(QTextCursor.MoveOperation.End) + self.verify_text.setTextCursor(cursor) + self.verify_text.insertPlainText(f"{key}: ") + self.verify_text.setCurrentCharFormat(green_format if exists else red_format) + self.verify_text.insertPlainText(f"{status}\n") + + def save_debug_info(self): + debug_info = "=== Pipeline Debug Information ===\n\n" + + debug_info += "=== Config Values ===\n" + for key, value in self.config_reader.config.items(): + debug_info += f"{key} = {value}\n" + + debug_info += "\n=== Directory Paths ===\n" + for key, path in self.config_reader.directories.items(): + exists = os.path.exists(path) + status = "exists" if exists else "missing" + debug_info += f"{key}: {path} ({status})\n" + + debug_info += "\n=== File Paths ===\n" + for key, path in self.config_reader.file_paths.items(): + exists = os.path.exists(path) + status = "exists" if exists else "missing" + debug_info += f"{key}: {path} ({status})\n" + + try: + debug_path = os.path.join(self.config_reader.directories['debugDir'], + "debug_config_info.txt") + with open(debug_path, "w") as f: + f.write(debug_info) + QMessageBox.information(self, "Success", + f"Debug information saved to:\n{debug_path}") + except Exception as e: + QMessageBox.critical(self, "Error", + f"Failed to save debug info: {str(e)}") \ No newline at end of file diff --git a/scripts/debug_tool/tabs/depth_tab.py b/scripts/debug_tool/tabs/depth_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..30da9e51ae17ff8b1b2e3a9cddced0aa26365fb0 --- /dev/null +++ b/scripts/debug_tool/tabs/depth_tab.py @@ -0,0 +1,158 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QGroupBox, QMessageBox) +from PyQt6.QtCore import Qt +import os + +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, clean_directory, copy_file, run_command +from utils.image_handlers import update_preview + +class DepthTab(QWidget): + def __init__(self, config_reader): + super().__init__() + self.config_reader = config_reader + self.depth_input_path = None + + # Initialize paths + self.copy_dest_path = os.path.join( + self.config_reader.directories['monoDepthDir'], + 'rgb.jpg' + ) + self.depth_output_path = os.path.join( + self.config_reader.directories['edgeNetDir'], + 'Data', 'Input', 'depth_e.png' + ) + self.input_dir = os.path.join( + self.config_reader.directories['edgeNetDir'], + 'Data', 'Input' + ) + + self.setup_ui() + + def setup_ui(self): + 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, layout): + # Info display + info_rows = [ + ("Input file:", "No file selected"), + ("Copy destination:", self.copy_dest_path), + ("Depth map output:", self.depth_output_path) + ] + info_group, self.info_labels = create_info_group("Controls", info_rows) + layout.addWidget(info_group) + + # Buttons + buttons = [ + ("Select Input", self.handle_file_select, 'left'), + ("Clean Data/Input Dir", self.clean_input_dir, 'left'), + ("Remove rgb.png from dest path", self.remove_rgb, 'left'), + ("Copy File", self.copy_file, 'left'), + ("Run Depth Est.", self.run_depth_estimation, '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, layout): + preview_group = QGroupBox("Image Previews") + preview_layout = QHBoxLayout(preview_group) + + input_group, self.input_preview = create_preview_group("RGB Image") + output_group, self.output_preview = create_preview_group("Depth Map") + + preview_layout.addWidget(input_group) + preview_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)" + ) + + if file_path: + self.depth_input_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) + + def remove_rgb(self): + rgb_path = self.copy_dest_path + if os.path.exists(rgb_path): + os.remove(rgb_path) + self.update_status("Removed rgb.png") + else: + self.update_status("rgb.png not found") + + def clean_input_dir(self): + success = clean_directory(self.input_dir, self.update_status) + if success: + self.output_preview.clear() + else: + QMessageBox.critical(self, "Error", "Failed to clean input directory") + + def copy_file(self): + if not self.depth_input_path: + QMessageBox.warning(self, "Warning", "Please select an input file first") + return + + # Copy to monodepth directory + if not copy_file(self.depth_input_path, self.copy_dest_path, self.update_status): + QMessageBox.critical(self, "Error", "Failed to copy file to monodepth directory") + return + + # Copy to edgenet directory + edge_rgb_path = os.path.join(self.input_dir, 'rgb.png') + if not copy_file(self.depth_input_path, edge_rgb_path, self.update_status): + QMessageBox.critical(self, "Error", "Failed to copy file to edgenet directory") + + def run_depth_estimation(self): + try: + self.update_status("Running depth estimation...") + + # Change to mono depth directory and run script + original_dir = os.getcwd() + os.chdir(self.config_reader.directories['monoDepthDir']) + + success, output = run_command( + self, + "powershell.exe -File masterscript.ps1", + self.update_status + ) + + # Change back to original directory + os.chdir(original_dir) + + if success and os.path.exists(self.depth_output_path): + self.update_status(f"Depth map generated at: {self.depth_output_path}") + update_preview(self.output_preview, self.depth_output_path, + error_callback=self.update_status) + + except Exception as e: + error_msg = f"Failed to run depth estimation: {str(e)}" + self.update_status(error_msg) + QMessageBox.critical(self, "Error", error_msg) + + def update_status(self, message): + self.status_text.append(message) + # Scroll to bottom + scrollbar = self.status_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) \ No newline at end of file diff --git a/scripts/debug_tool/tabs/edge_net_tab.py b/scripts/debug_tool/tabs/edge_net_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..dc888bed9a226847193e7eecfc4546fc2c1e37dc --- /dev/null +++ b/scripts/debug_tool/tabs/edge_net_tab.py @@ -0,0 +1,518 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QLabel, + QPushButton, QCheckBox, QProgressBar, QTextEdit, + QMessageBox, QFileDialog) +from PyQt6.QtCore import Qt, QThread, pyqtSignal, QTimer +from PyQt6.QtGui import QPixmap +import os +import threading +import queue +import time +import sys + +from utils.file_handlers import (select_file, run_command, clean_directory, + copy_file) +from utils.image_handlers import update_preview, clear_previews +from utils.qt_widgets import (create_group_with_text, create_button_layout, + create_preview_group, create_status_label, + update_status_indicator, show_confirmation_dialog) + +class ProcessThread(QThread): + finished = pyqtSignal(bool, str) + progress = pyqtSignal(str) + + def __init__(self, func, *args, **kwargs): + super().__init__() + self.func = func + self.args = args + self.kwargs = kwargs + self.running = True + + def run(self): + try: + if self.running: + result = self.func(*self.args, **self.kwargs) + self.finished.emit(True, "") + except Exception as e: + self.finished.emit(False, str(e)) + + def stop(self): + self.running = False + self.wait() + +class EdgeNetTab(QWidget): + def __init__(self, config_reader): + super().__init__() + self.config_reader = config_reader + self.setup_paths() + self.init_variables() + self.init_ui() + self.connect_signals() + self.current_thread = None + + def closeEvent(self, event): + """Handle widget close event""" + if self.current_thread and self.current_thread.isRunning(): + self.current_thread.stop() + self.current_thread.wait() + event.accept() + + def setup_paths(self): + """Initialize directory and file paths""" + self.edge_net_dir = self.config_reader.directories['edgeNetDir'] + self.output_dir = self.config_reader.directories['outputDir'] + self.input_dir = os.path.join(self.edge_net_dir, "Data", "Input") + + def init_variables(self): + """Initialize class variables""" + self.include_top = False + self.is_processing = False + self.manual_inputs = { + 'depth_e.png': None, + 'rgb.png': None, + 'material.png': None + } + self.output_queue = queue.Queue() + + def init_ui(self): + """Initialize user interface""" + layout = QVBoxLayout() + + # Split into left and right sections + hlayout = QHBoxLayout() + left_panel = self.create_left_panel() + right_panel = self.create_right_panel() + + hlayout.addWidget(left_panel) + hlayout.addWidget(right_panel) + layout.addLayout(hlayout) + + # Initialize progress bar to stopped state + self.progress_bar.setMaximum(100) + self.progress_bar.setValue(0) + + self.setLayout(layout) + + def create_left_panel(self): + """Create left control panel""" + widget = QWidget() + layout = QVBoxLayout() + + # Input files section + files_group = self.create_input_files_group() + layout.addWidget(files_group) + + # Include top checkbox with Qt6 state + self.top_checkbox = QCheckBox("Include Top in Mesh") + self.top_checkbox.setCheckState(Qt.CheckState.Unchecked) + self.top_checkbox.stateChanged.connect(self.on_include_top_changed) + layout.addWidget(self.top_checkbox) + + # Status section + self.status_group, self.status_text = create_group_with_text("Status") + layout.addWidget(self.status_group) + + # Progress section + progress_group = self.create_progress_group() + layout.addWidget(progress_group) + + # Control buttons + button_layout = self.create_control_buttons() + layout.addLayout(button_layout) + + widget.setLayout(layout) + return widget + + def create_input_files_group(self): + """Create input files selection group""" + group = QWidget() + layout = QVBoxLayout() + + # File selection buttons + for file_type in self.manual_inputs.keys(): + row = QHBoxLayout() + row.addWidget(QLabel(f"{file_type}:")) + label = QLabel("Using default") + setattr(self, f"{file_type.split('.')[0]}_label", label) + row.addWidget(label) + + btn = QPushButton("Select") + btn.clicked.connect(lambda checked, ft=file_type: self.select_input_file(ft)) + row.addWidget(btn) + + layout.addLayout(row) + + # Reset button + reset_btn = QPushButton("Reset to Default Inputs") + reset_btn.clicked.connect(self.reset_inputs) + layout.addWidget(reset_btn) + + group.setLayout(layout) + return group + + def create_progress_group(self): + """Create progress indicators group""" + group = QWidget() + layout = QVBoxLayout() + + # Status indicators + self.edge_status = create_status_label("EdgeNet:") + self.split_status = create_status_label("Mesh Split:") + self.flip_status = create_status_label("Blender Flip:") + + layout.addLayout(self.edge_status) + layout.addLayout(self.split_status) + layout.addLayout(self.flip_status) + + # Progress bar + self.progress_bar = QProgressBar() + layout.addWidget(self.progress_bar) + + # Operation label + self.operation_label = QLabel() + layout.addWidget(self.operation_label) + + group.setLayout(layout) + return group + + def create_right_panel(self): + """Create right preview panel""" + widget = QWidget() + layout = QVBoxLayout() + + # Preview groups + depth_group, self.depth_preview = create_preview_group("Enhanced Depth") + mesh_group, self.mesh_preview = create_preview_group("Generated Mesh") + + layout.addWidget(depth_group) + layout.addWidget(mesh_group) + + widget.setLayout(layout) + return widget + + def create_control_buttons(self): + """Create control buttons layout""" + return create_button_layout( + ("Run EdgeNet", self.run_edge_net, 'left'), + ("Run Mesh Split", self.run_mesh_split, 'left'), + ("Run Blender Flip", self.run_blender_flip, 'left'), + ("Run All Steps", self.run_all_steps, 'left'), + ("Clean Output", self.clean_output_directory, 'right'), + ("Clean All", self.clean_all_files, 'right') + ) + + def connect_signals(self): + """Connect signals and slots""" + # Add any additional signal connections here + pass + + def select_input_file(self, file_type): + """Handle input file selection""" + file_path = select_file(self, "Select Input File", "Image Files (*.png)", initial_dir=self.input_dir) + if file_path: + self.manual_inputs[file_type] = file_path + label = getattr(self, f"{file_type.split('.')[0]}_label") + label.setText(os.path.basename(file_path)) + self.update_status(f"Selected {file_type}: {file_path}") + + def reset_inputs(self): + """Reset input selections to default""" + self.manual_inputs = {k: None for k in self.manual_inputs} + for file_type in self.manual_inputs: + label = getattr(self, f"{file_type.split('.')[0]}_label") + label.setText("Using default") + self.update_status("Reset all inputs to default") + + def run_edge_net(self): + """Run EdgeNet processing""" + if self.is_processing: + QMessageBox.warning(self, "Warning", "A process is already running!") + return + + self.is_processing = True + self.progress_bar.setMaximum(0) + + self.current_thread = ProcessThread(self._run_edge_net_process) + self.current_thread.finished.connect(self.on_edge_net_complete) + self.current_thread.progress.connect(self.update_status) + self.current_thread.start() + + def clean_output_directory(self): + if show_confirmation_dialog(self, "Confirm Clean", "Clean output directory?"): + if clean_directory(self.output_dir, self.update_status): + self.update_status("Output directory cleaned") + update_status_indicator(self.split_status, "Not started") + update_status_indicator(self.flip_status, "Not started") + clear_previews(self.mesh_preview) + + def clean_all_files(self): + if show_confirmation_dialog(self, "Confirm Clean", "Clean all files?"): + directories = [self.input_dir, self.output_dir] + for directory in directories: + clean_directory(directory, self.update_status) + + # remove monodepth image copied in 360monodepth if exists + if os.path.exists(os.path.join(self.config_reader.directories['monoDepthDir'], 'rgb.jpg')): + monodepth_image = os.path.join(self.config_reader.directories['monoDepthDir'], 'rgb.jpg') + os.remove(monodepth_image) + # remove shifted image from shifter if exists + if os.path.exists(os.path.join(self.config_reader.directories['scriptDir'], 'shifted_t.png')): + shifted_image = os.path.join(self.config_reader.directories['scriptDir'], 'shifted_t.png') + os.remove(shifted_image) + + update_status_indicator(self.edge_status, "Not started") + update_status_indicator(self.split_status, "Not started") + update_status_indicator(self.flip_status, "Not started") + clear_previews(self.depth_preview, self.mesh_preview) + + def update_status(self, message): + """Update status text""" + self.status_text.append(message) + + def on_include_top_changed(self, state): + """Handle include top checkbox change""" + # In PyQt6, CheckState.Checked has value 2 + self.include_top = (state == 2) # or state == Qt.CheckState.Checked + self.update_status(f"Include top changed to: {self.include_top}") + + def verify_inputs(self): + """Verify input files exist""" + required_files = { + 'depth_e.png': self.get_input_path('depth_e.png'), + 'rgb.png': self.get_input_path('rgb.png') + } + + missing = [f for f, p in required_files.items() if not os.path.exists(p)] + if missing: + self.update_status("Missing required files: " + ", ".join(missing)) + return False + return True + + def get_input_path(self, file_type): + """Get path for input file""" + return self.manual_inputs.get(file_type) or os.path.join(self.input_dir, file_type) + + def _run_edge_net_process(self): + """Run EdgeNet processing""" + # Change to EdgeNet directory + os.chdir(self.edge_net_dir) + try: + if not self.verify_inputs(): + self.copy_input_files() + if not self.verify_inputs(): + raise Exception("Missing required input files") + + # Run enhance360.py + enhance_cmd = self._build_enhance_command() + if run_command(self, enhance_cmd, self.update_status)[0]: + # Run infer360.py + infer_cmd = self._build_infer_command() + return run_command(self, infer_cmd, self.update_status)[0] + return False + + except Exception as e: + self.update_status(f"Error: {str(e)}") + return False + + def _build_enhance_command(self): + """Build enhance360.py command""" + return (f'wsl bash -c "source {self.config_reader.config["wslAnacondaDir"]}/activate' + f' {self.config_reader.config["edgeNetEnv"]} && ' + f'python enhance360.py Input depth_e.png rgb.png enhanced_depth_e.png"') + + def _build_infer_command(self): + """Build infer360.py command""" + # Debug print to verify include_top state + self.update_status(f"Current include_top state: {self.include_top}") + + base_cmd = (f'wsl bash -c "source {self.config_reader.config["wslAnacondaDir"]}/activate' + f' {self.config_reader.config["edgeNetEnv"]} && ' + f'python infer360.py Input enhanced_depth_e.png material.png rgb.png Input') + + if self.include_top: + command = base_cmd + ' --include_top y"' + else: + command = base_cmd + '"' + + # Log final command + self.update_status(f"Final command: {command}") + + return command + + def on_edge_net_complete(self, success, error_message): + """Handle EdgeNet completion""" + self.is_processing = False + self.progress_bar.setMaximum(100) + + if success: + update_status_indicator(self.edge_status, "Complete") + self.update_depth_preview() + else: + update_status_indicator(self.edge_status, "Failed") + QMessageBox.critical(self, "Error", f"EdgeNet failed: {error_message}") + + def update_depth_preview(self): + """Update depth preview image""" + depth_path = os.path.join(self.input_dir, "enhanced_depth_e.png") + update_preview(self.depth_preview, depth_path, 300) + + def run_mesh_split(self): + if self.is_processing: + QMessageBox.warning(self, "Warning", "A process is already running!") + return + + self.is_processing = True + self.progress_bar.setMaximum(0) + + self.current_thread = ProcessThread(self._run_mesh_split_process) + self.current_thread.finished.connect(self.on_mesh_split_complete) + self.current_thread.progress.connect(self.update_status) + self.current_thread.start() + + def _run_mesh_split_process(self): + """Execute mesh splitting process""" + # Change to EdgeNet directory + os.chdir(self.edge_net_dir) + try: + if not os.path.exists(os.path.join(self.output_dir, "Input_prediction.obj")): + raise Exception("Missing Input_prediction.obj file") + + cmd = (f'call "{self.config_reader.config["condaDir"]}\\Scripts\\activate.bat" ' + f'{self.config_reader.config["materialEnv"]} && ' + f'python replace.py') + + return run_command(self, cmd, self.update_status)[0] + + except Exception as e: + self.update_status(f"Error: {str(e)}") + return False + + def on_mesh_split_complete(self, success, error_message): + """Handle mesh split completion""" + self.is_processing = False + self.progress_bar.setMaximum(100) + + if success: + update_status_indicator(self.split_status, "Complete") + self.update_mesh_preview() + else: + update_status_indicator(self.split_status, "Failed") + QMessageBox.critical(self, "Error", f"Mesh splitting failed: {error_message}") + + def run_blender_flip(self): + if self.is_processing: + QMessageBox.warning(self, "Warning", "A process is already running!") + return + + self.is_processing = True + self.progress_bar.setMaximum(0) + + self.current_thread = ProcessThread(self._run_blender_flip_process) + self.current_thread.finished.connect(self.on_blender_flip_complete) + self.current_thread.progress.connect(self.update_status) + self.current_thread.start() + + def _run_blender_flip_process(self): + """Execute Blender flip process""" + # Change to scripts directory + os.chdir(self.config_reader.directories['scriptDir']) + try: + mesh_path = os.path.join(self.output_dir, "Input_prediction_mesh.obj") + if not os.path.exists(mesh_path): + raise Exception("Missing Input_prediction_mesh.obj file") + + cmd = (f'call "{self.config_reader.config["condaDir"]}\\Scripts\\activate.bat" ' + f'{self.config_reader.config["unityEnv"]} && ' + f'python blenderFlip.py "{mesh_path}"') + + return run_command(self, cmd, self.update_status)[0] + + except Exception as e: + self.update_status(f"Error: {str(e)}") + return False + + def on_blender_flip_complete(self, success, error_message): + """Handle Blender flip completion""" + self.is_processing = False + self.progress_bar.setMaximum(100) + + if success: + update_status_indicator(self.flip_status, "Complete") + self.update_mesh_preview() + else: + update_status_indicator(self.flip_status, "Failed") + QMessageBox.critical(self, "Error", f"Blender flip failed: {error_message}") + + def run_all_steps(self): + if self.is_processing: + QMessageBox.warning(self, "Warning", "A process is already running!") + return + + self.is_processing = True + self.progress_bar.setMaximum(0) # Set to indeterminate mode + + self.current_thread = ProcessThread(self._run_all_steps_process) + self.current_thread.finished.connect(self.on_all_steps_complete) + self.current_thread.progress.connect(self.update_status) + self.current_thread.start() + + def _run_all_steps_process(self): + """Execute complete pipeline process""" + try: + self.update_status("Starting complete pipeline processing...") + + # Run EdgeNet + self.update_status("Running EdgeNet...") + update_status_indicator(self.edge_status, "Running") + if not self._run_edge_net_process(): + raise Exception("EdgeNet processing failed") + + # Run Mesh Split + self.update_status("Running Mesh Split...") + update_status_indicator(self.split_status, "Running") + if not self._run_mesh_split_process(): + raise Exception("Mesh splitting failed") + + # Run Blender Flip + self.update_status("Running Blender Flip...") + update_status_indicator(self.flip_status, "Running") + if not self._run_blender_flip_process(): + raise Exception("Blender flip failed") + + return True + + except Exception as e: + self.update_status(f"Pipeline error: {str(e)}") + return False + + def on_all_steps_complete(self, success, error_message): + """Handle complete pipeline completion""" + self.is_processing = False + self.progress_bar.setMaximum(100) + self.progress_bar.setValue(0) + + if success: + self.update_status("Complete pipeline processing finished successfully!") + update_status_indicator(self.edge_status, "Complete") + update_status_indicator(self.split_status, "Complete") + update_status_indicator(self.flip_status, "Complete") + QMessageBox.information(self, "Success", + "All processing steps completed successfully!") + else: + self.update_status(f"Pipeline failed: {error_message}") + QMessageBox.critical(self, "Error", f"Pipeline failed: {error_message}") + + def update_mesh_preview(self): + """Update mesh preview image""" + # Implement mesh preview rendering + # This could involve taking a screenshot of the mesh + # or loading a pre-rendered preview image + pass + + def copy_input_files(self): + """Copy manual input files to input directory""" + os.makedirs(self.input_dir, exist_ok=True) + + for file_type, path in self.manual_inputs.items(): + if path: + dest = os.path.join(self.input_dir, file_type) + copy_file(path, dest, self.update_status) diff --git a/scripts/debug_tool/tabs/material_tab.py b/scripts/debug_tool/tabs/material_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..b38111daed8eeae8998acc77a474f9f2c6a22683 --- /dev/null +++ b/scripts/debug_tool/tabs/material_tab.py @@ -0,0 +1,271 @@ +# tabs/material_tab.py +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGroupBox, + QMessageBox, QTabWidget) +from PyQt6.QtCore import Qt +import os + +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 + + # Setup paths + 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): + 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, 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 + 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, layout): + preview_tabs = QTabWidget() + + # Input/Output preview tab + io_tab = QWidget() + io_layout = QVBoxLayout(io_tab) + + input_group, self.input_preview = create_preview_group("Input Image") + output_group, self.output_preview = create_preview_group("Material Map") + + io_layout.addWidget(input_group) + io_layout.addWidget(output_group) + preview_tabs.addTab(io_tab, "Input/Output") + + # RGB faces preview tab + rgb_tab = QWidget() + rgb_layout = QVBoxLayout(rgb_tab) + + self.rgb_face_previews = {} + 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) + + self.material_face_previews = {} + 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) + + 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): + 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 + + clear_previews( + self.input_preview, + self.output_preview, + self.rgb_face_previews, + self.material_face_previews + ) + self.reset_status_indicators() + + def run_split_360(self): + if not self.input_file_path: + QMessageBox.warning(self, "Warning", "Please select an input file first") + return + + self.update_status("Running 360 image splitting...") + update_status_indicator(self.split_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 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): + QMessageBox.warning(self, "Warning", "Please run Split 360 first") + return + + self.update_status("Running material recognition...") + update_status_indicator(self.recognition_status, "Running") + + 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): + 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' + ) + 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): + if not self.input_file_path: + QMessageBox.warning(self, "Warning", "Please select an input file first") + return + + 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.append(message) + scrollbar = self.status_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + 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_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 + ) + + def verify_checkpoint(self): + 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/tabs/shifter_tab.py b/scripts/debug_tool/tabs/shifter_tab.py new file mode 100644 index 0000000000000000000000000000000000000000..d532b5d8784654212562dd14723ce6a0724ab98e --- /dev/null +++ b/scripts/debug_tool/tabs/shifter_tab.py @@ -0,0 +1,121 @@ +from PyQt6.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, + QGroupBox, QMessageBox) +from PyQt6.QtCore import Qt +import os + +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): + 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, 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_group, self.cmd_text = create_group_with_text("Command Preview", 80) + layout.addWidget(cmd_group) + + # 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 display + status_group, self.status_text = create_group_with_text("Status", 150) + layout.addWidget(status_group) + + self.update_command_preview() + + def setup_preview_panel(self, layout): + preview_layout = QVBoxLayout() + + # Split into two preview groups + preview_group = QGroupBox("Image Previews") + previews_layout = QHBoxLayout(preview_group) + + input_group, self.input_preview = create_preview_group("Input Image") + output_group, self.output_preview = create_preview_group("Shifted Image") + + 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)" + ) + + 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.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.setText(cmd) + + def update_status(self, message): + self.status_text.append(message) + scrollbar = self.status_text.verticalScrollBar() + scrollbar.setValue(scrollbar.maximum()) + + def run_shifter(self): + if not self.input_file_path: + 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 + ) + + 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/_init__.py b/scripts/debug_tool/utils/_init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/scripts/debug_tool/utils/config_reader.py b/scripts/debug_tool/utils/config_reader.py new file mode 100644 index 0000000000000000000000000000000000000000..6101e9127f52cf234a1cba1cbd1246122b76af44 --- /dev/null +++ b/scripts/debug_tool/utils/config_reader.py @@ -0,0 +1,50 @@ +import os + +class ConfigReader: + def __init__(self, script_dir, root_dir): + self.SCRIPT_DIR = script_dir # debug_tool directory + self.PIPELINE_DIR = os.path.dirname(script_dir) # scripts directory + self.ROOT_DIR = root_dir + + # Read from pipeline's config.ini + self.config = self.read_config() + self.directories = self.setup_directories() + self.file_paths = self.setup_file_paths() + + def read_config(self): + config = {} + # Use config.ini from scripts directory + config_path = os.path.join(self.PIPELINE_DIR, "config.ini") + try: + with open(config_path, 'r') as f: + for line in f: + if '=' in line: + key, value = line.strip().split('=', 1) + config[key.strip()] = value.strip() + except FileNotFoundError: + raise Exception(f"config.ini not found in {self.PIPELINE_DIR}") + return config + + def setup_directories(self): + return { + 'scriptDir': self.PIPELINE_DIR, # Point to scripts directory + 'debugDir': self.SCRIPT_DIR, # debug_tool directory + 'rootDir': self.ROOT_DIR, + 'monoDepthDir': os.path.join(self.ROOT_DIR, "scripts", "360monodepthexecution"), + 'outputDir': os.path.join(self.ROOT_DIR, "edgenet-360", "Output"), + 'materialRecogDir': os.path.join(self.ROOT_DIR, "Dynamic-Backward-Attention-Transformer"), + 'edgeNetDir': os.path.join(self.ROOT_DIR, "edgenet-360") + } + + def setup_file_paths(self): + return { + 'checkpointFile': os.path.join( + self.directories['materialRecogDir'], + "checkpoints/dpglt_mode95/accuracy/epoch=126-valid_acc_epoch=0.87.ckpt" + ), + 'shiftedImage': os.path.join(self.PIPELINE_DIR, "shifted_t.png"), # Use scripts directory + 'monoDepthImage': os.path.join( + self.directories['monoDepthDir'], + "rgb.jpg" + ) + } \ No newline at end of file diff --git a/scripts/debug_tool/utils/file_handlers.py b/scripts/debug_tool/utils/file_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..43486fbbed9abcafa154c03226ddebaf51cf24b1 --- /dev/null +++ b/scripts/debug_tool/utils/file_handlers.py @@ -0,0 +1,196 @@ +# utils/file_handlers.py +from PyQt6.QtWidgets import QFileDialog, QMessageBox +import subprocess +import os +import shutil + +def select_file(parent, title="Select File", file_types="All Files (*)", initial_dir=None): + """ + Opens a file selection dialog. + + Args: + 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. 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, + initial_dir, + file_types + ) + return file_path + +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 (should have config_reader attribute) + title: Dialog window title + file_types: File filter + 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, + os.path.join(initial_dir, suggested_name), + file_types + ) + return file_path + +def select_directory(parent, title="Select Directory", initial_dir=None): + """ + Opens a directory selection dialog. + + Args: + parent: Parent widget (should have config_reader attribute) + title: Dialog window title + 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 + +def clean_directory(directory_path, status_callback=None): + """ + Cleans all files in a directory. + + Args: + directory_path: Directory to clean + status_callback: Optional callback for status updates + Returns: + bool: Success status + """ + try: + if not os.path.exists(directory_path): + os.makedirs(directory_path) + if status_callback: + status_callback(f"Created directory: {directory_path}") + return True + + files = os.listdir(directory_path) + if not files: + if status_callback: + status_callback("Directory is already empty") + return True + + for file in files: + file_path = os.path.join(directory_path, file) + try: + if os.path.isfile(file_path): + os.remove(file_path) + elif os.path.isdir(file_path): + shutil.rmtree(file_path) + except Exception as e: + if status_callback: + status_callback(f"Error removing {file}: {str(e)}") + return False + + if status_callback: + status_callback("Directory cleaned successfully") + return True + + except Exception as e: + if status_callback: + status_callback(f"Failed to clean directory: {str(e)}") + return False + +def copy_file(src, dest, status_callback=None): + """ + Copies a file to destination. + + Args: + src: Source file path + dest: Destination file path + status_callback: Optional callback for status updates + Returns: + bool: Success status + """ + try: + os.makedirs(os.path.dirname(dest), exist_ok=True) + shutil.copy2(src, dest) + if status_callback: + status_callback(f"Copied file to: {dest}") + return True + except Exception as e: + if status_callback: + status_callback(f"Failed to copy file: {str(e)}") + return False diff --git a/scripts/debug_tool/utils/image_handlers.py b/scripts/debug_tool/utils/image_handlers.py new file mode 100644 index 0000000000000000000000000000000000000000..e1cdcdd8a8d34721cff7464c7f4c975c00869a9f --- /dev/null +++ b/scripts/debug_tool/utils/image_handlers.py @@ -0,0 +1,105 @@ +# utils/image_handlers.py +from PyQt6.QtGui import QPixmap, QImage +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 + qt_img = QImage(cv_img.data, width, height, bytes_per_line, QImage.Format.Format_RGB888) + else: + bytes_per_line = width + qt_img = QImage(cv_img.data, width, height, bytes_per_line, QImage.Format.Format_Grayscale8) + 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: + raise Exception("Failed to load image") + + # Convert to RGB + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + # Calculate resize ratio + height, width = img.shape[:2] + ratio = min(max_size/width, max_size/height) + + if ratio < 1: + new_size = (int(width * ratio), int(height * ratio)) + img = cv2.resize(img, new_size, interpolation=cv2.INTER_AREA) + + return img + except Exception as e: + 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}") + +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 new file mode 100644 index 0000000000000000000000000000000000000000..5014f16a4603c408761e91e81746f5ceb1c67a3f --- /dev/null +++ b/scripts/debug_tool/utils/qt_widgets.py @@ -0,0 +1,195 @@ +# utils/qt_widgets.py +from PyQt6.QtWidgets import (QTextEdit, QGroupBox, QVBoxLayout, + QHBoxLayout, QPushButton, QLabel, + QMessageBox) +from PyQt6.QtCore import Qt + +def create_group_with_text(title, height=100): + """ + Creates a QGroupBox containing a QTextEdit. + + Args: + title: Group box title + height: Fixed height for text edit + Returns: + tuple: (QGroupBox, QTextEdit) + """ + group = QGroupBox(title) + layout = QVBoxLayout(group) + text_edit = QTextEdit() + text_edit.setFixedHeight(height) + text_edit.setReadOnly(True) + layout.addWidget(text_edit) + return group, text_edit + +def create_button_layout(*buttons): + """ + Creates a horizontal button layout with optional stretch. + + Args: + buttons: List of tuples (label, callback, position) + position can be 'left', 'right', or None for default + Returns: + QHBoxLayout with arranged buttons + """ + layout = QHBoxLayout() + + # Add left-aligned buttons + for label, callback, position in buttons: + if position == 'left': + btn = QPushButton(label) + btn.clicked.connect(callback) + layout.addWidget(btn) + + # Add stretch in the middle + layout.addStretch() + + # Add right-aligned buttons + for label, callback, position in buttons: + if position == 'right': + btn = QPushButton(label) + btn.clicked.connect(callback) + layout.addWidget(btn) + + return layout + +def create_status_group(): + """ + Creates a standard status display group. + + Returns: + tuple: (QGroupBox, QTextEdit) + """ + 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 + +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("✗ ", "") + +def show_confirmation_dialog(parent, title, message): + """Show a confirmation dialog and return True if user clicks Yes""" + return QMessageBox.question( + parent, + title, + message, + QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No + ) == QMessageBox.StandardButton.Yes \ No newline at end of file