From d4fe8c95c88d069f4b786df11cac5b2120ce5a3a Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 14:27:29 -0400 Subject: [PATCH 1/6] add readme explaining sacrum pipeline --- scripts/generate_sacrum_masks/README.md | 99 +++++++++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 scripts/generate_sacrum_masks/README.md 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) | +| :---: | :---: | :---: | +| Screenshot 2024-01-02 at 5 10 39 AM | Screenshot 2024-01-02 at 5 10 53 AM | Screenshot 2024-01-02 at 5 11 19 AM | + +> 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" +``` + + + + + From da7efdab61b4b43cd7bd21ae3ee30c47e5a76033 Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 14:28:08 -0400 Subject: [PATCH 2/6] Add scripts from sacrum pipeline --- pyproject.toml | 1 + .../generate_sacrum_masks/generate_sacrum.sh | 136 ++++++ .../convert_config_to_nnunet.py | 200 ++++++++ .../data_management/init_data_config.py | 105 ++++ totalspineseg/data_management/utils.py | 87 ++++ .../resources/configs/test_sacrum.json | 462 ++++++++++++++++++ totalspineseg/utils/create_json_sidecar.py | 36 ++ .../utils/download_weights_nnunet.py | 63 +++ .../utils/largest_component_filewise.py | 89 ++++ totalspineseg/utils/register_CT_to_MR.py | 85 ++++ .../run_nnunet_inference_single_subject.py | 216 ++++++++ 11 files changed, 1480 insertions(+) create mode 100644 scripts/generate_sacrum_masks/generate_sacrum.sh create mode 100644 totalspineseg/data_management/convert_config_to_nnunet.py create mode 100644 totalspineseg/data_management/init_data_config.py create mode 100644 totalspineseg/data_management/utils.py create mode 100644 totalspineseg/resources/configs/test_sacrum.json create mode 100644 totalspineseg/utils/create_json_sidecar.py create mode 100644 totalspineseg/utils/download_weights_nnunet.py create mode 100644 totalspineseg/utils/largest_component_filewise.py create mode 100644 totalspineseg/utils/register_CT_to_MR.py create mode 100644 totalspineseg/utils/run_nnunet_inference_single_subject.py diff --git a/pyproject.toml b/pyproject.toml index 1c843a1..9aa4448 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 = "" [project.scripts] totalspineseg = "totalspineseg.inference:main" diff --git a/scripts/generate_sacrum_masks/generate_sacrum.sh b/scripts/generate_sacrum_masks/generate_sacrum.sh new file mode 100644 index 0000000..18990c6 --- /dev/null +++ b/scripts/generate_sacrum_masks/generate_sacrum.sh @@ -0,0 +1,136 @@ +#!/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/" +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 download_path="$2" + local export_path="$2" + + # Call python function + python3 "${PATH_REPO}"/totalspineseg/utils/download_weights_nnunet.py --nnunet-dataset "${dataset}" --download-folder "${download_path}" --export-folder "${export_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" "$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/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/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/download_weights_nnunet.py b/totalspineseg/utils/download_weights_nnunet.py new file mode 100644 index 0000000..0dd78c1 --- /dev/null +++ b/totalspineseg/utils/download_weights_nnunet.py @@ -0,0 +1,63 @@ +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 get_parser(): + # 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('--download-folder', required=True, type=Path, help='Download folder where the weights will be stored (Required)') + parser.add_argument('--export-folder', required=True, type=Path, help='Export 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).') + return parser + +def main(): + parser = get_parser() + args = parser.parse_args() + + # Datasets data + nnunet_dataset = args.nnunet_dataset + download_folder = args.download_folder + export_folder = args.export_folder + quiet = args.quiet + + # Read urls from 'pyproject.toml' + zip_url = dict([_.split(', ') for _ in metadata('totalspineseg').get_all('Project-URL')])[nnunet_dataset] + + # Create the download and export folder if they do not exist + download_folder.mkdir(parents=True, exist_ok=True) + export_folder.mkdir(parents=True, exist_ok=True) + + # Installing the pretrained models if not already installed + if not (download_folder / nnunet_dataset).is_dir(): + # Get the zip file name and path + zip_name = zip_url.split('/')[-1] + zip_file = export_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(download_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 From e204a860e687a57f9a2b4e73f26e7bcc3033ab1c Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 14:31:49 -0400 Subject: [PATCH 3/6] download weights --- scripts/generate_sacrum_masks/generate_sacrum.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/generate_sacrum_masks/generate_sacrum.sh b/scripts/generate_sacrum_masks/generate_sacrum.sh index 18990c6..8875b12 100644 --- a/scripts/generate_sacrum_masks/generate_sacrum.sh +++ b/scripts/generate_sacrum_masks/generate_sacrum.sh @@ -103,7 +103,7 @@ cd "$DATASETS_PATH" FILES=$(jq -r '.TESTING[]' "${PATH_CONFIG}") # Download and install nnUNet weights -# download_weights "$NNUNET_MODEL" "$NNUNET_RESULTS" "$NNUNET_EXPORTS" +download_weights "$NNUNET_MODEL" "$NNUNET_RESULTS" "$NNUNET_EXPORTS" # Loop across the files for FILE_PATH in $FILES; do From d576cc092743ee10664b612308f0d5a3589c5df7 Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 15:39:08 -0400 Subject: [PATCH 4/6] update link sacrum weights --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9aa4448..6c502be 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +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 = "" +Dataset300_SacrumDataset = "https://github.com/neuropoly/totalspineseg/releases/download/sacrum-seg/Dataset300_SacrumDataset.zip" [project.scripts] totalspineseg = "totalspineseg.inference:main" From d439cb18a8815a4417416095f606f6e56694dfa1 Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 15:40:00 -0400 Subject: [PATCH 5/6] add install weight function to utils --- pyproject.toml | 1 + .../generate_sacrum_masks/generate_sacrum.sh | 10 +++-- totalspineseg/__init__.py | 3 +- totalspineseg/inference.py | 30 +++---------- ...ad_weights_nnunet.py => install_weight.py} | 44 ++++++++++++------- 5 files changed, 44 insertions(+), 44 deletions(-) rename totalspineseg/utils/{download_weights_nnunet.py => install_weight.py} (64%) diff --git a/pyproject.toml b/pyproject.toml index 6c502be..9aedd80 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,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_weight = "totalspineseg.utils.install_weight:main" [build-system] requires = ["pip>=23", "setuptools>=67"] diff --git a/scripts/generate_sacrum_masks/generate_sacrum.sh b/scripts/generate_sacrum_masks/generate_sacrum.sh index 8875b12..07d6ae8 100644 --- a/scripts/generate_sacrum_masks/generate_sacrum.sh +++ b/scripts/generate_sacrum_masks/generate_sacrum.sh @@ -32,6 +32,7 @@ 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" @@ -83,11 +84,12 @@ keep_largest_component(){ # Keep largest component only download_weights(){ local dataset="$1" - local download_path="$2" - local export_path="$2" + local url="$2" + local results_path="$3" + local exports_path="$4" # Call python function - python3 "${PATH_REPO}"/totalspineseg/utils/download_weights_nnunet.py --nnunet-dataset "${dataset}" --download-folder "${download_path}" --export-folder "${export_path}" + totalspineseg_install_weight --nnunet-dataset "${dataset}" --zip-url "${url}" --results-folder "${results_path}" --exports-folder "${exports_path}" } # ====================================================================================================================== @@ -103,7 +105,7 @@ cd "$DATASETS_PATH" FILES=$(jq -r '.TESTING[]' "${PATH_CONFIG}") # Download and install nnUNet weights -download_weights "$NNUNET_MODEL" "$NNUNET_RESULTS" "$NNUNET_EXPORTS" +download_weights "$NNUNET_MODEL" "$ZIP_URL" "$NNUNET_RESULTS" "$NNUNET_EXPORTS" # Loop across the files for FILE_PATH in $FILES; do diff --git a/totalspineseg/__init__.py b/totalspineseg/__init__.py index 8feb9e9..e0767aa 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_weight import install_weight \ No newline at end of file diff --git a/totalspineseg/inference.py b/totalspineseg/inference.py index 4956491..77e9c52 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_weight( + 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/utils/download_weights_nnunet.py b/totalspineseg/utils/install_weight.py similarity index 64% rename from totalspineseg/utils/download_weights_nnunet.py rename to totalspineseg/utils/install_weight.py index 0dd78c1..ccb61d7 100644 --- a/totalspineseg/utils/download_weights_nnunet.py +++ b/totalspineseg/utils/install_weight.py @@ -6,37 +6,49 @@ from urllib.request import urlretrieve from tqdm import tqdm -def get_parser(): +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('--download-folder', required=True, type=Path, help='Download folder where the weights will be stored (Required)') - parser.add_argument('--export-folder', required=True, type=Path, help='Export folder where the zipped weights will be dowloaded (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).') - return parser - -def main(): - parser = get_parser() args = parser.parse_args() # Datasets data nnunet_dataset = args.nnunet_dataset - download_folder = args.download_folder - export_folder = args.export_folder + zip_url=args.zip_url + results_folder = args.results_folder + exports_folder = args.exports_folder quiet = args.quiet - # Read urls from 'pyproject.toml' - zip_url = dict([_.split(', ') for _ in metadata('totalspineseg').get_all('Project-URL')])[nnunet_dataset] + # Install nnUNet weight + install_weight( + nnunet_dataset=nnunet_dataset, + zip_url=zip_url, + results_folder=results_folder, + exports_folder=exports_folder, + quiet=quiet + ) + +def install_weight( + nnunet_dataset, + zip_url, + results_folder, + exports_folder, + quiet +): # Create the download and export folder if they do not exist - download_folder.mkdir(parents=True, exist_ok=True) - export_folder.mkdir(parents=True, exist_ok=True) + 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 (download_folder / nnunet_dataset).is_dir(): + if not (results_folder / nnunet_dataset).is_dir(): # Get the zip file name and path zip_name = zip_url.split('/')[-1] - zip_file = export_folder / zip_name + zip_file = exports_folder / zip_name # Check if the zip file exists if not zip_file.is_file(): @@ -55,7 +67,7 @@ def main(): # 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(download_folder) + os.environ['nnUNet_results'] = str(results_folder) subprocess.run(['nnUNetv2_install_pretrained_model_from_zip', str(zip_file)]) From 96f3eb71de5a1385f0430d864dab233ea0490295 Mon Sep 17 00:00:00 2001 From: Nathan Molinier Date: Fri, 4 Oct 2024 17:48:59 -0400 Subject: [PATCH 6/6] rename to weights --- pyproject.toml | 2 +- scripts/generate_sacrum_masks/generate_sacrum.sh | 2 +- totalspineseg/__init__.py | 2 +- totalspineseg/inference.py | 2 +- totalspineseg/utils/{install_weight.py => install_weights.py} | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) rename totalspineseg/utils/{install_weight.py => install_weights.py} (98%) diff --git a/pyproject.toml b/pyproject.toml index 9aedd80..b17c0b3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,7 +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_weight = "totalspineseg.utils.install_weight:main" +totalspineseg_install_weights = "totalspineseg.utils.install_weights:main" [build-system] requires = ["pip>=23", "setuptools>=67"] diff --git a/scripts/generate_sacrum_masks/generate_sacrum.sh b/scripts/generate_sacrum_masks/generate_sacrum.sh index 07d6ae8..1e2b030 100644 --- a/scripts/generate_sacrum_masks/generate_sacrum.sh +++ b/scripts/generate_sacrum_masks/generate_sacrum.sh @@ -89,7 +89,7 @@ download_weights(){ local exports_path="$4" # Call python function - totalspineseg_install_weight --nnunet-dataset "${dataset}" --zip-url "${url}" --results-folder "${results_path}" --exports-folder "${exports_path}" + totalspineseg_install_weights --nnunet-dataset "${dataset}" --zip-url "${url}" --results-folder "${results_path}" --exports-folder "${exports_path}" } # ====================================================================================================================== diff --git a/totalspineseg/__init__.py b/totalspineseg/__init__.py index e0767aa..b4c49f6 100644 --- a/totalspineseg/__init__.py +++ b/totalspineseg/__init__.py @@ -13,4 +13,4 @@ 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 -from .utils.install_weight import install_weight \ No newline at end of file +from .utils.install_weights import install_weights \ No newline at end of file diff --git a/totalspineseg/inference.py b/totalspineseg/inference.py index 77e9c52..3c6f224 100644 --- a/totalspineseg/inference.py +++ b/totalspineseg/inference.py @@ -164,7 +164,7 @@ 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)]: - install_weight( + install_weights( nnunet_dataset=dataset, zip_url=zip_url, results_folder=nnUNet_results, diff --git a/totalspineseg/utils/install_weight.py b/totalspineseg/utils/install_weights.py similarity index 98% rename from totalspineseg/utils/install_weight.py rename to totalspineseg/utils/install_weights.py index ccb61d7..97a6415 100644 --- a/totalspineseg/utils/install_weight.py +++ b/totalspineseg/utils/install_weights.py @@ -24,7 +24,7 @@ def main(): quiet = args.quiet # Install nnUNet weight - install_weight( + install_weights( nnunet_dataset=nnunet_dataset, zip_url=zip_url, results_folder=results_folder, @@ -32,7 +32,7 @@ def main(): quiet=quiet ) -def install_weight( +def install_weights( nnunet_dataset, zip_url, results_folder,