ui.skull_stripping_page
1import torch 2from PyQt6.QtWidgets import ( 3 QVBoxLayout, QLabel, QPushButton, QMessageBox, QCheckBox, 4 QHBoxLayout, QWidget, QDoubleSpinBox, QSpinBox, QGroupBox, 5 QProgressBar, QFrame 6) 7from PyQt6.QtCore import Qt, pyqtSignal, QCoreApplication 8import os 9import subprocess 10 11from components.file_selector_widget import FileSelectorWidget 12from threads.skull_strip_thread import SkullStripThread 13from page import Page 14from logger import get_logger 15 16log = get_logger() 17 18 19class SkullStrippingPage(Page): 20 """ 21 GUI page for performing skull stripping on NIfTI files. 22 23 This class provides a user interface to select anatomical MRI images 24 and perform skull stripping using either: 25 - BET (Brain Extraction Tool) from FSL (if available) 26 - HD-BET (deep learning-based alternative) 27 28 It handles: 29 * User input for tool parameters 30 * Background processing using a worker thread 31 * Progress and status updates 32 33 Signals: 34 processing (bool): emitted when processing starts or stops. 35 """ 36 37 processing = pyqtSignal(bool) 38 39 def __init__(self, context=None, previous_page=None): 40 """ 41 Initialize the SkullStrippingPage. 42 43 Args: 44 context (dict, optional): Shared app context for state and communication. 45 previous_page (Page, optional): Reference to previous navigation page. 46 """ 47 super().__init__() 48 self.canceled = False 49 self.context = context 50 self.previous_page = previous_page 51 self.next_page = None 52 self.worker = None # Background thread reference 53 self.has_cuda = torch.cuda.is_available() 54 self.bet_tool = "" 55 56 self._setup_ui() 57 self._translate_ui() 58 59 # Re-translate if app language changes 60 if context and "language_changed" in context: 61 context["language_changed"].connect(self._translate_ui) 62 63 def _setup_ui(self): 64 """Create and configure all UI components.""" 65 self.processing.connect(self.set_processing_mode) 66 self.layout = QVBoxLayout(self) 67 self.setLayout(self.layout) 68 69 # Title 70 self.title = QLabel("Select a NIfTI file for Skull Stripping") 71 self.title.setStyleSheet("font-size: 18px; font-weight: bold;") 72 self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) 73 self.layout.addWidget(self.title) 74 75 # Info label (link to BET or HD-BET) 76 self.info_label = QLabel( 77 'Using tool: <a href="https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/BET">BET from FSL toolkit</a>' 78 ) 79 self.info_label.setOpenExternalLinks(True) 80 self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) 81 self.info_label.setStyleSheet(""" 82 font-size: 11px; 83 color: #666; 84 font-style: italic; 85 margin-bottom: 6px; 86 """) 87 self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 88 self.info_label.setWordWrap(True) 89 self.layout.addWidget(self.info_label) 90 91 # File selector widget for NIfTI files 92 self.file_selector_widget = FileSelectorWidget( 93 parent=self, 94 context=self.context, 95 has_existing_function=self.has_existing_skull_strip, 96 label="skull strip", 97 allow_multiple=True, 98 processing=self.processing, 99 forced_filters={"datatype": "anat"} 100 ) 101 self.layout.addWidget(self.file_selector_widget) 102 103 # Check if BET command exists in system 104 try: 105 subprocess.run(['bet'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 106 self.has_bet = True 107 except FileNotFoundError: 108 self.has_bet = False 109 110 # Setup parameters for BET (if installed) 111 if self.has_bet: 112 self._setup_bet_options() 113 else: 114 try: 115 subprocess.run(['mri_synthstrip'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 116 self.has_synthstrip = True 117 except FileNotFoundError: 118 self.has_synthstrip = False 119 120 if self.has_synthstrip: 121 self.info_label.setText( 122 QCoreApplication.translate("SkullStrippingPage", 'Using tool: <a href="https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/">SynthStrip</a> <br>') 123 ) 124 else: 125 self.info_label.setText( 126 QCoreApplication.translate("SkullStrippingPage", 127 'Using tool: <a href="https://github.com/MIC-DKFZ/HD-BET">hd-bet</a> <br>' 128 'To use BET from FSL toolkit, install it following the instructions at: ' 129 '<a href="https://fsl.fmrib.ox.ac.uk/fsl/docs/#/install/index">FSL installation</a>') 130 ) 131 self.info_label.setOpenExternalLinks(True) 132 self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) 133 self.info_label.setToolTip("Open link") 134 135 # Progress bar 136 self.progress_bar = QProgressBar() 137 self.progress_bar.setVisible(False) 138 self.layout.addWidget(self.progress_bar) 139 140 # Action buttons (Run / Cancel) 141 button_frame = QFrame() 142 button_layout = QHBoxLayout(button_frame) 143 button_layout.setContentsMargins(0, 15, 0, 0) 144 145 # --- RUN button (green) --- 146 self.run_button = QPushButton("Run Skull Stripping") 147 self.file_selector_widget.has_file.connect(self.run_button.setEnabled) 148 self.run_button.setEnabled(False) 149 self.run_button.clicked.connect(self.run_bet) 150 self.run_button.setStyleSheet(""" 151 QPushButton { 152 font-size: 14px; 153 font-weight: bold; 154 background-color: #27ae60; 155 color: white; 156 border-radius: 8px; 157 padding: 10px 20px; 158 min-width: 140px; 159 } 160 QPushButton:hover { 161 background-color: #229954; 162 } 163 QPushButton:disabled { 164 background-color: #bdc3c7; 165 color: #7f8c8d; 166 } 167 """) 168 button_layout.addStretch() 169 button_layout.addWidget(self.run_button) 170 171 # --- CANCEL button (red) --- 172 self.cancel_button = QPushButton("Stop Processing") 173 self.cancel_button.setVisible(False) 174 self.cancel_button.clicked.connect(self.cancel_processing) 175 self.cancel_button.setStyleSheet(""" 176 QPushButton { 177 font-size: 14px; 178 font-weight: bold; 179 background-color: #e74c3c; 180 color: white; 181 border-radius: 8px; 182 padding: 10px 20px; 183 min-width: 140px; 184 } 185 QPushButton:hover { 186 background-color: #c0392b; 187 } 188 QPushButton:disabled { 189 background-color: #bdc3c7; 190 color: #7f8c8d; 191 } 192 """) 193 button_layout.addWidget(self.cancel_button) 194 button_layout.addStretch() 195 196 self.layout.addWidget(button_frame) 197 198 # Stato 199 self.status_label = QLabel("") 200 self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 201 self.layout.addWidget(self.status_label) 202 203 def _setup_bet_options(self): 204 """Setup BET-specific parameter inputs and advanced options.""" 205 # Fractional intensity threshold (main parameter) 206 self.f_box = QGroupBox() 207 f_layout = QHBoxLayout() 208 self.f_label = QLabel("Fractional intensity threshold, smaller values give larger brain outline estimates") 209 f_layout.addWidget(self.f_label) 210 211 self.f_spinbox = QDoubleSpinBox() 212 self.f_spinbox.setRange(0.0, 1.0) 213 self.f_spinbox.setSingleStep(0.05) 214 self.f_spinbox.setValue(0.50) 215 self.f_spinbox.setDecimals(2) 216 self.f_spinbox.setMinimumWidth(60) 217 self.f_spinbox.setMaximumWidth(80) 218 f_layout.addWidget(self.f_spinbox) 219 f_layout.addStretch() 220 self.f_box.setLayout(f_layout) 221 self.layout.addWidget(self.f_box) 222 223 # Toggle for advanced options 224 self.advanced_btn = QPushButton("Show Advanced Options") 225 self.advanced_btn.setCheckable(True) 226 self.advanced_btn.clicked.connect(self.toggle_advanced) 227 self.layout.addWidget(self.advanced_btn) 228 229 # Advanced parameters box 230 self.is_checked = False 231 self.advanced_box = QGroupBox() 232 self.advanced_layout = QVBoxLayout() 233 234 # Output options 235 self.output_label = QLabel("Advanced options:") 236 self.output_label.setStyleSheet("font-weight: bold; margin-top: 5px;") 237 self.advanced_layout.addWidget(self.output_label) 238 239 self.opt_brain_extracted = QCheckBox("Output brain-extracted image") 240 self.opt_brain_extracted.setChecked(True) 241 self.advanced_layout.addWidget(self.opt_brain_extracted) 242 243 self.opt_m = QCheckBox("Output binary brain mask image") 244 self.advanced_layout.addWidget(self.opt_m) 245 246 self.opt_t = QCheckBox("Apply thresholding to brain and mask image") 247 self.advanced_layout.addWidget(self.opt_t) 248 249 self.opt_s = QCheckBox("Output exterior skull surface image") 250 self.advanced_layout.addWidget(self.opt_s) 251 252 self.opt_o = QCheckBox("Output brain surface overlaid onto original image") 253 self.advanced_layout.addWidget(self.opt_o) 254 255 # Threshold gradient 256 self.threshold_layout = QHBoxLayout() 257 self.threshold_label = QLabel("Threshold gradient; positive values give larger brain outline at bottom, smaller at top") 258 self.threshold_layout.addWidget(self.threshold_label) 259 self.g_spinbox = QDoubleSpinBox() 260 self.g_spinbox.setRange(-1.0, 1.0) 261 self.g_spinbox.setSingleStep(0.1) 262 self.g_spinbox.setValue(0.0) 263 self.g_spinbox.setDecimals(1) 264 self.g_spinbox.setMinimumWidth(60) 265 self.g_spinbox.setMaximumWidth(80) 266 self.threshold_layout.addWidget(self.g_spinbox) 267 self.threshold_layout.addStretch() 268 self.advanced_layout.addLayout(self.threshold_layout) 269 270 # Sphere coordinates 271 coords_layout = QHBoxLayout() 272 self.coords_label = QLabel("Coordinates (voxels) for centre of initial brain surface sphere") 273 coords_layout.addWidget(self.coords_label) 274 275 self.c_x_spinbox = QSpinBox() 276 self.c_x_spinbox.setRange(0, 9999) 277 coords_layout.addWidget(self.c_x_spinbox) 278 coords_layout.addWidget(QLabel("Y")) 279 280 self.c_y_spinbox = QSpinBox() 281 self.c_y_spinbox.setRange(0, 9999) 282 coords_layout.addWidget(self.c_y_spinbox) 283 coords_layout.addWidget(QLabel("Z")) 284 285 self.c_z_spinbox = QSpinBox() 286 self.c_z_spinbox.setRange(0, 9999) 287 coords_layout.addWidget(self.c_z_spinbox) 288 coords_layout.addStretch() 289 290 self.advanced_layout.addLayout(coords_layout) 291 self.advanced_box.setLayout(self.advanced_layout) 292 self.advanced_box.setVisible(False) 293 self.layout.addWidget(self.advanced_box) 294 295 def has_existing_skull_strip(self, nifti_file_path, workspace_path): 296 """ 297 Check if a skull-stripped file already exists for the selected subject. 298 299 Args: 300 nifti_file_path (str): Full path to input NIfTI file. 301 workspace_path (str): Base workspace directory path. 302 303 Returns: 304 bool: True if an existing skull strip is found, False otherwise. 305 """ 306 path_parts = nifti_file_path.replace(workspace_path, '').strip(os.sep).split(os.sep) 307 subject_id = next((p for p in path_parts if p.startswith('sub-')), None) 308 if not subject_id: 309 return False 310 311 skull_strip_dir = os.path.join(workspace_path, 'derivatives', 'skullstrips', subject_id, 'anat') 312 if not os.path.exists(skull_strip_dir): 313 return False 314 315 return any(f.endswith('.nii.gz') for f in os.listdir(skull_strip_dir)) 316 317 def toggle_advanced(self): 318 """Toggle visibility of advanced BET parameters.""" 319 self.is_checked = self.advanced_btn.isChecked() 320 self.advanced_box.setVisible(self.is_checked) 321 self.advanced_btn.setText("Hide Advanced Options" if self.is_checked else "Show Advanced Options") 322 323 def run_bet(self): 324 """Start the skull stripping process in a background thread.""" 325 selected_files = self.file_selector_widget.get_selected_files() 326 if not selected_files: 327 QMessageBox.warning(self, "No files", "Please select at least one NIfTI file first.") 328 return 329 330 parameters = None 331 if self.has_bet: 332 parameters = { 333 'f_val': self.f_spinbox.value(), 334 'opt_brain_extracted': self.opt_brain_extracted.isChecked(), 335 'opt_m': self.opt_m.isChecked(), 336 'opt_t': self.opt_t.isChecked(), 337 'opt_s': self.opt_s.isChecked(), 338 'opt_o': self.opt_o.isChecked(), 339 'g_val': self.g_spinbox.value(), 340 'c_x': self.c_x_spinbox.value(), 341 'c_y': self.c_y_spinbox.value(), 342 'c_z': self.c_z_spinbox.value(), 343 } 344 345 if self.has_bet: 346 self.bet_tool = "fsl-bet" 347 elif self.has_synthstrip: 348 self.bet_tool = "synthstrip" 349 else: 350 self.bet_tool = "hd-bet" 351 352 # Create worker thread 353 self.worker = SkullStripThread(selected_files, self.context["workspace_path"], parameters, self.has_cuda, self.bet_tool) 354 355 # Connect worker signals 356 self.worker.progress_updated.connect(self.on_progress_updated) 357 self.worker.progress_value_updated.connect(self.on_progress_value_updated) 358 self.worker.file_started.connect(self.on_file_started) 359 self.worker.file_completed.connect(self.on_file_completed) 360 self.worker.all_completed.connect(self.on_all_completed) 361 self.worker.finished.connect(self.on_worker_finished) 362 363 # Update UI 364 self.processing.emit(True) 365 self.progress_bar.setRange(0, 100) 366 self.progress_bar.setValue(0) 367 self.progress_bar.setVisible(True) 368 369 self.worker.start() 370 371 def cancel_processing(self): 372 """Cancel the currently running process.""" 373 self.canceled = True 374 if self.worker and self.worker.isRunning(): 375 376 self.worker.cancel() 377 self.status_label.setText("Cancelling...") 378 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold;") 379 380 def set_processing_mode(self, processing): 381 """Enable or disable UI controls depending on processing state.""" 382 self.run_button.setVisible(not processing and bool(self.file_selector_widget.get_selected_files())) 383 self.cancel_button.setVisible(processing) 384 if hasattr(self, "f_spinbox"): 385 self.f_spinbox.setEnabled(not processing) 386 if hasattr(self, "advanced_btn"): 387 self.advanced_btn.setEnabled(not processing) 388 if hasattr(self, "advanced_box"): 389 for widget in self.advanced_box.findChildren(QWidget): 390 widget.setEnabled(not processing) 391 self.cancel_button.setVisible(processing) 392 393 394 def on_progress_updated(self, message): 395 """Update progress text message.""" 396 self.status_label.setText(message) 397 self.status_label.setStyleSheet("color: #2196F3; font-weight: bold;") 398 399 def on_progress_value_updated(self, value): 400 """Update numeric progress bar value.""" 401 self.progress_bar.setValue(value) 402 403 def on_file_started(self, filename): 404 """Triggered when a new file starts processing.""" 405 pass 406 407 def on_file_completed(self, filename, success, error_message): 408 """Triggered when a file finishes processing.""" 409 if not success and error_message: 410 log.error(f"Error processing {filename}: {error_message}") 411 412 def on_all_completed(self, success_count, failed_files): 413 """Triggered after all files are processed.""" 414 # Nascondi progress bar 415 self.progress_bar.setVisible(False) 416 417 # Aggiorna il messaggio di stato finale 418 if success_count > 0: 419 summary = QCoreApplication.translate("SkullStrippingPage", 420 "Skull Stripping completed successfully for {0} file(s)").format( 421 success_count) 422 if failed_files: 423 failed_summary = QCoreApplication.translate("SkullStrippingPage", 424 "{count} file(s) failed: {files}").format( 425 count=len(failed_files), 426 files=', '.join([os.path.basename(f) for f in failed_files]) 427 ) 428 summary += f"\n{failed_summary}" 429 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold; font-size: 12pt; padding: 5px;") 430 else: 431 self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold; font-size: 12pt; padding: 5px;") 432 else: 433 failed_files_num = len(failed_files) 434 if failed_files_num == 0: 435 failed_files_num = "" 436 summary = QCoreApplication.translate("SkullStrippingPage", "All {0} file(s) failed to process").format( 437 failed_files_num) 438 self.status_label.setStyleSheet("color: #FF0000; font-weight: bold; font-size: 12pt; padding: 5px;") 439 440 self.status_label.setText(summary) 441 442 443 def on_worker_finished(self): 444 """Triggered when worker thread finishes execution.""" 445 self.processing.emit(False) 446 if self.worker: 447 self.worker.deleteLater() 448 self.worker = None 449 if self.context and "update_main_buttons" in self.context: 450 self.context["update_main_buttons"]() 451 452 def back(self): 453 """Navigate back to the previous page if not processing.""" 454 if self.worker and self.worker.isRunning(): 455 QMessageBox.warning(self, "Processing in progress", 456 "Cannot go back while skull stripping is in progress. Please wait or cancel.") 457 log.warning("Processing in progress") 458 return None 459 if self.previous_page: 460 self.previous_page.on_enter() 461 return self.previous_page 462 return None 463 464 def on_enter(self): 465 """Reset status label when page is entered.""" 466 self.status_label.setText("") 467 468 def is_ready_to_advance(self): 469 """Return False since this page doesn't advance automatically.""" 470 return False 471 472 def is_ready_to_go_back(self): 473 """Disable back navigation during processing.""" 474 return not (self.worker and self.worker.isRunning()) 475 476 def reset_page(self): 477 """Reset the page state to its initial configuration.""" 478 if self.worker and self.worker.isRunning(): 479 self.worker.cancel() 480 self.worker.wait() 481 482 self.file_selector_widget.clear_selected_files() 483 self.run_button.setEnabled(False) 484 485 # Reset advanced options 486 self.f_spinbox.setValue(0.50) 487 self.advanced_btn.setChecked(False) 488 self.advanced_box.setVisible(False) 489 self.advanced_btn.setText("Show Advanced Options") 490 491 self.opt_brain_extracted.setChecked(True) 492 self.opt_m.setChecked(False) 493 self.opt_t.setChecked(False) 494 self.opt_s.setChecked(False) 495 self.opt_o.setChecked(False) 496 497 self.g_spinbox.setValue(0.0) 498 self.c_x_spinbox.setValue(0) 499 self.c_y_spinbox.setValue(0) 500 self.c_z_spinbox.setValue(0) 501 502 self.progress_bar.setVisible(False) 503 self.cancel_button.setVisible(False) 504 self.processing.emit(False) 505 self.status_label.setText("") 506 507 def _translate_ui(self): 508 """Translate UI strings according to the active language.""" 509 self.title.setText(QCoreApplication.translate("SkullStrippingPage", "Select a NIfTI file for Skull Stripping")) 510 if self.has_bet: 511 self.f_label.setText(QCoreApplication.translate("SkullStrippingPage", "Fractional intensity threshold, smaller values give larger brain outline estimates")) 512 self.advanced_btn.setText(QCoreApplication.translate("SkullStrippingPage", "Show Advanced Options")) 513 self.output_label.setText(QCoreApplication.translate("SkullStrippingPage", "Advanced options:")) 514 self.opt_brain_extracted.setText(QCoreApplication.translate("SkullStrippingPage", "Output brain-extracted image")) 515 self.opt_m.setText(QCoreApplication.translate("SkullStrippingPage", "Output binary brain mask image")) 516 self.opt_t.setText(QCoreApplication.translate("SkullStrippingPage", "Apply thresholding to brain and mask image")) 517 self.opt_s.setText(QCoreApplication.translate("SkullStrippingPage", "Output exterior skull surface image")) 518 self.opt_o.setText(QCoreApplication.translate("SkullStrippingPage", "Output brain surface overlaid onto original image")) 519 self.threshold_label.setText(QCoreApplication.translate("SkullStrippingPage", "Threshold gradient; positive values give larger brain outline at bottom, smaller at top")) 520 self.coords_label.setText(QCoreApplication.translate("SkullStrippingPage", "Coordinates (voxels) for centre of initial brain surface sphere")) 521 else: 522 self.info_label.setToolTip(QCoreApplication.translate("SkullStrippingPage", "Open link")) 523 524 self.run_button.setText(QCoreApplication.translate("SkullStrippingPage", "Run Skull Stripping")) 525 self.cancel_button.setText(QCoreApplication.translate("SkullStrippingPage", "Cancel"))
20class SkullStrippingPage(Page): 21 """ 22 GUI page for performing skull stripping on NIfTI files. 23 24 This class provides a user interface to select anatomical MRI images 25 and perform skull stripping using either: 26 - BET (Brain Extraction Tool) from FSL (if available) 27 - HD-BET (deep learning-based alternative) 28 29 It handles: 30 * User input for tool parameters 31 * Background processing using a worker thread 32 * Progress and status updates 33 34 Signals: 35 processing (bool): emitted when processing starts or stops. 36 """ 37 38 processing = pyqtSignal(bool) 39 40 def __init__(self, context=None, previous_page=None): 41 """ 42 Initialize the SkullStrippingPage. 43 44 Args: 45 context (dict, optional): Shared app context for state and communication. 46 previous_page (Page, optional): Reference to previous navigation page. 47 """ 48 super().__init__() 49 self.canceled = False 50 self.context = context 51 self.previous_page = previous_page 52 self.next_page = None 53 self.worker = None # Background thread reference 54 self.has_cuda = torch.cuda.is_available() 55 self.bet_tool = "" 56 57 self._setup_ui() 58 self._translate_ui() 59 60 # Re-translate if app language changes 61 if context and "language_changed" in context: 62 context["language_changed"].connect(self._translate_ui) 63 64 def _setup_ui(self): 65 """Create and configure all UI components.""" 66 self.processing.connect(self.set_processing_mode) 67 self.layout = QVBoxLayout(self) 68 self.setLayout(self.layout) 69 70 # Title 71 self.title = QLabel("Select a NIfTI file for Skull Stripping") 72 self.title.setStyleSheet("font-size: 18px; font-weight: bold;") 73 self.title.setAlignment(Qt.AlignmentFlag.AlignCenter) 74 self.layout.addWidget(self.title) 75 76 # Info label (link to BET or HD-BET) 77 self.info_label = QLabel( 78 'Using tool: <a href="https://fsl.fmrib.ox.ac.uk/fsl/fslwiki/BET">BET from FSL toolkit</a>' 79 ) 80 self.info_label.setOpenExternalLinks(True) 81 self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) 82 self.info_label.setStyleSheet(""" 83 font-size: 11px; 84 color: #666; 85 font-style: italic; 86 margin-bottom: 6px; 87 """) 88 self.info_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 89 self.info_label.setWordWrap(True) 90 self.layout.addWidget(self.info_label) 91 92 # File selector widget for NIfTI files 93 self.file_selector_widget = FileSelectorWidget( 94 parent=self, 95 context=self.context, 96 has_existing_function=self.has_existing_skull_strip, 97 label="skull strip", 98 allow_multiple=True, 99 processing=self.processing, 100 forced_filters={"datatype": "anat"} 101 ) 102 self.layout.addWidget(self.file_selector_widget) 103 104 # Check if BET command exists in system 105 try: 106 subprocess.run(['bet'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 107 self.has_bet = True 108 except FileNotFoundError: 109 self.has_bet = False 110 111 # Setup parameters for BET (if installed) 112 if self.has_bet: 113 self._setup_bet_options() 114 else: 115 try: 116 subprocess.run(['mri_synthstrip'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) 117 self.has_synthstrip = True 118 except FileNotFoundError: 119 self.has_synthstrip = False 120 121 if self.has_synthstrip: 122 self.info_label.setText( 123 QCoreApplication.translate("SkullStrippingPage", 'Using tool: <a href="https://surfer.nmr.mgh.harvard.edu/docs/synthstrip/">SynthStrip</a> <br>') 124 ) 125 else: 126 self.info_label.setText( 127 QCoreApplication.translate("SkullStrippingPage", 128 'Using tool: <a href="https://github.com/MIC-DKFZ/HD-BET">hd-bet</a> <br>' 129 'To use BET from FSL toolkit, install it following the instructions at: ' 130 '<a href="https://fsl.fmrib.ox.ac.uk/fsl/docs/#/install/index">FSL installation</a>') 131 ) 132 self.info_label.setOpenExternalLinks(True) 133 self.info_label.setTextInteractionFlags(Qt.TextInteractionFlag.TextBrowserInteraction) 134 self.info_label.setToolTip("Open link") 135 136 # Progress bar 137 self.progress_bar = QProgressBar() 138 self.progress_bar.setVisible(False) 139 self.layout.addWidget(self.progress_bar) 140 141 # Action buttons (Run / Cancel) 142 button_frame = QFrame() 143 button_layout = QHBoxLayout(button_frame) 144 button_layout.setContentsMargins(0, 15, 0, 0) 145 146 # --- RUN button (green) --- 147 self.run_button = QPushButton("Run Skull Stripping") 148 self.file_selector_widget.has_file.connect(self.run_button.setEnabled) 149 self.run_button.setEnabled(False) 150 self.run_button.clicked.connect(self.run_bet) 151 self.run_button.setStyleSheet(""" 152 QPushButton { 153 font-size: 14px; 154 font-weight: bold; 155 background-color: #27ae60; 156 color: white; 157 border-radius: 8px; 158 padding: 10px 20px; 159 min-width: 140px; 160 } 161 QPushButton:hover { 162 background-color: #229954; 163 } 164 QPushButton:disabled { 165 background-color: #bdc3c7; 166 color: #7f8c8d; 167 } 168 """) 169 button_layout.addStretch() 170 button_layout.addWidget(self.run_button) 171 172 # --- CANCEL button (red) --- 173 self.cancel_button = QPushButton("Stop Processing") 174 self.cancel_button.setVisible(False) 175 self.cancel_button.clicked.connect(self.cancel_processing) 176 self.cancel_button.setStyleSheet(""" 177 QPushButton { 178 font-size: 14px; 179 font-weight: bold; 180 background-color: #e74c3c; 181 color: white; 182 border-radius: 8px; 183 padding: 10px 20px; 184 min-width: 140px; 185 } 186 QPushButton:hover { 187 background-color: #c0392b; 188 } 189 QPushButton:disabled { 190 background-color: #bdc3c7; 191 color: #7f8c8d; 192 } 193 """) 194 button_layout.addWidget(self.cancel_button) 195 button_layout.addStretch() 196 197 self.layout.addWidget(button_frame) 198 199 # Stato 200 self.status_label = QLabel("") 201 self.status_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 202 self.layout.addWidget(self.status_label) 203 204 def _setup_bet_options(self): 205 """Setup BET-specific parameter inputs and advanced options.""" 206 # Fractional intensity threshold (main parameter) 207 self.f_box = QGroupBox() 208 f_layout = QHBoxLayout() 209 self.f_label = QLabel("Fractional intensity threshold, smaller values give larger brain outline estimates") 210 f_layout.addWidget(self.f_label) 211 212 self.f_spinbox = QDoubleSpinBox() 213 self.f_spinbox.setRange(0.0, 1.0) 214 self.f_spinbox.setSingleStep(0.05) 215 self.f_spinbox.setValue(0.50) 216 self.f_spinbox.setDecimals(2) 217 self.f_spinbox.setMinimumWidth(60) 218 self.f_spinbox.setMaximumWidth(80) 219 f_layout.addWidget(self.f_spinbox) 220 f_layout.addStretch() 221 self.f_box.setLayout(f_layout) 222 self.layout.addWidget(self.f_box) 223 224 # Toggle for advanced options 225 self.advanced_btn = QPushButton("Show Advanced Options") 226 self.advanced_btn.setCheckable(True) 227 self.advanced_btn.clicked.connect(self.toggle_advanced) 228 self.layout.addWidget(self.advanced_btn) 229 230 # Advanced parameters box 231 self.is_checked = False 232 self.advanced_box = QGroupBox() 233 self.advanced_layout = QVBoxLayout() 234 235 # Output options 236 self.output_label = QLabel("Advanced options:") 237 self.output_label.setStyleSheet("font-weight: bold; margin-top: 5px;") 238 self.advanced_layout.addWidget(self.output_label) 239 240 self.opt_brain_extracted = QCheckBox("Output brain-extracted image") 241 self.opt_brain_extracted.setChecked(True) 242 self.advanced_layout.addWidget(self.opt_brain_extracted) 243 244 self.opt_m = QCheckBox("Output binary brain mask image") 245 self.advanced_layout.addWidget(self.opt_m) 246 247 self.opt_t = QCheckBox("Apply thresholding to brain and mask image") 248 self.advanced_layout.addWidget(self.opt_t) 249 250 self.opt_s = QCheckBox("Output exterior skull surface image") 251 self.advanced_layout.addWidget(self.opt_s) 252 253 self.opt_o = QCheckBox("Output brain surface overlaid onto original image") 254 self.advanced_layout.addWidget(self.opt_o) 255 256 # Threshold gradient 257 self.threshold_layout = QHBoxLayout() 258 self.threshold_label = QLabel("Threshold gradient; positive values give larger brain outline at bottom, smaller at top") 259 self.threshold_layout.addWidget(self.threshold_label) 260 self.g_spinbox = QDoubleSpinBox() 261 self.g_spinbox.setRange(-1.0, 1.0) 262 self.g_spinbox.setSingleStep(0.1) 263 self.g_spinbox.setValue(0.0) 264 self.g_spinbox.setDecimals(1) 265 self.g_spinbox.setMinimumWidth(60) 266 self.g_spinbox.setMaximumWidth(80) 267 self.threshold_layout.addWidget(self.g_spinbox) 268 self.threshold_layout.addStretch() 269 self.advanced_layout.addLayout(self.threshold_layout) 270 271 # Sphere coordinates 272 coords_layout = QHBoxLayout() 273 self.coords_label = QLabel("Coordinates (voxels) for centre of initial brain surface sphere") 274 coords_layout.addWidget(self.coords_label) 275 276 self.c_x_spinbox = QSpinBox() 277 self.c_x_spinbox.setRange(0, 9999) 278 coords_layout.addWidget(self.c_x_spinbox) 279 coords_layout.addWidget(QLabel("Y")) 280 281 self.c_y_spinbox = QSpinBox() 282 self.c_y_spinbox.setRange(0, 9999) 283 coords_layout.addWidget(self.c_y_spinbox) 284 coords_layout.addWidget(QLabel("Z")) 285 286 self.c_z_spinbox = QSpinBox() 287 self.c_z_spinbox.setRange(0, 9999) 288 coords_layout.addWidget(self.c_z_spinbox) 289 coords_layout.addStretch() 290 291 self.advanced_layout.addLayout(coords_layout) 292 self.advanced_box.setLayout(self.advanced_layout) 293 self.advanced_box.setVisible(False) 294 self.layout.addWidget(self.advanced_box) 295 296 def has_existing_skull_strip(self, nifti_file_path, workspace_path): 297 """ 298 Check if a skull-stripped file already exists for the selected subject. 299 300 Args: 301 nifti_file_path (str): Full path to input NIfTI file. 302 workspace_path (str): Base workspace directory path. 303 304 Returns: 305 bool: True if an existing skull strip is found, False otherwise. 306 """ 307 path_parts = nifti_file_path.replace(workspace_path, '').strip(os.sep).split(os.sep) 308 subject_id = next((p for p in path_parts if p.startswith('sub-')), None) 309 if not subject_id: 310 return False 311 312 skull_strip_dir = os.path.join(workspace_path, 'derivatives', 'skullstrips', subject_id, 'anat') 313 if not os.path.exists(skull_strip_dir): 314 return False 315 316 return any(f.endswith('.nii.gz') for f in os.listdir(skull_strip_dir)) 317 318 def toggle_advanced(self): 319 """Toggle visibility of advanced BET parameters.""" 320 self.is_checked = self.advanced_btn.isChecked() 321 self.advanced_box.setVisible(self.is_checked) 322 self.advanced_btn.setText("Hide Advanced Options" if self.is_checked else "Show Advanced Options") 323 324 def run_bet(self): 325 """Start the skull stripping process in a background thread.""" 326 selected_files = self.file_selector_widget.get_selected_files() 327 if not selected_files: 328 QMessageBox.warning(self, "No files", "Please select at least one NIfTI file first.") 329 return 330 331 parameters = None 332 if self.has_bet: 333 parameters = { 334 'f_val': self.f_spinbox.value(), 335 'opt_brain_extracted': self.opt_brain_extracted.isChecked(), 336 'opt_m': self.opt_m.isChecked(), 337 'opt_t': self.opt_t.isChecked(), 338 'opt_s': self.opt_s.isChecked(), 339 'opt_o': self.opt_o.isChecked(), 340 'g_val': self.g_spinbox.value(), 341 'c_x': self.c_x_spinbox.value(), 342 'c_y': self.c_y_spinbox.value(), 343 'c_z': self.c_z_spinbox.value(), 344 } 345 346 if self.has_bet: 347 self.bet_tool = "fsl-bet" 348 elif self.has_synthstrip: 349 self.bet_tool = "synthstrip" 350 else: 351 self.bet_tool = "hd-bet" 352 353 # Create worker thread 354 self.worker = SkullStripThread(selected_files, self.context["workspace_path"], parameters, self.has_cuda, self.bet_tool) 355 356 # Connect worker signals 357 self.worker.progress_updated.connect(self.on_progress_updated) 358 self.worker.progress_value_updated.connect(self.on_progress_value_updated) 359 self.worker.file_started.connect(self.on_file_started) 360 self.worker.file_completed.connect(self.on_file_completed) 361 self.worker.all_completed.connect(self.on_all_completed) 362 self.worker.finished.connect(self.on_worker_finished) 363 364 # Update UI 365 self.processing.emit(True) 366 self.progress_bar.setRange(0, 100) 367 self.progress_bar.setValue(0) 368 self.progress_bar.setVisible(True) 369 370 self.worker.start() 371 372 def cancel_processing(self): 373 """Cancel the currently running process.""" 374 self.canceled = True 375 if self.worker and self.worker.isRunning(): 376 377 self.worker.cancel() 378 self.status_label.setText("Cancelling...") 379 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold;") 380 381 def set_processing_mode(self, processing): 382 """Enable or disable UI controls depending on processing state.""" 383 self.run_button.setVisible(not processing and bool(self.file_selector_widget.get_selected_files())) 384 self.cancel_button.setVisible(processing) 385 if hasattr(self, "f_spinbox"): 386 self.f_spinbox.setEnabled(not processing) 387 if hasattr(self, "advanced_btn"): 388 self.advanced_btn.setEnabled(not processing) 389 if hasattr(self, "advanced_box"): 390 for widget in self.advanced_box.findChildren(QWidget): 391 widget.setEnabled(not processing) 392 self.cancel_button.setVisible(processing) 393 394 395 def on_progress_updated(self, message): 396 """Update progress text message.""" 397 self.status_label.setText(message) 398 self.status_label.setStyleSheet("color: #2196F3; font-weight: bold;") 399 400 def on_progress_value_updated(self, value): 401 """Update numeric progress bar value.""" 402 self.progress_bar.setValue(value) 403 404 def on_file_started(self, filename): 405 """Triggered when a new file starts processing.""" 406 pass 407 408 def on_file_completed(self, filename, success, error_message): 409 """Triggered when a file finishes processing.""" 410 if not success and error_message: 411 log.error(f"Error processing {filename}: {error_message}") 412 413 def on_all_completed(self, success_count, failed_files): 414 """Triggered after all files are processed.""" 415 # Nascondi progress bar 416 self.progress_bar.setVisible(False) 417 418 # Aggiorna il messaggio di stato finale 419 if success_count > 0: 420 summary = QCoreApplication.translate("SkullStrippingPage", 421 "Skull Stripping completed successfully for {0} file(s)").format( 422 success_count) 423 if failed_files: 424 failed_summary = QCoreApplication.translate("SkullStrippingPage", 425 "{count} file(s) failed: {files}").format( 426 count=len(failed_files), 427 files=', '.join([os.path.basename(f) for f in failed_files]) 428 ) 429 summary += f"\n{failed_summary}" 430 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold; font-size: 12pt; padding: 5px;") 431 else: 432 self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold; font-size: 12pt; padding: 5px;") 433 else: 434 failed_files_num = len(failed_files) 435 if failed_files_num == 0: 436 failed_files_num = "" 437 summary = QCoreApplication.translate("SkullStrippingPage", "All {0} file(s) failed to process").format( 438 failed_files_num) 439 self.status_label.setStyleSheet("color: #FF0000; font-weight: bold; font-size: 12pt; padding: 5px;") 440 441 self.status_label.setText(summary) 442 443 444 def on_worker_finished(self): 445 """Triggered when worker thread finishes execution.""" 446 self.processing.emit(False) 447 if self.worker: 448 self.worker.deleteLater() 449 self.worker = None 450 if self.context and "update_main_buttons" in self.context: 451 self.context["update_main_buttons"]() 452 453 def back(self): 454 """Navigate back to the previous page if not processing.""" 455 if self.worker and self.worker.isRunning(): 456 QMessageBox.warning(self, "Processing in progress", 457 "Cannot go back while skull stripping is in progress. Please wait or cancel.") 458 log.warning("Processing in progress") 459 return None 460 if self.previous_page: 461 self.previous_page.on_enter() 462 return self.previous_page 463 return None 464 465 def on_enter(self): 466 """Reset status label when page is entered.""" 467 self.status_label.setText("") 468 469 def is_ready_to_advance(self): 470 """Return False since this page doesn't advance automatically.""" 471 return False 472 473 def is_ready_to_go_back(self): 474 """Disable back navigation during processing.""" 475 return not (self.worker and self.worker.isRunning()) 476 477 def reset_page(self): 478 """Reset the page state to its initial configuration.""" 479 if self.worker and self.worker.isRunning(): 480 self.worker.cancel() 481 self.worker.wait() 482 483 self.file_selector_widget.clear_selected_files() 484 self.run_button.setEnabled(False) 485 486 # Reset advanced options 487 self.f_spinbox.setValue(0.50) 488 self.advanced_btn.setChecked(False) 489 self.advanced_box.setVisible(False) 490 self.advanced_btn.setText("Show Advanced Options") 491 492 self.opt_brain_extracted.setChecked(True) 493 self.opt_m.setChecked(False) 494 self.opt_t.setChecked(False) 495 self.opt_s.setChecked(False) 496 self.opt_o.setChecked(False) 497 498 self.g_spinbox.setValue(0.0) 499 self.c_x_spinbox.setValue(0) 500 self.c_y_spinbox.setValue(0) 501 self.c_z_spinbox.setValue(0) 502 503 self.progress_bar.setVisible(False) 504 self.cancel_button.setVisible(False) 505 self.processing.emit(False) 506 self.status_label.setText("") 507 508 def _translate_ui(self): 509 """Translate UI strings according to the active language.""" 510 self.title.setText(QCoreApplication.translate("SkullStrippingPage", "Select a NIfTI file for Skull Stripping")) 511 if self.has_bet: 512 self.f_label.setText(QCoreApplication.translate("SkullStrippingPage", "Fractional intensity threshold, smaller values give larger brain outline estimates")) 513 self.advanced_btn.setText(QCoreApplication.translate("SkullStrippingPage", "Show Advanced Options")) 514 self.output_label.setText(QCoreApplication.translate("SkullStrippingPage", "Advanced options:")) 515 self.opt_brain_extracted.setText(QCoreApplication.translate("SkullStrippingPage", "Output brain-extracted image")) 516 self.opt_m.setText(QCoreApplication.translate("SkullStrippingPage", "Output binary brain mask image")) 517 self.opt_t.setText(QCoreApplication.translate("SkullStrippingPage", "Apply thresholding to brain and mask image")) 518 self.opt_s.setText(QCoreApplication.translate("SkullStrippingPage", "Output exterior skull surface image")) 519 self.opt_o.setText(QCoreApplication.translate("SkullStrippingPage", "Output brain surface overlaid onto original image")) 520 self.threshold_label.setText(QCoreApplication.translate("SkullStrippingPage", "Threshold gradient; positive values give larger brain outline at bottom, smaller at top")) 521 self.coords_label.setText(QCoreApplication.translate("SkullStrippingPage", "Coordinates (voxels) for centre of initial brain surface sphere")) 522 else: 523 self.info_label.setToolTip(QCoreApplication.translate("SkullStrippingPage", "Open link")) 524 525 self.run_button.setText(QCoreApplication.translate("SkullStrippingPage", "Run Skull Stripping")) 526 self.cancel_button.setText(QCoreApplication.translate("SkullStrippingPage", "Cancel"))
GUI page for performing skull stripping on NIfTI files.
This class provides a user interface to select anatomical MRI images and perform skull stripping using either: - BET (Brain Extraction Tool) from FSL (if available) - HD-BET (deep learning-based alternative)
It handles: * User input for tool parameters * Background processing using a worker thread * Progress and status updates
Signals: processing (bool): emitted when processing starts or stops.
40 def __init__(self, context=None, previous_page=None): 41 """ 42 Initialize the SkullStrippingPage. 43 44 Args: 45 context (dict, optional): Shared app context for state and communication. 46 previous_page (Page, optional): Reference to previous navigation page. 47 """ 48 super().__init__() 49 self.canceled = False 50 self.context = context 51 self.previous_page = previous_page 52 self.next_page = None 53 self.worker = None # Background thread reference 54 self.has_cuda = torch.cuda.is_available() 55 self.bet_tool = "" 56 57 self._setup_ui() 58 self._translate_ui() 59 60 # Re-translate if app language changes 61 if context and "language_changed" in context: 62 context["language_changed"].connect(self._translate_ui)
Initialize the SkullStrippingPage.
Args: context (dict, optional): Shared app context for state and communication. previous_page (Page, optional): Reference to previous navigation page.
pyqtSignal(*types, name: str = ..., revision: int = ..., arguments: Sequence = ...) -> PYQT_SIGNAL
types is normally a sequence of individual types. Each type is either a type object or a string that is the name of a C++ type. Alternatively each type could itself be a sequence of types each describing a different overloaded signal. name is the optional C++ name of the signal. If it is not specified then the name of the class attribute that is bound to the signal is used. revision is the optional revision of the signal that is exported to QML. If it is not specified then 0 is used. arguments is the optional sequence of the names of the signal's arguments.
296 def has_existing_skull_strip(self, nifti_file_path, workspace_path): 297 """ 298 Check if a skull-stripped file already exists for the selected subject. 299 300 Args: 301 nifti_file_path (str): Full path to input NIfTI file. 302 workspace_path (str): Base workspace directory path. 303 304 Returns: 305 bool: True if an existing skull strip is found, False otherwise. 306 """ 307 path_parts = nifti_file_path.replace(workspace_path, '').strip(os.sep).split(os.sep) 308 subject_id = next((p for p in path_parts if p.startswith('sub-')), None) 309 if not subject_id: 310 return False 311 312 skull_strip_dir = os.path.join(workspace_path, 'derivatives', 'skullstrips', subject_id, 'anat') 313 if not os.path.exists(skull_strip_dir): 314 return False 315 316 return any(f.endswith('.nii.gz') for f in os.listdir(skull_strip_dir))
Check if a skull-stripped file already exists for the selected subject.
Args: nifti_file_path (str): Full path to input NIfTI file. workspace_path (str): Base workspace directory path.
Returns: bool: True if an existing skull strip is found, False otherwise.
318 def toggle_advanced(self): 319 """Toggle visibility of advanced BET parameters.""" 320 self.is_checked = self.advanced_btn.isChecked() 321 self.advanced_box.setVisible(self.is_checked) 322 self.advanced_btn.setText("Hide Advanced Options" if self.is_checked else "Show Advanced Options")
Toggle visibility of advanced BET parameters.
324 def run_bet(self): 325 """Start the skull stripping process in a background thread.""" 326 selected_files = self.file_selector_widget.get_selected_files() 327 if not selected_files: 328 QMessageBox.warning(self, "No files", "Please select at least one NIfTI file first.") 329 return 330 331 parameters = None 332 if self.has_bet: 333 parameters = { 334 'f_val': self.f_spinbox.value(), 335 'opt_brain_extracted': self.opt_brain_extracted.isChecked(), 336 'opt_m': self.opt_m.isChecked(), 337 'opt_t': self.opt_t.isChecked(), 338 'opt_s': self.opt_s.isChecked(), 339 'opt_o': self.opt_o.isChecked(), 340 'g_val': self.g_spinbox.value(), 341 'c_x': self.c_x_spinbox.value(), 342 'c_y': self.c_y_spinbox.value(), 343 'c_z': self.c_z_spinbox.value(), 344 } 345 346 if self.has_bet: 347 self.bet_tool = "fsl-bet" 348 elif self.has_synthstrip: 349 self.bet_tool = "synthstrip" 350 else: 351 self.bet_tool = "hd-bet" 352 353 # Create worker thread 354 self.worker = SkullStripThread(selected_files, self.context["workspace_path"], parameters, self.has_cuda, self.bet_tool) 355 356 # Connect worker signals 357 self.worker.progress_updated.connect(self.on_progress_updated) 358 self.worker.progress_value_updated.connect(self.on_progress_value_updated) 359 self.worker.file_started.connect(self.on_file_started) 360 self.worker.file_completed.connect(self.on_file_completed) 361 self.worker.all_completed.connect(self.on_all_completed) 362 self.worker.finished.connect(self.on_worker_finished) 363 364 # Update UI 365 self.processing.emit(True) 366 self.progress_bar.setRange(0, 100) 367 self.progress_bar.setValue(0) 368 self.progress_bar.setVisible(True) 369 370 self.worker.start()
Start the skull stripping process in a background thread.
372 def cancel_processing(self): 373 """Cancel the currently running process.""" 374 self.canceled = True 375 if self.worker and self.worker.isRunning(): 376 377 self.worker.cancel() 378 self.status_label.setText("Cancelling...") 379 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold;")
Cancel the currently running process.
381 def set_processing_mode(self, processing): 382 """Enable or disable UI controls depending on processing state.""" 383 self.run_button.setVisible(not processing and bool(self.file_selector_widget.get_selected_files())) 384 self.cancel_button.setVisible(processing) 385 if hasattr(self, "f_spinbox"): 386 self.f_spinbox.setEnabled(not processing) 387 if hasattr(self, "advanced_btn"): 388 self.advanced_btn.setEnabled(not processing) 389 if hasattr(self, "advanced_box"): 390 for widget in self.advanced_box.findChildren(QWidget): 391 widget.setEnabled(not processing) 392 self.cancel_button.setVisible(processing)
Enable or disable UI controls depending on processing state.
395 def on_progress_updated(self, message): 396 """Update progress text message.""" 397 self.status_label.setText(message) 398 self.status_label.setStyleSheet("color: #2196F3; font-weight: bold;")
Update progress text message.
400 def on_progress_value_updated(self, value): 401 """Update numeric progress bar value.""" 402 self.progress_bar.setValue(value)
Update numeric progress bar value.
404 def on_file_started(self, filename): 405 """Triggered when a new file starts processing.""" 406 pass
Triggered when a new file starts processing.
408 def on_file_completed(self, filename, success, error_message): 409 """Triggered when a file finishes processing.""" 410 if not success and error_message: 411 log.error(f"Error processing {filename}: {error_message}")
Triggered when a file finishes processing.
413 def on_all_completed(self, success_count, failed_files): 414 """Triggered after all files are processed.""" 415 # Nascondi progress bar 416 self.progress_bar.setVisible(False) 417 418 # Aggiorna il messaggio di stato finale 419 if success_count > 0: 420 summary = QCoreApplication.translate("SkullStrippingPage", 421 "Skull Stripping completed successfully for {0} file(s)").format( 422 success_count) 423 if failed_files: 424 failed_summary = QCoreApplication.translate("SkullStrippingPage", 425 "{count} file(s) failed: {files}").format( 426 count=len(failed_files), 427 files=', '.join([os.path.basename(f) for f in failed_files]) 428 ) 429 summary += f"\n{failed_summary}" 430 self.status_label.setStyleSheet("color: #FF9800; font-weight: bold; font-size: 12pt; padding: 5px;") 431 else: 432 self.status_label.setStyleSheet("color: #4CAF50; font-weight: bold; font-size: 12pt; padding: 5px;") 433 else: 434 failed_files_num = len(failed_files) 435 if failed_files_num == 0: 436 failed_files_num = "" 437 summary = QCoreApplication.translate("SkullStrippingPage", "All {0} file(s) failed to process").format( 438 failed_files_num) 439 self.status_label.setStyleSheet("color: #FF0000; font-weight: bold; font-size: 12pt; padding: 5px;") 440 441 self.status_label.setText(summary)
Triggered after all files are processed.
444 def on_worker_finished(self): 445 """Triggered when worker thread finishes execution.""" 446 self.processing.emit(False) 447 if self.worker: 448 self.worker.deleteLater() 449 self.worker = None 450 if self.context and "update_main_buttons" in self.context: 451 self.context["update_main_buttons"]()
Triggered when worker thread finishes execution.
453 def back(self): 454 """Navigate back to the previous page if not processing.""" 455 if self.worker and self.worker.isRunning(): 456 QMessageBox.warning(self, "Processing in progress", 457 "Cannot go back while skull stripping is in progress. Please wait or cancel.") 458 log.warning("Processing in progress") 459 return None 460 if self.previous_page: 461 self.previous_page.on_enter() 462 return self.previous_page 463 return None
Navigate back to the previous page if not processing.
465 def on_enter(self): 466 """Reset status label when page is entered.""" 467 self.status_label.setText("")
Reset status label when page is entered.
469 def is_ready_to_advance(self): 470 """Return False since this page doesn't advance automatically.""" 471 return False
Return False since this page doesn't advance automatically.
473 def is_ready_to_go_back(self): 474 """Disable back navigation during processing.""" 475 return not (self.worker and self.worker.isRunning())
Disable back navigation during processing.
477 def reset_page(self): 478 """Reset the page state to its initial configuration.""" 479 if self.worker and self.worker.isRunning(): 480 self.worker.cancel() 481 self.worker.wait() 482 483 self.file_selector_widget.clear_selected_files() 484 self.run_button.setEnabled(False) 485 486 # Reset advanced options 487 self.f_spinbox.setValue(0.50) 488 self.advanced_btn.setChecked(False) 489 self.advanced_box.setVisible(False) 490 self.advanced_btn.setText("Show Advanced Options") 491 492 self.opt_brain_extracted.setChecked(True) 493 self.opt_m.setChecked(False) 494 self.opt_t.setChecked(False) 495 self.opt_s.setChecked(False) 496 self.opt_o.setChecked(False) 497 498 self.g_spinbox.setValue(0.0) 499 self.c_x_spinbox.setValue(0) 500 self.c_y_spinbox.setValue(0) 501 self.c_z_spinbox.setValue(0) 502 503 self.progress_bar.setVisible(False) 504 self.cancel_button.setVisible(False) 505 self.processing.emit(False) 506 self.status_label.setText("")
Reset the page state to its initial configuration.