threads.dl_worker

  1import os
  2import shutil
  3import sys
  4import tempfile
  5from pathlib import Path
  6
  7from PyQt6.QtCore import QThread, pyqtSignal, QProcess, QObject, QCoreApplication
  8
  9from logger import get_logger
 10from utils import get_bin_path, resource_path, get_dl_python_executable, get_script_path
 11
 12log = get_logger()
 13
 14
 15class DlWorker(QObject):
 16    progressbar_update = pyqtSignal(int)  # Overall progress (0-100)
 17    file_update = pyqtSignal(str, str)  # (filename, status)
 18    log_update = pyqtSignal(str, str)  # Log messages
 19    finished = pyqtSignal(bool, str)  # (success, message)
 20    cancel_requested = pyqtSignal()
 21
 22    def __init__(self, input_files, workspace_path, has_freesurfer):
 23        super().__init__()
 24        self.input_files = input_files
 25        self.workspace_path = workspace_path
 26        self.has_freesurfer = has_freesurfer
 27
 28        self.output_dir = None
 29
 30        self.total_files = None
 31        self.processed_files = None
 32        self.failed_files = None
 33
 34        self.is_cancelled = False
 35
 36        # Progress tracking
 37        self.total_phases = 6  # Total number of phases per file
 38        self.current_file_index = 0
 39        self.current_phase = 0
 40
 41        self.current_input_file = None
 42        self.current_input_file_basename = None
 43        self.current_synthstrip_file = None
 44        self.atlas_file = get_script_path("deep_learning/atlas/T1.nii.gz")
 45        self.brats_file = get_script_path("deep_learning/atlas/BraTS-GLI-01-001.nii")
 46        self.synthstrip_model = get_script_path("deep_learning/synthstrip.1.pt")
 47
 48        # All processes for all phases
 49        self.synthstrip_process = None
 50        self.coregistration_process = None
 51        self.reorientation_process = None
 52        self.dl_preprocess = None
 53        self.dl_process = None
 54        self.dl_postprocess = None
 55
 56        try:
 57            self.nipreps_synthstrip_bin_path = get_bin_path("nipreps-synthstrip")
 58        except FileNotFoundError:
 59            log.error(str(FileNotFoundError))
 60            raise RuntimeError
 61        log.debug(f"nipreps synthstrip binary path: {self.nipreps_synthstrip_bin_path}")
 62
 63        self.python_executable = get_dl_python_executable()
 64
 65        self.cancel_requested.connect(self.cancel)
 66
 67    def update_progress(self):
 68        """Calculates and updates the progress bar based on files and phases"""
 69        if self.total_files == 0:
 70            return
 71
 72        # Calculate progress: (completed files * 6 + current file phases) / (total files * 6) * 100
 73        total_phases_completed = (self.current_file_index * self.total_phases) + self.current_phase
 74        total_phases_overall = self.total_files * self.total_phases
 75
 76        progress = int((total_phases_completed / total_phases_overall) * 100)
 77        progress = min(progress, 100)  # Ensure it doesn't exceed 100
 78
 79        self.progressbar_update.emit(progress)
 80
 81        # Debug log (optional)
 82        self.log_update.emit(
 83            QCoreApplication.translate("DlWorker", "Progress: {0}% (File {1}/{2}, "
 84            "Phase {3}/{4})").format(progress, self.current_file_index + 1, self.total_files, self.current_phase, self.total_phases),
 85            'd'
 86        )
 87
 88    def start(self):
 89        """Processes all NIfTI files with the Deep Learning pipeline"""
 90
 91        self.total_files = len(self.input_files)
 92        log.debug(self.input_files)
 93        self.processed_files = 0
 94        self.failed_files = []
 95
 96        # Initialize progress bar
 97        self.current_file_index = 0
 98        self.current_phase = 0
 99        self.update_progress()
100        self.progressbar_update.emit(5)
101
102        self.process_single_file()
103
104    def process_single_file(self):
105        """Process single file with the Deep Learning pipeline"""
106        temp_dir = tempfile.mkdtemp(prefix=f"dl_processing_{self.current_file_index+1}_")
107        # out_dir = os.path.join(self.workspace_path, "outputs")
108        # os.makedirs(out_dir, exist_ok=True)
109        self.output_dir = temp_dir
110
111        self.current_input_file = self.input_files[self.current_file_index]
112        self.current_input_file_basename = os.path.basename(self.current_input_file)
113
114        self.log_update.emit(QCoreApplication.translate("DlWorker", "=== PROCESSING: {0} ===").format(self.current_input_file_basename), 'i')
115
116        # PHASE 1: SynthStrip
117        self.current_phase = 1
118        self.run_synthstrip()
119
120    def run_synthstrip(self):
121        """Synthstrip on a single file"""
122        phase = "Synthstrip"
123        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 1/6: Synthstrip skull strip..."))
124        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 1: Skull strip with Synthstrip"), 'i')
125
126        self.log_update.emit(QCoreApplication.translate("DlWorker", "SynthStrip started: {0}").format(os.path.basename(self.current_input_file)), 'i')
127
128        # Skull stripped file name
129        base_name = self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
130        self.current_synthstrip_file = os.path.join(self.output_dir, f"{base_name}_skull_stripped.nii.gz")
131
132        self.synthstrip_process = QProcess()
133        self.synthstrip_process.finished.connect(self.on_synthstrip_finished)
134        self.synthstrip_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
135        self.synthstrip_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.synthstrip_process.readAllStandardOutput()))
136        self.synthstrip_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.synthstrip_process.readAllStandardError()))
137
138        if self.has_freesurfer:
139            cmd = [
140                "mri_synthstrip",
141                "-i", self.current_input_file,
142                "-o", self.current_synthstrip_file
143            ]
144        else:
145            cmd = [
146                self.nipreps_synthstrip_bin_path,
147                "-i", self.current_input_file,
148                "-o", self.current_synthstrip_file,
149                "-g",
150                "--model", self.synthstrip_model
151            ]
152
153        self.synthstrip_process.start(cmd[0], cmd[1:])
154
155    def on_synthstrip_finished(self, exit_code, exit_status):
156        if self.is_cancelled:
157            return
158
159        # This slot is called by the QProcess when it finishes
160        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
161            self.log_update.emit("SynthStrip failed", f"Exit code: {exit_code}")
162            if self.current_file_index + 1 < self.total_files:
163                self.current_file_index += 1
164                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
165                self.process_single_file()
166            return
167
168        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Skull stripping completed"), 'i')
169        self.update_progress()
170        # PHASE 2: Coregistration
171        self.current_phase = 2
172        self.run_coregistration()  # Start the next phase
173
174    def run_coregistration(self):
175        """Performs coregistration with atlas"""
176        phase = "Coregistration"
177
178        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 2/6: Coregistration..."))
179        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 2: Coregistration"), 'i')
180
181        self.log_update.emit(QCoreApplication.translate("DlWorker", "Coregistration started: {0}").format(os.path.basename(self.current_input_file)), 'i')
182
183        # Create directory for coregistration
184        coreg_dir = os.path.join(self.output_dir, "coregistration")
185        os.makedirs(coreg_dir, exist_ok=True)
186
187        self.coregistration_process = QProcess()
188        self.coregistration_process.finished.connect(self.on_coregistration_finished)
189        self.coregistration_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
190        self.coregistration_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.coregistration_process.readAllStandardOutput()))
191        self.coregistration_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.coregistration_process.readAllStandardError()))
192
193        args = [
194            get_script_path("deep_learning/coregistration.py"),
195            "--mri", self.current_input_file,
196            "--skull", self.current_synthstrip_file,
197            "--atlas", self.atlas_file,
198            "-o", coreg_dir
199        ]
200
201        self.coregistration_process.start(self.python_executable, args)
202
203    def on_coregistration_finished(self, exit_code, exit_status):
204        if self.is_cancelled:
205            return
206
207        # This slot is called by the QProcess when it finishes
208        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
209            self.log_update.emit("Coregistration failed", f"Exit code: {exit_code}")
210            if self.current_file_index + 1 < self.total_files:
211                self.current_file_index += 1
212                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
213                self.process_single_file()
214            return
215
216        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Coregistration completed"), 'i')
217        self.update_progress()
218        # PHASE 3: Reorientation
219        self.current_phase = 3
220        self.run_reorientation()  # Start the next phase
221
222    def run_reorientation(self):
223        """Performs reorientation of the brain_in_atlas file using the BraTS affine matrix"""
224        phase = "Reorientation"
225
226        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 3/6: Reorientation..."))
227        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 3: Reorientation"), 'i')
228
229        self.log_update.emit(QCoreApplication.translate("DlWorker", "Reorientation started: {0}").format(os.path.basename(self.current_input_file)), 'i')
230
231        coreg_dir = Path(os.path.join(self.output_dir, "coregistration"))
232        brain_in_atlas_files = list(coreg_dir.glob("*.nii.gz")) + list(coreg_dir.glob("*.nii"))
233
234        if not brain_in_atlas_files:
235            self.log_update.emit(QCoreApplication.translate("DlWorker", "✗ No brain_in_atlas file found"), 'e')
236            return False
237
238        rsl_files = [f for f in brain_in_atlas_files if f.name.endswith("_rsl.nii") or f.name.endswith("_rsl.nii.gz")]
239
240        if rsl_files:
241            brain_in_atlas_file = str(rsl_files[0])
242        else:
243            # fallback
244            brain_in_atlas_file = str(brain_in_atlas_files[0])
245
246        self.reorientation_process = QProcess()
247        self.reorientation_process.finished.connect(self.on_reorientation_finished)
248        self.reorientation_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
249        self.reorientation_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.reorientation_process.readAllStandardOutput()))
250        self.reorientation_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.reorientation_process.readAllStandardError()))
251
252        args = [
253            get_script_path("deep_learning/reorientation.py"),
254            "--input", brain_in_atlas_file,
255            "--output", self.output_dir + "/reoriented",
256            "--brats", self.brats_file,
257            "--basename", self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
258        ]
259
260        self.reorientation_process.start(self.python_executable, args)
261
262    def on_reorientation_finished(self, exit_code, exit_status):
263        if self.is_cancelled:
264            return
265
266        # This slot is called by the QProcess when it finishes
267        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
268            self.log_update.emit("Reorientation failed", f"Exit code: {exit_code}")
269            if self.current_file_index + 1 < self.total_files:
270                self.current_file_index += 1
271                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
272                self.process_single_file()
273            return
274
275        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Reorientation completed"), 'i')
276        self.update_progress()
277        # PHASE 4: Preprocess
278        self.current_phase = 4
279        self.run_preprocess()  # Start the next phase
280
281    def run_preprocess(self):
282        """Runs PHASE 4: PREPARE and PREPROCESS"""
283        phase = "Preprocessing"
284
285        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 4/6: Preparing and preprocessing..."))
286        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 4: Prepare and preprocess"), 'i')
287
288        data_path = os.path.join(self.output_dir, "reoriented")
289        results_path = os.path.join(self.output_dir, "preprocess")
290
291        self.dl_preprocess = QProcess()
292        self.dl_preprocess.finished.connect(self.on_preprocess_finished)
293        self.dl_preprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
294        self.dl_preprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_preprocess.readAllStandardOutput()))
295        self.dl_preprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_preprocess.readAllStandardError()))
296
297        args = [
298            get_script_path("deep_learning/preprocess.py"),
299            '--data', data_path,
300            '--results', results_path,
301            '--ohe'
302        ]
303
304        self.dl_preprocess.start(self.python_executable, args)
305
306    def on_preprocess_finished(self, exit_code, exit_status):
307        # This slot is called by the QProcess when it finishes
308        if self.is_cancelled:
309            return
310
311        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
312            self.log_update.emit("Preprocess failed", f"Exit code: {exit_code}")
313            if self.current_file_index + 1 < self.total_files:
314                self.current_file_index += 1
315                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
316                self.process_single_file()
317            return
318
319        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Preprocess completed"), 'i')
320        self.update_progress()
321        # PHASE 5: Deep learning execution
322        self.current_phase = 5
323        self.run_deep_learning()  # Start the next phase
324
325    def run_deep_learning(self):
326        """Runs PHASE 5: DEEP LEARNING"""
327        phase = "Deep Learning execution"
328
329        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 5/6: Deep Learning..."))
330        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 5: Deep learning execution"), 'i')
331
332        self.dl_process = QProcess()
333        self.dl_process.finished.connect(self.on_dl_finished)
334        self.dl_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
335        self.dl_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_process.readAllStandardOutput()))
336        self.dl_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_process.readAllStandardError()))
337
338        args = [
339            get_script_path("deep_learning/deep_learning_runner.py"),
340            '--depth', '6',
341            '--filters', '64', '96', '128', '192', '256', '384', '512',
342            '--min_fmap', '2',
343            '--gpus', '1',
344            '--amp',
345            '--save_preds',
346            '--exec_mode', 'predict',
347            '--data', f'{self.output_dir}/preprocess/val_3d/test',
348            '--ckpt_path', get_script_path('deep_learning/checkpoints/fold3/epoch=146-dice=88.05.ckpt'),
349            '--tta',
350            '--results', f'{self.output_dir}/dl_results'
351        ]
352
353        self.dl_process.start(self.python_executable, args)
354
355    def on_dl_finished(self, exit_code, exit_status):
356        if self.is_cancelled:
357            return
358
359        # This slot is called by the QProcess when it finishes
360        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
361            self.log_update.emit("Deep learning execution failed", f"Exit code: {exit_code}")
362            if self.current_file_index + 1 < self.total_files:
363                self.current_file_index += 1
364                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
365                self.process_single_file()
366            return
367
368        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Deep learning execution completed"), 'i')
369        self.update_progress()
370        # PHASE 6: Postprocess
371        self.current_phase = 6
372        self.run_postprocess()  # Start the next phase
373
374    def run_postprocess(self):
375        phase = "Postprocessing"
376
377        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 6/6: Postprocessing..."))
378        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 6: Postprocess"), 'i')
379
380        self.dl_postprocess = QProcess()
381        self.dl_postprocess.finished.connect(self.on_postprocess_finished)
382        self.dl_postprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
383        self.dl_postprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_postprocess.readAllStandardOutput()))
384        self.dl_postprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_postprocess.readAllStandardError()))
385
386        args = [
387            get_script_path("deep_learning/postprocess.py"),
388            '-i', f'{self.output_dir}/dl_results/predictions_epoch=146-dice=88_05_task=train_fold=0_tta',
389            '-o', f'{self.output_dir}/dl_postprocess',
390            '--w', f'{self.workspace_path}',
391            '--atlas', self.atlas_file,
392            "--brats", self.brats_file,
393            '--mri', f'{self.current_input_file}'
394        ]
395
396        self.dl_postprocess.start(self.python_executable, args)
397
398    def on_postprocess_finished(self, exit_code, exit_status):
399        if self.is_cancelled:
400            return
401
402        # This slot is called by the QProcess when it finishes
403        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
404            self.log_update.emit("Postprocess failed", f"Exit code: {exit_code}")
405            if self.current_file_index + 1 < self.total_files:
406                self.current_file_index += 1
407                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
408                self.process_single_file()
409            return
410
411        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Postprocess completed"), 'i')
412        self.file_update.emit(self.current_input_file_basename,
413                              QCoreApplication.translate("DlWorker", "Segmentation completed"))
414        self.update_progress()
415
416        if self.current_file_index + 1 < self.total_files:
417            self.current_file_index += 1
418            self.process_single_file()
419        else:
420            self.finished.emit(True, QCoreApplication.translate("DlWorker", "Processing completed"))
421        return
422
423    def cancel(self):
424        self.is_cancelled = True
425        self.log_update.emit(QCoreApplication.translate("DlWorker", "Cancellation requested - stopping all processes..."), 'w')
426
427        processes = {
428            'SynthStrip': self.synthstrip_process,
429            'Coregistration': self.coregistration_process,
430            'Reorientation': self.reorientation_process,
431            'Preprocess': self.dl_preprocess,
432            'Deep Learning': self.dl_process,
433            'Postprocess': self.dl_postprocess
434        }
435
436        for name, process in processes.items():
437            if process is not None and process.state() != QProcess.ProcessState.NotRunning:
438                self.log_update.emit(QCoreApplication.translate("DlWorker", "Stopping {name}...").format(name=name), 'w')
439
440                # No need to disconnect signals manually!
441                process.terminate()
442
443                # only kill if it doesn't respond after a timeout
444                if not process.waitForFinished(2000):
445                    self.log_update.emit(QCoreApplication.translate("DlWorker", "Forcing {name} to quit...").format(name=name), 'e')
446                    process.kill()
447                    process.waitForFinished(1000)
448
449        self.finished.emit(False, QCoreApplication.translate("DlWorker", "Processing cancelled by user"))
450
451    def on_stdout(self, phase, data):
452        """Correct handler for QProcess stdout"""
453        try:
454            # Correctly decode data from QByteArray
455            text = data.data().decode("utf-8", errors='replace').strip()
456            if text:
457                # Split by lines and log each one
458                for line in text.splitlines():
459                    if line.strip():  # Avoid empty lines
460                        self.log_update.emit(f"[{phase}] {line.strip()}", 'i')
461        except Exception as e:
462            self.log_update.emit(f"[{phase}] Error decoding stdout: {str(e)}", 'e')
463
464    def on_stderr(self, phase, data):
465        """Correct handler for QProcess stderr"""
466        try:
467            # Correctly decode data from QByteArray
468            text = data.data().decode("utf-8", errors='replace').strip()
469            if text:
470                # Split by lines and log each one
471                for line in text.splitlines():
472                    if line.strip():  # Avoid empty lines
473                        self.log_update.emit(f"[{phase}] {line.strip()}", 'e')
474        except Exception as e:
475            self.log_update.emit(f"[{phase}] Error decoding stderr: {str(e)}", 'e')
476
477    def on_error(self, phase, error):
478        """Handler for QProcess errors"""
479        try:
480            error_messages = {
481                QProcess.ProcessError.FailedToStart: QCoreApplication.translate("DlWorker", "Failed to start process"),
482                QProcess.ProcessError.Crashed: QCoreApplication.translate("DlWorker", "Process crashed"),
483                QProcess.ProcessError.Timedout: QCoreApplication.translate("DlWorker", "Process timed out"),
484                QProcess.ProcessError.WriteError: QCoreApplication.translate("DlWorker", "Write error"),
485                QProcess.ProcessError.ReadError: QCoreApplication.translate("DlWorker", "Read error"),
486                QProcess.ProcessError.UnknownError: QCoreApplication.translate("DlWorker", "Unknown error")
487            }
488
489            error_msg = error_messages.get(error, QCoreApplication.translate("DlWorker", "Unknown error code: {error}").format(error=error))
490            self.log_update.emit(f"[{phase}] Process error: {error_msg}", 'e')
491
492        except Exception as e:
493            self.log_update.emit(f"[{phase}] Error in error handler: {str(e)}", 'e')
log = <Logger GliAAns-UI (WARNING)>
class DlWorker(PyQt6.QtCore.QObject):
 16class DlWorker(QObject):
 17    progressbar_update = pyqtSignal(int)  # Overall progress (0-100)
 18    file_update = pyqtSignal(str, str)  # (filename, status)
 19    log_update = pyqtSignal(str, str)  # Log messages
 20    finished = pyqtSignal(bool, str)  # (success, message)
 21    cancel_requested = pyqtSignal()
 22
 23    def __init__(self, input_files, workspace_path, has_freesurfer):
 24        super().__init__()
 25        self.input_files = input_files
 26        self.workspace_path = workspace_path
 27        self.has_freesurfer = has_freesurfer
 28
 29        self.output_dir = None
 30
 31        self.total_files = None
 32        self.processed_files = None
 33        self.failed_files = None
 34
 35        self.is_cancelled = False
 36
 37        # Progress tracking
 38        self.total_phases = 6  # Total number of phases per file
 39        self.current_file_index = 0
 40        self.current_phase = 0
 41
 42        self.current_input_file = None
 43        self.current_input_file_basename = None
 44        self.current_synthstrip_file = None
 45        self.atlas_file = get_script_path("deep_learning/atlas/T1.nii.gz")
 46        self.brats_file = get_script_path("deep_learning/atlas/BraTS-GLI-01-001.nii")
 47        self.synthstrip_model = get_script_path("deep_learning/synthstrip.1.pt")
 48
 49        # All processes for all phases
 50        self.synthstrip_process = None
 51        self.coregistration_process = None
 52        self.reorientation_process = None
 53        self.dl_preprocess = None
 54        self.dl_process = None
 55        self.dl_postprocess = None
 56
 57        try:
 58            self.nipreps_synthstrip_bin_path = get_bin_path("nipreps-synthstrip")
 59        except FileNotFoundError:
 60            log.error(str(FileNotFoundError))
 61            raise RuntimeError
 62        log.debug(f"nipreps synthstrip binary path: {self.nipreps_synthstrip_bin_path}")
 63
 64        self.python_executable = get_dl_python_executable()
 65
 66        self.cancel_requested.connect(self.cancel)
 67
 68    def update_progress(self):
 69        """Calculates and updates the progress bar based on files and phases"""
 70        if self.total_files == 0:
 71            return
 72
 73        # Calculate progress: (completed files * 6 + current file phases) / (total files * 6) * 100
 74        total_phases_completed = (self.current_file_index * self.total_phases) + self.current_phase
 75        total_phases_overall = self.total_files * self.total_phases
 76
 77        progress = int((total_phases_completed / total_phases_overall) * 100)
 78        progress = min(progress, 100)  # Ensure it doesn't exceed 100
 79
 80        self.progressbar_update.emit(progress)
 81
 82        # Debug log (optional)
 83        self.log_update.emit(
 84            QCoreApplication.translate("DlWorker", "Progress: {0}% (File {1}/{2}, "
 85            "Phase {3}/{4})").format(progress, self.current_file_index + 1, self.total_files, self.current_phase, self.total_phases),
 86            'd'
 87        )
 88
 89    def start(self):
 90        """Processes all NIfTI files with the Deep Learning pipeline"""
 91
 92        self.total_files = len(self.input_files)
 93        log.debug(self.input_files)
 94        self.processed_files = 0
 95        self.failed_files = []
 96
 97        # Initialize progress bar
 98        self.current_file_index = 0
 99        self.current_phase = 0
100        self.update_progress()
101        self.progressbar_update.emit(5)
102
103        self.process_single_file()
104
105    def process_single_file(self):
106        """Process single file with the Deep Learning pipeline"""
107        temp_dir = tempfile.mkdtemp(prefix=f"dl_processing_{self.current_file_index+1}_")
108        # out_dir = os.path.join(self.workspace_path, "outputs")
109        # os.makedirs(out_dir, exist_ok=True)
110        self.output_dir = temp_dir
111
112        self.current_input_file = self.input_files[self.current_file_index]
113        self.current_input_file_basename = os.path.basename(self.current_input_file)
114
115        self.log_update.emit(QCoreApplication.translate("DlWorker", "=== PROCESSING: {0} ===").format(self.current_input_file_basename), 'i')
116
117        # PHASE 1: SynthStrip
118        self.current_phase = 1
119        self.run_synthstrip()
120
121    def run_synthstrip(self):
122        """Synthstrip on a single file"""
123        phase = "Synthstrip"
124        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 1/6: Synthstrip skull strip..."))
125        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 1: Skull strip with Synthstrip"), 'i')
126
127        self.log_update.emit(QCoreApplication.translate("DlWorker", "SynthStrip started: {0}").format(os.path.basename(self.current_input_file)), 'i')
128
129        # Skull stripped file name
130        base_name = self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
131        self.current_synthstrip_file = os.path.join(self.output_dir, f"{base_name}_skull_stripped.nii.gz")
132
133        self.synthstrip_process = QProcess()
134        self.synthstrip_process.finished.connect(self.on_synthstrip_finished)
135        self.synthstrip_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
136        self.synthstrip_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.synthstrip_process.readAllStandardOutput()))
137        self.synthstrip_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.synthstrip_process.readAllStandardError()))
138
139        if self.has_freesurfer:
140            cmd = [
141                "mri_synthstrip",
142                "-i", self.current_input_file,
143                "-o", self.current_synthstrip_file
144            ]
145        else:
146            cmd = [
147                self.nipreps_synthstrip_bin_path,
148                "-i", self.current_input_file,
149                "-o", self.current_synthstrip_file,
150                "-g",
151                "--model", self.synthstrip_model
152            ]
153
154        self.synthstrip_process.start(cmd[0], cmd[1:])
155
156    def on_synthstrip_finished(self, exit_code, exit_status):
157        if self.is_cancelled:
158            return
159
160        # This slot is called by the QProcess when it finishes
161        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
162            self.log_update.emit("SynthStrip failed", f"Exit code: {exit_code}")
163            if self.current_file_index + 1 < self.total_files:
164                self.current_file_index += 1
165                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
166                self.process_single_file()
167            return
168
169        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Skull stripping completed"), 'i')
170        self.update_progress()
171        # PHASE 2: Coregistration
172        self.current_phase = 2
173        self.run_coregistration()  # Start the next phase
174
175    def run_coregistration(self):
176        """Performs coregistration with atlas"""
177        phase = "Coregistration"
178
179        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 2/6: Coregistration..."))
180        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 2: Coregistration"), 'i')
181
182        self.log_update.emit(QCoreApplication.translate("DlWorker", "Coregistration started: {0}").format(os.path.basename(self.current_input_file)), 'i')
183
184        # Create directory for coregistration
185        coreg_dir = os.path.join(self.output_dir, "coregistration")
186        os.makedirs(coreg_dir, exist_ok=True)
187
188        self.coregistration_process = QProcess()
189        self.coregistration_process.finished.connect(self.on_coregistration_finished)
190        self.coregistration_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
191        self.coregistration_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.coregistration_process.readAllStandardOutput()))
192        self.coregistration_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.coregistration_process.readAllStandardError()))
193
194        args = [
195            get_script_path("deep_learning/coregistration.py"),
196            "--mri", self.current_input_file,
197            "--skull", self.current_synthstrip_file,
198            "--atlas", self.atlas_file,
199            "-o", coreg_dir
200        ]
201
202        self.coregistration_process.start(self.python_executable, args)
203
204    def on_coregistration_finished(self, exit_code, exit_status):
205        if self.is_cancelled:
206            return
207
208        # This slot is called by the QProcess when it finishes
209        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
210            self.log_update.emit("Coregistration failed", f"Exit code: {exit_code}")
211            if self.current_file_index + 1 < self.total_files:
212                self.current_file_index += 1
213                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
214                self.process_single_file()
215            return
216
217        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Coregistration completed"), 'i')
218        self.update_progress()
219        # PHASE 3: Reorientation
220        self.current_phase = 3
221        self.run_reorientation()  # Start the next phase
222
223    def run_reorientation(self):
224        """Performs reorientation of the brain_in_atlas file using the BraTS affine matrix"""
225        phase = "Reorientation"
226
227        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 3/6: Reorientation..."))
228        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 3: Reorientation"), 'i')
229
230        self.log_update.emit(QCoreApplication.translate("DlWorker", "Reorientation started: {0}").format(os.path.basename(self.current_input_file)), 'i')
231
232        coreg_dir = Path(os.path.join(self.output_dir, "coregistration"))
233        brain_in_atlas_files = list(coreg_dir.glob("*.nii.gz")) + list(coreg_dir.glob("*.nii"))
234
235        if not brain_in_atlas_files:
236            self.log_update.emit(QCoreApplication.translate("DlWorker", "✗ No brain_in_atlas file found"), 'e')
237            return False
238
239        rsl_files = [f for f in brain_in_atlas_files if f.name.endswith("_rsl.nii") or f.name.endswith("_rsl.nii.gz")]
240
241        if rsl_files:
242            brain_in_atlas_file = str(rsl_files[0])
243        else:
244            # fallback
245            brain_in_atlas_file = str(brain_in_atlas_files[0])
246
247        self.reorientation_process = QProcess()
248        self.reorientation_process.finished.connect(self.on_reorientation_finished)
249        self.reorientation_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
250        self.reorientation_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.reorientation_process.readAllStandardOutput()))
251        self.reorientation_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.reorientation_process.readAllStandardError()))
252
253        args = [
254            get_script_path("deep_learning/reorientation.py"),
255            "--input", brain_in_atlas_file,
256            "--output", self.output_dir + "/reoriented",
257            "--brats", self.brats_file,
258            "--basename", self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
259        ]
260
261        self.reorientation_process.start(self.python_executable, args)
262
263    def on_reorientation_finished(self, exit_code, exit_status):
264        if self.is_cancelled:
265            return
266
267        # This slot is called by the QProcess when it finishes
268        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
269            self.log_update.emit("Reorientation failed", f"Exit code: {exit_code}")
270            if self.current_file_index + 1 < self.total_files:
271                self.current_file_index += 1
272                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
273                self.process_single_file()
274            return
275
276        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Reorientation completed"), 'i')
277        self.update_progress()
278        # PHASE 4: Preprocess
279        self.current_phase = 4
280        self.run_preprocess()  # Start the next phase
281
282    def run_preprocess(self):
283        """Runs PHASE 4: PREPARE and PREPROCESS"""
284        phase = "Preprocessing"
285
286        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 4/6: Preparing and preprocessing..."))
287        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 4: Prepare and preprocess"), 'i')
288
289        data_path = os.path.join(self.output_dir, "reoriented")
290        results_path = os.path.join(self.output_dir, "preprocess")
291
292        self.dl_preprocess = QProcess()
293        self.dl_preprocess.finished.connect(self.on_preprocess_finished)
294        self.dl_preprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
295        self.dl_preprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_preprocess.readAllStandardOutput()))
296        self.dl_preprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_preprocess.readAllStandardError()))
297
298        args = [
299            get_script_path("deep_learning/preprocess.py"),
300            '--data', data_path,
301            '--results', results_path,
302            '--ohe'
303        ]
304
305        self.dl_preprocess.start(self.python_executable, args)
306
307    def on_preprocess_finished(self, exit_code, exit_status):
308        # This slot is called by the QProcess when it finishes
309        if self.is_cancelled:
310            return
311
312        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
313            self.log_update.emit("Preprocess failed", f"Exit code: {exit_code}")
314            if self.current_file_index + 1 < self.total_files:
315                self.current_file_index += 1
316                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
317                self.process_single_file()
318            return
319
320        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Preprocess completed"), 'i')
321        self.update_progress()
322        # PHASE 5: Deep learning execution
323        self.current_phase = 5
324        self.run_deep_learning()  # Start the next phase
325
326    def run_deep_learning(self):
327        """Runs PHASE 5: DEEP LEARNING"""
328        phase = "Deep Learning execution"
329
330        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 5/6: Deep Learning..."))
331        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 5: Deep learning execution"), 'i')
332
333        self.dl_process = QProcess()
334        self.dl_process.finished.connect(self.on_dl_finished)
335        self.dl_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
336        self.dl_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_process.readAllStandardOutput()))
337        self.dl_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_process.readAllStandardError()))
338
339        args = [
340            get_script_path("deep_learning/deep_learning_runner.py"),
341            '--depth', '6',
342            '--filters', '64', '96', '128', '192', '256', '384', '512',
343            '--min_fmap', '2',
344            '--gpus', '1',
345            '--amp',
346            '--save_preds',
347            '--exec_mode', 'predict',
348            '--data', f'{self.output_dir}/preprocess/val_3d/test',
349            '--ckpt_path', get_script_path('deep_learning/checkpoints/fold3/epoch=146-dice=88.05.ckpt'),
350            '--tta',
351            '--results', f'{self.output_dir}/dl_results'
352        ]
353
354        self.dl_process.start(self.python_executable, args)
355
356    def on_dl_finished(self, exit_code, exit_status):
357        if self.is_cancelled:
358            return
359
360        # This slot is called by the QProcess when it finishes
361        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
362            self.log_update.emit("Deep learning execution failed", f"Exit code: {exit_code}")
363            if self.current_file_index + 1 < self.total_files:
364                self.current_file_index += 1
365                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
366                self.process_single_file()
367            return
368
369        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Deep learning execution completed"), 'i')
370        self.update_progress()
371        # PHASE 6: Postprocess
372        self.current_phase = 6
373        self.run_postprocess()  # Start the next phase
374
375    def run_postprocess(self):
376        phase = "Postprocessing"
377
378        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 6/6: Postprocessing..."))
379        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 6: Postprocess"), 'i')
380
381        self.dl_postprocess = QProcess()
382        self.dl_postprocess.finished.connect(self.on_postprocess_finished)
383        self.dl_postprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
384        self.dl_postprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_postprocess.readAllStandardOutput()))
385        self.dl_postprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_postprocess.readAllStandardError()))
386
387        args = [
388            get_script_path("deep_learning/postprocess.py"),
389            '-i', f'{self.output_dir}/dl_results/predictions_epoch=146-dice=88_05_task=train_fold=0_tta',
390            '-o', f'{self.output_dir}/dl_postprocess',
391            '--w', f'{self.workspace_path}',
392            '--atlas', self.atlas_file,
393            "--brats", self.brats_file,
394            '--mri', f'{self.current_input_file}'
395        ]
396
397        self.dl_postprocess.start(self.python_executable, args)
398
399    def on_postprocess_finished(self, exit_code, exit_status):
400        if self.is_cancelled:
401            return
402
403        # This slot is called by the QProcess when it finishes
404        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
405            self.log_update.emit("Postprocess failed", f"Exit code: {exit_code}")
406            if self.current_file_index + 1 < self.total_files:
407                self.current_file_index += 1
408                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
409                self.process_single_file()
410            return
411
412        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Postprocess completed"), 'i')
413        self.file_update.emit(self.current_input_file_basename,
414                              QCoreApplication.translate("DlWorker", "Segmentation completed"))
415        self.update_progress()
416
417        if self.current_file_index + 1 < self.total_files:
418            self.current_file_index += 1
419            self.process_single_file()
420        else:
421            self.finished.emit(True, QCoreApplication.translate("DlWorker", "Processing completed"))
422        return
423
424    def cancel(self):
425        self.is_cancelled = True
426        self.log_update.emit(QCoreApplication.translate("DlWorker", "Cancellation requested - stopping all processes..."), 'w')
427
428        processes = {
429            'SynthStrip': self.synthstrip_process,
430            'Coregistration': self.coregistration_process,
431            'Reorientation': self.reorientation_process,
432            'Preprocess': self.dl_preprocess,
433            'Deep Learning': self.dl_process,
434            'Postprocess': self.dl_postprocess
435        }
436
437        for name, process in processes.items():
438            if process is not None and process.state() != QProcess.ProcessState.NotRunning:
439                self.log_update.emit(QCoreApplication.translate("DlWorker", "Stopping {name}...").format(name=name), 'w')
440
441                # No need to disconnect signals manually!
442                process.terminate()
443
444                # only kill if it doesn't respond after a timeout
445                if not process.waitForFinished(2000):
446                    self.log_update.emit(QCoreApplication.translate("DlWorker", "Forcing {name} to quit...").format(name=name), 'e')
447                    process.kill()
448                    process.waitForFinished(1000)
449
450        self.finished.emit(False, QCoreApplication.translate("DlWorker", "Processing cancelled by user"))
451
452    def on_stdout(self, phase, data):
453        """Correct handler for QProcess stdout"""
454        try:
455            # Correctly decode data from QByteArray
456            text = data.data().decode("utf-8", errors='replace').strip()
457            if text:
458                # Split by lines and log each one
459                for line in text.splitlines():
460                    if line.strip():  # Avoid empty lines
461                        self.log_update.emit(f"[{phase}] {line.strip()}", 'i')
462        except Exception as e:
463            self.log_update.emit(f"[{phase}] Error decoding stdout: {str(e)}", 'e')
464
465    def on_stderr(self, phase, data):
466        """Correct handler for QProcess stderr"""
467        try:
468            # Correctly decode data from QByteArray
469            text = data.data().decode("utf-8", errors='replace').strip()
470            if text:
471                # Split by lines and log each one
472                for line in text.splitlines():
473                    if line.strip():  # Avoid empty lines
474                        self.log_update.emit(f"[{phase}] {line.strip()}", 'e')
475        except Exception as e:
476            self.log_update.emit(f"[{phase}] Error decoding stderr: {str(e)}", 'e')
477
478    def on_error(self, phase, error):
479        """Handler for QProcess errors"""
480        try:
481            error_messages = {
482                QProcess.ProcessError.FailedToStart: QCoreApplication.translate("DlWorker", "Failed to start process"),
483                QProcess.ProcessError.Crashed: QCoreApplication.translate("DlWorker", "Process crashed"),
484                QProcess.ProcessError.Timedout: QCoreApplication.translate("DlWorker", "Process timed out"),
485                QProcess.ProcessError.WriteError: QCoreApplication.translate("DlWorker", "Write error"),
486                QProcess.ProcessError.ReadError: QCoreApplication.translate("DlWorker", "Read error"),
487                QProcess.ProcessError.UnknownError: QCoreApplication.translate("DlWorker", "Unknown error")
488            }
489
490            error_msg = error_messages.get(error, QCoreApplication.translate("DlWorker", "Unknown error code: {error}").format(error=error))
491            self.log_update.emit(f"[{phase}] Process error: {error_msg}", 'e')
492
493        except Exception as e:
494            self.log_update.emit(f"[{phase}] Error in error handler: {str(e)}", 'e')

QObject(parent: QObject|None = None)

DlWorker(input_files, workspace_path, has_freesurfer)
23    def __init__(self, input_files, workspace_path, has_freesurfer):
24        super().__init__()
25        self.input_files = input_files
26        self.workspace_path = workspace_path
27        self.has_freesurfer = has_freesurfer
28
29        self.output_dir = None
30
31        self.total_files = None
32        self.processed_files = None
33        self.failed_files = None
34
35        self.is_cancelled = False
36
37        # Progress tracking
38        self.total_phases = 6  # Total number of phases per file
39        self.current_file_index = 0
40        self.current_phase = 0
41
42        self.current_input_file = None
43        self.current_input_file_basename = None
44        self.current_synthstrip_file = None
45        self.atlas_file = get_script_path("deep_learning/atlas/T1.nii.gz")
46        self.brats_file = get_script_path("deep_learning/atlas/BraTS-GLI-01-001.nii")
47        self.synthstrip_model = get_script_path("deep_learning/synthstrip.1.pt")
48
49        # All processes for all phases
50        self.synthstrip_process = None
51        self.coregistration_process = None
52        self.reorientation_process = None
53        self.dl_preprocess = None
54        self.dl_process = None
55        self.dl_postprocess = None
56
57        try:
58            self.nipreps_synthstrip_bin_path = get_bin_path("nipreps-synthstrip")
59        except FileNotFoundError:
60            log.error(str(FileNotFoundError))
61            raise RuntimeError
62        log.debug(f"nipreps synthstrip binary path: {self.nipreps_synthstrip_bin_path}")
63
64        self.python_executable = get_dl_python_executable()
65
66        self.cancel_requested.connect(self.cancel)
def progressbar_update(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.

def file_update(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.

def log_update(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.

def finished(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.

def cancel_requested(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.

input_files
workspace_path
has_freesurfer
output_dir
total_files
processed_files
failed_files
is_cancelled
total_phases
current_file_index
current_phase
current_input_file
current_input_file_basename
current_synthstrip_file
atlas_file
brats_file
synthstrip_model
synthstrip_process
coregistration_process
reorientation_process
dl_preprocess
dl_process
dl_postprocess
python_executable
def update_progress(self):
68    def update_progress(self):
69        """Calculates and updates the progress bar based on files and phases"""
70        if self.total_files == 0:
71            return
72
73        # Calculate progress: (completed files * 6 + current file phases) / (total files * 6) * 100
74        total_phases_completed = (self.current_file_index * self.total_phases) + self.current_phase
75        total_phases_overall = self.total_files * self.total_phases
76
77        progress = int((total_phases_completed / total_phases_overall) * 100)
78        progress = min(progress, 100)  # Ensure it doesn't exceed 100
79
80        self.progressbar_update.emit(progress)
81
82        # Debug log (optional)
83        self.log_update.emit(
84            QCoreApplication.translate("DlWorker", "Progress: {0}% (File {1}/{2}, "
85            "Phase {3}/{4})").format(progress, self.current_file_index + 1, self.total_files, self.current_phase, self.total_phases),
86            'd'
87        )

Calculates and updates the progress bar based on files and phases

def start(self):
 89    def start(self):
 90        """Processes all NIfTI files with the Deep Learning pipeline"""
 91
 92        self.total_files = len(self.input_files)
 93        log.debug(self.input_files)
 94        self.processed_files = 0
 95        self.failed_files = []
 96
 97        # Initialize progress bar
 98        self.current_file_index = 0
 99        self.current_phase = 0
100        self.update_progress()
101        self.progressbar_update.emit(5)
102
103        self.process_single_file()

Processes all NIfTI files with the Deep Learning pipeline

def process_single_file(self):
105    def process_single_file(self):
106        """Process single file with the Deep Learning pipeline"""
107        temp_dir = tempfile.mkdtemp(prefix=f"dl_processing_{self.current_file_index+1}_")
108        # out_dir = os.path.join(self.workspace_path, "outputs")
109        # os.makedirs(out_dir, exist_ok=True)
110        self.output_dir = temp_dir
111
112        self.current_input_file = self.input_files[self.current_file_index]
113        self.current_input_file_basename = os.path.basename(self.current_input_file)
114
115        self.log_update.emit(QCoreApplication.translate("DlWorker", "=== PROCESSING: {0} ===").format(self.current_input_file_basename), 'i')
116
117        # PHASE 1: SynthStrip
118        self.current_phase = 1
119        self.run_synthstrip()

Process single file with the Deep Learning pipeline

def run_synthstrip(self):
121    def run_synthstrip(self):
122        """Synthstrip on a single file"""
123        phase = "Synthstrip"
124        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 1/6: Synthstrip skull strip..."))
125        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 1: Skull strip with Synthstrip"), 'i')
126
127        self.log_update.emit(QCoreApplication.translate("DlWorker", "SynthStrip started: {0}").format(os.path.basename(self.current_input_file)), 'i')
128
129        # Skull stripped file name
130        base_name = self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
131        self.current_synthstrip_file = os.path.join(self.output_dir, f"{base_name}_skull_stripped.nii.gz")
132
133        self.synthstrip_process = QProcess()
134        self.synthstrip_process.finished.connect(self.on_synthstrip_finished)
135        self.synthstrip_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
136        self.synthstrip_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.synthstrip_process.readAllStandardOutput()))
137        self.synthstrip_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.synthstrip_process.readAllStandardError()))
138
139        if self.has_freesurfer:
140            cmd = [
141                "mri_synthstrip",
142                "-i", self.current_input_file,
143                "-o", self.current_synthstrip_file
144            ]
145        else:
146            cmd = [
147                self.nipreps_synthstrip_bin_path,
148                "-i", self.current_input_file,
149                "-o", self.current_synthstrip_file,
150                "-g",
151                "--model", self.synthstrip_model
152            ]
153
154        self.synthstrip_process.start(cmd[0], cmd[1:])

Synthstrip on a single file

def on_synthstrip_finished(self, exit_code, exit_status):
156    def on_synthstrip_finished(self, exit_code, exit_status):
157        if self.is_cancelled:
158            return
159
160        # This slot is called by the QProcess when it finishes
161        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
162            self.log_update.emit("SynthStrip failed", f"Exit code: {exit_code}")
163            if self.current_file_index + 1 < self.total_files:
164                self.current_file_index += 1
165                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
166                self.process_single_file()
167            return
168
169        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Skull stripping completed"), 'i')
170        self.update_progress()
171        # PHASE 2: Coregistration
172        self.current_phase = 2
173        self.run_coregistration()  # Start the next phase
def run_coregistration(self):
175    def run_coregistration(self):
176        """Performs coregistration with atlas"""
177        phase = "Coregistration"
178
179        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 2/6: Coregistration..."))
180        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 2: Coregistration"), 'i')
181
182        self.log_update.emit(QCoreApplication.translate("DlWorker", "Coregistration started: {0}").format(os.path.basename(self.current_input_file)), 'i')
183
184        # Create directory for coregistration
185        coreg_dir = os.path.join(self.output_dir, "coregistration")
186        os.makedirs(coreg_dir, exist_ok=True)
187
188        self.coregistration_process = QProcess()
189        self.coregistration_process.finished.connect(self.on_coregistration_finished)
190        self.coregistration_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
191        self.coregistration_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.coregistration_process.readAllStandardOutput()))
192        self.coregistration_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.coregistration_process.readAllStandardError()))
193
194        args = [
195            get_script_path("deep_learning/coregistration.py"),
196            "--mri", self.current_input_file,
197            "--skull", self.current_synthstrip_file,
198            "--atlas", self.atlas_file,
199            "-o", coreg_dir
200        ]
201
202        self.coregistration_process.start(self.python_executable, args)

Performs coregistration with atlas

def on_coregistration_finished(self, exit_code, exit_status):
204    def on_coregistration_finished(self, exit_code, exit_status):
205        if self.is_cancelled:
206            return
207
208        # This slot is called by the QProcess when it finishes
209        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
210            self.log_update.emit("Coregistration failed", f"Exit code: {exit_code}")
211            if self.current_file_index + 1 < self.total_files:
212                self.current_file_index += 1
213                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
214                self.process_single_file()
215            return
216
217        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Coregistration completed"), 'i')
218        self.update_progress()
219        # PHASE 3: Reorientation
220        self.current_phase = 3
221        self.run_reorientation()  # Start the next phase
def run_reorientation(self):
223    def run_reorientation(self):
224        """Performs reorientation of the brain_in_atlas file using the BraTS affine matrix"""
225        phase = "Reorientation"
226
227        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 3/6: Reorientation..."))
228        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 3: Reorientation"), 'i')
229
230        self.log_update.emit(QCoreApplication.translate("DlWorker", "Reorientation started: {0}").format(os.path.basename(self.current_input_file)), 'i')
231
232        coreg_dir = Path(os.path.join(self.output_dir, "coregistration"))
233        brain_in_atlas_files = list(coreg_dir.glob("*.nii.gz")) + list(coreg_dir.glob("*.nii"))
234
235        if not brain_in_atlas_files:
236            self.log_update.emit(QCoreApplication.translate("DlWorker", "✗ No brain_in_atlas file found"), 'e')
237            return False
238
239        rsl_files = [f for f in brain_in_atlas_files if f.name.endswith("_rsl.nii") or f.name.endswith("_rsl.nii.gz")]
240
241        if rsl_files:
242            brain_in_atlas_file = str(rsl_files[0])
243        else:
244            # fallback
245            brain_in_atlas_file = str(brain_in_atlas_files[0])
246
247        self.reorientation_process = QProcess()
248        self.reorientation_process.finished.connect(self.on_reorientation_finished)
249        self.reorientation_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
250        self.reorientation_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.reorientation_process.readAllStandardOutput()))
251        self.reorientation_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.reorientation_process.readAllStandardError()))
252
253        args = [
254            get_script_path("deep_learning/reorientation.py"),
255            "--input", brain_in_atlas_file,
256            "--output", self.output_dir + "/reoriented",
257            "--brats", self.brats_file,
258            "--basename", self.current_input_file_basename.replace(".nii.gz", "").replace(".nii", "")
259        ]
260
261        self.reorientation_process.start(self.python_executable, args)

Performs reorientation of the brain_in_atlas file using the BraTS affine matrix

def on_reorientation_finished(self, exit_code, exit_status):
263    def on_reorientation_finished(self, exit_code, exit_status):
264        if self.is_cancelled:
265            return
266
267        # This slot is called by the QProcess when it finishes
268        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
269            self.log_update.emit("Reorientation failed", f"Exit code: {exit_code}")
270            if self.current_file_index + 1 < self.total_files:
271                self.current_file_index += 1
272                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
273                self.process_single_file()
274            return
275
276        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Reorientation completed"), 'i')
277        self.update_progress()
278        # PHASE 4: Preprocess
279        self.current_phase = 4
280        self.run_preprocess()  # Start the next phase
def run_preprocess(self):
282    def run_preprocess(self):
283        """Runs PHASE 4: PREPARE and PREPROCESS"""
284        phase = "Preprocessing"
285
286        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 4/6: Preparing and preprocessing..."))
287        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 4: Prepare and preprocess"), 'i')
288
289        data_path = os.path.join(self.output_dir, "reoriented")
290        results_path = os.path.join(self.output_dir, "preprocess")
291
292        self.dl_preprocess = QProcess()
293        self.dl_preprocess.finished.connect(self.on_preprocess_finished)
294        self.dl_preprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
295        self.dl_preprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_preprocess.readAllStandardOutput()))
296        self.dl_preprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_preprocess.readAllStandardError()))
297
298        args = [
299            get_script_path("deep_learning/preprocess.py"),
300            '--data', data_path,
301            '--results', results_path,
302            '--ohe'
303        ]
304
305        self.dl_preprocess.start(self.python_executable, args)

Runs PHASE 4: PREPARE and PREPROCESS

def on_preprocess_finished(self, exit_code, exit_status):
307    def on_preprocess_finished(self, exit_code, exit_status):
308        # This slot is called by the QProcess when it finishes
309        if self.is_cancelled:
310            return
311
312        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
313            self.log_update.emit("Preprocess failed", f"Exit code: {exit_code}")
314            if self.current_file_index + 1 < self.total_files:
315                self.current_file_index += 1
316                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
317                self.process_single_file()
318            return
319
320        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Preprocess completed"), 'i')
321        self.update_progress()
322        # PHASE 5: Deep learning execution
323        self.current_phase = 5
324        self.run_deep_learning()  # Start the next phase
def run_deep_learning(self):
326    def run_deep_learning(self):
327        """Runs PHASE 5: DEEP LEARNING"""
328        phase = "Deep Learning execution"
329
330        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 5/6: Deep Learning..."))
331        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 5: Deep learning execution"), 'i')
332
333        self.dl_process = QProcess()
334        self.dl_process.finished.connect(self.on_dl_finished)
335        self.dl_process.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
336        self.dl_process.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_process.readAllStandardOutput()))
337        self.dl_process.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_process.readAllStandardError()))
338
339        args = [
340            get_script_path("deep_learning/deep_learning_runner.py"),
341            '--depth', '6',
342            '--filters', '64', '96', '128', '192', '256', '384', '512',
343            '--min_fmap', '2',
344            '--gpus', '1',
345            '--amp',
346            '--save_preds',
347            '--exec_mode', 'predict',
348            '--data', f'{self.output_dir}/preprocess/val_3d/test',
349            '--ckpt_path', get_script_path('deep_learning/checkpoints/fold3/epoch=146-dice=88.05.ckpt'),
350            '--tta',
351            '--results', f'{self.output_dir}/dl_results'
352        ]
353
354        self.dl_process.start(self.python_executable, args)

Runs PHASE 5: DEEP LEARNING

def on_dl_finished(self, exit_code, exit_status):
356    def on_dl_finished(self, exit_code, exit_status):
357        if self.is_cancelled:
358            return
359
360        # This slot is called by the QProcess when it finishes
361        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
362            self.log_update.emit("Deep learning execution failed", f"Exit code: {exit_code}")
363            if self.current_file_index + 1 < self.total_files:
364                self.current_file_index += 1
365                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
366                self.process_single_file()
367            return
368
369        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Deep learning execution completed"), 'i')
370        self.update_progress()
371        # PHASE 6: Postprocess
372        self.current_phase = 6
373        self.run_postprocess()  # Start the next phase
def run_postprocess(self):
375    def run_postprocess(self):
376        phase = "Postprocessing"
377
378        self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Phase 6/6: Postprocessing..."))
379        self.log_update.emit(QCoreApplication.translate("DlWorker", "PHASE 6: Postprocess"), 'i')
380
381        self.dl_postprocess = QProcess()
382        self.dl_postprocess.finished.connect(self.on_postprocess_finished)
383        self.dl_postprocess.errorOccurred.connect(lambda error, string=phase: self.on_error(string, error))
384        self.dl_postprocess.readyReadStandardOutput.connect(lambda string=phase: self.on_stdout(string, self.dl_postprocess.readAllStandardOutput()))
385        self.dl_postprocess.readyReadStandardError.connect(lambda string=phase: self.on_stderr(string, self.dl_postprocess.readAllStandardError()))
386
387        args = [
388            get_script_path("deep_learning/postprocess.py"),
389            '-i', f'{self.output_dir}/dl_results/predictions_epoch=146-dice=88_05_task=train_fold=0_tta',
390            '-o', f'{self.output_dir}/dl_postprocess',
391            '--w', f'{self.workspace_path}',
392            '--atlas', self.atlas_file,
393            "--brats", self.brats_file,
394            '--mri', f'{self.current_input_file}'
395        ]
396
397        self.dl_postprocess.start(self.python_executable, args)
def on_postprocess_finished(self, exit_code, exit_status):
399    def on_postprocess_finished(self, exit_code, exit_status):
400        if self.is_cancelled:
401            return
402
403        # This slot is called by the QProcess when it finishes
404        if exit_code != 0 or exit_status != QProcess.ExitStatus.NormalExit:
405            self.log_update.emit("Postprocess failed", f"Exit code: {exit_code}")
406            if self.current_file_index + 1 < self.total_files:
407                self.current_file_index += 1
408                self.file_update.emit(self.current_input_file_basename, QCoreApplication.translate("DlWorker", "Segmentation failed for this file"))
409                self.process_single_file()
410            return
411
412        self.log_update.emit(QCoreApplication.translate("DlWorker", "✓ Postprocess completed"), 'i')
413        self.file_update.emit(self.current_input_file_basename,
414                              QCoreApplication.translate("DlWorker", "Segmentation completed"))
415        self.update_progress()
416
417        if self.current_file_index + 1 < self.total_files:
418            self.current_file_index += 1
419            self.process_single_file()
420        else:
421            self.finished.emit(True, QCoreApplication.translate("DlWorker", "Processing completed"))
422        return
def cancel(self):
424    def cancel(self):
425        self.is_cancelled = True
426        self.log_update.emit(QCoreApplication.translate("DlWorker", "Cancellation requested - stopping all processes..."), 'w')
427
428        processes = {
429            'SynthStrip': self.synthstrip_process,
430            'Coregistration': self.coregistration_process,
431            'Reorientation': self.reorientation_process,
432            'Preprocess': self.dl_preprocess,
433            'Deep Learning': self.dl_process,
434            'Postprocess': self.dl_postprocess
435        }
436
437        for name, process in processes.items():
438            if process is not None and process.state() != QProcess.ProcessState.NotRunning:
439                self.log_update.emit(QCoreApplication.translate("DlWorker", "Stopping {name}...").format(name=name), 'w')
440
441                # No need to disconnect signals manually!
442                process.terminate()
443
444                # only kill if it doesn't respond after a timeout
445                if not process.waitForFinished(2000):
446                    self.log_update.emit(QCoreApplication.translate("DlWorker", "Forcing {name} to quit...").format(name=name), 'e')
447                    process.kill()
448                    process.waitForFinished(1000)
449
450        self.finished.emit(False, QCoreApplication.translate("DlWorker", "Processing cancelled by user"))
def on_stdout(self, phase, data):
452    def on_stdout(self, phase, data):
453        """Correct handler for QProcess stdout"""
454        try:
455            # Correctly decode data from QByteArray
456            text = data.data().decode("utf-8", errors='replace').strip()
457            if text:
458                # Split by lines and log each one
459                for line in text.splitlines():
460                    if line.strip():  # Avoid empty lines
461                        self.log_update.emit(f"[{phase}] {line.strip()}", 'i')
462        except Exception as e:
463            self.log_update.emit(f"[{phase}] Error decoding stdout: {str(e)}", 'e')

Correct handler for QProcess stdout

def on_stderr(self, phase, data):
465    def on_stderr(self, phase, data):
466        """Correct handler for QProcess stderr"""
467        try:
468            # Correctly decode data from QByteArray
469            text = data.data().decode("utf-8", errors='replace').strip()
470            if text:
471                # Split by lines and log each one
472                for line in text.splitlines():
473                    if line.strip():  # Avoid empty lines
474                        self.log_update.emit(f"[{phase}] {line.strip()}", 'e')
475        except Exception as e:
476            self.log_update.emit(f"[{phase}] Error decoding stderr: {str(e)}", 'e')

Correct handler for QProcess stderr

def on_error(self, phase, error):
478    def on_error(self, phase, error):
479        """Handler for QProcess errors"""
480        try:
481            error_messages = {
482                QProcess.ProcessError.FailedToStart: QCoreApplication.translate("DlWorker", "Failed to start process"),
483                QProcess.ProcessError.Crashed: QCoreApplication.translate("DlWorker", "Process crashed"),
484                QProcess.ProcessError.Timedout: QCoreApplication.translate("DlWorker", "Process timed out"),
485                QProcess.ProcessError.WriteError: QCoreApplication.translate("DlWorker", "Write error"),
486                QProcess.ProcessError.ReadError: QCoreApplication.translate("DlWorker", "Read error"),
487                QProcess.ProcessError.UnknownError: QCoreApplication.translate("DlWorker", "Unknown error")
488            }
489
490            error_msg = error_messages.get(error, QCoreApplication.translate("DlWorker", "Unknown error code: {error}").format(error=error))
491            self.log_update.emit(f"[{phase}] Process error: {error_msg}", 'e')
492
493        except Exception as e:
494            self.log_update.emit(f"[{phase}] Error in error handler: {str(e)}", 'e')

Handler for QProcess errors