diff --git a/pyproject.toml b/pyproject.toml
index 1c843a1..b17c0b3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -62,6 +62,7 @@ homepage = "https://github.com/neuropoly/totalspineseg"
repository = "https://github.com/neuropoly/totalspineseg"
Dataset101_TotalSpineSeg_step1 = "https://github.com/neuropoly/totalspineseg/releases/download/r20240921/Dataset101_TotalSpineSeg_step1_r20240921.zip"
Dataset102_TotalSpineSeg_step2 = "https://github.com/neuropoly/totalspineseg/releases/download/r20240921/Dataset102_TotalSpineSeg_step2_r20240921.zip"
+Dataset300_SacrumDataset = "https://github.com/neuropoly/totalspineseg/releases/download/sacrum-seg/Dataset300_SacrumDataset.zip"
[project.scripts]
totalspineseg = "totalspineseg.inference:main"
@@ -80,6 +81,7 @@ totalspineseg_crop_image2seg = "totalspineseg.utils.crop_image2seg:main"
totalspineseg_extract_soft = "totalspineseg.utils.extract_soft:main"
totalspineseg_extract_levels = "totalspineseg.utils.extract_levels:main"
totalspineseg_extract_alternate = "totalspineseg.utils.extract_alternate:main"
+totalspineseg_install_weights = "totalspineseg.utils.install_weights:main"
[build-system]
requires = ["pip>=23", "setuptools>=67"]
diff --git a/scripts/generate_sacrum_masks/README.md b/scripts/generate_sacrum_masks/README.md
new file mode 100644
index 0000000..2ef7ec9
--- /dev/null
+++ b/scripts/generate_sacrum_masks/README.md
@@ -0,0 +1,99 @@
+# Generate sacrum masks guides
+
+This file provides the different steps that were carried out to generate sacrum masks for the MRI totalspineseg project.
+
+More information about the context and problems can be found in this [issue](https://github.com/neuropoly/totalspineseg/issues/18).
+
+The main idea was to use open-source datasets with "easy to make" sacrum masks on MRI (T1w and T2w) scans to train a [nnUNetV2](https://github.com/MIC-DKFZ/nnUNet) model that will be able to segment sacrums on the whole-spine dataset and the spider dataset.
+
+# Training
+
+If you want to retrain the model, you can follow these steps.
+
+## I - Dowload the training datasets
+
+To generate the sacrum masks, 3 open-source datasets were used:
+
+| [GoldAtlas](https://zenodo.org/records/583096) | [SynthRAD2023](https://aapm.onlinelibrary.wiley.com/doi/full/10.1002/mp.16529) | [MRSpineSeg](https://paperswithcode.com/dataset/mrspineseg-challenge) |
+| :---: | :---: | :---: |
+| | | |
+
+> These datasets were chosen because they had these sacrum masks available or because they had co-registered MRI and CT images that allowed us to rely on the [CT total segmentator network](https://github.com/wasserth/TotalSegmentator) to generate these labels.
+
+These datasets were BIDSified and stored on our internal servers:
+- SynthRAD2023 (Internal access: `git@data.neuro.polymtl.ca:datasets/synthrad-challenge-2023.git`)
+- MRSpineSeg (Internal access: `git@data.neuro.polymtl.ca:datasets/mrspineseg-challenge-2021.git`)
+- GoldAtlas (Internal access: `git@data.neuro.polymtl.ca:datasets/goldatlas.git`)
+
+## II - Register CT labels to MRI
+
+> For this step, the https://github.com/spinalcordtoolbox/spinalcordtoolbox (SCT) was used.
+
+As specified before, some sacrum masks were generated using the [CT total segmentator network](https://github.com/wasserth/TotalSegmentator) but due to slightly different image shape between MRI (T1w and T2w) and CT scans (see [issue](https://github.com/neuropoly/totalspineseg/issues/18)), CT segmentations were registered to MRI space. To do that, the script `totalspineseg/utils/register_CT_seg_to_MR.py` was used on the three datasets.
+
+> Registration was also performed on the dataset `MRSpineSeg` due to slightly different q-form and s-form between segmentations and images.
+
+```bash
+python "$TOTALSPINESEG/totalspineseg/utils/register_CT_to_MR.py" --path-img
+```
+
+## III - Generate a config file to select the data for training
+
+To select the data used for training, a [config file](https://github.com/spinalcordtoolbox/disc-labeling-hourglass/issues/25#issuecomment-1695818382) was used.
+
+First fetch the paths to all the sacrum masks that will be used for TRAINING/VALIDATION/TESTING. The datasets should be stored inside the same parent folder
+
+> Run this following command in the parent folder folder of the datasets.
+
+```bash
+find ~+ -type f -name *_seg.nii.gz | grep -v CT | sort > train_sacrum.txt
+```
+
+Then run this command to generate the JSON config file.
+
+```bash
+python "$TOTALSPINESEG/totalspineseg/data_management/init_data_config.py" --txt train_sacrum.txt --type LABEL --split-validation SPLIT_VAL --split-test SPLIT_TEST
+```
+
+With `SPLIT_VAL` the fraction of the data used for validation and `SPLIT_TEST` the fraction of the data used for testing.
+
+Finally, to organize your data according to nnUNetV2 format, run this last command.
+
+```bash
+export nnUNet_raw="$TOTALSPINESEG_DATA"/nnUNet/raw
+python "$TOTALSPINESEG/totalspineseg/data_management/convert_config_to_nnunet.py" --config train_sacrum.json --path-out "$nnUNet_raw" -dnum 300
+```
+
+## IV - Train with nnUNetV2
+
+> Regarding nnUNetV2 installation and general usage, please check https://github.com/ivadomed/utilities/blob/main/quick_start_guides/nnU-Net_quick_start_guide.md
+
+Now that your data is ready, you can run nnUNetV2 preprocessing
+
+```bash
+export nnUNet_preprocessed="$TOTALSPINESEG_DATA"/nnUNet/preprocessed
+export nnUNet_results="$TOTALSPINESEG_DATA"/nnUNet/results/sacrum
+nnUNetv2_plan_and_preprocess -d 300 --verify_dataset_integrity -c 3d_fullres
+```
+
+Then train using this command
+
+```bash
+CUDA_VISIBLE_DEVICES= nnUNetv2_train 300 3d_fullres 0
+```
+
+# Inference on whole-spine and spider dataset
+
+To run nnUNetV2's inference, keep the largest component and store the data according to BIDS standard you must run:
+
+> Before running you must download the datasets `whole-spine` and `spider-challenge-2023` and update the variable `DATASETS_PATH` inside the config file `totalspineseg/resources/configs/test_sacrum.json`.
+> This last path corresponds to the parent folder of the two datasets `whole-spine` and `spider-challenge-2023`.
+
+```bash
+bash "$TOTALSPINESEG/scripts/generate_sacrum_masks/generate_sacrum.sh"
+```
+
+
+
+
+
diff --git a/scripts/generate_sacrum_masks/generate_sacrum.sh b/scripts/generate_sacrum_masks/generate_sacrum.sh
new file mode 100644
index 0000000..1e2b030
--- /dev/null
+++ b/scripts/generate_sacrum_masks/generate_sacrum.sh
@@ -0,0 +1,138 @@
+#!/bin/bash
+
+# This script calls nnUNetV2's inference to generate sacrum masks using a JSON config file (see totalspineseg/ressources/configs) and saves labels following BIDS' convention.
+
+# The following variables and paths MUST be updated before running the script:
+# - PATH_CONFIG: to the config file `test_sacrum.json`
+# - DERIVATIVE_FOLDER: name of the derivative folder (default=labels)
+# - PATH_REPO: to the repository
+# - PATH_NNUNET_MODEL: to the nnunet model Dataset300_SacrumDataset
+# - AUTHOR: the author
+
+# The totalspineseg environment must be activated before running the script
+
+# Uncomment for full verbose
+# set -x
+
+# Immediately exit if error
+set -e -o pipefail
+
+# Exit if user presses CTRL+C (Linux) or CMD+C (OSX)
+trap "echo Caught Keyboard Interrupt within script. Exiting now.; exit" INT
+
+# GET PARAMS
+# ======================================================================================================================
+# SET DEFAULT VALUES FOR PARAMETERS.
+# ----------------------------------------------------------------------------------------------------------------------
+PATH_CONFIG="$TOTALSPINESEG/totalspineseg/resources/configs/test_sacrum.json"
+
+LABEL_SUFFIX="_label-sacrum_seg"
+PATH_REPO="$TOTALSPINESEG"
+NNUNET_RESULTS="$TOTALSPINESEG_DATA/nnUNet/results/sacrum"
+NNUNET_EXPORTS="$TOTALSPINESEG_DATA/nnUNet/exports"
+NNUNET_MODEL="Dataset300_SacrumDataset"
+PATH_NNUNET_MODEL="$NNUNET_RESULTS/$NNUNET_MODEL/nnUNetTrainer__nnUNetPlans__3d_fullres/"
+ZIP_URL="https://github.com/neuropoly/totalspineseg/releases/download/sacrum-seg/Dataset300_SacrumDataset.zip"
+PROCESS="nnUNet3D"
+DERIVATIVE_FOLDER="labels"
+FOLD="0"
+
+# Print variables to allow easier debug
+echo "See variables:"
+echo "PATH_CONFIG: ${PATH_CONFIG}"
+echo "DERIVATIVE_FOLDER: ${DERIVATIVE_FOLDER}"
+echo "LABEL_SUFFIX: ${LABEL_SUFFIX}"
+echo
+echo "PATH_REPO: ${PATH_REPO}"
+echo "NNUNET_RESULTS: ${NNUNET_RESULTS}"
+echo "NNUNET_EXPORTS: ${NNUNET_EXPORTS}"
+echo "NNUNET_MODEL: ${NNUNET_MODEL}"
+echo "FOLD: ${FOLD}"
+echo
+
+# FUNCTIONS
+# ======================================================================================================================
+# Segment sacrum using our nnUNet model
+segment_sacrum_nnUNet(){
+ local file_in="$1"
+ local file_out="$2"
+ local nnunet_model="$3"
+ local fold="$4"
+
+ # Call python function
+ python3 "${PATH_REPO}"/totalspineseg/utils/run_nnunet_inference_single_subject.py -i "${file_in}" -o "${file_out}" -path-model "${nnunet_model}" -fold "${fold}" -use-gpu -use-best-checkpoint
+}
+
+# Generate a json sidecar file
+generate_json(){
+ local path_json="$1"
+ local process="$2"
+
+ # Call python function
+ python3 "${PATH_REPO}"/totalspineseg/utils/create_json_sidecar.py -path-json "${path_json}" -process "${process}"
+}
+
+# Keep largest component only
+keep_largest_component(){
+ local seg_in="$1"
+ local seg_out="$2"
+
+ # Call python function
+ python3 "${PATH_REPO}"/totalspineseg/utils/largest_component_filewise.py --seg-in "${seg_in}" --seg-out "${seg_out}"
+}
+
+# Keep largest component only
+download_weights(){
+ local dataset="$1"
+ local url="$2"
+ local results_path="$3"
+ local exports_path="$4"
+
+ # Call python function
+ totalspineseg_install_weights --nnunet-dataset "${dataset}" --zip-url "${url}" --results-folder "${results_path}" --exports-folder "${exports_path}"
+}
+
+# ======================================================================================================================
+# SCRIPT STARTS HERE
+# ======================================================================================================================
+# Fetch datasets path
+DATASETS_PATH=$(jq -r '.DATASETS_PATH' "${PATH_CONFIG}")
+
+# Go to folder where data will be copied and processed
+cd "$DATASETS_PATH"
+
+# Fetch TESTING files
+FILES=$(jq -r '.TESTING[]' "${PATH_CONFIG}")
+
+# Download and install nnUNet weights
+download_weights "$NNUNET_MODEL" "$ZIP_URL" "$NNUNET_RESULTS" "$NNUNET_EXPORTS"
+
+# Loop across the files
+for FILE_PATH in $FILES; do
+ BIDS_FOLDER=$(echo "$FILE_PATH" | cut -d / -f 1)
+ IN_FILE_NAME=$(echo "$FILE_PATH" | awk -F / '{print $NF}' )
+ OUT_FILE_NAME=${IN_FILE_NAME/".nii.gz"/"${LABEL_SUFFIX}.nii.gz"}
+ IMG_PATH=${FILE_PATH/"${BIDS_FOLDER}/"/}
+ SUB_PATH=${IMG_PATH/"/${IN_FILE_NAME}"/}
+ BIDS_DERIVATIVES="${BIDS_FOLDER}/derivatives/${DERIVATIVE_FOLDER}"
+ OUT_FOLDER="${BIDS_DERIVATIVES}/${SUB_PATH}"
+ OUT_PATH="${OUT_FOLDER}/${OUT_FILE_NAME}"
+
+ # Create DERIVATIVES_FOLDER if missing
+ if [[ ! -d ${OUT_FOLDER} ]]; then
+ echo "Creating folders $OUT_FOLDER"
+ mkdir -p "${OUT_FOLDER}"
+ fi
+
+ # Generate output segmentation
+ echo "Generate segmentation ${FILE_PATH} ${OUT_PATH}"
+ segment_sacrum_nnUNet "$FILE_PATH" "$OUT_PATH" "$PATH_NNUNET_MODEL" "$FOLD"
+ keep_largest_component "$OUT_PATH" "$OUT_PATH"
+
+ # Generate json sidecar
+ JSON_PATH=${OUT_PATH/".nii.gz"/".json"}
+ echo "Generate jsonsidecar ${JSON_PATH}"
+ generate_json "$JSON_PATH" "$PROCESS"
+
+done
+
diff --git a/totalspineseg/__init__.py b/totalspineseg/__init__.py
index 8feb9e9..b4c49f6 100644
--- a/totalspineseg/__init__.py
+++ b/totalspineseg/__init__.py
@@ -12,4 +12,5 @@
from .utils.preview_jpg import preview_jpg_mp
from .utils.reorient_canonical import reorient_canonical_mp
from .utils.resample import resample, resample_mp
-from .utils.transform_seg2image import transform_seg2image, transform_seg2image_mp
\ No newline at end of file
+from .utils.transform_seg2image import transform_seg2image, transform_seg2image_mp
+from .utils.install_weights import install_weights
\ No newline at end of file
diff --git a/totalspineseg/data_management/convert_config_to_nnunet.py b/totalspineseg/data_management/convert_config_to_nnunet.py
new file mode 100644
index 0000000..01ded5f
--- /dev/null
+++ b/totalspineseg/data_management/convert_config_to_nnunet.py
@@ -0,0 +1,200 @@
+"""
+This script is based on https://github.com/ivadomed/utilities/blob/main/dataset_conversion/convert_bids_to_nnUNetV2.py
+
+Converts BIDS-structured dataset to the nnUNetv2 dataset format. Full details about
+the format can be found here: https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/dataset_format.md
+
+Naga Karthik, Jan Valosek, Théo Mathieu modified by Nathan Molinier
+"""
+import argparse
+import pathlib
+from pathlib import Path
+import json
+import os
+from collections import OrderedDict
+
+from totalspineseg.data_management.utils import CONTRAST, get_img_path_from_label_path, fetch_subject_and_session, fetch_contrast
+from totalspineseg.utils.image import Image
+
+
+def get_parser():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(description='Convert BIDS-structured dataset to nnUNetV2 database format.')
+ parser.add_argument('--config', required=True, help='Config JSON file where every label used for TRAINING, VALIDATION and TESTING has its path specified ~//config_data.json (Required)')
+ parser.add_argument('--path-out', required=True, help='Path to output directory. Example: ~/data/dataset-nnunet (Required)')
+ parser.add_argument('--dataset-name', '-dname', default='SacrumDataset', type=str,
+ help='Specify the task name. (Default=SacrumDataset)')
+ parser.add_argument('--dataset-number', '-dnum', default=501, type=int,
+ help='Specify the task number, has to be greater than 500 but less than 999. (Default=501)')
+ parser.add_argument('--registered', default=False, type=bool,
+ help='Set this variable to True if all the modalities/contrasts are available and corregistered for every subject (Default=False)')
+ return parser
+
+
+def convert_subjects(list_labels, path_out_images, path_out_labels, channel_dict, DS_name, counter_indent=0):
+ """Convert an image from an original BIDS dataset to nnunet format modify.
+
+ Args:
+ list_labels (list): List containing the paths of training/testing labels in the BIDS format.
+ path_out_images (str): path to the images directory in the new dataset (test or train).
+ path_out_labels (str): path to the labels directory in the new dataset (test or train).
+ channel_dict (dict): Association dictionary between MRI contrasts and integer values compatible with nnUNet documentation (ex: T1w = 1, T2w = 2, FLAIR = 3).
+ DS_name (str): Dataset name.
+ counter_indent (int): indent for file numbering.
+
+ Returns:
+ counter (int): Last file number used
+
+ """
+ counter = counter_indent
+
+ for label_path in list_labels:
+ img_path = get_img_path_from_label_path(label_path)
+ if not os.path.exists(img_path) or not os.path.exists(label_path):
+ print(f'Error while loading subject\n {img_path} or {label_path} might not exist --> skipping subject')
+ else:
+ # Load and reorient image and label to RPI
+ label = Image(label_path).change_orientation('RPI')
+ img = Image(img_path).change_orientation('RPI')
+
+ if img.data.shape == label.data.shape:
+ # Increment counter for every path --> different from nnunet conventional use where the same number is the same for every subject (but need full registration)
+ # TODO: fix this case to keep the subject number for each contrast
+ counter+=1
+
+ # Extract information from the img_path
+ sub_name, sessionID, filename, modality = fetch_subject_and_session(img_path)
+
+ # Extract contrast from channel_dict
+ if 'multi_contrasts' in channel_dict.keys():
+ contrast = 'multi_contrasts'
+ else:
+ contrast = fetch_contrast(img_path)
+
+ # Create new nnunet paths
+ nnunet_label_path = os.path.join(path_out_labels, f"{DS_name}-{sub_name}_{counter:03d}.nii.gz")
+ nnunet_img_path = os.path.join(path_out_images, f"{DS_name}-{sub_name}_{counter:03d}_{channel_dict[contrast]:04d}.nii.gz")
+
+ # Save images
+ label.save(nnunet_label_path)
+ img.save(nnunet_img_path)
+ else:
+ print(f'Error while loading subject\n {img_path} and {label_path} don"t have the same shape --> skipping subject')
+ return counter
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+ DS_name = args.dataset_name
+ path_out = Path(os.path.join(os.path.abspath(os.path.expanduser(args.path_out)),
+ f'Dataset{args.dataset_number:03d}_{args.dataset_name}'))
+ # Read json file and create a dictionary
+ with open(args.config, "r") as file:
+ config = json.load(file)
+ if config['TYPE'] != 'LABEL':
+ raise ValueError('Type error: please specify LABEL paths')
+ if 'DATASETS_PATH' in config.keys():
+ config['TRAINING'] = [os.path.join(config['DATASETS_PATH'], rel_path) for rel_path in config['TRAINING']]
+ config['VALIDATION'] = [os.path.join(config['DATASETS_PATH'], rel_path) for rel_path in config['VALIDATION']]
+ config['TESTING'] = [os.path.join(config['DATASETS_PATH'], rel_path) for rel_path in config['TESTING']]
+
+ # To use channel dict with different modalities/contrasts, images need to be corregistered and all modalities/contrasts
+ # need to be available.
+ channel_dict = {}
+ if args.registered:
+ for i, contrast in enumerate(CONTRAST[config['CONTRASTS']]):
+ channel_dict[contrast] = i
+ else:
+ channel_dict['multi_contrasts'] = 0
+
+ # create individual directories for train and test images and labels
+ path_out_imagesTr = Path(os.path.join(path_out, 'imagesTr'))
+ path_out_imagesTs = Path(os.path.join(path_out, 'imagesTs'))
+ path_out_labelsTr = Path(os.path.join(path_out, 'labelsTr'))
+ path_out_labelsTs = Path(os.path.join(path_out, 'labelsTs'))
+
+ train_labels = config['TRAINING'] + config['VALIDATION']
+ test_labels = config['TESTING']
+
+ # make the directories
+ pathlib.Path(path_out).mkdir(parents=True, exist_ok=True)
+ pathlib.Path(path_out_imagesTr).mkdir(parents=True, exist_ok=True)
+ pathlib.Path(path_out_imagesTs).mkdir(parents=True, exist_ok=True)
+ pathlib.Path(path_out_labelsTr).mkdir(parents=True, exist_ok=True)
+ pathlib.Path(path_out_labelsTs).mkdir(parents=True, exist_ok=True)
+
+ # Convert training and validation subjects to nnunet format
+ counter_train = convert_subjects(list_labels=train_labels,
+ path_out_images=path_out_imagesTr,
+ path_out_labels=path_out_labelsTr,
+ channel_dict=channel_dict,
+ DS_name=DS_name)
+
+ # Convert testing subjects to nnunet format
+ counter_test = convert_subjects(list_labels=test_labels,
+ path_out_images=path_out_imagesTs,
+ path_out_labels=path_out_labelsTs,
+ channel_dict=channel_dict,
+ DS_name=DS_name,
+ counter_indent=counter_train)
+
+ print(f"Number of training and validation subjects: {counter_train}")
+ print(f"Number of test subjects: {counter_test-counter_train}")
+
+ # c.f. dataset json generation
+ # In nnUNet V2, dataset.json file has become much shorter. The description of the fields and changes
+ # can be found here: https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/dataset_format.md#datasetjson
+ # this file can be automatically generated using the following code here:
+ # https://github.com/MIC-DKFZ/nnUNet/blob/master/nnunetv2/dataset_conversion/generate_dataset_json.py
+ # example: https://github.com/MIC-DKFZ/nnUNet/blob/master/nnunet/dataset_conversion/Task055_SegTHOR.py
+
+ json_dict = OrderedDict()
+
+ # The following keys are the most important ones.
+ """
+ channel_names:
+ Channel names must map the index to the name of the channel. For BIDS, this refers to the contrast suffix.
+ {
+ "0": "FLAIR",
+ "1": "T1w",
+ "2": "T2",
+ "3": "T2w"
+ }
+ Note that the channel names may influence the normalization scheme!! Learn more in the documentation.
+
+ labels:
+ This will tell nnU-Net what labels to expect. Important: This will also determine whether you use region-based
+ training or not.
+ Example regular labels:
+ {
+ 'background': 0,
+ 'left atrium': 1,
+ 'some other label': 2
+ }
+ Example region-based training:
+ https://github.com/MIC-DKFZ/nnUNet/blob/master/documentation/region_based_training.md
+ {
+ 'background': 0,
+ 'whole tumor': (1, 2, 3),
+ 'tumor core': (2, 3),
+ 'enhancing tumor': 3
+ }
+ Remember that nnU-Net expects consecutive values for labels! nnU-Net also expects 0 to be background!
+ """
+
+ json_dict['channel_names'] = {v: k for k, v in channel_dict.items()}
+
+ json_dict['labels'] = {"background": 0, "sacrum": 1}
+
+ json_dict["numTraining"] = counter_train
+
+ # Needed for finding the files correctly. IMPORTANT! File endings must match between images and segmentations!
+ json_dict['file_ending'] = ".nii.gz"
+ json_dict["overwrite_image_reader_writer"] = "SimpleITKIO"
+
+ # create dataset.json
+ json.dump(json_dict, open(os.path.join(path_out, "dataset.json"), "w"), indent=4)
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/totalspineseg/data_management/init_data_config.py b/totalspineseg/data_management/init_data_config.py
new file mode 100644
index 0000000..089607f
--- /dev/null
+++ b/totalspineseg/data_management/init_data_config.py
@@ -0,0 +1,105 @@
+"""
+Script based on https://github.com/spinalcordtoolbox/disc-labeling-hourglass
+"""
+
+import os
+import argparse
+import random
+import json
+import itertools
+import numpy as np
+
+from utils import CONTRAST, get_img_path_from_label_path, fetch_contrast
+
+CONTRAST_LOOKUP = {tuple(sorted(value)): key for key, value in CONTRAST.items()}
+
+def init_data_config(args):
+ """
+ Create a JSON configuration file from a TXT file where images paths are specified
+ """
+ if (args.split_validation + args.split_test) > 1:
+ raise ValueError("The sum of the ratio between testing and validation cannot exceed 1")
+
+ # Get input paths, could be label files or image files,
+ # and make sure they all exist.
+ file_paths = [os.path.abspath(path.replace('\n', '')) for path in open(args.txt)]
+ if args.type == 'LABEL':
+ label_paths = file_paths
+ img_paths = [get_img_path_from_label_path(lp) for lp in label_paths]
+ file_paths = label_paths + img_paths
+ elif args.type == 'IMAGE':
+ img_paths = file_paths
+ else:
+ raise ValueError(f"invalid args.type: {args.type}")
+ missing_paths = [
+ path for path in file_paths
+ if not os.path.isfile(path)
+ ]
+
+ if missing_paths:
+ raise ValueError("missing files:\n" + '\n'.join(missing_paths))
+
+ # Extract BIDS parent folder path
+ dataset_parent_path_list = ['/'.join(path.split('/sub')[0].split('/')[:-1]) for path in img_paths]
+
+ # Check if all the BIDS folders are stored inside the same parent repository
+ if (np.array(dataset_parent_path_list) == dataset_parent_path_list[0]).all():
+ dataset_parent_path = dataset_parent_path_list[0]
+ else:
+ raise ValueError('Please store all the BIDS datasets inside the same parent folder !')
+
+ # Look up the right code for the set of contrasts present
+ contrasts = CONTRAST_LOOKUP[tuple(sorted(set(map(fetch_contrast, img_paths))))]
+
+ config = {
+ 'TYPE': args.type,
+ 'CONTRASTS': contrasts,
+ 'DATASETS_PATH': dataset_parent_path
+ }
+
+ # Split into training, validation, and testing sets
+ split_ratio = (1 - (args.split_validation + args.split_test), args.split_validation, args.split_test) # TRAIN, VALIDATION, and TEST
+ config_paths = label_paths if args.type == 'LABEL' else img_paths
+ config_paths = [path.split(dataset_parent_path + '/')[-1] for path in config_paths] # Remove DATASETS_PATH
+ random.shuffle(config_paths)
+ splits = [0] + [
+ int(len(config_paths) * ratio)
+ for ratio in itertools.accumulate(split_ratio)
+ ]
+ for key, (begin, end) in zip(
+ ['TRAINING', 'VALIDATION', 'TESTING'],
+ pairwise(splits),
+ ):
+ config[key] = config_paths[begin:end]
+
+ # Save the config
+ config_path = args.txt.replace('.txt', '') + '.json'
+ json.dump(config, open(config_path, 'w'), indent=4)
+
+def pairwise(iterable):
+ # pairwise('ABCDEFG') --> AB BC CD DE EF FG
+ # based on https://docs.python.org/3.11/library/itertools.html
+ a, b = itertools.tee(iterable)
+ next(b, None)
+ return zip(a, b)
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser(description='Create config JSON from a TXT file which contains list of paths')
+
+ ## Parameters
+ parser.add_argument('--txt', required=True,
+ help='Path to TXT file that contains only image or label paths. (Required)')
+ parser.add_argument('--type', choices=('LABEL', 'IMAGE'),
+ help='Type of paths specified. Choices "LABEL" or "IMAGE". (Required)')
+ parser.add_argument('--split-validation', type=float, default=0.1,
+ help='Split ratio for validation. Default=0.1')
+ parser.add_argument('--split-test', type=float, default=0.1,
+ help='Split ratio for testing. Default=0.1')
+
+ args = parser.parse_args()
+
+ if args.split_test > 0.9:
+ args.split_validation = 1 - args.split_test
+
+ init_data_config(args)
diff --git a/totalspineseg/data_management/utils.py b/totalspineseg/data_management/utils.py
new file mode 100644
index 0000000..787a80a
--- /dev/null
+++ b/totalspineseg/data_management/utils.py
@@ -0,0 +1,87 @@
+"""
+Script based on https://github.com/spinalcordtoolbox/disc-labeling-hourglass
+"""
+
+import os
+import re
+from pathlib import Path
+
+## Variables
+CONTRAST = {'t1': ['T1w'],
+ 't2': ['T2w'],
+ 't2s':['T2star'],
+ 't1_t2': ['T1w', 'T2w'],
+ 'psir': ['PSIR'],
+ 'stir': ['STIR'],
+ 'psir_stir': ['PSIR', 'STIR'],
+ 't1_t2_psir_stir': ['T1w', 'T2w', 'PSIR', 'STIR']
+ }
+
+## Functions
+def get_img_path_from_label_path(str_path):
+ """
+ This function does 2 things: ⚠️ Files need to be stored in a BIDS compliant dataset
+ - Step 1: Remove label suffix (e.g. "_labels-disc-manual"). The suffix is always between the MRI contrast and the file extension.
+ - Step 2: Remove derivatives path (e.g. derivatives/labels/). The first folders is always called derivatives but the second may vary (e.g. labels_soft)
+
+ :param path: absolute path to the label img. Example: //derivatives/labels/sub-amuALT/anat/sub-amuALT_T1w_labels-disc-manual.nii.gz
+ :return: img path. Example: //sub-amuALT/anat/sub-amuALT_T1w.nii.gz
+ Copied from https://github.com/spinalcordtoolbox/disc-labeling-hourglass
+
+ """
+ # Load path
+ path = Path(str_path)
+
+ # Extract file extension
+ ext = ''.join(path.suffixes)
+
+ # Get img name
+ img_name = '_'.join(path.name.split('_')[:-1]) + ext
+
+ # Create a list of the directories
+ dir_list = str(path.parent).split('/')
+
+ # Remove "derivatives" and "labels" folders
+ derivatives_idx = dir_list.index('derivatives')
+ dir_path = '/'.join(dir_list[0:derivatives_idx] + dir_list[derivatives_idx+2:])
+
+ # Recreate img path
+ img_path = os.path.join(dir_path, img_name)
+
+ return img_path
+
+def fetch_subject_and_session(filename_path):
+ """
+ Get subject ID, session ID and filename from the input BIDS-compatible filename or file path
+ The function works both on absolute file path as well as filename
+ :param filename_path: input nifti filename (e.g., sub-001_ses-01_T1w.nii.gz) or file path
+ (e.g., /home/user/MRI/bids/derivatives/labels/sub-001/ses-01/anat/sub-001_ses-01_T1w.nii.gz
+ :return: subjectID: subject ID (e.g., sub-001)
+ :return: sessionID: session ID (e.g., ses-01)
+ :return: filename: nii filename (e.g., sub-001_ses-01_T1w.nii.gz)
+ Copied from https://github.com/spinalcordtoolbox/manual-correction
+ """
+
+ _, filename = os.path.split(filename_path) # Get just the filename (i.e., remove the path)
+ subject = re.search('sub-(.*?)[_/]', filename_path) # [_/] means either underscore or slash
+ subjectID = subject.group(0)[:-1] if subject else "" # [:-1] removes the last underscore or slash
+
+ session = re.search('ses-(.*?)[_/]', filename_path) # [_/] means either underscore or slash
+ sessionID = session.group(0)[:-1] if session else "" # [:-1] removes the last underscore or slash
+ # REGEX explanation
+ # . - match any character (except newline)
+ # *? - match the previous element as few times as possible (zero or more times)
+
+ contrast = 'dwi' if 'dwi' in filename_path else 'anat' # Return contrast (dwi or anat)
+
+ return subjectID, sessionID, filename, contrast
+
+
+def fetch_contrast(filename_path):
+ '''
+ Extract MRI contrast from a BIDS-compatible filename/filepath
+ The function handles images only.
+ :param filename_path: image file path or file name. (e.g sub-001_ses-01_T1w.nii.gz)
+ Copied from https://github.com/spinalcordtoolbox/disc-labeling-hourglass
+ '''
+ return filename_path.rstrip(''.join(Path(filename_path).suffixes)).split('_')[-1]
\ No newline at end of file
diff --git a/totalspineseg/inference.py b/totalspineseg/inference.py
index 4956491..3c6f224 100644
--- a/totalspineseg/inference.py
+++ b/totalspineseg/inference.py
@@ -164,29 +164,13 @@ def main():
# Installing the pretrained models if not already installed
for dataset, zip_url in [(step1_dataset, step1_zip_url), (step2_dataset, step2_zip_url)]:
- if not (nnUNet_results / dataset).is_dir():
- # Get the zip file name and path
- zip_name = zip_url.split('/')[-1]
- zip_file = nnUNet_exports / zip_name
-
- # Check if the zip file exists
- if not zip_file.is_file():
- # If the zip file is not found, download it from the releases
- if not quiet: print(f'Downloading the pretrained model from {zip_url}...')
- with tqdm(unit='B', unit_scale=True, miniters=1, unit_divisor=1024, disable=quiet) as pbar:
- urlretrieve(
- zip_url,
- nnUNet_exports / zip_name,
- lambda b, bsize, tsize=None: (pbar.total == tsize or pbar.reset(tsize)) and pbar.update(b * bsize - pbar.n),
- )
-
- if not zip_file.is_file():
- raise FileNotFoundError(f'Could not download the pretrained model for {dataset}.')
-
- # If the pretrained model is not installed, install it from zip
- if not quiet: print(f'Installing the pretrained model from {zip_file}...')
- # Install the pretrained model from the zip file
- subprocess.run(['nnUNetv2_install_pretrained_model_from_zip', str(zip_file)])
+ install_weights(
+ nnunet_dataset=dataset,
+ zip_url=zip_url,
+ results_folder=nnUNet_results,
+ exports_folder=nnUNet_exports,
+ quiet=quiet
+ )
if not quiet: print('\n' 'Making input dir with _0000 suffix:')
if input_path.name.endswith('.nii.gz'):
diff --git a/totalspineseg/resources/configs/test_sacrum.json b/totalspineseg/resources/configs/test_sacrum.json
new file mode 100644
index 0000000..aec5ecb
--- /dev/null
+++ b/totalspineseg/resources/configs/test_sacrum.json
@@ -0,0 +1,462 @@
+{
+ "TYPE": "IMAGE",
+ "CONTRASTS": "t1_t2",
+ "DATASETS_PATH": "/home/GRAMES.POLYMTL.CA/p118739/data/datasets",
+ "TRAINING": [],
+ "VALIDATION": [],
+ "TESTING": [
+ "spider-challenge-2023/sub-053/anat/sub-053_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-112/anat/sub-112_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-030/anat/sub-030_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-167/anat/sub-167_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-100/anat/sub-100_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-098/anat/sub-098_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-146/anat/sub-146_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-093/anat/sub-093_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-078/anat/sub-078_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-004/anat/sub-004_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-088/anat/sub-088_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-242/anat/sub-242_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-005/anat/sub-005_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-050/anat/sub-050_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-160/anat/sub-160_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-162/anat/sub-162_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-060/anat/sub-060_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-163/anat/sub-163_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-012/anat/sub-012_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-145/anat/sub-145_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-207/anat/sub-207_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-053/anat/sub-053_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-198/anat/sub-198_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-081/anat/sub-081_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-038/anat/sub-038_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-136/anat/sub-136_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-209/anat/sub-209_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-155/anat/sub-155_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-007/anat/sub-007_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-011/anat/sub-011_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-109/anat/sub-109_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-244/anat/sub-244_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-015/anat/sub-015_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-190/anat/sub-190_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-011/anat/sub-011_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-159/anat/sub-159_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-145/anat/sub-145_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-220/anat/sub-220_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-156/anat/sub-156_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-253/anat/sub-253_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-077/anat/sub-077_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-202/anat/sub-202_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-201/anat/sub-201_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-223/anat/sub-223_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-252/anat/sub-252_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-002/anat/sub-002_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-015/anat/sub-015_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-035/anat/sub-035_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-189/anat/sub-189_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-215/anat/sub-215_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-066/anat/sub-066_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-056/anat/sub-056_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-181/anat/sub-181_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-034/anat/sub-034_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-225/anat/sub-225_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-204/anat/sub-204_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-089/anat/sub-089_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-144/anat/sub-144_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-165/anat/sub-165_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-177/anat/sub-177_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-205/anat/sub-205_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-123/anat/sub-123_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-069/anat/sub-069_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-094/anat/sub-094_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-075/anat/sub-075_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-065/anat/sub-065_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-186/anat/sub-186_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-197/anat/sub-197_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-115/anat/sub-115_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-188/anat/sub-188_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-113/anat/sub-113_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-086/anat/sub-086_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-200/anat/sub-200_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-090/anat/sub-090_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-249/anat/sub-249_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-175/anat/sub-175_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-035/anat/sub-035_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-072/anat/sub-072_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-024/anat/sub-024_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-117/anat/sub-117_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-233/anat/sub-233_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-105/anat/sub-105_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-251/anat/sub-251_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-071/anat/sub-071_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-008/anat/sub-008_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-048/anat/sub-048_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-008/anat/sub-008_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-055/anat/sub-055_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-218/anat/sub-218_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-203/anat/sub-203_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-209/anat/sub-209_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-209/anat/sub-209_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-234/anat/sub-234_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-229/anat/sub-229_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-052/anat/sub-052_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-134/anat/sub-134_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-207/anat/sub-207_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-034/anat/sub-034_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-063/anat/sub-063_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-061/anat/sub-061_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-231/anat/sub-231_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-166/anat/sub-166_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-210/anat/sub-210_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-138/anat/sub-138_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-237/anat/sub-237_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-001/anat/sub-001_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-203/anat/sub-203_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-226/anat/sub-226_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-202/anat/sub-202_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-166/anat/sub-166_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-257/anat/sub-257_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-007/anat/sub-007_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-200/anat/sub-200_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-009/anat/sub-009_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-064/anat/sub-064_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-020/anat/sub-020_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-214/anat/sub-214_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-220/anat/sub-220_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-035/anat/sub-035_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-062/anat/sub-062_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-245/anat/sub-245_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-133/anat/sub-133_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-192/anat/sub-192_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-011/anat/sub-011_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-081/anat/sub-081_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-161/anat/sub-161_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-255/anat/sub-255_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-031/anat/sub-031_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-030/anat/sub-030_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-171/anat/sub-171_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-252/anat/sub-252_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-100/anat/sub-100_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-099/anat/sub-099_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-071/anat/sub-071_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-108/anat/sub-108_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-185/anat/sub-185_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-221/anat/sub-221_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-074/anat/sub-074_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-106/anat/sub-106_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-172/anat/sub-172_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-174/anat/sub-174_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-104/anat/sub-104_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-037/anat/sub-037_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-044/anat/sub-044_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-227/anat/sub-227_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-086/anat/sub-086_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-017/anat/sub-017_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-108/anat/sub-108_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-171/anat/sub-171_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-236/anat/sub-236_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-217/anat/sub-217_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-253/anat/sub-253_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-045/anat/sub-045_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-007/anat/sub-007_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-245/anat/sub-245_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-090/anat/sub-090_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-146/anat/sub-146_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-208/anat/sub-208_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-031/anat/sub-031_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-210/anat/sub-210_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-141/anat/sub-141_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-137/anat/sub-137_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-241/anat/sub-241_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-109/anat/sub-109_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-226/anat/sub-226_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-017/anat/sub-017_acq-lowresSag_T1w.nii.gz",
+ "whole-spine/sub-amuVG/anat/sub-amuVG_T2w.nii.gz",
+ "spider-challenge-2023/sub-117/anat/sub-117_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-057/anat/sub-057_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-156/anat/sub-156_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-085/anat/sub-085_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-168/anat/sub-168_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-151/anat/sub-151_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-028/anat/sub-028_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-228/anat/sub-228_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-032/anat/sub-032_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-217/anat/sub-217_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-065/anat/sub-065_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-189/anat/sub-189_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-191/anat/sub-191_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-193/anat/sub-193_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-019/anat/sub-019_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-057/anat/sub-057_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-232/anat/sub-232_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-010/anat/sub-010_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-059/anat/sub-059_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-166/anat/sub-166_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-033/anat/sub-033_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-061/anat/sub-061_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-143/anat/sub-143_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-062/anat/sub-062_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-116/anat/sub-116_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-200/anat/sub-200_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-122/anat/sub-122_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-006/anat/sub-006_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-167/anat/sub-167_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-142/anat/sub-142_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-165/anat/sub-165_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-154/anat/sub-154_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-078/anat/sub-078_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-163/anat/sub-163_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-095/anat/sub-095_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-104/anat/sub-104_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-039/anat/sub-039_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-181/anat/sub-181_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-059/anat/sub-059_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-022/anat/sub-022_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-140/anat/sub-140_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-018/anat/sub-018_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-255/anat/sub-255_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-234/anat/sub-234_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-115/anat/sub-115_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-129/anat/sub-129_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-187/anat/sub-187_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-012/anat/sub-012_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-169/anat/sub-169_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-023/anat/sub-023_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-196/anat/sub-196_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-195/anat/sub-195_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-184/anat/sub-184_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-152/anat/sub-152_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-239/anat/sub-239_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-085/anat/sub-085_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-170/anat/sub-170_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-096/anat/sub-096_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-024/anat/sub-024_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-198/anat/sub-198_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-132/anat/sub-132_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-215/anat/sub-215_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-080/anat/sub-080_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-161/anat/sub-161_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-251/anat/sub-251_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-013/anat/sub-013_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-023/anat/sub-023_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-231/anat/sub-231_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-048/anat/sub-048_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-124/anat/sub-124_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-113/anat/sub-113_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-107/anat/sub-107_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-182/anat/sub-182_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-061/anat/sub-061_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-191/anat/sub-191_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-174/anat/sub-174_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-242/anat/sub-242_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-083/anat/sub-083_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-193/anat/sub-193_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-016/anat/sub-016_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-257/anat/sub-257_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-121/anat/sub-121_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-001/anat/sub-001_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-018/anat/sub-018_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-241/anat/sub-241_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-056/anat/sub-056_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-249/anat/sub-249_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-040/anat/sub-040_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-064/anat/sub-064_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-244/anat/sub-244_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-022/anat/sub-022_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-172/anat/sub-172_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-169/anat/sub-169_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-236/anat/sub-236_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-122/anat/sub-122_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-050/anat/sub-050_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-065/anat/sub-065_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-082/anat/sub-082_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-036/anat/sub-036_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-250/anat/sub-250_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-120/anat/sub-120_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-074/anat/sub-074_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-127/anat/sub-127_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-107/anat/sub-107_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-025/anat/sub-025_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-066/anat/sub-066_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-037/anat/sub-037_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-125/anat/sub-125_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-183/anat/sub-183_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-137/anat/sub-137_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-058/anat/sub-058_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-144/anat/sub-144_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-088/anat/sub-088_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-040/anat/sub-040_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-241/anat/sub-241_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-225/anat/sub-225_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-180/anat/sub-180_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-075/anat/sub-075_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-118/anat/sub-118_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-081/anat/sub-081_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-073/anat/sub-073_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-036/anat/sub-036_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-110/anat/sub-110_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-118/anat/sub-118_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-116/anat/sub-116_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-125/anat/sub-125_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-078/anat/sub-078_acq-lowresSag_T2w.nii.gz",
+ "whole-spine/sub-unfErssm012/anat/sub-unfErssm012_T2w.nii.gz",
+ "spider-challenge-2023/sub-243/anat/sub-243_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-118/anat/sub-118_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-097/anat/sub-097_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-213/anat/sub-213_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-250/anat/sub-250_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-196/anat/sub-196_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-091/anat/sub-091_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-155/anat/sub-155_acq-lowresSag_T1w.nii.gz",
+ "whole-spine/sub-unfErssm001/anat/sub-unfErssm001_T1w.nii.gz",
+ "spider-challenge-2023/sub-050/anat/sub-050_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-129/anat/sub-129_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-073/anat/sub-073_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-110/anat/sub-110_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-018/anat/sub-018_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-177/anat/sub-177_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-239/anat/sub-239_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-025/anat/sub-025_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-219/anat/sub-219_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-214/anat/sub-214_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-095/anat/sub-095_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-237/anat/sub-237_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-185/anat/sub-185_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-068/anat/sub-068_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-087/anat/sub-087_acq-lowresSag_T2w.nii.gz",
+ "whole-spine/sub-unfErssm029/anat/sub-unfErssm029_T1w.nii.gz",
+ "spider-challenge-2023/sub-098/anat/sub-098_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-127/anat/sub-127_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-162/anat/sub-162_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-069/anat/sub-069_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-107/anat/sub-107_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-160/anat/sub-160_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-152/anat/sub-152_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-152/anat/sub-152_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-223/anat/sub-223_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-186/anat/sub-186_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-243/anat/sub-243_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-069/anat/sub-069_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-034/anat/sub-034_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-044/anat/sub-044_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-112/anat/sub-112_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-121/anat/sub-121_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-185/anat/sub-185_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-256/anat/sub-256_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-138/anat/sub-138_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-063/anat/sub-063_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-224/anat/sub-224_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-042/anat/sub-042_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-094/anat/sub-094_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-173/anat/sub-173_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-188/anat/sub-188_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-052/anat/sub-052_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-045/anat/sub-045_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-232/anat/sub-232_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-067/anat/sub-067_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-130/anat/sub-130_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-212/anat/sub-212_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-256/anat/sub-256_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-055/anat/sub-055_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-045/anat/sub-045_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-136/anat/sub-136_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-105/anat/sub-105_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-127/anat/sub-127_acq-highresSag_T2w.nii.gz",
+ "whole-spine/sub-amuFR/anat/sub-amuFR_T1w.nii.gz",
+ "spider-challenge-2023/sub-133/anat/sub-133_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-140/anat/sub-140_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-019/anat/sub-019_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-147/anat/sub-147_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-129/anat/sub-129_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-213/anat/sub-213_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-149/anat/sub-149_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-051/anat/sub-051_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-234/anat/sub-234_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-052/anat/sub-052_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-060/anat/sub-060_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-101/anat/sub-101_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-179/anat/sub-179_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-192/anat/sub-192_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-190/anat/sub-190_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-106/anat/sub-106_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-161/anat/sub-161_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-047/anat/sub-047_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-143/anat/sub-143_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-110/anat/sub-110_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-239/anat/sub-239_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-068/anat/sub-068_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-033/anat/sub-033_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-131/anat/sub-131_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-195/anat/sub-195_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-028/anat/sub-028_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-154/anat/sub-154_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-029/anat/sub-029_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-005/anat/sub-005_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-224/anat/sub-224_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-021/anat/sub-021_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-032/anat/sub-032_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-228/anat/sub-228_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-131/anat/sub-131_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-022/anat/sub-022_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-039/anat/sub-039_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-013/anat/sub-013_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-058/anat/sub-058_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-029/anat/sub-029_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-221/anat/sub-221_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-233/anat/sub-233_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-182/anat/sub-182_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-136/anat/sub-136_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-254/anat/sub-254_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-130/anat/sub-130_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-218/anat/sub-218_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-254/anat/sub-254_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-177/anat/sub-177_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-010/anat/sub-010_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-041/anat/sub-041_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-055/anat/sub-055_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-187/anat/sub-187_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-057/anat/sub-057_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-170/anat/sub-170_acq-lowresSag_T2w.nii.gz",
+ "whole-spine/sub-unfErssm012/anat/sub-unfErssm012_T1w.nii.gz",
+ "spider-challenge-2023/sub-220/anat/sub-220_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-051/anat/sub-051_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-126/anat/sub-126_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-227/anat/sub-227_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-184/anat/sub-184_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-101/anat/sub-101_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-098/anat/sub-098_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-179/anat/sub-179_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-009/anat/sub-009_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-093/anat/sub-093_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-004/anat/sub-004_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-162/anat/sub-162_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-080/anat/sub-080_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-212/anat/sub-212_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-146/anat/sub-146_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-142/anat/sub-142_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-083/anat/sub-083_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-038/anat/sub-038_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-191/anat/sub-191_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-222/anat/sub-222_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-246/anat/sub-246_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-222/anat/sub-222_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-005/anat/sub-005_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-002/anat/sub-002_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-096/anat/sub-096_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-229/anat/sub-229_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-082/anat/sub-082_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-205/anat/sub-205_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-204/anat/sub-204_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-142/anat/sub-142_acq-highresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-067/anat/sub-067_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-047/anat/sub-047_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-168/anat/sub-168_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-087/anat/sub-087_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-003/anat/sub-003_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-149/anat/sub-149_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-173/anat/sub-173_acq-lowresSag_T1w.nii.gz",
+ "spider-challenge-2023/sub-003/anat/sub-003_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-016/anat/sub-016_acq-lowresSag_T2w.nii.gz",
+ "spider-challenge-2023/sub-175/anat/sub-175_acq-lowresSag_T2w.nii.gz"
+ ]
+}
diff --git a/totalspineseg/utils/create_json_sidecar.py b/totalspineseg/utils/create_json_sidecar.py
new file mode 100644
index 0000000..90603dd
--- /dev/null
+++ b/totalspineseg/utils/create_json_sidecar.py
@@ -0,0 +1,36 @@
+import json
+import time
+import argparse
+
+def get_parser():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(description='Segment an image using nnUNetV2 model.')
+ parser.add_argument('-path-json', help='Input image to segment. Example: derivatives/labels/sub-001/anat/sub-001_T2w_label-sacrum_seg.json', required=True)
+ parser.add_argument('-process', help='Process used to generate the data. Example: nnUNet3D', required=True, type=str)
+ return parser
+
+def create_json_file():
+ """
+ Create a json sidecar file
+ :param path_file_out: path to the output file
+ """
+ parser = get_parser()
+ args = parser.parse_args()
+
+ path_json_out = args.path_json
+ process=args.process
+ data_json = {
+ "SpatialReference": "orig",
+ "GeneratedBy": [
+ {
+ "Name": process,
+ "Date": time.strftime('%Y-%m-%d %H:%M:%S')
+ }
+ ]
+ }
+ with open(path_json_out, 'w') as f:
+ json.dump(data_json, f, indent=4)
+ print(f'Created: {path_json_out}')
+
+if __name__=='__main__':
+ create_json_file()
\ No newline at end of file
diff --git a/totalspineseg/utils/install_weights.py b/totalspineseg/utils/install_weights.py
new file mode 100644
index 0000000..97a6415
--- /dev/null
+++ b/totalspineseg/utils/install_weights.py
@@ -0,0 +1,75 @@
+import os
+import argparse
+from pathlib import Path
+import subprocess
+from importlib.metadata import metadata
+from urllib.request import urlretrieve
+from tqdm import tqdm
+
+def main():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(description='Download nnunet weights from totalspineseg repository.')
+ parser.add_argument('--nnunet-dataset', required=True, type=str, help='Name of the nnUNet dataset present in the pyproject.toml (Required)')
+ parser.add_argument('--zip-url', required=True, type=str, help='URL of the weights contained inside the pyproject.toml (Required)')
+ parser.add_argument('--results-folder', required=True, type=Path, help='Results folder where the weights will be stored (Required)')
+ parser.add_argument('--exports-folder', required=True, type=Path, help='Exports folder where the zipped weights will be dowloaded (Required)')
+ parser.add_argument('--quiet', '-q', action="store_true", default=False, help='Do not display inputs and progress bar, defaults to false (Default=False).')
+ args = parser.parse_args()
+
+ # Datasets data
+ nnunet_dataset = args.nnunet_dataset
+ zip_url=args.zip_url
+ results_folder = args.results_folder
+ exports_folder = args.exports_folder
+ quiet = args.quiet
+
+ # Install nnUNet weight
+ install_weights(
+ nnunet_dataset=nnunet_dataset,
+ zip_url=zip_url,
+ results_folder=results_folder,
+ exports_folder=exports_folder,
+ quiet=quiet
+ )
+
+def install_weights(
+ nnunet_dataset,
+ zip_url,
+ results_folder,
+ exports_folder,
+ quiet
+):
+
+ # Create the download and export folder if they do not exist
+ results_folder.mkdir(parents=True, exist_ok=True)
+ exports_folder.mkdir(parents=True, exist_ok=True)
+
+ # Installing the pretrained models if not already installed
+ if not (results_folder / nnunet_dataset).is_dir():
+ # Get the zip file name and path
+ zip_name = zip_url.split('/')[-1]
+ zip_file = exports_folder / zip_name
+
+ # Check if the zip file exists
+ if not zip_file.is_file():
+ # If the zip file is not found, download it from the releases
+ if not quiet: print(f'Downloading the pretrained model from {zip_url}...')
+ with tqdm(unit='B', unit_scale=True, miniters=1, unit_divisor=1024, disable=quiet) as pbar:
+ urlretrieve(
+ zip_url,
+ zip_file,
+ lambda b, bsize, tsize=None: (pbar.total == tsize or pbar.reset(tsize)) and pbar.update(b * bsize - pbar.n),
+ )
+
+ if not zip_file.is_file():
+ raise FileNotFoundError(f'Could not download the pretrained model for {nnunet_dataset}.')
+
+ # If the pretrained model is not installed, install it from zip
+ if not quiet: print(f'Installing the pretrained model from {zip_file}...')
+ # Install the pretrained model from the zip file
+ os.environ['nnUNet_results'] = str(results_folder)
+ subprocess.run(['nnUNetv2_install_pretrained_model_from_zip', str(zip_file)])
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/totalspineseg/utils/largest_component_filewise.py b/totalspineseg/utils/largest_component_filewise.py
new file mode 100644
index 0000000..09bb46f
--- /dev/null
+++ b/totalspineseg/utils/largest_component_filewise.py
@@ -0,0 +1,89 @@
+import os, argparse, textwrap
+from scipy.ndimage import label
+from pathlib import Path
+import numpy as np
+import nibabel as nib
+
+
+def get_parser():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(
+ description=textwrap.dedent(f'''
+ This script processes a NIfTI segmentation file, leaving the largest component for each label.
+ '''),
+ formatter_class=argparse.RawTextHelpFormatter
+ )
+ parser.add_argument(
+ '--seg-in', type=str, required=True,
+ help='Input segmentation path'
+ )
+ parser.add_argument(
+ '--seg-out', type=str, required=True,
+ help='Output segmentation path'
+ )
+ parser.add_argument(
+ '--verbose', '-v', type=int, default=1, choices=[0, 1],
+ help='Verbosity level. 0: Errors/warnings only, 1: Errors/warnings + info (default: 1)'
+ )
+ return parser
+
+
+def main():
+ """
+ """
+ parser = get_parser()
+ args = parser.parse_args()
+
+ # Get arguments
+ seg_in = args.seg_in
+ seg_out = args.seg_out
+ verbose = args.verbose
+
+ if verbose:
+ print(textwrap.dedent(f'''
+ Running {Path(__file__).stem} with the following params:
+ seg_in = "{seg_in}"
+ seg_out = "{seg_out}"
+ verbose = {verbose}
+ '''))
+
+ # Keep largest component
+ keep_largest_component(seg_in, seg_out)
+
+
+def keep_largest_component(
+ seg_in,
+ seg_out
+ ):
+
+ # Load segmentation
+ seg = nib.load(seg_in)
+ seg_data = seg.get_fdata()
+
+ # Convert data to uint8 to avoid issues with segmentation IDs
+ seg_data_src = seg_data.astype(np.uint8)
+
+ seg_data = np.zeros_like(seg_data_src)
+
+ for l in [_ for _ in np.unique(seg_data_src) if _ != 0]:
+ mask = seg_data_src == l
+ mask_labeled, num_labels = label(mask, np.ones((3, 3, 3)))
+ # Find the label of the largest component
+ label_sizes = np.bincount(mask_labeled.ravel())[1:] # Skip 0 label size
+ largest_label = label_sizes.argmax() + 1 # +1 because bincount labels start at 0
+ seg_data[mask_labeled == largest_label] = l
+
+ # Create result segmentation
+ seg = nib.Nifti1Image(seg_data, seg.affine, seg.header)
+ seg.set_data_dtype(np.uint8)
+
+ # Make sure output directory exists
+ if not os.path.exists(os.path.dirname(seg_out)):
+ os.makedirs(os.path.dirname(seg_out))
+
+ # Save mapped segmentation
+ nib.save(seg, seg_out)
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/totalspineseg/utils/register_CT_to_MR.py b/totalspineseg/utils/register_CT_to_MR.py
new file mode 100644
index 0000000..663c67f
--- /dev/null
+++ b/totalspineseg/utils/register_CT_to_MR.py
@@ -0,0 +1,85 @@
+import os
+import argparse
+import subprocess
+
+from totalspineseg.utils.image import Image
+
+
+def get_parser():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(description='Register and duplicate segmentations from CT to MRI if not available.')
+ parser.add_argument('--path-img', required=True, type=str, help='Path to the BIDS-compliant folder where CT and MRI images are stored (Required)')
+ parser.add_argument('--path-label', type=str, default='', help='Path to the folder with labels where CT and MRI segmentations are stored. If not specified "--path-img + derivatives/labels" will be used')
+ parser.add_argument('--path-out', type=str, default='', help='Path to output directory where registered segmentations will be stored. If not specified "--path-img + derivatives/reg-labels" will be used')
+ parser.add_argument('--seg-suffix', type=str, default='_seg', help='Suffix used for segmentations. This suffix will be added to the raw filename to name the segmentation file. Default="_seg"')
+ parser.add_argument('--sacrum-idx', type=int, default=92, help='Index used to map the sacrum area. Default=92')
+ return parser
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ # Fetch input variables
+ path_img = args.path_img
+ path_label = os.path.join(args.path_img, 'derivatives/labels') if not args.path_label else args.path_label
+ path_out = os.path.join(args.path_img, 'derivatives/reg-labels') if not args.path_out else args.path_out
+ suffix_seg = args.seg_suffix
+
+ # Loop inside BIDS raw folder
+ for sub in os.listdir(path_img):
+ if sub.startswith('sub'):
+ # Fetch subject files
+ path_sub_anat = os.path.join(path_img, sub, 'anat')
+ path_der_sub_anat = os.path.join(path_label, sub, 'anat')
+ path_out_sub_anat = os.path.join(path_out, sub, 'anat')
+ raw_files = os.listdir(path_sub_anat)
+ der_files = os.listdir(path_der_sub_anat)
+
+ # Check if segmentations are available for a given contrast
+ for raw_file in raw_files:
+ path_raw = os.path.join(path_sub_anat, raw_file)
+ ext = '.' + raw_file.split('_')[-1].split('.', 1)[-1]
+ if ext == '.nii.gz':
+ cont = raw_file.split('_')[-1].split(ext)[0]
+ out_seg = raw_file.split('.')[0] + suffix_seg + ext
+ file_seg = raw_file.split('.')[0] + suffix_seg + ext
+
+ # If no segmentation is available, the CT segmentation is used instead
+ if not file_seg in der_files:
+ file_seg = '_'.join(raw_file.split('_')[:-1]) + '_CT' + suffix_seg + ext
+ seg_from_CT = True
+
+ # Check if CT segmentation does not exists
+ if not file_seg in der_files:
+ raise ValueError(f'Subject {sub} has no segmentations')
+ else:
+ seg_from_CT = False
+
+ path_seg = os.path.join(path_der_sub_anat, file_seg)
+ path_out_seg = os.path.join(path_out_sub_anat, out_seg)
+
+ # Keep only sacrum segmentation
+ seg = Image(path_seg)
+ if not args.sacrum_idx in seg.data:
+ raise ValueError(f'The value {args.sacrum_idx} was not detected in the mask')
+
+ seg.data[seg.data != args.sacrum_idx] = 0
+ seg.data[seg.data == args.sacrum_idx] = 1
+
+ # Save the new segmentation in the output folder
+ os.makedirs(os.path.dirname(path_out_seg), exist_ok=True)
+ seg.save(path=path_out_seg, dtype='float32')
+
+ # Perform a registration to align the image shapes and resolutions
+ subprocess.check_call([
+ 'sct_register_multimodal',
+ '-i', path_out_seg,
+ '-d', path_raw,
+ '-o', path_out_seg,
+ '-identity', '1',
+ '-x', 'nn'
+ ])
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file
diff --git a/totalspineseg/utils/run_nnunet_inference_single_subject.py b/totalspineseg/utils/run_nnunet_inference_single_subject.py
new file mode 100644
index 0000000..7d8f963
--- /dev/null
+++ b/totalspineseg/utils/run_nnunet_inference_single_subject.py
@@ -0,0 +1,216 @@
+"""
+Author: Jan Valosek
+Modified by Nathan Molinier
+
+Copied from https://github.com/ivadomed/model-spinal-rootlets/blob/main/packaging/run_inference_single_subject.py
+
+This script is used to run inference on a single subject using a nnUNetV2 model.
+
+Note: conda environment with nnUNetV2 is required to run this script.
+For details how to install nnUNetV2, see:
+https://github.com/ivadomed/utilities/blob/main/quick_start_guides/nnU-Net_quick_start_guide.md#installation
+
+Example:
+ python run_inference_single_subject.py
+ -i sub-001_T2w.nii.gz
+ -o sub-001_T2w_label-rootlet.nii.gz
+ -path-model
+ -tile-step-size 0.5
+ -fold 1
+"""
+
+
+import os
+import shutil
+import subprocess
+import argparse
+import datetime
+
+import torch
+import glob
+import time
+import tempfile
+
+from totalspineseg.utils.image import Image
+from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
+from batchgenerators.utilities.file_and_folder_operations import join
+
+
+def get_parser():
+ # parse command line arguments
+ parser = argparse.ArgumentParser(description='Segment an image using nnUNetV2 model.')
+ parser.add_argument('-i', help='Input image to segment. Example: sub-001_T2w.nii.gz', required=True)
+ parser.add_argument('-o', help='Output filename. Example: sub-001_T2w_label-sacrum_seg.nii.gz', required=True)
+ parser.add_argument('-path-model', help='Path to the model folder. This folder should contain individual '
+ 'folders like fold_0, fold_1, etc. and dataset.json, '
+ 'dataset_fingerprint.json and plans.json files.', required=True, type=str)
+ parser.add_argument('-use-gpu', action='store_true', default=False,
+ help='Use GPU for inference. Default: False')
+ parser.add_argument('-fold', type=str, required=True,
+ help='Fold(s) to use for inference. Example(s): 2 (single fold), 2,3 (multiple folds), '
+ 'all (fold_all).', choices=['0', '1', '2', '3', '4', 'all'])
+ parser.add_argument('-use-best-checkpoint', action='store_true', default=False,
+ help='Use the best checkpoint (instead of the final checkpoint) for prediction. '
+ 'NOTE: nnUNet by default uses the final checkpoint. Default: False')
+ parser.add_argument('-tile-step-size', default=0.5, type=float,
+ help='Tile step size defining the overlap between images patches during inference. '
+ 'Default: 0.5 '
+ 'NOTE: changing it from 0.5 to 0.9 makes inference faster but there is a small drop in '
+ 'performance.')
+
+ return parser
+
+
+def tmp_create():
+ """
+ Create temporary folder and return its path
+ """
+ prefix = f"sciseg_prediction_{datetime.datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}_"
+ tmpdir = tempfile.mkdtemp(prefix=prefix)
+ print(f"Creating temporary folder ({tmpdir})")
+ return tmpdir
+
+
+def splitext(fname):
+ """
+ Split a fname (folder/file + ext) into a folder/file and extension.
+ Note: for .nii.gz the extension is understandably .nii.gz, not .gz
+ (``os.path.splitext()`` would want to do the latter, hence the special case).
+ Taken (shamelessly) from: https://github.com/spinalcordtoolbox/manual-correction/blob/main/utils.py
+ """
+ dir, filename = os.path.split(fname)
+ for special_ext in ['.nii.gz', '.tar.gz']:
+ if filename.endswith(special_ext):
+ stem, ext = filename[:-len(special_ext)], special_ext
+ return os.path.join(dir, stem), ext
+ # If no special case, behaves like the regular splitext
+ stem, ext = os.path.splitext(filename)
+ return os.path.join(dir, stem), ext
+
+
+def add_suffix(fname, suffix):
+ """
+ Add suffix between end of file name and extension. Taken (shamelessly) from:
+ https://github.com/spinalcordtoolbox/manual-correction/blob/main/utils.py
+ :param fname: absolute or relative file name. Example: t2.nii.gz
+ :param suffix: suffix. Example: _mean
+ :return: file name with suffix. Example: t2_mean.nii
+ Examples:
+ - add_suffix(t2.nii, _mean) -> t2_mean.nii
+ - add_suffix(t2.nii.gz, a) -> t2a.nii.gz
+ """
+ stem, ext = splitext(fname)
+ return os.path.join(stem + suffix + ext)
+
+
+def main():
+ parser = get_parser()
+ args = parser.parse_args()
+
+ fname_file = args.i
+ fname_file_out = args.o
+ print(f'\nFound {fname_file} file.')
+
+ # Create temporary directory in the temp to store the reoriented images
+ tmpdir = tmp_create()
+ # Copy the file to the temporary directory using shutil.copyfile
+ fname_file_tmp = os.path.join(tmpdir, os.path.basename(fname_file))
+ shutil.copyfile(fname_file, fname_file_tmp)
+ print(f'Copied {fname_file} to {fname_file_tmp}')
+
+ # Get the original orientation of the image
+ img = Image(fname_file_tmp)
+ orig_orientation = img.orientation
+
+ # Reorient the image to RPI orientation if not already in RPI
+ if orig_orientation != 'RPI':
+ print(f'Original orientation: {orig_orientation}')
+ print(f'Reorienting to RPI orientation...')
+ # reorient the image to RPI using SCT Image
+ img.change_orientation('RPI').save(fname_file_tmp)
+
+ # NOTE: for individual images, the _0000 suffix is not needed.
+ # BUT, the images should be in a list of lists
+ fname_file_tmp_list = [[fname_file_tmp]]
+
+ # Use fold_all (all train/val subjects were used for training) or specific fold(s)
+ folds_avail = 'all' if args.fold == 'all' else [int(f) for f in args.fold.split(',')]
+ print(f'Using fold(s): {folds_avail}')
+
+ # Create directory for nnUNet prediction
+ tmpdir_nnunet = os.path.join(tmpdir, 'nnUNet_prediction')
+ fname_prediction = os.path.join(tmpdir_nnunet, os.path.basename(add_suffix(fname_file_tmp, '_pred')))
+ os.mkdir(tmpdir_nnunet)
+
+ # Run nnUNet prediction
+ print('Starting inference...it may take a few minutes...\n')
+ start = time.time()
+ # directly call the predict function
+ predictor = nnUNetPredictor(
+ tile_step_size=args.tile_step_size, # changing it from 0.5 to 0.9 makes inference faster
+ use_gaussian=True, # applies gaussian noise and gaussian blur
+ use_mirroring=False, # test time augmentation by mirroring on all axes
+ perform_everything_on_device=True if args.use_gpu else False,
+ device=torch.device('cuda') if args.use_gpu else torch.device('cpu'),
+ verbose_preprocessing=False,
+ allow_tqdm=True
+ )
+
+ print('Running inference on device: {}'.format(predictor.device))
+
+ # initializes the network architecture, loads the checkpoint
+ predictor.initialize_from_trained_model_folder(
+ join(args.path_model),
+ use_folds=folds_avail,
+ checkpoint_name='checkpoint_final.pth' if not args.use_best_checkpoint else 'checkpoint_best.pth',
+ )
+ print('Model loaded successfully. Fetching data...')
+
+ # NOTE: for individual files, the image should be in a list of lists
+ predictor.predict_from_files(
+ list_of_lists_or_source_folder=fname_file_tmp_list,
+ output_folder_or_list_of_truncated_output_files=tmpdir_nnunet,
+ save_probabilities=False,
+ overwrite=True,
+ num_processes_preprocessing=4,
+ num_processes_segmentation_export=4,
+ folder_with_segs_from_prev_stage=None,
+ num_parts=1,
+ part_id=0
+ )
+
+ end = time.time()
+
+ print('Inference done.')
+ total_time = end - start
+ print('Total inference time: {} minute(s) {} seconds\n'.format(int(total_time // 60), int(round(total_time % 60))))
+
+ # Copy .nii.gz file from tmpdir_nnunet to tmpdir
+ pred_file = glob.glob(os.path.join(tmpdir_nnunet, '*.nii.gz'))[0]
+ shutil.copyfile(pred_file, fname_prediction)
+ print(f'Copied {pred_file} to {fname_prediction}')
+
+ # Reorient the image back to original orientation
+ # skip if already in RPI
+ if orig_orientation != 'RPI':
+ print(f'Reorienting to original orientation {orig_orientation}...')
+ # reorient the image to the original orientation using SCT Image
+ pred = Image(fname_prediction)
+ pred.change_orientation(orig_orientation).save(fname_prediction)
+
+ # Copy level-specific (i.e., non-binary) segmentation
+ shutil.copyfile(fname_prediction, fname_file_out)
+ print(f'Copied {fname_prediction} to {fname_file_out}')
+
+ print('Deleting the temporary folder...')
+ # Delete the temporary folder
+ shutil.rmtree(tmpdir)
+
+ print('-' * 50)
+ print(f"Input file: {fname_file}")
+ print(f"nnUNet segmentation: {fname_file_out}")
+ print('-' * 50)
+
+
+if __name__ == '__main__':
+ main()
\ No newline at end of file