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"))
log = <Logger GliAAns-UI (WARNING)>
class SkullStrippingPage(page.Page):
 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.

SkullStrippingPage(context=None, previous_page=None)
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.

def processing(unknown):

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.

canceled
context
previous_page
next_page
worker
has_cuda
bet_tool
def has_existing_skull_strip(self, nifti_file_path, workspace_path):
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.

def toggle_advanced(self):
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.

def run_bet(self):
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.

def cancel_processing(self):
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.

def set_processing_mode(self, processing):
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.

def on_progress_updated(self, message):
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.

def on_progress_value_updated(self, value):
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.

def on_file_started(self, filename):
404    def on_file_started(self, filename):
405        """Triggered when a new file starts processing."""
406        pass

Triggered when a new file starts processing.

def on_file_completed(self, filename, success, error_message):
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.

def on_all_completed(self, success_count, failed_files):
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.

def on_worker_finished(self):
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.

def back(self):
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.

def on_enter(self):
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.

def is_ready_to_advance(self):
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.

def is_ready_to_go_back(self):
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.

def reset_page(self):
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.