From 44aa7be63e05c5bc84a53a04c0b7d6ff2ff4e496 Mon Sep 17 00:00:00 2001 From: oonim Date: Wed, 1 Apr 2026 17:08:39 +0200 Subject: [PATCH 1/3] topup and eddy preproc added --- src/original/MG_LU/TOPED_fsl_commands.py | 313 +++++++++++++++++++ src/original/MG_LU/TOPED_fsl_runner.py | 372 +++++++++++++++++++++++ 2 files changed, 685 insertions(+) create mode 100644 src/original/MG_LU/TOPED_fsl_commands.py create mode 100644 src/original/MG_LU/TOPED_fsl_runner.py diff --git a/src/original/MG_LU/TOPED_fsl_commands.py b/src/original/MG_LU/TOPED_fsl_commands.py new file mode 100644 index 0000000..236a55c --- /dev/null +++ b/src/original/MG_LU/TOPED_fsl_commands.py @@ -0,0 +1,313 @@ +import os +import re +import shutil +import subprocess +from typing import Optional, Tuple + + +class TOPED: + def __init__(self, distribution: str, user: str, linux_workdir: str): + """ Specify WSL distribution, user, and working directory for FSL. + Parameters: + distribution : str The WSL distribution where FSL is installed (e.g., "Ubuntu"). + user : str The Linux username to run commands as. + linux_workdir : str The directory in the Linux filesystem to use for temporary files. + """ + + if not os.path.exists(linux_workdir): + #raise ValueError(f"Linux working directory not found: {linux_workdir}") + print(f"Linux working directory not found, creating: {linux_workdir}") + os.makedirs(linux_workdir) + + self.distribution = distribution + self.user = user + self.linux_workdir = linux_workdir + + # CORE COMMAND FUNCTION + def run_command(self, command: str, workdir: str): + """ Run a generic FSL command through WSL. + Parameters: + command : str The FSL command to execute. + Returns: + subprocess.CompletedProcess The completed process. + """ + # print the full command in the IDE output when running the fsl_runner. + print(f" command: {command}") + + full_cmd = f"source ~/.profile; source ~/.bashrc; {command}" + return subprocess.run( + ['wsl', '-d', self.distribution, '-u', self.user, '-e', 'bash', '-c', full_cmd], + cwd=workdir, + check=True, + capture_output=True, + text=True + ) + + # TOPUP + def run_topup( + self, + b0_path: str, + acq_parameter_path: str, + config_file_path: Optional[str] = None, + nthr: Optional[int] = None, + logout: Optional[str] = None, # new + dfout: Optional[str] = None, # new + jacout: Optional[str] = None, # new + patient_id: Optional[str] = None, + output_dir: Optional[str] = None # iout mandatory bruh! + ) -> str: + """ Run FSL TOPUP on a stack of b0 images. + Parameters: + b0_path : str The path to the b0 image to process with TOPUP. + acq_parameter_path : str Path to the acquisition parameters file. + config_file_path : str, optional Path to the TOPUP configuration file, by default None, which uses the default FSL config b02b0.cnf. + nthr : int, optional Number of threads to use, by default None (uses default_threads) + Returns: + whatever specified in the TOPUP command output (dfout, iout, jacout) + Corrected image, fieldmap, and Jacobian determinant paths are generated in the Linux working directory. + """ + workdir = self.get_patient_workdir(patient_id) + + if not os.path.exists(b0_path): + raise ValueError("b0_path invalid") + if not os.path.exists(acq_parameter_path): + raise ValueError("acq_parameter_path invalid") + + output_dir = output_dir or os.path.dirname(b0_path) + + # Copy inputs + b0_name = self.copy_to_wsl(b0_path, workdir) + b0_basename, _ = self.split_nifti_gz(b0_name) + + acqparam = self.copy_to_wsl(acq_parameter_path, workdir) + if config_file_path: + config_filename = self.copy_to_wsl(config_file_path, workdir) + config_arg = f"--config={config_filename} " + else: + # Force FSL default config + config_arg = "--config=b02b0.cnf " + + # Build command + cmd = ( + f"topup " + f"--imain={b0_name} " + f"--datain={acqparam} " + f"--out={b0_basename} " + #f"--dfout={self.get_fieldmap_name(b0_basename)} " + f"--iout={self.get_corrected_name(b0_basename)} " + #f"--jacout={self.get_jacobian_name(b0_basename)} " + ) + + if logout is not None: cmd += f"--logout={self.get_log_name(b0_basename)} " + if dfout is not None: cmd += f"--dfout={self.get_warpfig_name(b0_basename)} " + if jacout is not None: cmd += f"--jacout={self.get_jacobian_name(b0_basename)} " + if nthr is not None: + cmd += f"--nthr={nthr} " + cmd += config_arg + + # Run + self.run_command(cmd, workdir) + + # Copy corrected image back + self.ensure_dir(output_dir) + corrected_filename = self.get_corrected_name(b0_basename, ext=True) + self.copy_from_wsl(corrected_filename, output_dir, workdir) + + # Return topup base for Eddy + return b0_basename + + # Eddy + def run_eddy( + self, + dwi_path: str, + mask_path: str, + acq_parameter_path: str, + index_path: str, + bvecs_path: str, + bvals_path: str, + topup_base: str, + slspec_path: Optional[str] = None, + b_range: Optional[int] = None, + flm: Optional[str] = None, + slm: Optional[str] = None, + niter: Optional[int] = None, + fwhm: Optional[str] = None, + resamp: Optional[str] = None, + fep: Optional[bool] = None, + repol: Optional[bool] = None, + estimate_move_by_susceptibility: Optional[bool] = None, + mporder: Optional[int] = None, + patient_id: Optional[str] = None, + output_dir: Optional[str] = None, + json_path: Optional[str] = None, # new from here. + interp: Optional[str] = None, + nvoxhp: Optional[int] = None, + ff: Optional[float] = None, + dont_sep_offs_move: Optional[bool] = None, + dont_peas: Optional[bool] = None, + ol_nstd: Optional[float] = None, + ol_nvox: Optional[int] = None, + ol_type: Optional[str] = None, + ol_ss: Optional[int] = None, + ol_pos: Optional[bool] = None, + ol_sqr: Optional[bool] = None, + s2v_niter: Optional[int] = None, + s2v_lambda: Optional[float] = None, + s2v_interp: Optional[str] = None, + mbs_niter: Optional[int] = None, + mbs_lambda: Optional[float] = None, + mbs_ksp: Optional[float] = None, + cnr_maps: Optional[bool] = None, + residuals: Optional[bool] = None, + data_is_shelled: Optional[bool] = None, + nthr: Optional[int] = None + ) -> str: + + """ Run FSL Eddy. + Parameters: + dwi_path : str Path to the input DWI image. This is the stack to be corrected by Eddy & TOPUP. + mask_path : str Path to the mask image. + acqparams_path : str Path to acquisition parameters file + index_path : str Path to index file + bvecs_path : str Path to bvecs file + bvals_path : str Path to bvals file + topup_base : str Base name used for topup output. is is automatically set by TOUP in the linux working directory and is used as input for Eddy. + output_dir : str Output directory + bunch of optional Eddy flags (set to None to use FSL defaults). + Returns: + (Eddy + TOPUP) Corrected DWI image (path to output image) + """ + + workdir = self.get_patient_workdir(patient_id) + + for p in [dwi_path, mask_path, acq_parameter_path, index_path, bvecs_path, bvals_path]: + if not os.path.exists(p): + raise ValueError(f"Missing compulsory file: {p}") + + output_dir = output_dir or os.path.dirname(dwi_path) + + # Copy inputs + dwi_name = self.copy_to_wsl(dwi_path, workdir) + dwi_basename, _ = self.split_nifti_gz(dwi_name) + + mask = self.copy_to_wsl(mask_path, workdir) + acqp = self.copy_to_wsl(acq_parameter_path, workdir) + index = self.copy_to_wsl(index_path, workdir) + bvecs = self.copy_to_wsl(bvecs_path, workdir) + bvals = self.copy_to_wsl(bvals_path, workdir) + + slspec = self.copy_to_wsl(slspec_path, workdir) if slspec_path else None + json_file = self.copy_to_wsl(json_path, workdir) if json_path else None + + # Build command dynamically + cmd = ( + f"eddy " + f"--imain={dwi_name} " + f"--mask={mask} " + f"--acqp={acqp} " + f"--index={index} " + f"--bvecs={bvecs} " + f"--bvals={bvals} " + f"--topup={topup_base} " + f"--out={self.get_corrected_name_eddy(dwi_basename)} " + ) + + # Optional params + if slspec: cmd += f"--slspec={slspec} " + if json_file: cmd += f"--json={json_file} " + if b_range is not None: cmd += f"--b_range={b_range} " + + if flm is not None: cmd += f"--flm={flm} " + if slm is not None: cmd += f"--slm={slm} " + if fwhm is not None: cmd += f"--fwhm={fwhm} " + if niter is not None: cmd += f"--niter={niter} " + if interp is not None: cmd += f"--interp={interp} " + if resamp is not None: cmd += f"--resamp={resamp} " + if fep is not None: cmd += f"--fep " + if nvoxhp is not None: cmd += f"--nvoxhp={nvoxhp} " + if ff is not None: cmd += f"--ff={ff} " + if dont_sep_offs_move is not None: cmd += f"--dont_sep_offs_move " + if dont_peas is not None: cmd += f"--dont_peas " + + if repol is not None: cmd += f"--repol " + if ol_nstd is not None: cmd += f"--ol_nstd={ol_nstd} " + if ol_nvox is not None: cmd += f"--ol_nvox={ol_nvox} " + if ol_type is not None: cmd += f"--ol_type={ol_type} " + if ol_ss is not None: cmd += f"--ol_ss={ol_ss} " + if ol_pos is not None: cmd += f"--ol_pos " + if ol_sqr is not None: cmd += f"--ol_sqr " + + if mporder is not None: cmd += f"--mporder={mporder} " + if s2v_niter is not None: cmd += f"--s2v_niter={s2v_niter} " + if s2v_lambda is not None: cmd += f"--s2v_lambda={s2v_lambda} " + if s2v_interp is not None: cmd += f"--s2v_interp={s2v_interp} " + + if estimate_move_by_susceptibility is not None: + cmd += f"--estimate_move_by_susceptibility={int(estimate_move_by_susceptibility)} " + if mbs_niter is not None: cmd += f"--mbs_niter={mbs_niter} " + if mbs_lambda is not None: cmd += f"--mbs_lambda={mbs_lambda} " + if mbs_ksp is not None: cmd += f"--mbs_ksp={mbs_ksp} " + + if cnr_maps is not None: cmd += f"--cnr_maps " + if residuals is not None: cmd += f"--residuals " + if data_is_shelled is not None: cmd += f"--data_is_shelled " + if nthr is not None: cmd += f"--nthr={nthr} " + + + # Run + self.run_command(cmd, workdir) + + # Copy output back + self.ensure_dir(output_dir) + output_filename = self.get_corrected_name_eddy(dwi_basename, ext=True) + self.copy_from_wsl(output_filename, output_dir, workdir) + + return os.path.join(output_dir, output_filename) + + + # FILE HANDLING between windows and wsl + def copy_to_wsl(self, path: str, workdir: str) -> str: + filename = os.path.basename(path) + dst = os.path.join(workdir, filename) + shutil.copy(path, dst) + return filename + + def copy_from_wsl(self, filename: str, output_dir: str, workdir: str): + src = os.path.join(workdir, filename) + dst = os.path.join(output_dir, filename) + if not os.path.exists(src): + raise FileNotFoundError(f"Expected output not found: {src}") + shutil.copy(src, dst) + + + # HELPERS - filenames and folders etc. + def split_nifti_gz(self, filename: str) -> Tuple[str, str]: + match = re.match(r"^(.*)(\.nii\.gz)$", filename) + return match.group(1), match.group(2) if match else os.path.splitext(filename) + + def get_warpfig_name(self, base: str): + return f"{base}_warpfig" + + def get_jacobian_name(self, base: str): + return f"{base}_jacobian" + + def get_log_name(self, base: str): + return f"{base}_log" + + def get_corrected_name(self, base: str, ext=False): + return f"{base}_corrected.nii.gz" if ext else f"{base}_corrected" + + def get_corrected_name_eddy(self, base: str, ext=False): + return f"{base}_EddyCorrected.nii.gz" if ext else f"{base}_EddyCorrected" + + def ensure_dir(self, path: str): + if not os.path.exists(path): + os.makedirs(path) + + def get_patient_workdir(self, patient_id: Optional[str]) -> str: + if not patient_id: + return self.linux_workdir + path = os.path.join(self.linux_workdir, patient_id) + if not os.path.exists(path): + os.makedirs(path) + return path \ No newline at end of file diff --git a/src/original/MG_LU/TOPED_fsl_runner.py b/src/original/MG_LU/TOPED_fsl_runner.py new file mode 100644 index 0000000..d16c867 --- /dev/null +++ b/src/original/MG_LU/TOPED_fsl_runner.py @@ -0,0 +1,372 @@ +import os +from typing import Optional +from TOPED_fsl_commands import TOPED # script containing subprocess commands + +""" +About this script: +The script is run in Windows but runs FSL commands through a WSL subprocess. The corrected files are copied back into windows from linux. +On the user-end, everything should be done in windows. + +This script runs all linux commands as a subprocess using the TOPED class in TOPED_fsl_commands.py. + The input data is read from your specified windows directory 'main_dir' and the final output is saved in the same directory. + However, all processing is done in the specified linux_workdir. All files for each loop (/patient) are saved there and then copied back to the respective folders in windows main_dir. +To run the script you will need to specify the parameters in the configuration section below. +Input files for topup and and eddy are identified through keywords specified in the IDENTIFIERS dict. + for each input type (e.g. b0, dwi stack, mask, acqp, bvecs, bvals, index), specify a keyword that is contained in the respective file names in your dataset. + This should be unique, i.e. if the keyword for the topup imain input is "b0", the script looks for files containing "b0" in the name and takes that as input. If there are multiple files containing "b0", an error is raised. + The assumed directory strucure is seen below. The script looks for the input files in the 'patient' folders first, then in the common folder if not found in the patient folder. + If you keep all data in the 'patient' folders and do not have a common folder, set COMMON_FOLDER_NAME to None and the script will only look in the patient folders. + The structure of the main directory will be copied into the Linux working directory (i.e. the same folders and files will be generated). + +Requirements: +- Python packages os, re, typing, subprocess, shutil) +- FSL installed with an WSL distro. user needs to be configured. Make sure FSL can be run from the command line in WSL. + + +- Files needed for topup and eddy: + Topup: + - b0 image (4D file containing multiple b0 volumes). nii.gz + - acqparams.txt (PE directions) + - configuration file (optional, if not specified the default FSL config b02b0.cnf is used) .cnf + Eddy: + - dwi image (4D stack contaning whatever should be topup+eddy corrected). nii.gz + - mask (typically brain mask, but i dont do brain so its whatever i guess). nii.gz + - acqparams (assumed to be the same as for topup). + - index (to match the dwi stack volumes to the acqparams lines). + - bvecs (as generated by e.g. dcm2niix). make sure they match the full stack. + - bvals (as generated by e.g. dcm2niix). make sure they match the full stack. + - slspec (optional). + - JSON (optional) +""" + +# main_directory +# ├── patient_1 +# │ ├── pt1_b0.nii.gz +# │ ├── pt1_dwi_stack.nii.gz +# │ ├── pt1_mask.nii.gz +# │ └── ... (other patient-specific files, e.g. pt1_jsonfile.json) +# ├── patient_2 +# │ ├── pt2_b0.nii.gz +# │ ├── pt2_dwi_stack.nii.gz +# │ ├── pt2_mask.nii.gz +# │ └── ... (other patient-specific files) +# └── common_files (optional) +# ├── acqparams.txt +# ├── bvecs +# ├── bvals +# ├── index.txt +# ├── slspec.txt +# └── config.cnf + +# Note: 'patient_1' etc. are examples and can be any suitable name. +# The names of these folders are copied into the linux working directory and are used to identify the respective entries. +# a 'common_files' folder is optional. The purpose is is you e.g. have the same config file or acqparams/bvecs/bvals/index/slspec files for all patients. + +# ======================================================================================================================================================= +"""CONFIGURATION - Adjust these parameters according to your data""" # change things in this section! + +#main directory in WINDOWS containing patient folders and common folder. +MAIN_DIR = r"C:\path\to\your\main_dir" # replace with path to your main_dir. + +# folder inside main_dir that contains files which are common for all patients (e.g. acqparams, bvecs, bvals, index, slspec). +COMMON_FOLDER_NAME = "common_files" # replace with your path, or set to None if there is no common folder. +# The script looks in the patient folders first. if it does not find the required files there, it looks in the common folder. + + +# Filename identifiers - Change the RHS here according to your own file naming conventions!!! +# The script will look for files containing these keywords in the patient folders first (then in the common folder). +IDENTIFIERS = { + "b0": "b0.nii.gz", # e.g. topup --imain. assumed to contain str 'b0' in the filename. + # the name of the b0 file determines the names of generated output files. + # a b0 file named 'patient1_ddmmyyyy_b0.nii.gz' will generate output files which all have the prefix 'patient1_ddmmyyyy'. + "dwi": "stack.nii.gz", # e.g. eddy --imain. DWI stack assumed to contain 'stack' in the filename. Should have the same prefix as the b0 file, e.g. 'patient1_ddmmyyyy_stack.nii.gz' to match with 'patient1_ddmmyyyy_b0.nii.gz'. + "mask": "mask", # eddy --mask. contains 'mask' in the filename. + "bvec": "bvec", + "bval": "bval", + "index": "index", # index for eddy + "acqp": "acq", # acqparams for topup and eddy. + "slspec": "slspec", # optional slice spec for eddy. + "json": "json", # optional json file for eddy. + "config": ".cnf" # optional config file for topup. If not found, the FSL default config b02b0.cnf will be used. +} + +# FSL PARAMETERS - adjust as needed. +TOPUP_PARAMS = { + "nthr": 14, # --nthr to run topup. + "jacout": None, # Default False. + "logout": None, # Default False. + "dfout": None, # Default False. + "output_dir": None # all files are saved in the linux working directory by default (None). The corrected files are copied back into main_dir in windows. + # remaining Topup parameters are set with the config file. You can specify a config file for each patient or a common one in the common folder by placing the .cnf file in the directory. +} + + +EDDY_PARAMS = { + "b_range": None, # None defaults to 50. + "flm": None, # ('linear', 'quadratic', 'cubic'). None defaults to 'quadratic' + "slm": None, # ('none', 'linear', 'quadratic'). None defaults to 'none'. + "fwhm": 10, # Default: 0 mm. Set to 10 for testing. + "niter": 1, # Default: 5. Set to 1 for testing. + "interp": None, # ('spline', 'trilinear'). None defaults to 'spline'. + "resamp": None, # ('jac', 'lsr'). None defaults to 'jac'. + "fep": None, # True/False. None defaults to False. + "nvoxhp": None, # None defaults to 1000. + "ff": None, # [0,10]. None defaults to 10. + "dont_sep_offs_move": None, # True/False. None defaults to False. + "dont_peas": None, # True/False. None defaults to False. + + "repol": None, # True/False. None defaults to False. set to True to enable outlier replacement. + "ol_nstd": None, # When repol=True; None defaults to 4. + "ol_nvox": None, # When repol=True; None defaults to 250. + "ol_type": None, # When repol=True; ('sw', 'gw', 'both'). None defaults to 'sw'. + "ol_ss": None, # When repol=True; ('sw', 'pooled'). None defaults to 'sw'. + "ol_pos": None, # When repol=True; True/False. None defaults to False. + "ol_sqr": None, # When repol=True; True/False. None defaults to False. + + "mporder": None, # Default 0. + "s2v_niter": None, # When mporder>0; None defaults to 5. + "s2v_lambda": None, # When mporder>0; None defaults to 1. + "s2v_interp": None, # When mporder>0; ('trilinear', 'spline'). None defaults to 'trilinear'. + + "estimate_move_by_susceptibility": None, # True/Flase. None defaults to False. + "mbs_niter": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + "mbs_lambda": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + "mbs_ksp": None, # When estimate_move_by_susceptibility=True; None defaults to 10. + + "cnr_maps": None, # True/False. None defaults to False. + "residuals": None, # True/False. None defaults to False. + "data_is_shelled": None,# True/False. None defaults to False. + "nthr": None, # --nthr to run eddy. only relevant for CPU! + + "output_dir": None # all files are saved in the linux working directory by default (None). The corrected files are copied back into original dir in windows. +} + +# WSL commands are run through your distro and in whatever directory you specify as your linux_workdir. +fsl = TOPED( + + distribution="Ubuntu-22.04", # your WSL distribution name you can find it by running 'wsl -l -v' in powershell. + user="username", # replace with your WSL user. + linux_workdir=r"\\wsl.localhost\Ubuntu-22.04\home\user\yourfolder" # replace with the name of a folder located in home/user in your WSL distribution. + # yourfolder is the wd for FSL commands. It will be created if it does not exist. + # All output files will be saved here. Corrected files from TOPUP and Eddy will be copied back to the patient folders in windows. + # The data will be organised into folders matching your main_dir structure, i.e. for each patient folder in main_dir, a corresponding folder is created in linux_workdir and all processing for that patient is done in that folder. + # Make sure to change this path between different runs so that you dont overwrite your data. :) +) + + + + + +# ======================================================================================================================================================= +"""RUNNING LOOP THOUGH FOLDERS IN MAIN_DIR, WITH PARAMTERS SPECIFIED ABOVE""" +# ALL lops are run with the parameters as specified in the configuration section above. parameters are global for the run. +# be careful if/when changing this section below (needs to match with TOPED_fsl_commands.py). + + +# HELPER functions to find input files based in IDENTIFIERS dict. +def find_file(folder, keyword) -> Optional[str]: + matches = [f for f in os.listdir(folder) if keyword.lower() in f.lower()] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple files found for '{keyword}' in {folder}") + return os.path.join(folder, matches[0]) + +# function for finding cnf files. +def find_cnf_file(folder): + matches = [f for f in os.listdir(folder) if f.lower().endswith(".cnf")] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple .cnf files found in {folder}! clean this shit up") + return os.path.join(folder, matches[0]) +# function for finding json files. +def find_json_file(folder): + matches = [f for f in os.listdir(folder) if f.lower().endswith(".json")] + if len(matches) == 0: + return None + if len(matches) > 1: + raise RuntimeError(f"Multiple .json files found in {folder}!") + return os.path.join(folder, matches[0]) + + +# searches for the file in the patient folder first, then in the common folder if not found in the patient folder. +def resolve_file(patient_dir, common_dir, key): + local = find_file(patient_dir, IDENTIFIERS[key]) + if local: + return local + if common_dir: + return find_file(common_dir, IDENTIFIERS[key]) + return None + + +# main loops start HERE - runs TOPUP and Eddy for each patient folder in main_dir +common_dir = (os.path.join(MAIN_DIR, COMMON_FOLDER_NAME) if COMMON_FOLDER_NAME else None) + +# Input file check - check all of main_dir, do the files exist for all patients? +# this is so that you wont leeave this overnight and come back to some shit error that could have been prevented. + +print("\n File check: Checking if all required files are found...") +file_check = True + +for patient in os.listdir(MAIN_DIR): + patient_dir = os.path.join(MAIN_DIR, patient) + if not os.path.isdir(patient_dir) or patient == COMMON_FOLDER_NAME: + continue + + issues = [] + + # Locate files using same logic as the main loop + b0_path = find_file(patient_dir, IDENTIFIERS["b0"]) + dwi_path = find_file(patient_dir, IDENTIFIERS["dwi"]) + mask_path = find_file(patient_dir, IDENTIFIERS["mask"]) + acqp_path = resolve_file(patient_dir, common_dir, "acqp") + bvecs_path = resolve_file(patient_dir, common_dir, "bvec") + bvals_path = resolve_file(patient_dir, common_dir, "bval") + index_path = resolve_file(patient_dir, common_dir, "index") + + # TOPUP compulsory inputs + if not b0_path: issues.append("TOPUP: cannot find b0 file") + if not acqp_path: issues.append("TOPUP: cannot find acqisition parameters file") + + # EDDY compulsory (only warn if b0 exists, i.e. TOPUP would run) + if b0_path: + if not dwi_path: issues.append("EDDY: cannot find 4D dwi file") + if not mask_path: issues.append("EDDY: cannot find mask file") + if not bvecs_path: issues.append("EDDY: cannot find bvecs file") + if not bvals_path: issues.append("EDDY: cannot find bvals file") + if not index_path: issues.append("EDDY: cannot find index file") + + if issues: + print(f" FAIL {patient}:") + for issue in issues: + print(f" - {issue}") + file_check = False + else: + print(f" OK {patient}") + +if not file_check: + print("\n WARNING!!!!!!!!! File check failed. Some files cannot be found. You may want to abort and fix the issues. " \ + "\n Consider renaming your files to contain the specified keyword or adjust the IDENTIFIERS dict accordingly. " \ + "\n TOPUP/Eddy will run only for the 'OK' cases.") +else: + print("\n File check passed — all patients have all compulosry inputs.") + + +for patient in os.listdir(MAIN_DIR): + # searches ALL folders in main_dir (except for specified common_folder). -> Attempts to run topup+eddy on all folders in main_dir. + patient_dir = os.path.join(MAIN_DIR, patient) + if not os.path.isdir(patient_dir) or patient == COMMON_FOLDER_NAME: + continue + print(f"\n \nProcessing folder: {patient}--------------------------------------------------------------") + patient_id = patient # this is the name of the folder both in windows and in linux workdir. + + try: + # Locate compulsory files - these files should be in the 'patient' directory, not in common. + b0_path = find_file(patient_dir, IDENTIFIERS["b0"]) + dwi_path = find_file(patient_dir, IDENTIFIERS["dwi"]) + mask_path = find_file(patient_dir, IDENTIFIERS["mask"]) + # these can be in both common and patient dir. + acqp_path = resolve_file(patient_dir, common_dir, "acqp") + bvecs_path = resolve_file(patient_dir, common_dir, "bvec") + bvals_path = resolve_file(patient_dir, common_dir, "bval") + index_path = resolve_file(patient_dir, common_dir, "index") + slspec_path = resolve_file(patient_dir, common_dir, "slspec") + config_path = find_cnf_file(patient_dir) + if not config_path and common_dir: + config_path = find_cnf_file(common_dir) + json_path = find_json_file(patient_dir) + if not json_path and common_dir: + json_path = find_json_file(common_dir) + + # Check TOPUP inputs + if not b0_path: + print(f" o_O No b0 found in {patient} → Cannot run TOPUP. skipping dataset. Rename your b0 file to contain the specified keyword or adjust the IDENTIFIERS dict accordingly.") + continue + if not acqp_path: + raise RuntimeError(" Missing acquisition parameters (acqp). Please add file or update IDENTIFIERS dict.") + + print(f" → Running TOPUP :)") + # RUN TOPUP + topup_base = fsl.run_topup( + b0_path=b0_path, + acq_parameter_path=acqp_path, + config_file_path=config_path, + nthr=TOPUP_PARAMS["nthr"], + logout=TOPUP_PARAMS["logout"], + jacout=TOPUP_PARAMS["jacout"], + dfout=TOPUP_PARAMS["dfout"], + patient_id=patient, + output_dir=TOPUP_PARAMS["output_dir"] or patient_dir + ) + + # Check EDDY inputs + missing_eddy = [] + if not dwi_path: missing_eddy.append("dwi") + if not mask_path: missing_eddy.append("mask") + if not bvecs_path: missing_eddy.append("bvecs") + if not bvals_path: missing_eddy.append("bvals") + if not index_path: missing_eddy.append("index") + + if missing_eddy: + print(f" o_O Eddy missing compulsory parameters: {missing_eddy} → Cannot run EDDY. Rename your files to contain the respective keywords or adjust the IDENTIFIERS dict accordingly.") + continue +# + print(f" → Running Eddy :)") + + # RUN EDDY + fsl.run_eddy( + dwi_path=dwi_path, + mask_path=mask_path, + acq_parameter_path=acqp_path, + index_path=index_path, + bvecs_path=bvecs_path, + bvals_path=bvals_path, + patient_id=patient, + topup_base=topup_base, + + slspec_path=slspec_path, + json_path=json_path, + b_range=EDDY_PARAMS["b_range"], + + flm=EDDY_PARAMS["flm"], + slm=EDDY_PARAMS["slm"], + fwhm=EDDY_PARAMS["fwhm"], + niter=EDDY_PARAMS["niter"], + interp=EDDY_PARAMS["interp"], + resamp=EDDY_PARAMS["resamp"], + fep=EDDY_PARAMS["fep"], + nvoxhp=EDDY_PARAMS["nvoxhp"], + ff=EDDY_PARAMS["ff"], + dont_sep_offs_move=EDDY_PARAMS["dont_sep_offs_move"], + dont_peas=EDDY_PARAMS["dont_peas"], + + repol=EDDY_PARAMS["repol"], + ol_nstd=EDDY_PARAMS["ol_nstd"], + ol_nvox=EDDY_PARAMS["ol_nvox"], + ol_type=EDDY_PARAMS["ol_type"], + ol_ss=EDDY_PARAMS["ol_ss"], + ol_pos=EDDY_PARAMS["ol_pos"], + ol_sqr=EDDY_PARAMS["ol_sqr"], + mporder=EDDY_PARAMS["mporder"], + s2v_niter=EDDY_PARAMS["s2v_niter"], + s2v_lambda=EDDY_PARAMS["s2v_lambda"], + s2v_interp=EDDY_PARAMS["s2v_interp"], + + estimate_move_by_susceptibility=EDDY_PARAMS["estimate_move_by_susceptibility"], + mbs_niter=EDDY_PARAMS["mbs_niter"], + mbs_lambda=EDDY_PARAMS["mbs_lambda"], + mbs_ksp=EDDY_PARAMS["mbs_ksp"], + + cnr_maps=EDDY_PARAMS["cnr_maps"], + residuals=EDDY_PARAMS["residuals"], + data_is_shelled=EDDY_PARAMS["data_is_shelled"], + + + output_dir=EDDY_PARAMS["output_dir"] or patient_dir + ) + + print(f"\n Done. Check your output for {patient} :)") + + except Exception as e: + print(f"ERROR in {patient}: {str(e)}") \ No newline at end of file From e5a7d7b403b0c74e3fe2f5c1d5b180e80b004e58 Mon Sep 17 00:00:00 2001 From: oonim Date: Wed, 1 Apr 2026 17:36:06 +0200 Subject: [PATCH 2/3] typos etc --- src/original/MG_LU/TOPED_fsl_commands.py | 19 +++++------ src/original/MG_LU/TOPED_fsl_runner.py | 43 +++++++++++------------- 2 files changed, 28 insertions(+), 34 deletions(-) diff --git a/src/original/MG_LU/TOPED_fsl_commands.py b/src/original/MG_LU/TOPED_fsl_commands.py index 236a55c..9453369 100644 --- a/src/original/MG_LU/TOPED_fsl_commands.py +++ b/src/original/MG_LU/TOPED_fsl_commands.py @@ -28,8 +28,7 @@ def run_command(self, command: str, workdir: str): """ Run a generic FSL command through WSL. Parameters: command : str The FSL command to execute. - Returns: - subprocess.CompletedProcess The completed process. + workdir : str The working directory in the Linux filesystem where the command should be executed. """ # print the full command in the IDE output when running the fsl_runner. print(f" command: {command}") @@ -50,11 +49,11 @@ def run_topup( acq_parameter_path: str, config_file_path: Optional[str] = None, nthr: Optional[int] = None, - logout: Optional[str] = None, # new - dfout: Optional[str] = None, # new - jacout: Optional[str] = None, # new + logout: Optional[str] = None, + dfout: Optional[str] = None, + jacout: Optional[str] = None, patient_id: Optional[str] = None, - output_dir: Optional[str] = None # iout mandatory bruh! + output_dir: Optional[str] = None ) -> str: """ Run FSL TOPUP on a stack of b0 images. Parameters: @@ -84,7 +83,7 @@ def run_topup( config_filename = self.copy_to_wsl(config_file_path, workdir) config_arg = f"--config={config_filename} " else: - # Force FSL default config + # FSL default config config_arg = "--config=b02b0.cnf " # Build command @@ -93,9 +92,7 @@ def run_topup( f"--imain={b0_name} " f"--datain={acqparam} " f"--out={b0_basename} " - #f"--dfout={self.get_fieldmap_name(b0_basename)} " f"--iout={self.get_corrected_name(b0_basename)} " - #f"--jacout={self.get_jacobian_name(b0_basename)} " ) if logout is not None: cmd += f"--logout={self.get_log_name(b0_basename)} " @@ -139,7 +136,7 @@ def run_eddy( mporder: Optional[int] = None, patient_id: Optional[str] = None, output_dir: Optional[str] = None, - json_path: Optional[str] = None, # new from here. + json_path: Optional[str] = None, interp: Optional[str] = None, nvoxhp: Optional[int] = None, ff: Optional[float] = None, @@ -199,7 +196,7 @@ def run_eddy( slspec = self.copy_to_wsl(slspec_path, workdir) if slspec_path else None json_file = self.copy_to_wsl(json_path, workdir) if json_path else None - # Build command dynamically + # Build command cmd = ( f"eddy " f"--imain={dwi_name} " diff --git a/src/original/MG_LU/TOPED_fsl_runner.py b/src/original/MG_LU/TOPED_fsl_runner.py index d16c867..0e488ea 100644 --- a/src/original/MG_LU/TOPED_fsl_runner.py +++ b/src/original/MG_LU/TOPED_fsl_runner.py @@ -4,37 +4,35 @@ """ About this script: -The script is run in Windows but runs FSL commands through a WSL subprocess. The corrected files are copied back into windows from linux. -On the user-end, everything should be done in windows. - +The script is run in Windows and expects files located in windows directories. + This script runs all linux commands as a subprocess using the TOPED class in TOPED_fsl_commands.py. The input data is read from your specified windows directory 'main_dir' and the final output is saved in the same directory. - However, all processing is done in the specified linux_workdir. All files for each loop (/patient) are saved there and then copied back to the respective folders in windows main_dir. + All processing is done in a specified linux working directory. All files for each patient are saved there and then copied back to the respective folders in the windows directory. To run the script you will need to specify the parameters in the configuration section below. -Input files for topup and and eddy are identified through keywords specified in the IDENTIFIERS dict. +Input files for topup and and eddy are identified through keywords specified in the IDENTIFIERS dictionary. for each input type (e.g. b0, dwi stack, mask, acqp, bvecs, bvals, index), specify a keyword that is contained in the respective file names in your dataset. This should be unique, i.e. if the keyword for the topup imain input is "b0", the script looks for files containing "b0" in the name and takes that as input. If there are multiple files containing "b0", an error is raised. The assumed directory strucure is seen below. The script looks for the input files in the 'patient' folders first, then in the common folder if not found in the patient folder. - If you keep all data in the 'patient' folders and do not have a common folder, set COMMON_FOLDER_NAME to None and the script will only look in the patient folders. The structure of the main directory will be copied into the Linux working directory (i.e. the same folders and files will be generated). Requirements: -- Python packages os, re, typing, subprocess, shutil) +- Python packages (os, re, typing, subprocess, shutil) - FSL installed with an WSL distro. user needs to be configured. Make sure FSL can be run from the command line in WSL. -- Files needed for topup and eddy: +Supported file inputs: Topup: - - b0 image (4D file containing multiple b0 volumes). nii.gz - - acqparams.txt (PE directions) - - configuration file (optional, if not specified the default FSL config b02b0.cnf is used) .cnf + - b0 image (4D file containing multiple b0 volumes). nii.gz (compulsory) + - acqparams.txt (PE directions) (compulsory) + - configuration file (if not specified the default FSL config b02b0.cnf is used) .cnf (optional) Eddy: - - dwi image (4D stack contaning whatever should be topup+eddy corrected). nii.gz - - mask (typically brain mask, but i dont do brain so its whatever i guess). nii.gz - - acqparams (assumed to be the same as for topup). - - index (to match the dwi stack volumes to the acqparams lines). - - bvecs (as generated by e.g. dcm2niix). make sure they match the full stack. - - bvals (as generated by e.g. dcm2niix). make sure they match the full stack. + - dwi image (4D stack contaning whatever should be topup+eddy corrected). nii.gz (compulsory) + - mask (typically brain mask, but i dont do brain so its whatever i guess). nii.gz (compulsory) + - acqparams (assumed to be the same as for topup). (compulsory) + - index (to match the dwi stack volumes to the acqparams lines). (compulsory) + - bvecs (as generated by e.g. dcm2niix). make sure they match the full stack. (compulsory) + - bvals (as generated by e.g. dcm2niix). make sure they match the full stack. (compulsory) - slspec (optional). - JSON (optional) """ @@ -80,7 +78,7 @@ # the name of the b0 file determines the names of generated output files. # a b0 file named 'patient1_ddmmyyyy_b0.nii.gz' will generate output files which all have the prefix 'patient1_ddmmyyyy'. "dwi": "stack.nii.gz", # e.g. eddy --imain. DWI stack assumed to contain 'stack' in the filename. Should have the same prefix as the b0 file, e.g. 'patient1_ddmmyyyy_stack.nii.gz' to match with 'patient1_ddmmyyyy_b0.nii.gz'. - "mask": "mask", # eddy --mask. contains 'mask' in the filename. + "mask": "mask", # eddy --mask. contains 'mask' in the filename. "bvec": "bvec", "bval": "bval", "index": "index", # index for eddy @@ -144,13 +142,12 @@ # WSL commands are run through your distro and in whatever directory you specify as your linux_workdir. fsl = TOPED( - distribution="Ubuntu-22.04", # your WSL distribution name you can find it by running 'wsl -l -v' in powershell. + distribution="Ubuntu-22.04", # your WSL distribution name. you can find it by running 'wsl -l -v' in powershell. user="username", # replace with your WSL user. - linux_workdir=r"\\wsl.localhost\Ubuntu-22.04\home\user\yourfolder" # replace with the name of a folder located in home/user in your WSL distribution. - # yourfolder is the wd for FSL commands. It will be created if it does not exist. + linux_workdir=r"\\wsl.localhost\Ubuntu-22.04\home\user\yourfolder" # replace with a path to a folder in your WSL distribution. + # yourfolder will be created if it does not exist. # All output files will be saved here. Corrected files from TOPUP and Eddy will be copied back to the patient folders in windows. - # The data will be organised into folders matching your main_dir structure, i.e. for each patient folder in main_dir, a corresponding folder is created in linux_workdir and all processing for that patient is done in that folder. - # Make sure to change this path between different runs so that you dont overwrite your data. :) + # linux_workdir will be organised into folders matching your main_dir structure, i.e. for each patient folder in main_dir, a corresponding folder is created in linux_workdir and all processing for that patient is done in that folder. ) From 75efef19269da807d780f72b6683eaef09d01c8a Mon Sep 17 00:00:00 2001 From: oonim Date: Wed, 1 Apr 2026 19:23:59 +0200 Subject: [PATCH 3/3] minor changes --- src/original/MG_LU/TOPED_fsl_commands.py | 26 +++++++++++------------- src/original/MG_LU/TOPED_fsl_runner.py | 18 ++++++++++------ 2 files changed, 24 insertions(+), 20 deletions(-) diff --git a/src/original/MG_LU/TOPED_fsl_commands.py b/src/original/MG_LU/TOPED_fsl_commands.py index 9453369..28a330f 100644 --- a/src/original/MG_LU/TOPED_fsl_commands.py +++ b/src/original/MG_LU/TOPED_fsl_commands.py @@ -98,8 +98,7 @@ def run_topup( if logout is not None: cmd += f"--logout={self.get_log_name(b0_basename)} " if dfout is not None: cmd += f"--dfout={self.get_warpfig_name(b0_basename)} " if jacout is not None: cmd += f"--jacout={self.get_jacobian_name(b0_basename)} " - if nthr is not None: - cmd += f"--nthr={nthr} " + if nthr is not None: cmd += f"--nthr={nthr} " cmd += config_arg # Run @@ -145,7 +144,7 @@ def run_eddy( ol_nstd: Optional[float] = None, ol_nvox: Optional[int] = None, ol_type: Optional[str] = None, - ol_ss: Optional[int] = None, + ol_ss: Optional[str] = None, ol_pos: Optional[bool] = None, ol_sqr: Optional[bool] = None, s2v_niter: Optional[int] = None, @@ -220,34 +219,33 @@ def run_eddy( if niter is not None: cmd += f"--niter={niter} " if interp is not None: cmd += f"--interp={interp} " if resamp is not None: cmd += f"--resamp={resamp} " - if fep is not None: cmd += f"--fep " + if fep: cmd += f"--fep " if nvoxhp is not None: cmd += f"--nvoxhp={nvoxhp} " if ff is not None: cmd += f"--ff={ff} " - if dont_sep_offs_move is not None: cmd += f"--dont_sep_offs_move " - if dont_peas is not None: cmd += f"--dont_peas " + if dont_sep_offs_move: cmd += f"--dont_sep_offs_move " + if dont_peas: cmd += f"--dont_peas " - if repol is not None: cmd += f"--repol " + if repol: cmd += f"--repol " if ol_nstd is not None: cmd += f"--ol_nstd={ol_nstd} " if ol_nvox is not None: cmd += f"--ol_nvox={ol_nvox} " if ol_type is not None: cmd += f"--ol_type={ol_type} " if ol_ss is not None: cmd += f"--ol_ss={ol_ss} " - if ol_pos is not None: cmd += f"--ol_pos " - if ol_sqr is not None: cmd += f"--ol_sqr " + if ol_pos: cmd += f"--ol_pos " + if ol_sqr: cmd += f"--ol_sqr " if mporder is not None: cmd += f"--mporder={mporder} " if s2v_niter is not None: cmd += f"--s2v_niter={s2v_niter} " if s2v_lambda is not None: cmd += f"--s2v_lambda={s2v_lambda} " if s2v_interp is not None: cmd += f"--s2v_interp={s2v_interp} " - if estimate_move_by_susceptibility is not None: - cmd += f"--estimate_move_by_susceptibility={int(estimate_move_by_susceptibility)} " + if estimate_move_by_susceptibility: cmd += f"--estimate_move_by_susceptibility " if mbs_niter is not None: cmd += f"--mbs_niter={mbs_niter} " if mbs_lambda is not None: cmd += f"--mbs_lambda={mbs_lambda} " if mbs_ksp is not None: cmd += f"--mbs_ksp={mbs_ksp} " - if cnr_maps is not None: cmd += f"--cnr_maps " - if residuals is not None: cmd += f"--residuals " - if data_is_shelled is not None: cmd += f"--data_is_shelled " + if cnr_maps: cmd += f"--cnr_maps " + if residuals: cmd += f"--residuals " + if data_is_shelled: cmd += f"--data_is_shelled " if nthr is not None: cmd += f"--nthr={nthr} " diff --git a/src/original/MG_LU/TOPED_fsl_runner.py b/src/original/MG_LU/TOPED_fsl_runner.py index 0e488ea..80100f8 100644 --- a/src/original/MG_LU/TOPED_fsl_runner.py +++ b/src/original/MG_LU/TOPED_fsl_runner.py @@ -50,13 +50,13 @@ # │ └── ... (other patient-specific files) # └── common_files (optional) # ├── acqparams.txt -# ├── bvecs -# ├── bvals +# ├── bvecs.txt +# ├── bvals.txt # ├── index.txt # ├── slspec.txt # └── config.cnf -# Note: 'patient_1' etc. are examples and can be any suitable name. +# names of folders are arbitrary. # The names of these folders are copied into the linux working directory and are used to identify the respective entries. # a 'common_files' folder is optional. The purpose is is you e.g. have the same config file or acqparams/bvecs/bvals/index/slspec files for all patients. @@ -198,7 +198,13 @@ def resolve_file(patient_dir, common_dir, key): # main loops start HERE - runs TOPUP and Eddy for each patient folder in main_dir -common_dir = (os.path.join(MAIN_DIR, COMMON_FOLDER_NAME) if COMMON_FOLDER_NAME else None) +common_dir = None +if COMMON_FOLDER_NAME: + _common_path = os.path.join(MAIN_DIR, COMMON_FOLDER_NAME) + if os.path.isdir(_common_path): + common_dir = _common_path + else: + print("note: no common folder found -> checking compulsory files in remaining folders...") # Input file check - check all of main_dir, do the files exist for all patients? # this is so that you wont leeave this overnight and come back to some shit error that could have been prevented. @@ -224,7 +230,7 @@ def resolve_file(patient_dir, common_dir, key): # TOPUP compulsory inputs if not b0_path: issues.append("TOPUP: cannot find b0 file") - if not acqp_path: issues.append("TOPUP: cannot find acqisition parameters file") + if not acqp_path: issues.append("TOPUP: cannot find acquisition parameters file") # EDDY compulsory (only warn if b0 exists, i.e. TOPUP would run) if b0_path: @@ -247,7 +253,7 @@ def resolve_file(patient_dir, common_dir, key): "\n Consider renaming your files to contain the specified keyword or adjust the IDENTIFIERS dict accordingly. " \ "\n TOPUP/Eddy will run only for the 'OK' cases.") else: - print("\n File check passed — all patients have all compulosry inputs.") + print("\n File check passed — all patients have all compulsory inputs.") for patient in os.listdir(MAIN_DIR):