threads.skull_strip_thread

  1import os
  2import json
  3import shutil
  4import tempfile
  5
  6from PyQt6.QtCore import pyqtSignal, QThread, QProcess, QCoreApplication
  7from logger import get_logger
  8from utils import setup_fsl_env, get_bin_path
  9
 10log = get_logger()
 11
 12
 13class SkullStripThread(QThread):
 14    """
 15    Background worker thread for performing skull-stripping operations on NIfTI files.
 16
 17    This thread executes either **FSL BET** or **HD-BET** commands in the background using
 18    a `QProcess`, preventing the GUI from freezing during long-running neuroimaging operations.
 19
 20    It supports batch processing of multiple files, progress reporting, cancellation, and
 21    automatic generation of BIDS-like JSON sidecar metadata.
 22
 23    ---
 24    **Signals**
 25    - `progress_updated (str)`: Emits a human-readable progress message.
 26    - `progress_value_updated (int)`: Emits a numeric progress percentage.
 27    - `file_started (str)`: Emitted when processing of a specific file begins.
 28    - `file_completed (str, bool, str)`: Emitted when a file finishes (filename, success flag, message).
 29    - `all_completed (int, list)`: Emitted after all files are processed (success count, list of failed files).
 30
 31    ---
 32
 33    **Parameters**
 34    - `files (list[str])`: List of NIfTI file paths to process.
 35    - `workspace_path (str)`: Root directory of the workspace, containing derivatives.
 36    - `parameters (dict)`: Dictionary of BET/HD-BET options (e.g., `f_val`, `opt_m`, `opt_t`).
 37    - `has_cuda (bool)`: Whether CUDA GPU acceleration is available (for HD-BET).
 38    - `has_bet (bool)`: Whether to use FSL BET instead of HD-BET.
 39    """
 40
 41    # Signal emitted when a progress message is updated.
 42    progress_updated = pyqtSignal(str)
 43    """**Signal(str):** Emitted when the progress message text changes."""
 44
 45    # Signal emitted when the progress bar value changes.
 46    progress_value_updated = pyqtSignal(int)
 47    """**Signal(int):** Emitted when the progress value is updated."""
 48
 49    # Signal emitted when processing of an individual file starts.
 50    file_started = pyqtSignal(str)
 51    """**Signal(str):** Emitted when processing of a file begins. The parameter is the file path or name."""
 52
 53    # Signal emitted when processing of an individual file completes.
 54    file_completed = pyqtSignal(str, bool, str)
 55    """**Signal(str, bool, str):** Emitted when a file has finished processing.
 56    The parameters are: file path/name, success status (True if completed successfully), and an optional message or error description.
 57    """
 58
 59    # Signal emitted when all files have completed processing.
 60    all_completed = pyqtSignal(int, list)
 61    """**Signal(int, list):** Emitted when all files are processed.
 62    The parameters are: the total number of processed files and a list of results or statuses.
 63    """
 64
 65    def __init__(self, files, workspace_path, parameters, has_cuda, bet_tool):
 66        super().__init__()
 67        self.files = files
 68        self.workspace_path = workspace_path
 69        self.parameters = parameters
 70        self.has_cuda = has_cuda
 71
 72        self.bet_tool = bet_tool if bet_tool in ["fsl-bet","synthstrip","hd-bet"] else None
 73        self.is_cancelled = False
 74        self.success_count = 0
 75        self.failed_files = []
 76
 77    def cancel(self):
 78        """
 79        Cancels the ongoing operation.
 80
 81        If a subprocess (`QProcess`) is currently running, it is terminated and killed.
 82        This allows for safe interruption of long-running skull-stripping commands.
 83        """
 84        self.is_cancelled = True
 85
 86        if hasattr(self, "process") and self.process is not None:
 87            try:
 88                self.process.terminate()
 89                self.process.kill()
 90            except Exception as e:
 91                log.error(f"Error while cancelling process: {e}")
 92
 93    def run(self):
 94        """
 95        Executes the skull-stripping process for all input files.
 96
 97        For each file:
 98        1. Emits a `file_started` signal.
 99        2. Determines subject ID from the file path.
100        3. Chooses between **BET** or **HD-BET** and builds the command.
101        4. Runs the command asynchronously using `QProcess`.
102        5. Writes the stripped output and corresponding JSON metadata.
103        6. Emits `file_completed` for each processed file.
104
105        At the end, emits `all_completed` with a summary of successful and failed files.
106        """
107
108        if not self.files:
109            log.warning("Nessun file da processare (lista vuota).")
110            self.progress_value_updated.emit(100)
111            self.progress_updated.emit(
112                QCoreApplication.translate("Threads", "No files to process.")
113            )
114            self.all_completed.emit(0, [])
115            return
116
117        base_progress = 10
118        progress_per_file = int(90 / len(self.files))
119
120        for i, nifti_file in enumerate(self.files):
121            if self.is_cancelled:
122                break
123
124            filename = os.path.basename(nifti_file)
125            self.file_started.emit(filename)
126
127            base_progress = int(base_progress + i * progress_per_file)
128            self.progress_value_updated.emit(base_progress)
129            self.progress_updated.emit(
130                QCoreApplication.translate(
131                    "Threads",
132                    "Processing {0} ({1}/{2})"
133                ).format(filename, i + 1, len(self.files))
134            )
135
136            try:
137                nifti_file = os.path.normpath(nifti_file)
138                self.workspace_path = os.path.normpath(self.workspace_path)
139
140                # Extract subject ID from BIDS-like file path
141                path_parts = nifti_file.replace(self.workspace_path, '').strip(os.sep).split(os.sep)
142                subject_id = next((p for p in path_parts if p.startswith("sub-")), None)
143                if not subject_id:
144                    self.file_completed.emit(
145                        filename,
146                        False,
147                        QCoreApplication.translate("Threads", "Cannot extract subject ID")
148                    )
149                    self.failed_files.append(nifti_file)
150                    continue
151
152                # Define output directory for this subject
153                output_dir = os.path.join(
154                    self.workspace_path,
155                    'derivatives',
156                    'skullstrips',
157                    subject_id,
158                    'anat'
159                )
160                os.makedirs(output_dir, exist_ok=True)
161
162                base_name = filename.replace('.nii.gz', '').replace('.nii', '')
163
164                # Create an isolated temporary working directory
165                temp_dir = tempfile.mkdtemp(prefix="skullstrip_")
166
167                # Build the appropriate command (BET or HD-BET)
168                if self.bet_tool == "fsl-bet":
169                    # Configure FSL environment variables
170                    os.environ["FSLDIR"], os.environ["FSLOUTPUTTYPE"] = setup_fsl_env()
171                    f_val = self.parameters.get('f_val', 0.5)
172                    f_str = f"f{str(f_val).replace('.', '')}"
173
174                    temp_output = os.path.join(temp_dir, f"{base_name}_{f_str}_brain.nii.gz")
175                    final_output = os.path.join(output_dir, f"{base_name}_{f_str}_brain.nii.gz")
176
177                    cmd = ["bet", nifti_file, temp_output, "-f", str(f_val)]
178
179                    # Optional BET parameters
180                    for opt in ['opt_m', 'opt_t', 'opt_s', 'opt_o']:
181                        if self.parameters.get(opt, False):
182                            cmd.append(f"-{opt[-1]}")
183
184                    # Optional center coordinates
185                    for coord in ['c_x', 'c_y', 'c_z']:
186                        val = self.parameters.get(coord, 0)
187                        if val != 0:
188                            cmd += ["-c", str(val)]
189
190                    # Exclude brain extraction mask if requested
191                    if not self.parameters.get('opt_brain_extracted', True):
192                        cmd.append("-n")
193
194                    method = "FSL BET"
195
196                else:
197                    temp_output = os.path.join(temp_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
198                    final_output = os.path.join(output_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
199                    if self.bet_tool == "synthstrip":
200
201                        cmd = [get_bin_path("mri_synthstrip"), "-i", nifti_file, "-o", temp_output]
202
203                        method = "SynthStrip"
204
205                    elif self.bet_tool == "hd-bet":
206
207                        cmd = [get_bin_path("hd-bet"), "-i", nifti_file, "-o", temp_output]
208
209                        # Disable CUDA if unavailable
210                        if not self.has_cuda:
211                            cmd += ["-device", "cpu", "--disable_tta"]
212
213                        method = "HD-BET"
214
215                    else:
216                        log.error("Bet tool not recognized.")
217                        return
218
219                # Run skull-stripping command
220                self.process = QProcess()
221                self.process.start(cmd[0], cmd[1:])
222
223                # Wait for process completion, checking for cancellation
224                while not self.process.waitForFinished(-1):
225                    if self.is_cancelled:
226                        self.process.kill()
227                        self.process.waitForFinished()
228                        break
229
230                # Handle cancellation mid-run
231                if self.is_cancelled:
232                    shutil.rmtree(temp_dir, ignore_errors=True)
233                    break
234
235                # Retrieve exit information and logs
236                ret_code = self.process.exitCode()
237                stderr = bytes(self.process.readAllStandardError()).decode()
238                stdout = bytes(self.process.readAllStandardOutput()).decode()
239
240                # Handle process errors
241                if ret_code != 0:
242                    self.file_completed.emit(
243                        filename,
244                        False,
245                        stderr or QCoreApplication.translate("Threads", "Error executing command")
246                    )
247                    self.failed_files.append(nifti_file)
248                    shutil.rmtree(temp_dir, ignore_errors=True)
249                    continue
250
251                # Move temporary output to final destination
252                shutil.move(temp_output, final_output)
253                shutil.rmtree(temp_dir, ignore_errors=True)
254
255                # Create BIDS-like JSON metadata
256                json_file = final_output.replace(".nii.gz", ".json")
257                metadata = {
258                    "SkullStripped": True,
259                    "Description": "Skull-stripped brain image",
260                    "Sources": [filename],
261                    "SkullStrippingMethod": method
262                }
263
264                with open(json_file, 'w') as f:
265                    json.dump(metadata, f, indent=2)
266
267                # Success count and signal
268                self.success_count += 1
269                self.file_completed.emit(filename, True, "")
270
271            except Exception as e:
272                # Catch all exceptions per file
273                self.file_completed.emit(filename, False, str(e))
274                self.failed_files.append(nifti_file)
275                try:
276                    shutil.rmtree(temp_dir, ignore_errors=True)
277                except Exception:
278                    pass
279
280        # Emit final summary signal
281        self.all_completed.emit(self.success_count, self.failed_files)
log = <Logger GliAAns-UI (WARNING)>
class SkullStripThread(PyQt6.QtCore.QThread):
 14class SkullStripThread(QThread):
 15    """
 16    Background worker thread for performing skull-stripping operations on NIfTI files.
 17
 18    This thread executes either **FSL BET** or **HD-BET** commands in the background using
 19    a `QProcess`, preventing the GUI from freezing during long-running neuroimaging operations.
 20
 21    It supports batch processing of multiple files, progress reporting, cancellation, and
 22    automatic generation of BIDS-like JSON sidecar metadata.
 23
 24    ---
 25    **Signals**
 26    - `progress_updated (str)`: Emits a human-readable progress message.
 27    - `progress_value_updated (int)`: Emits a numeric progress percentage.
 28    - `file_started (str)`: Emitted when processing of a specific file begins.
 29    - `file_completed (str, bool, str)`: Emitted when a file finishes (filename, success flag, message).
 30    - `all_completed (int, list)`: Emitted after all files are processed (success count, list of failed files).
 31
 32    ---
 33
 34    **Parameters**
 35    - `files (list[str])`: List of NIfTI file paths to process.
 36    - `workspace_path (str)`: Root directory of the workspace, containing derivatives.
 37    - `parameters (dict)`: Dictionary of BET/HD-BET options (e.g., `f_val`, `opt_m`, `opt_t`).
 38    - `has_cuda (bool)`: Whether CUDA GPU acceleration is available (for HD-BET).
 39    - `has_bet (bool)`: Whether to use FSL BET instead of HD-BET.
 40    """
 41
 42    # Signal emitted when a progress message is updated.
 43    progress_updated = pyqtSignal(str)
 44    """**Signal(str):** Emitted when the progress message text changes."""
 45
 46    # Signal emitted when the progress bar value changes.
 47    progress_value_updated = pyqtSignal(int)
 48    """**Signal(int):** Emitted when the progress value is updated."""
 49
 50    # Signal emitted when processing of an individual file starts.
 51    file_started = pyqtSignal(str)
 52    """**Signal(str):** Emitted when processing of a file begins. The parameter is the file path or name."""
 53
 54    # Signal emitted when processing of an individual file completes.
 55    file_completed = pyqtSignal(str, bool, str)
 56    """**Signal(str, bool, str):** Emitted when a file has finished processing.
 57    The parameters are: file path/name, success status (True if completed successfully), and an optional message or error description.
 58    """
 59
 60    # Signal emitted when all files have completed processing.
 61    all_completed = pyqtSignal(int, list)
 62    """**Signal(int, list):** Emitted when all files are processed.
 63    The parameters are: the total number of processed files and a list of results or statuses.
 64    """
 65
 66    def __init__(self, files, workspace_path, parameters, has_cuda, bet_tool):
 67        super().__init__()
 68        self.files = files
 69        self.workspace_path = workspace_path
 70        self.parameters = parameters
 71        self.has_cuda = has_cuda
 72
 73        self.bet_tool = bet_tool if bet_tool in ["fsl-bet","synthstrip","hd-bet"] else None
 74        self.is_cancelled = False
 75        self.success_count = 0
 76        self.failed_files = []
 77
 78    def cancel(self):
 79        """
 80        Cancels the ongoing operation.
 81
 82        If a subprocess (`QProcess`) is currently running, it is terminated and killed.
 83        This allows for safe interruption of long-running skull-stripping commands.
 84        """
 85        self.is_cancelled = True
 86
 87        if hasattr(self, "process") and self.process is not None:
 88            try:
 89                self.process.terminate()
 90                self.process.kill()
 91            except Exception as e:
 92                log.error(f"Error while cancelling process: {e}")
 93
 94    def run(self):
 95        """
 96        Executes the skull-stripping process for all input files.
 97
 98        For each file:
 99        1. Emits a `file_started` signal.
100        2. Determines subject ID from the file path.
101        3. Chooses between **BET** or **HD-BET** and builds the command.
102        4. Runs the command asynchronously using `QProcess`.
103        5. Writes the stripped output and corresponding JSON metadata.
104        6. Emits `file_completed` for each processed file.
105
106        At the end, emits `all_completed` with a summary of successful and failed files.
107        """
108
109        if not self.files:
110            log.warning("Nessun file da processare (lista vuota).")
111            self.progress_value_updated.emit(100)
112            self.progress_updated.emit(
113                QCoreApplication.translate("Threads", "No files to process.")
114            )
115            self.all_completed.emit(0, [])
116            return
117
118        base_progress = 10
119        progress_per_file = int(90 / len(self.files))
120
121        for i, nifti_file in enumerate(self.files):
122            if self.is_cancelled:
123                break
124
125            filename = os.path.basename(nifti_file)
126            self.file_started.emit(filename)
127
128            base_progress = int(base_progress + i * progress_per_file)
129            self.progress_value_updated.emit(base_progress)
130            self.progress_updated.emit(
131                QCoreApplication.translate(
132                    "Threads",
133                    "Processing {0} ({1}/{2})"
134                ).format(filename, i + 1, len(self.files))
135            )
136
137            try:
138                nifti_file = os.path.normpath(nifti_file)
139                self.workspace_path = os.path.normpath(self.workspace_path)
140
141                # Extract subject ID from BIDS-like file path
142                path_parts = nifti_file.replace(self.workspace_path, '').strip(os.sep).split(os.sep)
143                subject_id = next((p for p in path_parts if p.startswith("sub-")), None)
144                if not subject_id:
145                    self.file_completed.emit(
146                        filename,
147                        False,
148                        QCoreApplication.translate("Threads", "Cannot extract subject ID")
149                    )
150                    self.failed_files.append(nifti_file)
151                    continue
152
153                # Define output directory for this subject
154                output_dir = os.path.join(
155                    self.workspace_path,
156                    'derivatives',
157                    'skullstrips',
158                    subject_id,
159                    'anat'
160                )
161                os.makedirs(output_dir, exist_ok=True)
162
163                base_name = filename.replace('.nii.gz', '').replace('.nii', '')
164
165                # Create an isolated temporary working directory
166                temp_dir = tempfile.mkdtemp(prefix="skullstrip_")
167
168                # Build the appropriate command (BET or HD-BET)
169                if self.bet_tool == "fsl-bet":
170                    # Configure FSL environment variables
171                    os.environ["FSLDIR"], os.environ["FSLOUTPUTTYPE"] = setup_fsl_env()
172                    f_val = self.parameters.get('f_val', 0.5)
173                    f_str = f"f{str(f_val).replace('.', '')}"
174
175                    temp_output = os.path.join(temp_dir, f"{base_name}_{f_str}_brain.nii.gz")
176                    final_output = os.path.join(output_dir, f"{base_name}_{f_str}_brain.nii.gz")
177
178                    cmd = ["bet", nifti_file, temp_output, "-f", str(f_val)]
179
180                    # Optional BET parameters
181                    for opt in ['opt_m', 'opt_t', 'opt_s', 'opt_o']:
182                        if self.parameters.get(opt, False):
183                            cmd.append(f"-{opt[-1]}")
184
185                    # Optional center coordinates
186                    for coord in ['c_x', 'c_y', 'c_z']:
187                        val = self.parameters.get(coord, 0)
188                        if val != 0:
189                            cmd += ["-c", str(val)]
190
191                    # Exclude brain extraction mask if requested
192                    if not self.parameters.get('opt_brain_extracted', True):
193                        cmd.append("-n")
194
195                    method = "FSL BET"
196
197                else:
198                    temp_output = os.path.join(temp_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
199                    final_output = os.path.join(output_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
200                    if self.bet_tool == "synthstrip":
201
202                        cmd = [get_bin_path("mri_synthstrip"), "-i", nifti_file, "-o", temp_output]
203
204                        method = "SynthStrip"
205
206                    elif self.bet_tool == "hd-bet":
207
208                        cmd = [get_bin_path("hd-bet"), "-i", nifti_file, "-o", temp_output]
209
210                        # Disable CUDA if unavailable
211                        if not self.has_cuda:
212                            cmd += ["-device", "cpu", "--disable_tta"]
213
214                        method = "HD-BET"
215
216                    else:
217                        log.error("Bet tool not recognized.")
218                        return
219
220                # Run skull-stripping command
221                self.process = QProcess()
222                self.process.start(cmd[0], cmd[1:])
223
224                # Wait for process completion, checking for cancellation
225                while not self.process.waitForFinished(-1):
226                    if self.is_cancelled:
227                        self.process.kill()
228                        self.process.waitForFinished()
229                        break
230
231                # Handle cancellation mid-run
232                if self.is_cancelled:
233                    shutil.rmtree(temp_dir, ignore_errors=True)
234                    break
235
236                # Retrieve exit information and logs
237                ret_code = self.process.exitCode()
238                stderr = bytes(self.process.readAllStandardError()).decode()
239                stdout = bytes(self.process.readAllStandardOutput()).decode()
240
241                # Handle process errors
242                if ret_code != 0:
243                    self.file_completed.emit(
244                        filename,
245                        False,
246                        stderr or QCoreApplication.translate("Threads", "Error executing command")
247                    )
248                    self.failed_files.append(nifti_file)
249                    shutil.rmtree(temp_dir, ignore_errors=True)
250                    continue
251
252                # Move temporary output to final destination
253                shutil.move(temp_output, final_output)
254                shutil.rmtree(temp_dir, ignore_errors=True)
255
256                # Create BIDS-like JSON metadata
257                json_file = final_output.replace(".nii.gz", ".json")
258                metadata = {
259                    "SkullStripped": True,
260                    "Description": "Skull-stripped brain image",
261                    "Sources": [filename],
262                    "SkullStrippingMethod": method
263                }
264
265                with open(json_file, 'w') as f:
266                    json.dump(metadata, f, indent=2)
267
268                # Success count and signal
269                self.success_count += 1
270                self.file_completed.emit(filename, True, "")
271
272            except Exception as e:
273                # Catch all exceptions per file
274                self.file_completed.emit(filename, False, str(e))
275                self.failed_files.append(nifti_file)
276                try:
277                    shutil.rmtree(temp_dir, ignore_errors=True)
278                except Exception:
279                    pass
280
281        # Emit final summary signal
282        self.all_completed.emit(self.success_count, self.failed_files)

Background worker thread for performing skull-stripping operations on NIfTI files.

This thread executes either FSL BET or HD-BET commands in the background using a QProcess, preventing the GUI from freezing during long-running neuroimaging operations.

It supports batch processing of multiple files, progress reporting, cancellation, and automatic generation of BIDS-like JSON sidecar metadata.


Signals

  • progress_updated (str): Emits a human-readable progress message.
  • progress_value_updated (int): Emits a numeric progress percentage.
  • file_started (str): Emitted when processing of a specific file begins.
  • file_completed (str, bool, str): Emitted when a file finishes (filename, success flag, message).
  • all_completed (int, list): Emitted after all files are processed (success count, list of failed files).

Parameters

  • files (list[str]): List of NIfTI file paths to process.
  • workspace_path (str): Root directory of the workspace, containing derivatives.
  • parameters (dict): Dictionary of BET/HD-BET options (e.g., f_val, opt_m, opt_t).
  • has_cuda (bool): Whether CUDA GPU acceleration is available (for HD-BET).
  • has_bet (bool): Whether to use FSL BET instead of HD-BET.
SkullStripThread(files, workspace_path, parameters, has_cuda, bet_tool)
66    def __init__(self, files, workspace_path, parameters, has_cuda, bet_tool):
67        super().__init__()
68        self.files = files
69        self.workspace_path = workspace_path
70        self.parameters = parameters
71        self.has_cuda = has_cuda
72
73        self.bet_tool = bet_tool if bet_tool in ["fsl-bet","synthstrip","hd-bet"] else None
74        self.is_cancelled = False
75        self.success_count = 0
76        self.failed_files = []
def progress_updated(unknown):

Signal(str): Emitted when the progress message text changes.

def progress_value_updated(unknown):

Signal(int): Emitted when the progress value is updated.

def file_started(unknown):

Signal(str): Emitted when processing of a file begins. The parameter is the file path or name.

def file_completed(unknown):

Signal(str, bool, str): Emitted when a file has finished processing. The parameters are: file path/name, success status (True if completed successfully), and an optional message or error description.

def all_completed(unknown):

Signal(int, list): Emitted when all files are processed. The parameters are: the total number of processed files and a list of results or statuses.

files
workspace_path
parameters
has_cuda
bet_tool
is_cancelled
success_count
failed_files
def cancel(self):
78    def cancel(self):
79        """
80        Cancels the ongoing operation.
81
82        If a subprocess (`QProcess`) is currently running, it is terminated and killed.
83        This allows for safe interruption of long-running skull-stripping commands.
84        """
85        self.is_cancelled = True
86
87        if hasattr(self, "process") and self.process is not None:
88            try:
89                self.process.terminate()
90                self.process.kill()
91            except Exception as e:
92                log.error(f"Error while cancelling process: {e}")

Cancels the ongoing operation.

If a subprocess (QProcess) is currently running, it is terminated and killed. This allows for safe interruption of long-running skull-stripping commands.

def run(self):
 94    def run(self):
 95        """
 96        Executes the skull-stripping process for all input files.
 97
 98        For each file:
 99        1. Emits a `file_started` signal.
100        2. Determines subject ID from the file path.
101        3. Chooses between **BET** or **HD-BET** and builds the command.
102        4. Runs the command asynchronously using `QProcess`.
103        5. Writes the stripped output and corresponding JSON metadata.
104        6. Emits `file_completed` for each processed file.
105
106        At the end, emits `all_completed` with a summary of successful and failed files.
107        """
108
109        if not self.files:
110            log.warning("Nessun file da processare (lista vuota).")
111            self.progress_value_updated.emit(100)
112            self.progress_updated.emit(
113                QCoreApplication.translate("Threads", "No files to process.")
114            )
115            self.all_completed.emit(0, [])
116            return
117
118        base_progress = 10
119        progress_per_file = int(90 / len(self.files))
120
121        for i, nifti_file in enumerate(self.files):
122            if self.is_cancelled:
123                break
124
125            filename = os.path.basename(nifti_file)
126            self.file_started.emit(filename)
127
128            base_progress = int(base_progress + i * progress_per_file)
129            self.progress_value_updated.emit(base_progress)
130            self.progress_updated.emit(
131                QCoreApplication.translate(
132                    "Threads",
133                    "Processing {0} ({1}/{2})"
134                ).format(filename, i + 1, len(self.files))
135            )
136
137            try:
138                nifti_file = os.path.normpath(nifti_file)
139                self.workspace_path = os.path.normpath(self.workspace_path)
140
141                # Extract subject ID from BIDS-like file path
142                path_parts = nifti_file.replace(self.workspace_path, '').strip(os.sep).split(os.sep)
143                subject_id = next((p for p in path_parts if p.startswith("sub-")), None)
144                if not subject_id:
145                    self.file_completed.emit(
146                        filename,
147                        False,
148                        QCoreApplication.translate("Threads", "Cannot extract subject ID")
149                    )
150                    self.failed_files.append(nifti_file)
151                    continue
152
153                # Define output directory for this subject
154                output_dir = os.path.join(
155                    self.workspace_path,
156                    'derivatives',
157                    'skullstrips',
158                    subject_id,
159                    'anat'
160                )
161                os.makedirs(output_dir, exist_ok=True)
162
163                base_name = filename.replace('.nii.gz', '').replace('.nii', '')
164
165                # Create an isolated temporary working directory
166                temp_dir = tempfile.mkdtemp(prefix="skullstrip_")
167
168                # Build the appropriate command (BET or HD-BET)
169                if self.bet_tool == "fsl-bet":
170                    # Configure FSL environment variables
171                    os.environ["FSLDIR"], os.environ["FSLOUTPUTTYPE"] = setup_fsl_env()
172                    f_val = self.parameters.get('f_val', 0.5)
173                    f_str = f"f{str(f_val).replace('.', '')}"
174
175                    temp_output = os.path.join(temp_dir, f"{base_name}_{f_str}_brain.nii.gz")
176                    final_output = os.path.join(output_dir, f"{base_name}_{f_str}_brain.nii.gz")
177
178                    cmd = ["bet", nifti_file, temp_output, "-f", str(f_val)]
179
180                    # Optional BET parameters
181                    for opt in ['opt_m', 'opt_t', 'opt_s', 'opt_o']:
182                        if self.parameters.get(opt, False):
183                            cmd.append(f"-{opt[-1]}")
184
185                    # Optional center coordinates
186                    for coord in ['c_x', 'c_y', 'c_z']:
187                        val = self.parameters.get(coord, 0)
188                        if val != 0:
189                            cmd += ["-c", str(val)]
190
191                    # Exclude brain extraction mask if requested
192                    if not self.parameters.get('opt_brain_extracted', True):
193                        cmd.append("-n")
194
195                    method = "FSL BET"
196
197                else:
198                    temp_output = os.path.join(temp_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
199                    final_output = os.path.join(output_dir, f"{base_name}_{self.bet_tool}_brain.nii.gz")
200                    if self.bet_tool == "synthstrip":
201
202                        cmd = [get_bin_path("mri_synthstrip"), "-i", nifti_file, "-o", temp_output]
203
204                        method = "SynthStrip"
205
206                    elif self.bet_tool == "hd-bet":
207
208                        cmd = [get_bin_path("hd-bet"), "-i", nifti_file, "-o", temp_output]
209
210                        # Disable CUDA if unavailable
211                        if not self.has_cuda:
212                            cmd += ["-device", "cpu", "--disable_tta"]
213
214                        method = "HD-BET"
215
216                    else:
217                        log.error("Bet tool not recognized.")
218                        return
219
220                # Run skull-stripping command
221                self.process = QProcess()
222                self.process.start(cmd[0], cmd[1:])
223
224                # Wait for process completion, checking for cancellation
225                while not self.process.waitForFinished(-1):
226                    if self.is_cancelled:
227                        self.process.kill()
228                        self.process.waitForFinished()
229                        break
230
231                # Handle cancellation mid-run
232                if self.is_cancelled:
233                    shutil.rmtree(temp_dir, ignore_errors=True)
234                    break
235
236                # Retrieve exit information and logs
237                ret_code = self.process.exitCode()
238                stderr = bytes(self.process.readAllStandardError()).decode()
239                stdout = bytes(self.process.readAllStandardOutput()).decode()
240
241                # Handle process errors
242                if ret_code != 0:
243                    self.file_completed.emit(
244                        filename,
245                        False,
246                        stderr or QCoreApplication.translate("Threads", "Error executing command")
247                    )
248                    self.failed_files.append(nifti_file)
249                    shutil.rmtree(temp_dir, ignore_errors=True)
250                    continue
251
252                # Move temporary output to final destination
253                shutil.move(temp_output, final_output)
254                shutil.rmtree(temp_dir, ignore_errors=True)
255
256                # Create BIDS-like JSON metadata
257                json_file = final_output.replace(".nii.gz", ".json")
258                metadata = {
259                    "SkullStripped": True,
260                    "Description": "Skull-stripped brain image",
261                    "Sources": [filename],
262                    "SkullStrippingMethod": method
263                }
264
265                with open(json_file, 'w') as f:
266                    json.dump(metadata, f, indent=2)
267
268                # Success count and signal
269                self.success_count += 1
270                self.file_completed.emit(filename, True, "")
271
272            except Exception as e:
273                # Catch all exceptions per file
274                self.file_completed.emit(filename, False, str(e))
275                self.failed_files.append(nifti_file)
276                try:
277                    shutil.rmtree(temp_dir, ignore_errors=True)
278                except Exception:
279                    pass
280
281        # Emit final summary signal
282        self.all_completed.emit(self.success_count, self.failed_files)

Executes the skull-stripping process for all input files.

For each file:

  1. Emits a file_started signal.
  2. Determines subject ID from the file path.
  3. Chooses between BET or HD-BET and builds the command.
  4. Runs the command asynchronously using QProcess.
  5. Writes the stripped output and corresponding JSON metadata.
  6. Emits file_completed for each processed file.

At the end, emits all_completed with a summary of successful and failed files.