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')
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)
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)
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.
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.
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.
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.
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.
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
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
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
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
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
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
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
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
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
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
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
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
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
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)
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
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"))
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
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
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