diff --git a/README.md b/README.md index 3c89385..c4ebaf7 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,7 @@ from brats import AdultGliomaSegmenter from brats.constants import AdultGliomaAlgorithms segmenter = AdultGliomaSegmenter(algorithm=AdultGliomaAlgorithms.BraTS23_1, cuda_devices="0") -# these parameters are optional, by default the winning algorithm will be used on cuda:0 +# these parameters are optional, by default the winning algorithm of 2023 will be used on cuda:0 segmenter.infer_single( t1c="path/to/t1c.nii.gz", t1n="path/to/t1n.nii.gz", @@ -65,6 +65,9 @@ segmenter.infer_single( | Year | Rank | Author | Paper | CPU Support | Key Enum | | ---- | ---- | --------------------------------- | ------------------------------------------ | ----------- | -------------------------------------------------------------------------------------------------------------------- | +| 2024 | 1st | _André Ferreira, et al._ | N/A | ❌ | [BraTS24_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS24_1) | +| 2024 | 2nd | _Team kimbab_ | N/A | ❌ | [BraTS24_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS24_2) | +| 2024 | 3rd | _Adrian Celaya_ | N/A | ✅ | [BraTS24_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS24_3) | | 2023 | 1st | _André Ferreira, et al._ | [Link](https://arxiv.org/abs/2402.17317v1) | ❌ | [BraTS23_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS23_1) | | 2023 | 2nd | _Andriy Myronenko, et al._ | N/A | ❌ | [BraTS23_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS23_2) | | 2023 | 3rd | _Fadillah Adamsyah Maani, et al._ | N/A | ❌ | [BraTS23_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AdultGliomaAlgorithms.BraTS23_3) | @@ -94,6 +97,9 @@ segmenter.infer_single( | Year | Rank | Author | Paper | CPU Support | Key Enum | | ---- | ---- | -------------------------- | ----- | ----------- | --------------------------------------------------------------------------------------------------------------- | +| 2024 | 1st | _Zhifan Jiang et al._ | N/A | ❌ | [BraTS24_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS24_1) | +| 2024 | 2nd | _Long Bai, et al._ | N/A | ✅ | [BraTS24_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS24_2) | +| 2024 | 1st | _Sarim Hashmi, et al._ | N/A | ❌ | [BraTS24_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS24_3) | | 2023 | 1st | _Andriy Myronenko, et al._ | TODO | ❌ | [BraTS23_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS23_1) | | 2023 | 2nd | _Alyssa R Amod, et al._ | N/A | ❌ | [BraTS23_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS23_2) | | 2023 | 3rd | _Ziyan Huang, et al._ | N/A | ✅ | [BraTS23_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.AfricaAlgorithms.BraTS23_3) | @@ -104,10 +110,18 @@ segmenter.infer_single( Meningioma Segmentation
+**Note** +Unlike other segmentation challenges the expected inputs for the Meningioma Segmentation Algorithms differ between years. +- _2023_: All 4 modalities are used (t1c, t1n, t2f, t2w) +- _2024_: Only t1c is used + +Therefore the usage differs slightly, depending on which algorithm is used. To understand why, please refer to the [2024 challenge manuscript](https://arxiv.org/abs/2405.18383). + ```python from brats import MeningiomaSegmenter from brats.constants import MeningiomaAlgorithms +### Example for 2023 algorithms segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS23_1, cuda_devices="0") # these parameters are optional, by default the winning algorithm will be used on cuda:0 segmenter.infer_single( @@ -115,7 +129,14 @@ segmenter.infer_single( t1n="path/to/t1n.nii.gz", t2f="path/to/t2f.nii.gz", t2w="path/to/t2w.nii.gz", - output_file="segmentation.nii.gz", + output_file="segmentation_23.nii.gz", +) + +### Example for 2024 algorithms +segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS24_1, cuda_devices="0") +segmenter.infer_single( + t1c="path/to/t1c.nii.gz", + output_file="segmentation_24.nii.gz", ) ``` @@ -123,6 +144,9 @@ segmenter.infer_single( | Year | Rank | Author | Paper | CPU Support | Key Enum | | ---- | ---- | -------------------------- | ----- | ----------- | ------------------------------------------------------------------------------------------------------------------- | +| 2024 | 1st | _Valeria Abramova_ | N/A | ❌ | [BraTS24_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS24_1) | +| 2024 | 2nd | _Mehdi Astaraki_ | N/A | ❌ | [BraTS24_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS24_2) | +| 2024 | 3rd | _Andre Ferreira, et al._ | N/A | ✅ | [BraTS24_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS24_3) | | 2023 | 1st | _Andriy Myronenko, et al._ | N/A | ❌ | [BraTS23_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS23_1) | | 2023 | 2nd | _Ziyan Huang, et al._ | N/A | ✅ | [BraTS23_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS23_2) | | 2023 | 3rd | _Zhifan Jiang et al._ | N/A | ❌ | [BraTS23_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.MeningiomaAlgorithms.BraTS23_3) | @@ -181,6 +205,9 @@ segmenter.infer_single( | Year | Rank | Author | Paper | CPU Support | Key Enum | | ---- | ---- | -------------------------- | ----- | ----------- | ------------------------------------------------------------------------------------------------------------------ | +| 2024 | 1st | _Tim Mulvany, et al._ | N/A | ❌ | [BraTS24_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS24_1) | +| 2024 | 2nd | _Mehdi Astaraki_ | N/A | ❌ | [BraTS24_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS24_2) | +| 2024 | 3rd | _Sarim Hashmi, et al._ | N/A | ❌ | [BraTS24_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS24_3) | | 2023 | 1st | _Zhifan Jiang et al._ | N/A | ❌ | [BraTS23_1](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS23_1) | | 2023 | 2nd | _Andriy Myronenko, et al._ | N/A | ❌ | [BraTS23_2](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS23_2) | | 2023 | 3rd | _Yubo Zhou_ | N/A | ❌ | [BraTS23_3](https://brats.readthedocs.io/en/latest/utils/utils.html#brats.constants.PediatricAlgorithms.BraTS23_3) | diff --git a/brats/constants.py b/brats/constants.py index 3bdf5ba..56aae2e 100644 --- a/brats/constants.py +++ b/brats/constants.py @@ -29,6 +29,13 @@ class Algorithms(str, Enum): class AdultGliomaAlgorithms(Algorithms): """Constants for the available adult glioma segmentation algorithms.""" + BraTS24_1 = "BraTS24_1" + """ BraTS24 Adult Glioma Segmentation 1st place """ + BraTS24_2 = "BraTS24_2" + """ BraTS24 Adult Glioma Segmentation 2nd place """ + BraTS24_3 = "BraTS24_3" + """ BraTS24 Adult Glioma Segmentation 3rd place """ + BraTS23_1 = "BraTS23_1" """BraTS23 Adult Glioma Segmentation 1st place (GPU only)""" BraTS23_2 = "BraTS23_2" @@ -40,6 +47,13 @@ class AdultGliomaAlgorithms(Algorithms): class MeningiomaAlgorithms(Algorithms): """Constants for the available meningioma segmentation algorithms.""" + BraTS24_1 = "BraTS24_1" + """ BraTS24 Meningioma Segmentation 1st place """ + BraTS24_2 = "BraTS24_2" + """ BraTS24 Meningioma Segmentation 2nd place """ + BraTS24_3 = "BraTS24_3" + """ BraTS24 Meningioma Segmentation 3rd place """ + BraTS23_1 = "BraTS23_1" """BraTS23 Meningioma Segmentation 1st place (GPU only)""" BraTS23_2 = "BraTS23_2" @@ -51,6 +65,13 @@ class MeningiomaAlgorithms(Algorithms): class PediatricAlgorithms(Algorithms): """Constants for the available pediatric segmentation algorithms.""" + BraTS24_1 = "BraTS24_1" + """ BraTS24 Pediatric Segmentation 1st place """ + BraTS24_2 = "BraTS24_2" + """ BraTS24 Pediatric Segmentation 2nd place """ + BraTS24_3 = "BraTS24_3" + """ BraTS24 Pediatric Segmentation 3rd place """ + BraTS23_1 = "BraTS23_1" """BraTS23 Pediatric Segmentation 1st place (GPU only)""" BraTS23_2 = "BraTS23_2" @@ -62,6 +83,13 @@ class PediatricAlgorithms(Algorithms): class AfricaAlgorithms(Algorithms): """Constants for the available africa segmentation algorithms.""" + BraTS24_1 = "BraTS24_1" + """ BraTS24 BraTS-Africa Segmentation 1st place """ + BraTS24_2 = "BraTS24_2" + """ BraTS24 BraTS-Africa Segmentation 2nd place """ + BraTS24_3 = "BraTS24_3" + """ BraTS24 BraTS-Africa Segmentation 3rd place """ + BraTS23_1 = "BraTS23_1" """BraTS23 BraTS-Africa Segmentation 1st place (GPU only)""" BraTS23_2 = "BraTS23_2" @@ -90,6 +118,7 @@ class InpaintingAlgorithms(Algorithms): """ BraTS24 Inpainting 2nd place """ BraTS24_3 = "BraTS24_3" """ BraTS24 Inpainting 3rd place """ + BraTS23_1 = "BraTS23_1" """ BraTS23 Inpainting 1st place """ BraTS23_2 = "BraTS23_2" diff --git a/brats/core/brats_algorithm.py b/brats/core/brats_algorithm.py index 4bc4bed..37836d4 100644 --- a/brats/core/brats_algorithm.py +++ b/brats/core/brats_algorithm.py @@ -4,7 +4,7 @@ import sys from abc import ABC, abstractmethod from pathlib import Path -from typing import Optional +from typing import Dict, Optional from loguru import logger @@ -51,7 +51,11 @@ def __init__( @abstractmethod def _standardize_single_inputs( - self, data_folder: Path, subject_id: str, inputs: dict[str, Path | str] + self, + data_folder: Path, + subject_id: str, + inputs: dict[str, Path | str], + subject_modality_separator: str, ) -> None: """ Standardize the input data to match the requirements of the selected algorithm. @@ -61,7 +65,7 @@ def _standardize_single_inputs( @abstractmethod def _standardize_batch_inputs( self, data_folder: Path, subjects: list[Path], input_name_schema: str - ) -> None: + ) -> Dict[str, str]: """ Standardize the input data to match the requirements of the selected algorithm. """ @@ -157,6 +161,7 @@ def _infer_single( data_folder=tmp_data_folder, subject_id=subject_id, inputs=inputs, + subject_modality_separator=self.algorithm.run_args.subject_modality_separator, ) run_container( @@ -208,6 +213,7 @@ def _infer_batch( output_path=tmp_output_folder, cuda_devices=self.cuda_devices, force_cpu=self.force_cpu, + internal_external_name_map=internal_external_name_map, ) self._process_batch_output( diff --git a/brats/core/docker.py b/brats/core/docker.py index 2de1b1c..4943a4b 100644 --- a/brats/core/docker.py +++ b/brats/core/docker.py @@ -124,11 +124,13 @@ def _get_additional_files_path(algorithm: AlgorithmData) -> Path: Returns: Path to the additional files """ - # ensure weights are present and get path - if algorithm.weights is not None: - return check_additional_files_path(record_id=algorithm.weights.record_id) + # ensure additional_files are present and get path + if algorithm.additional_files is not None: + return check_additional_files_path( + record_id=algorithm.additional_files.record_id + ) else: - # if no weights are directly specified a dummy weights folder will be mounted + # if no additional_files are directly specified a dummy additional_files folder will be mounted return get_dummy_path() @@ -195,12 +197,12 @@ def _build_args( """ # Build command that will be run in the docker container command_args = f"--data_path=/mlcube_io0 --output_path=/mlcube_io2" - if algorithm.weights is not None: - for i, param in enumerate(algorithm.weights.param_name): - weights_arg = f"--{param}=/mlcube_io1" - if algorithm.weights.checkpoint_path: - weights_arg += f"/{algorithm.weights.checkpoint_path[i]}" - command_args += f" {weights_arg}" + if algorithm.additional_files is not None: + for i, param in enumerate(algorithm.additional_files.param_name): + additional_files_arg = f"--{param}=/mlcube_io1" + if algorithm.additional_files.param_path: + additional_files_arg += f"/{algorithm.additional_files.param_path[i]}" + command_args += f" {additional_files_arg}" # Add parameters file arg if required params_arg = _get_parameters_arg(algorithm=algorithm) @@ -245,7 +247,10 @@ def _observe_docker_output(container: docker.models.containers.Container) -> str def _sanity_check_output( - data_path: Path, output_path: Path, container_output: str + data_path: Path, + output_path: Path, + container_output: str, + internal_external_name_map: Optional[Dict[str, str]] = None, ) -> None: """Sanity check that the number of output files matches the number of input files and the output is not empty. @@ -253,6 +258,7 @@ def _sanity_check_output( data_path (Path): The path to the input data output_path (Path): The path to the output data container_output (str): The output of the docker container + internal_external_name_map (Optional[Dict[str, str]]): Dictionary mapping internal name (in standardized format) to external subject name provided by user (only used for batch inference) Raises: BraTSContainerException: If not enough output files exist @@ -262,7 +268,6 @@ def _sanity_check_output( # (should result in only counting actual inputs) inputs = [e for e in data_path.iterdir() if e.name.startswith("BraTS")] outputs = list(output_path.iterdir()) - if len(outputs) < len(inputs): logger.error(f"Docker container output: \n\r{container_output}") raise BraTSContainerException( @@ -272,8 +277,18 @@ def _sanity_check_output( for i, output in enumerate(outputs, start=1): content = nib.load(output).get_fdata() if np.count_nonzero(content) == 0: + name = "" + if internal_external_name_map is not None: + name_key = [ + k + for k in internal_external_name_map.keys() + if output.name.startswith(k) + ] + if name_key: + name = internal_external_name_map[name_key[0]] + logger.warning( - f"""Output file {i} contains only zeros. + f"""Output file for subject {name + " "}contains only zeros. Potentially the selected algorithm might not work properly with your data unless this behavior is correct for your use case. If this seems wrong please try to use one of the other provided algorithms and file an issue on GitHub if the problem persists.""" ) @@ -286,7 +301,7 @@ def _log_algorithm_info(algorithm: AlgorithmData): algorithm (AlgorithmData): algorithm data """ logger.opt(colors=True).info( - f"Running algorithm: {algorithm.meta.challenge} [{algorithm.meta.rank} place]" + f"Running algorithm: BraTS {algorithm.meta.year} {algorithm.meta.challenge} [{algorithm.meta.rank} place]" ) logger.opt(colors=True).info( f"(Paper) Consider citing the corresponding paper: {algorithm.meta.paper} by {algorithm.meta.authors}" @@ -300,6 +315,7 @@ def run_container( output_path: Path, cuda_devices: str, force_cpu: bool, + internal_external_name_map: Optional[Dict[str, str]] = None, ): """Run a docker container for the provided algorithm. @@ -309,6 +325,7 @@ def run_container( output_path (Path | str): The path to save the output cuda_devices (str): The CUDA devices to use force_cpu (bool): Whether to force CPU execution + internal_external_name_map (Dict[str, str]): Dictionary mapping internal name (in standardized format) to external subject name provided by user (only used for batch inference) """ _log_algorithm_info(algorithm=algorithm) @@ -353,7 +370,10 @@ def run_container( ) container_output = _observe_docker_output(container=container) _sanity_check_output( - data_path=data_path, output_path=output_path, container_output=container_output + data_path=data_path, + output_path=output_path, + container_output=container_output, + internal_external_name_map=internal_external_name_map, ) logger.debug(f"Docker container output: \n\r{container_output}") diff --git a/brats/core/inpainting_algorithms.py b/brats/core/inpainting_algorithms.py index 665a0e5..a8fc7a6 100644 --- a/brats/core/inpainting_algorithms.py +++ b/brats/core/inpainting_algorithms.py @@ -3,7 +3,7 @@ from pathlib import Path import shutil import sys -from typing import Optional +from typing import Dict, Optional from loguru import logger @@ -29,7 +29,11 @@ def __init__( ) def _standardize_single_inputs( - self, data_folder: Path, subject_id: str, inputs: dict[str, Path | str] + self, + data_folder: Path, + subject_id: str, + inputs: dict[str, Path | str], + subject_modality_separator: str, ) -> None: """ Standardize the input data to match the requirements of the selected algorithm. @@ -38,6 +42,7 @@ def _standardize_single_inputs( data_folder (Path): Path to the data folder subject_id (str): Subject ID inputs (dict[str, Path | str]): Dictionary with the input data + subject_modality_separator (str): Separator between the subject ID and the modality """ subject_folder = data_folder / subject_id @@ -45,8 +50,15 @@ def _standardize_single_inputs( # TODO: investigate usage of symlinks (might cause issues on windows and would probably require different volume handling) t1n, mask = inputs["t1n"], inputs["mask"] try: - shutil.copy(t1n, subject_folder / f"{subject_id}-t1n-voided.nii.gz") - shutil.copy(mask, subject_folder / f"{subject_id}-mask.nii.gz") + shutil.copy( + t1n, + subject_folder + / f"{subject_id}{subject_modality_separator}t1n-voided.nii.gz", + ) + shutil.copy( + mask, + subject_folder / f"{subject_id}{subject_modality_separator}mask.nii.gz", + ) except FileNotFoundError as e: logger.error(f"Error while standardizing files: {e}") sys.exit(1) @@ -56,7 +68,7 @@ def _standardize_single_inputs( def _standardize_batch_inputs( self, data_folder: Path, subjects: list[Path], input_name_schema: str - ) -> None: + ) -> Dict[str, str]: """Standardize the input images for a list of subjects to match requirements of all algorithms and save them in @tmp_data_folder/@subject_id. Args: @@ -80,6 +92,7 @@ def _standardize_batch_inputs( "t1n": subject / f"{subject.name}-t1n-voided.nii.gz", "mask": subject / f"{subject.name}-mask.nii.gz", }, + subject_modality_separator=self.algorithm.run_args.subject_modality_separator, ) return internal_external_name_map diff --git a/brats/core/missing_mri_algorithms.py b/brats/core/missing_mri_algorithms.py index 22bc73c..caf6f3d 100644 --- a/brats/core/missing_mri_algorithms.py +++ b/brats/core/missing_mri_algorithms.py @@ -3,7 +3,7 @@ from pathlib import Path import shutil import sys -from typing import Optional, Union +from typing import Dict, List, Optional, Union from loguru import logger @@ -29,7 +29,11 @@ def __init__( ) def _standardize_single_inputs( - self, data_folder: Path, subject_id: str, inputs: dict[str, Path | str] + self, + data_folder: Path, + subject_id: str, + inputs: dict[str, Path | str], + subject_modality_separator: str, ) -> None: """ Standardize the input data to match the requirements of the selected algorithm. @@ -38,6 +42,7 @@ def _standardize_single_inputs( data_folder (Path): Path to the data folder subject_id (str): Subject ID inputs (dict[str, Path | str]): Dictionary with the input data + subject_modality_separator (str): Separator between the subject ID and the modality """ subject_folder = data_folder / subject_id @@ -45,7 +50,11 @@ def _standardize_single_inputs( try: for modality, path in inputs.items(): - shutil.copy(path, subject_folder / f"{subject_id}-{modality}.nii.gz") + shutil.copy( + path, + subject_folder + / f"{subject_id}{subject_modality_separator}{modality}.nii.gz", + ) except FileNotFoundError as e: logger.error(f"Error while standardizing files: {e}") sys.exit(1) @@ -58,7 +67,12 @@ def _standardize_single_inputs( t2w=inputs.get("t2w"), ) - def _standardize_batch_inputs(self, data_folder, subjects, input_name_schema): + def _standardize_batch_inputs( + self, + data_folder: Path, + subjects: List[Path], + input_name_schema: str, + ) -> Dict[str, str]: """Standardize the input images for a list of subjects to match requirements of all algorithms and save them in @tmp_data_folder/@subject_id. Args: @@ -90,6 +104,7 @@ def _standardize_batch_inputs(self, data_folder, subjects, input_name_schema): data_folder=data_folder, subject_id=internal_name, inputs=valid_inputs, + subject_modality_separator=self.algorithm.run_args.subject_modality_separator, ) return internal_external_name_map @@ -106,7 +121,7 @@ def infer_single( Perform synthesis of the missing modality for a single subject with the provided images and save the result to the output file. Note: - Exactly 3 inputs are required to perform synthesis of the missing modality. + Exactly 3 input modalities are required to perform synthesis of the missing modality. Args: output_file (Path | str): Output file to save the synthesized image diff --git a/brats/core/segmentation_algorithms.py b/brats/core/segmentation_algorithms.py index e514299..f78ad5a 100644 --- a/brats/core/segmentation_algorithms.py +++ b/brats/core/segmentation_algorithms.py @@ -3,7 +3,7 @@ import shutil import sys from pathlib import Path -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Union from loguru import logger @@ -44,7 +44,11 @@ def __init__( ) def _standardize_single_inputs( - self, data_folder: Path, subject_id: str, inputs: Dict[str, Path | str] + self, + data_folder: Path, + subject_id: str, + inputs: Dict[str, Path | str], + subject_modality_separator: str, ) -> None: """Standardize the input images for a single subject to match requirements of all algorithms and save them in @data_folder/@subject_id. Example: @@ -59,17 +63,19 @@ def _standardize_single_inputs( data_folder (Path): Parent folder where the subject folder will be created subject_id (str): Subject ID to be used for the folder and filenames inputs (Dict[str, Path | str]): Dictionary with the input images + subject_modality_separator (str): Separator between the subject ID and the modality """ subject_folder = data_folder / subject_id subject_folder.mkdir(parents=True, exist_ok=True) # TODO: investigate usage of symlinks (might cause issues on windows and would probably require different volume handling) - t1c, t1n, t2f, t2w = inputs["t1c"], inputs["t1n"], inputs["t2f"], inputs["t2w"] try: - shutil.copy(t1c, subject_folder / f"{subject_id}-t1c.nii.gz") - shutil.copy(t1n, subject_folder / f"{subject_id}-t1n.nii.gz") - shutil.copy(t2f, subject_folder / f"{subject_id}-t2f.nii.gz") - shutil.copy(t2w, subject_folder / f"{subject_id}-t2w.nii.gz") + for modality, path in inputs.items(): + shutil.copy( + path, + subject_folder + / f"{subject_id}{subject_modality_separator}{modality}.nii.gz", + ) except FileNotFoundError as e: logger.error(f"Error while standardizing files: {e}") logger.error( @@ -78,13 +84,19 @@ def _standardize_single_inputs( sys.exit(1) # sanity check inputs - input_sanity_check(t1c=t1c, t1n=t1n, t2f=t2f, t2w=t2w) + input_sanity_check( + t1c=inputs.get("t1c"), + t1n=inputs.get("t1n"), + t2f=inputs.get("t2f"), + t2w=inputs.get("t2w"), + ) def _standardize_batch_inputs( self, data_folder: Path, subjects: List[Path], input_name_schema: str, + only_t1c: bool = False, ) -> Dict[str, str]: """Standardize the input images for a list of subjects to match requirements of all algorithms and save them in @tmp_data_folder/@subject_id. @@ -92,6 +104,7 @@ def _standardize_batch_inputs( subjects (List[Path]): List of subject folders, each with a t1c, t1n, t2f, t2w image in standard format data_folder (Path): Parent folder where the subject folders will be created input_name_schema (str): Schema to be used for the subject folder and filenames depending on the BraTS Challenge + only_t1c (bool, optional): If True, only the t1c image will be used. Defaults to False. Returns: Dict[str, str]: Dictionary mapping internal name (in standardized format) to external subject name provided by user @@ -100,17 +113,20 @@ def _standardize_batch_inputs( for i, subject in enumerate(subjects): internal_name = input_name_schema.format(id=i) internal_external_name_map[internal_name] = subject.name - # TODO Add support for .nii files + + inputs = { + "t1c": subject / f"{subject.name}-t1c.nii.gz", + } + if not only_t1c: + inputs["t1n"] = subject / f"{subject.name}-t1n.nii.gz" + inputs["t2f"] = subject / f"{subject.name}-t2f.nii.gz" + inputs["t2w"] = subject / f"{subject.name}-t2w.nii.gz" self._standardize_single_inputs( data_folder=data_folder, subject_id=internal_name, - inputs={ - "t1c": subject / f"{subject.name}-t1c.nii.gz", - "t1n": subject / f"{subject.name}-t1n.nii.gz", - "t2f": subject / f"{subject.name}-t2f.nii.gz", - "t2w": subject / f"{subject.name}-t2w.nii.gz", - }, + inputs=inputs, + subject_modality_separator=self.algorithm.run_args.subject_modality_separator, ) return internal_external_name_map @@ -213,6 +229,129 @@ def __init__( force_cpu=force_cpu, ) + def _standardize_batch_inputs( + self, + data_folder: Path, + subjects: List[Path], + input_name_schema: str, + only_t1c: bool = False, + ) -> Dict[str, str]: + """Standardize the input images for a list of subjects to match requirements of all algorithms and save them in @tmp_data_folder/@subject_id. + + Args: + subjects (List[Path]): List of subject folders, each with a t1c, t1n, t2f, t2w image in standard format + data_folder (Path): Parent folder where the subject folders will be created + input_name_schema (str): Schema to be used for the subject folder and filenames depending on the BraTS Challenge + + Returns: + Dict[str, str]: Dictionary mapping internal name (in standardized format) to external subject name provided by user + """ + only_t1c = self.algorithm.meta.year == 2024 + return super()._standardize_batch_inputs( + data_folder=data_folder, + subjects=subjects, + input_name_schema=input_name_schema, + only_t1c=only_t1c, + ) + + def infer_single( + self, + output_file: Path | str, + t1c: Union[Path, str] = None, + t1n: Optional[Union[Path, str]] = None, + t2f: Optional[Union[Path, str]] = None, + t2w: Optional[Union[Path, str]] = None, + log_file: Optional[Path | str] = None, + ) -> None: + """ + Perform segmentation on a single subject with the provided images and save the result to the output file. + + Note: + Unlike other segmentation challenges, not all modalities are required for all meningioma segmentation algorithms. + Differences by year: + + - **2024**: Only `t1c` is required and used by the algorithms. + - **2023**: All (`t1c`, `t1n`, `t2f`, `t2w`) are required. + + + Args: + output_file (Path | str): Output file to save the segmentation. + t1c (Union[Path, str]): Path to the T1c image. Defaults to None. + t1n (Optional[Union[Path, str]], optional): Path to the T1n image. Defaults to None. + t2f (Optional[Union[Path, str]], optional): Path to the T2f image. Defaults to None. + t2w (Optional[Union[Path, str]], optional): Path to the T2w image. Defaults to None. + log_file (Optional[Path | str], optional): Save logs to this file. Defaults to None + """ + inputs = {"t1c": t1c, "t1n": t1n, "t2f": t2f, "t2w": t2w} + # filter out None values + inputs = {k: v for k, v in inputs.items() if v is not None} + + year = self.algorithm.meta.year + if year == 2024: + if "t1c" not in inputs or len(inputs) > 1: + raise ValueError( + ( + "Only the T1C modality is required and used by the 2024 meningioma segmentation challenge and its algorithms. " + "Please provide only the T1C modality - Aborting to avoid confusion." + ) + ) + elif year == 2023: + if len(inputs) != 4: + raise ValueError( + ( + "All modalities (t1c, t1n, t2f, t2w) are required for the 2023 meningioma segmentation challenge and its algorithms. " + "Please provide all modalities" + ) + ) + else: + raise NotImplementedError( + f"Invalid algorithm {year=} .Only 2023 and 2024 are supported as of now" + ) + self._infer_single( + inputs=inputs, + output_file=output_file, + log_file=log_file, + ) + + def infer_batch( + self, + data_folder: Path | str, + output_folder: Path | str, + log_file: Path | str | None = None, + ) -> None: + """ + Perform segmentation on a batch of subjects with the provided images and save the results to the output folder. \n + + Note: + Unlike other segmentation challenges, not all modalities are required for all meningioma segmentation algorithms. + Differences by year: + + - **2024**: Only `t1c` is required and used by the algorithms. + - **2023**: All (`t1c`, `t1n`, `t2f`, `t2w`) are required. + + Requires the following structure (example for 2023, only t1c for 2024!):\n + data_folder\n + ┣ A\n + ┃ ┣ A-t1c.nii.gz\n + ┃ ┣ A-t1n.nii.gz\n + ┃ ┣ A-t2f.nii.gz\n + ┃ ┗ A-t2w.nii.gz\n + ┣ B\n + ┃ ┣ B-t1c.nii.gz\n + ┃ ┣ ...\n + + + Args: + data_folder (Path | str): Folder containing the subjects with required structure + output_folder (Path | str): Output folder to save the segmentations + log_file (Path | str, optional): Save logs to this file + """ + return self._infer_batch( + data_folder=data_folder, + output_folder=output_folder, + log_file=log_file, + ) + class PediatricSegmenter(SegmentationAlgorithm): """Provides algorithms to perform tumor segmentation on pediatric MRI data diff --git a/brats/data/meta/adult_glioma.yml b/brats/data/meta/adult_glioma.yml index 8036662..5e849a2 100644 --- a/brats/data/meta/adult_glioma.yml +++ b/brats/data/meta/adult_glioma.yml @@ -1,15 +1,80 @@ +constants: + input_name_schema: &input_name_schema "BraTS-GLI-{id:05d}-000" + challenge: &challenge "Adult Glioma Segmentation" + years: + 2024: &year_2024 2024 + 2023: &year_2023 2023 + ranks: + 1st: &rank_1 1st + 2nd: &rank_2 2nd + 3rd: &rank_3 3rd + + algorithms: +##### 2024 ##### + BraTS24_1: + meta: + authors: André Ferreira, et al. + paper: N/A + challenge: *challenge + rank: *rank_1 + year: *year_2024 + run_args: + docker_image: brainles/brats24_adult_glioma_faking_it:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: false + shm_size: "4gb" + + BraTS24_2: + meta: + authors: Team kimbab + paper: N/A + challenge: *challenge + rank: *rank_2 + year: *year_2024 + run_args: + docker_image: brainles/brats24_adult_glioma_kimbab:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: false + shm_size: "2gb" + additional_files: + record_id: "14413387" + param_name: ["nnunet_env_path"] + + BraTS24_3: + meta: + authors: Adrian Celaya + paper: N/A + challenge: *challenge + rank: *rank_3 + year: *year_2024 + run_args: + docker_image: brainles/brats24_adult_glioma_mist:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "2gb" + cpu_compatible: true + additional_files: + record_id: "14411467" + param_name: ["mist_models", "mist_config", "mist_dataset"] + param_path: ["models", "config.json", "dataset.json"] + +##### 2023 ##### + BraTS23_1: meta: authors: André Ferreira, et al. paper: https://arxiv.org/abs/2402.17317v1 - challenge: BraTS23 Adult Glioma Segmentation - rank: 1st - year: 2023 + challenge: *challenge + rank: *rank_1 + year: *year_2023 run_args: docker_image: brainles/brats23_faking_it:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: false shm_size: "2gb" @@ -18,12 +83,12 @@ algorithms: meta: authors: Andriy Myronenko, et al. paper: N/A - challenge: BraTS23 Adult Glioma Segmentation - rank: 2nd - year: 2023 + challenge: *challenge + rank: *rank_2 + year: *year_2023 run_args: docker_image: brainles/brats23_nvauto:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true shm_size: "32gb" @@ -32,15 +97,15 @@ algorithms: meta: authors: Fadillah Adamsyah Maani, et al. paper: N/A - challenge: BraTS23 Adult Glioma Segmentation - rank: 3rd - year: 2023 + challenge: *challenge + rank: *rank_3 + year: *year_2023 run_args: docker_image: brainles/brats23_biomedmbz:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: true shm_size: "2gb" - weights: + additional_files: record_id: "11573315" param_name: ["checkpoint_dir"] diff --git a/brats/data/meta/africa.yml b/brats/data/meta/africa.yml index 6ac0709..978fbed 100644 --- a/brats/data/meta/africa.yml +++ b/brats/data/meta/africa.yml @@ -1,15 +1,75 @@ +constants: + input_name_schema: &input_name_schema "BraTS-SSA-{id:05d}-000" + challenge: &challenge "BraTS-Africa Segmentation" + years: + 2024: &year_2024 2024 + 2023: &year_2023 2023 + ranks: + 1st: &rank_1 1st + 2nd: &rank_2 2nd + 3rd: &rank_3 3rd + + algorithms: +##### 2024 ##### + BraTS24_1: + meta: + authors: Zhifan Jiang et al. + paper: N/A + challenge: *challenge + rank: *rank_1 + year: *year_2024 + run_args: + docker_image: brainles/brats24_africa_cnmc_pmi:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: false + shm_size: "2gb" + + BraTS24_2: + meta: + authors: Long Bai, et al. + paper: N/A + challenge: *challenge + rank: *rank_2 + year: *year_2024 + run_args: + docker_image: brainles/brats24_africa_cuhk_rpai + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "2gb" + cpu_compatible: true + + BraTS24_3: + meta: + authors: Sarim Hashmi, et al. + paper: N/A + challenge: *challenge + rank: *rank_1 + year: *year_2024 + run_args: + docker_image: brainles/brats24_africa_biomedia-mbzu + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "2gb" + additional_files: + record_id: "14414932" + param_name: ["checkpoint_dir"] + +##### 2023 ##### BraTS23_1: meta: authors: Andriy Myronenko, et al. paper: N/A - challenge: BraTS23 BraTS-Africa Segmentation - rank: 1st - year: 2023 + challenge: *challenge + rank: *rank_1 + year: *year_2023 run_args: docker_image: brainles/brats23_africa_nvauto:latest - input_name_schema: "BraTS-SSA-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true shm_size: "32gb" @@ -18,27 +78,28 @@ algorithms: meta: authors: Alyssa R Amod, et al. paper: N/A - challenge: BraTS23 BraTS-Africa Segmentation - rank: 2nd - year: 2023 + challenge: *challenge + rank: *rank_2 + year: *year_2023 run_args: docker_image: brainles/brats23_africa_sparkunn:latest - input_name_schema: "BraTS-SSA-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: true - weights: + additional_files: record_id: "13373752" param_name: ["ckpts_path"] + BraTS23_3: meta: authors: Ziyan Huang, et al. paper: N/A - challenge: BraTS23 BraTS-Africa Segmentation - rank: 3rd - year: 2023 + challenge: *challenge + rank: *rank_3 + year: *year_2023 run_args: docker_image: brainles/brats23_africa_blackbean:latest - input_name_schema: "BraTS-SSA-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true cpu_compatible: true diff --git a/brats/data/meta/inpainting.yml b/brats/data/meta/inpainting.yml index fe40c84..2339213 100644 --- a/brats/data/meta/inpainting.yml +++ b/brats/data/meta/inpainting.yml @@ -1,3 +1,14 @@ +constants: + input_name_schema: &input_name_schema "BraTS-GLI-{id:05d}-000" + challenge: &challenge "Inpainting" + years: + 2024: &year_2024 2024 + 2023: &year_2023 2023 + ranks: + 1st: &rank_1 1st + 2nd: &rank_2 2nd + 3rd: &rank_3 3rd + algorithms: ######## 2024 Algorithms ######## @@ -6,29 +17,29 @@ algorithms: meta: authors: Ke Chen, Juexin Zhang, Ying Weng paper: N/A - challenge: BraTS24 Inpainting - rank: 1st - year: 2024 + challenge: *challenge + rank: *rank_1 + year: *year_2024 run_args: docker_image: brainles/brats24_inpainting_ying_weng:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: true cpu_compatible: true - weights: + additional_files: record_id: "14230865" - param_name: ["checkpoint_path"] + param_name: ["param_path"] BraTS24_2: meta: authors: André Ferreira, et al. paper: N/A - challenge: BraTS24 Inpainting - rank: 2nd - year: 2024 + challenge: *challenge + rank: *rank_2 + year: *year_2024 run_args: docker_image: brainles/brats24_inpainting_faking_it:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: false shm_size: "4gb" @@ -37,17 +48,17 @@ algorithms: meta: authors: Team SMINT paper: N/A - challenge: BraTS24 Inpainting - rank: 3rd - year: 2024 + challenge: *challenge + rank: *rank_3 + year: *year_2024 run_args: docker_image: brainles/brats24_inpainting_smint:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: true - weights: + additional_files: record_id: "14231079" - checkpoint_path: ["savedmodel395000.pt"] + param_path: ["savedmodel395000.pt"] ######## 2023 Algorithms ######## @@ -55,49 +66,49 @@ algorithms: meta: authors: Juexin Zhang, et al. paper: N/A - challenge: BraTS23 Inpainting - rank: 1st - year: 2023 + challenge: *challenge + rank: *rank_1 + year: *year_2023 run_args: docker_image: brainles/brats23_inpainting_ying_weng:latest - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: true cpu_compatible: true - weights: + additional_files: record_id: "13382922" - param_name: ["checkpoint_path"] + param_name: ["param_path"] BraTS23_2: meta: authors: Alicia Durrer, et al. paper: N/A - challenge: BraTS23 Inpainting - rank: 2nd - year: 2023 + challenge: *challenge + rank: *rank_2 + year: *year_2023 run_args: docker_image: brainles/brats23_inpainting_domaso - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true - weights: + additional_files: record_id: "13383452" - checkpoint_path: ["savedmodel2850000.pt"] + param_path: ["savedmodel2850000.pt"] BraTS23_3: meta: authors: Jiayu Huo, et al. paper: N/A - challenge: BraTS23 Inpainting - rank: 3rd - year: 2023 + challenge: *challenge + rank: *rank_3 + year: *year_2023 run_args: docker_image: brainles/brats23_inpainting_medsegctrl - input_name_schema: "BraTS-GLI-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true cpu_compatible: true - weights: + additional_files: record_id: "13383287" param_name: ["weight_path"] - checkpoint_path: ["epoch-19-step-197499.ckpt"] + param_path: ["epoch-19-step-197499.ckpt"] diff --git a/brats/data/meta/meningioma.yml b/brats/data/meta/meningioma.yml index 36d7df3..16f4ccd 100644 --- a/brats/data/meta/meningioma.yml +++ b/brats/data/meta/meningioma.yml @@ -1,14 +1,77 @@ +constants: + input_name_schema_by_year: + input_name_schema_2024: &input_name_schema_2024 "BraTS-MEN-RT-{id:04d}-1" + input_name_schema_2023: &input_name_schema_2023 "BraTS-MEN-{id:05d}-000" + challenge: &challenge "Meningioma Segmentation" + years: + 2024: &year_2024 2024 + 2023: &year_2023 2023 + ranks: + 1st: &rank_1 1st + 2nd: &rank_2 2nd + 3rd: &rank_3 3rd + + algorithms: + +##### 2024 ##### + BraTS24_1: + meta: + authors: Valeria Abramova + paper: N/A + challenge: *challenge + rank: *rank_1 + year: *year_2024 + run_args: + docker_image: brainles/brats24_meningioma_nic_vicorob + input_name_schema: *input_name_schema_2024 + requires_root: false + parameters_file: true + shm_size: "16gb" + subject_modality_separator: "_" + + BraTS24_2: + meta: + authors: Mehdi Astaraki + paper: N/A + challenge: *challenge + rank: *rank_2 + year: *year_2024 + run_args: + docker_image: brainles/brats24_meningioma_astaraki + input_name_schema: *input_name_schema_2024 + requires_root: false + parameters_file: true + shm_size: "2gb" + + BraTS24_3: + meta: + authors: Andre Ferreira, et al. + paper: N/A + challenge: *challenge + rank: *rank_3 + year: *year_2024 + run_args: + docker_image: brainles/brats24_meningioma_faking_it + input_name_schema: *input_name_schema_2024 + requires_root: false + parameters_file: false + shm_size: "4gb" + cpu_compatible: true # ~8 hours + subject_modality_separator: "_" + + +##### 2023 ##### BraTS23_1: meta: authors: Andriy Myronenko, et al. paper: N/A - challenge: BraTS23 Meningioma Segmentation - rank: 1st - year: 2023 + challenge: *challenge + rank: *rank_1 + year: *year_2023 run_args: docker_image: brainles/brats23_meningioma_nvauto:latest - input_name_schema: "BraTS-MEN-{id:05d}-000" + input_name_schema: *input_name_schema_2023 requires_root: true parameters_file: true shm_size: "32gb" @@ -17,12 +80,12 @@ algorithms: meta: authors: Ziyan Huang, et al. paper: N/A - challenge: BraTS23 Meningioma Segmentation - rank: 2nd - year: 2023 + challenge: *challenge + rank: *rank_2 + year: *year_2023 run_args: docker_image: brainles/brats23_meningioma_blackbean:latest - input_name_schema: "BraTS-MEN-{id:05d}-000" + input_name_schema: *input_name_schema_2023 requires_root: true parameters_file: true shm_size: "4gb" @@ -32,12 +95,12 @@ algorithms: meta: authors: Zhifan Jiang et al. paper: N/A - challenge: BraTS23 Meningioma Segmentation - rank: 3rd - year: 2023 + challenge: *challenge + rank: *rank_3 + year: *year_2023 run_args: docker_image: brainles/brats23_meningioma_cnmc_pmi2023:latest - input_name_schema: "BraTS-MEN-{id:05d}-000" + input_name_schema: *input_name_schema_2023 requires_root: true parameters_file: false shm_size: "2gb" diff --git a/brats/data/meta/metastases.yml b/brats/data/meta/metastases.yml index cf09c42..6929eb0 100644 --- a/brats/data/meta/metastases.yml +++ b/brats/data/meta/metastases.yml @@ -3,7 +3,7 @@ algorithms: meta: authors: Andriy Myronenko, et al. paper: N/A - challenge: BraTS23 Brain Metastases Segmentation + challenge: Brain Metastases Segmentation rank: 1st year: 2023 run_args: @@ -17,7 +17,7 @@ algorithms: meta: authors: Siwei Yang, et al. paper: N/A - challenge: BraTS23 Brain Metastases Segmentation + challenge: Brain Metastases Segmentation rank: 2nd year: 2023 run_args: @@ -25,14 +25,14 @@ algorithms: input_name_schema: "BraTS-MET-{id:05d}-000" requires_root: true parameters_file: false - weights: + additional_files: record_id: "13380822" BraTS23_3: meta: authors: Ziyan Huang, et al. paper: N/A - challenge: BraTS23 Brain Metastases Segmentation + challenge: Brain Metastases Segmentation rank: 3rd year: 2023 run_args: diff --git a/brats/data/meta/missing_mri.yml b/brats/data/meta/missing_mri.yml index 4859fef..6ac3773 100644 --- a/brats/data/meta/missing_mri.yml +++ b/brats/data/meta/missing_mri.yml @@ -6,7 +6,7 @@ algorithms: meta: authors: Jihoon Cho, Seunghyuck Park, Jinah Park paper: N/A - challenge: BraTS24 Missing MRI + challenge: Missing MRI rank: 1st year: 2024 run_args: @@ -14,16 +14,16 @@ algorithms: input_name_schema: "BraTS-GLI-{id:05d}-000" requires_root: false parameters_file: true - weights: + additional_files: record_id: "14287969" param_name: ["first_weights", "second_weights"] - checkpoint_path: ["first_weight.bin", "second_weight.pth"] + param_path: ["first_weight.bin", "second_weight.pth"] BraTS24_2: meta: authors: Haowen Pang paper: N/A - challenge: BraTS24 Missing MRI + challenge: Missing MRI rank: 2nd year: 2024 run_args: @@ -31,14 +31,14 @@ algorithms: input_name_schema: "BraTS-GLI-{id:05d}-000" requires_root: false parameters_file: true - weights: + additional_files: record_id: "14288120" BraTS24_3: meta: authors: Minjoo Lim, Bogyeong Kang paper: N/A - challenge: BraTS24 Missing MRI + challenge: Missing MRI rank: 3rd year: 2024 run_args: diff --git a/brats/data/meta/pediatric.yml b/brats/data/meta/pediatric.yml index df8d194..0c3b7bf 100644 --- a/brats/data/meta/pediatric.yml +++ b/brats/data/meta/pediatric.yml @@ -1,16 +1,75 @@ +constants: + input_name_schema: &input_name_schema "BraTS-PED-{id:05d}-000" + challenge: &challenge "Pediatric Segmentation" + years: + 2024: &year_2024 2024 + 2023: &year_2023 2023 + ranks: + 1st: &rank_1 1st + 2nd: &rank_2 2nd + 3rd: &rank_3 3rd + algorithms: +##### 2024 ##### + BraTS24_1: + meta: + authors: Tim Mulvany, et al. + paper: N/A + challenge: *challenge + rank: *rank_1 + year: *year_2024 + run_args: + docker_image: brainles/brats24_pediatric_aipni:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "8gb" + additional_files: + record_id: "14446259" + + BraTS24_2: + meta: + authors: Mehdi Astaraki + paper: N/A + challenge: *challenge + rank: *rank_2 + year: *year_2024 + run_args: + docker_image: brainles/brats24_pediatric_astaraki:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "8gb" + BraTS24_3: + meta: + authors: Sarim Hashmi, et al. + paper: N/A + challenge: *challenge + rank: *rank_3 + year: *year_2024 + run_args: + docker_image: brainles/brats24_pediatric_biomedia-mbzu:latest + input_name_schema: *input_name_schema + requires_root: false + parameters_file: true + shm_size: "2gb" + additional_files: + record_id: "14446377" + param_name: ["checkpoint_dir"] + +##### 2023 ##### BraTS23_1: meta: authors: Zhifan Jiang et al. paper: N/A - challenge: BraTS23 Pediatric Segmentation - rank: 1st - year: 2023 + challenge: *challenge + rank: *rank_1 + year: *year_2023 run_args: docker_image: brainles/brats23_pediatric_cnmc_pmi2023:latest - input_name_schema: "BraTS-PED-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: false shm_size: "2gb" @@ -19,12 +78,12 @@ algorithms: meta: authors: Andriy Myronenko, et al. paper: N/A - challenge: BraTS23 Pediatric Segmentation - rank: 2nd - year: 2023 + challenge: *challenge + rank: *rank_2 + year: *year_2023 run_args: docker_image: brainles/brats23_pediatric_nvauto:latest - input_name_schema: "BraTS-PED-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: true parameters_file: true shm_size: "32gb" @@ -33,12 +92,12 @@ algorithms: meta: authors: Yubo Zhou paper: N/A - challenge: BraTS23 Pediatric Segmentation - rank: 3rd - year: 2023 + challenge: *challenge + rank: *rank_3 + year: *year_2023 run_args: docker_image: brainles/brats23_pediatric_sherlock_zyb:latest - input_name_schema: "BraTS-PED-{id:05d}-000" + input_name_schema: *input_name_schema requires_root: false parameters_file: false shm_size: "10gb" diff --git a/brats/data/parameters/brats24_adult_glioma_mist.yml b/brats/data/parameters/brats24_adult_glioma_mist.yml new file mode 100644 index 0000000..b68df27 --- /dev/null +++ b/brats/data/parameters/brats24_adult_glioma_mist.yml @@ -0,0 +1,6 @@ +fast_inference: False +overlap: 0.5 +blend_mode: "gaussian" +tta: True +no_preprocess: False +output_std: False \ No newline at end of file diff --git a/brats/utils/algorithm_config.py b/brats/utils/algorithm_config.py index c0de34e..6d4aab9 100644 --- a/brats/utils/algorithm_config.py +++ b/brats/utils/algorithm_config.py @@ -39,18 +39,20 @@ class RunArgs: """The required shared memory size for the Docker container""" cpu_compatible: Optional[bool] = False """Whether the algorithm is compatible with CPU""" + subject_modality_separator: Optional[str] = "-" + """The separator between the subject ID and the modality, differs e.g. for BraTS24 Meningioma Challenge""" @dataclass -class WeightsData: - """Dataclass for the weights data""" +class AdditionalFilesData: + """Dataclass for the additional files data""" record_id: str - """The Zenodo record ID of the weights""" + """The Zenodo record ID of the additional files""" param_name: Optional[List[str]] = field(default_factory=lambda: ["weights"]) - """The parameter(s) that specify the weights folder(s) in the algorithm execution, typically 'weights' but differs for some and can even be multiple""" - checkpoint_path: Optional[List[str]] = None - """The path(s) to specific checkpoint file(s) in the weights folder. Not required since some algorithms accept the entire weights folder""" + """The parameter(s) that specify additional file(s) in the algorithm execution, typically 'weights' but differs for some and can be multiple""" + param_path: Optional[List[str]] = None + """The path(s) to specific file(s) / folder(s) in the additional files folder. Not required since some algorithms accept the entire additional files folder""" @dataclass @@ -61,8 +63,8 @@ class AlgorithmData: """The meta data of the algorithm""" run_args: RunArgs """The run arguments of the algorithm""" - weights: Optional[WeightsData] - """The weights data of the algorithm. Optional since some algorithms include weights in the docker image""" + additional_files: Optional[AdditionalFilesData] + """The additional files of the algorithm. Optional since some algorithms include them in the docker image""" @dataclass diff --git a/brats/utils/zenodo.py b/brats/utils/zenodo.py index 8fa5a90..c801a0b 100644 --- a/brats/utils/zenodo.py +++ b/brats/utils/zenodo.py @@ -34,18 +34,22 @@ def check_additional_files_path(record_id: str) -> Path: record_id=record_id ) - record_weights_pattern = f"{record_id}_v*.*.*" - matching_folders = list(ADDITIONAL_FILES_FOLDER.glob(record_weights_pattern)) - # Get the latest downloaded weights - latest_downloaded_weights = _get_latest_version_folder_name(matching_folders) + record_additional_files_pattern = f"{record_id}_v*.*.*" + matching_folders = list( + ADDITIONAL_FILES_FOLDER.glob(record_additional_files_pattern) + ) + # Get the latest downloaded additional_files + latest_downloaded_additional_files = _get_latest_version_folder_name( + matching_folders + ) - if not latest_downloaded_weights: + if not latest_downloaded_additional_files: if not zenodo_metadata: logger.error( - "Model weights not found locally and Zenodo could not be reached. Exiting..." + "Additional files not found locally and Zenodo could not be reached. Exiting..." ) sys.exit() - logger.info(f"Model weights not found locally") + logger.info(f"Additional files not found locally") return _download_additional_files( zenodo_metadata=zenodo_metadata, @@ -53,27 +57,29 @@ def check_additional_files_path(record_id: str) -> Path: archive_url=archive_url, ) - logger.info(f"Found downloaded local weights: {latest_downloaded_weights}") + logger.info( + f"Found downloaded local additional_files: {latest_downloaded_additional_files}" + ) if not zenodo_metadata: logger.warning( - "Zenodo server could not be reached. Using the latest downloaded weights." + "Zenodo server could not be reached. Using the latest downloaded additional files." ) - return ADDITIONAL_FILES_FOLDER / latest_downloaded_weights + return ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files - # Compare the latest downloaded weights with the latest Zenodo version - if zenodo_metadata["version"] == latest_downloaded_weights.split("_v")[1]: + # Compare the latest downloaded additional files with the latest Zenodo version + if zenodo_metadata["version"] == latest_downloaded_additional_files.split("_v")[1]: logger.info( - f"Latest model weights ({latest_downloaded_weights}) are already present." + f"Latest additional files ({latest_downloaded_additional_files}) are already present." ) - return ADDITIONAL_FILES_FOLDER / latest_downloaded_weights + return ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files logger.info( - f"New model weights available on Zenodo ({zenodo_metadata['version']}). Deleting old and fetching new weights..." + f"New additional files available on Zenodo ({zenodo_metadata['version']}). Deleting old and fetching new additional files..." ) - # delete old weights + # delete old additional files shutil.rmtree( - ADDITIONAL_FILES_FOLDER / latest_downloaded_weights, + ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files, onerror=lambda func, path, excinfo: logger.warning( f"Failed to delete {path}: {excinfo}" ), @@ -115,7 +121,7 @@ def _get_zenodo_metadata_and_archive_url(record_id: str) -> Dict | None: response = requests.get(f"{ZENODO_RECORD_BASE_URL}/{record_id}") if response.status_code != 200: logger.error( - f"Cant find model weights for record_id '{record_id}' on Zenodo. Exiting..." + f"Cant find additional files for record_id '{record_id}' on Zenodo. Exiting..." ) # TODO add proper exit exception data = response.json() @@ -145,13 +151,13 @@ def _download_additional_files( # ensure folder exists record_folder.mkdir(parents=True, exist_ok=True) - logger.info(f"Downloading model weights from Zenodo. This might take a while...") + logger.info(f"Downloading additional files from Zenodo. This might take a while...") # Make a GET request to the URL response = requests.get(archive_url, stream=True) # Ensure the request was successful if response.status_code != 200: logger.error( - f"Failed to download model weights. Status code: {response.status_code}" + f"Failed to download additional files. Status code: {response.status_code}" ) return @@ -168,7 +174,7 @@ def _extract_archive(response: requests.Response, record_folder: Path): with Progress( SpinnerColumn(), - TextColumn("[cyan]Downloading weights..."), + TextColumn("[cyan]Downloading additional files..."), TextColumn("[cyan]{task.completed:.2f} MB"), transient=True, ) as progress: diff --git a/tests/core/test_docker.py b/tests/core/test_docker.py index 5889767..bcfc2ae 100644 --- a/tests/core/test_docker.py +++ b/tests/core/test_docker.py @@ -48,8 +48,8 @@ def setUp(self): shm_size="1g", cpu_compatible=False, ), - weights=MagicMock( - param_name=["weights"], checkpoint_path=["checkpoint.pth"] + additional_files=MagicMock( + param_name=["weights"], param_path=["checkpoint.pth"] ), meta=MagicMock( challenge="Challenge", @@ -66,8 +66,8 @@ def setUp(self): shm_size="1g", cpu_compatible=True, ), - weights=MagicMock( - param_name=["weights"], checkpoint_path=["checkpoint.pth"] + additional_files=MagicMock( + param_name=["weights"], param_path=["checkpoint.pth"] ), meta=MagicMock( challenge="Challenge", @@ -301,7 +301,9 @@ def test_sanity_check_output_not_enough_outputs(self, mock_nib_load, mock_logger @patch("brats.core.docker.logger") @patch("brats.core.docker.nib.load") - def test_sanity_check_output_empty_warning(self, mock_nib_load, mock_logger): + def test_sanity_check_output_empty_warning_single_inference( + self, mock_nib_load, mock_logger + ): # Create mock paths mock_data_path = MagicMock(spec=Path) mock_output_path = MagicMock(spec=Path) @@ -336,6 +338,46 @@ def test_sanity_check_output_empty_warning(self, mock_nib_load, mock_logger): # assertions mock_logger.warning.assert_called_once() + @patch("brats.core.docker.logger") + @patch("brats.core.docker.nib.load") + def test_sanity_check_output_empty_warning_batch_inference( + self, mock_nib_load, mock_logger + ): + # Create mock paths + mock_data_path = MagicMock(spec=Path) + mock_output_path = MagicMock(spec=Path) + + # Simulate input files starting with "BraTS" and output files + mock_data_path.iterdir.return_value = [ + MagicMock(name="external_file_1", spec=Path), + ] + mock_output_path.iterdir.return_value = [ + MagicMock(name="internal_file_1", spec=Path), + ] + + # Create a mock object for the fdata + mock_nifti_img = MagicMock() + # zeros! + mock_nifti_img.get_fdata.return_value = np.zeros((2, 2, 2)) + + # Mock the nib.load to return the mock nifti image + mock_nib_load.return_value = mock_nifti_img + + # Define container_output + container_output = "Sample container output" + + # Check that no exception is raised + + _sanity_check_output( + data_path=mock_data_path, + output_path=mock_output_path, + container_output=container_output, + internal_external_name_map={"internal_file_1": "external_file_1"}, + ) + + # assertions + mock_logger.warning.assert_called_once() + @patch("brats.core.docker.logger.debug") def test_log_algorithm_info(self, MockLoggerDebug): _log_algorithm_info(algorithm=self.algorithm_gpu) diff --git a/tests/core/test_inpainting_algorithms.py b/tests/core/test_inpainting_algorithms.py index 78b3124..8b38031 100644 --- a/tests/core/test_inpainting_algorithms.py +++ b/tests/core/test_inpainting_algorithms.py @@ -46,6 +46,7 @@ def test_successful_single_standardization(self, mock_input_sanity_check): "t1n": self.t1n, "mask": self.mask, }, + subject_modality_separator="-", ) subject_folder = self.tmp_data_folder / subject_id self.assertTrue(subject_folder.exists()) @@ -68,6 +69,7 @@ def test_single_standardize_handle_file_not_found_error( "t1n": t1n, "mask": self.mask, }, + subject_modality_separator="-", ) mock_logger.assert_called() mock_exit.assert_called_with(1) diff --git a/tests/core/test_missing_mri_algorithms.py b/tests/core/test_missing_mri_algorithms.py index bf49ebf..2749a06 100644 --- a/tests/core/test_missing_mri_algorithms.py +++ b/tests/core/test_missing_mri_algorithms.py @@ -48,6 +48,7 @@ def test_successful_single_standardization(self, mock_input_sanity_check): "t1c": self.t1c, "t2w": self.t2w, }, + subject_modality_separator="-", ) subject_folder = self.tmp_data_folder / subject_id self.assertTrue(subject_folder.exists()) @@ -72,6 +73,7 @@ def test_single_standardize_handle_file_not_found_error( "t1c": self.t1c, "t2w": self.t2w, }, + subject_modality_separator="-", ) mock_logger.assert_called() mock_exit.assert_called_with(1) diff --git a/tests/core/test_segmentation_algorithms.py b/tests/core/test_segmentation_algorithms.py index db341a1..fd0602f 100644 --- a/tests/core/test_segmentation_algorithms.py +++ b/tests/core/test_segmentation_algorithms.py @@ -62,6 +62,7 @@ def test_successful_single_standardization(self, mock_input_sanity_check): "t2f": self.t2f, "t2w": self.t2w, }, + subject_modality_separator="-", ) subject_folder = self.tmp_data_folder / subject_id self.assertTrue(subject_folder.exists()) @@ -88,6 +89,7 @@ def test_single_standardize_handle_file_not_found_error( "t2f": self.t2f, "t2w": self.t2w, }, + subject_modality_separator="-", ) mock_logger.assert_called() mock_exit.assert_called_with(1) @@ -166,3 +168,137 @@ def test_metastases_segmenter_initialization(self): algorithm=MetastasesAlgorithms.BraTS23_2, cuda_devices="1", force_cpu=True ) self.assertIsInstance(custom_segmenter, MetastasesSegmenter) + + ## Test MeningiomaSegmenter specialty + + @patch("brats.core.segmentation_algorithms.MeningiomaSegmenter._infer_single") + def test_meningioma_segmenter_infer_single_2023_valid(self, mock_infer_single): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS23_1) + + segmenter.infer_single( + t1c=self.t1c, + t1n=self.t1n, + t2f=self.t2f, + t2w=self.t2w, + output_file=self.tmp_data_folder / "output.nii.gz", + ) + + mock_infer_single.assert_called_once() + + @patch("brats.core.segmentation_algorithms.MeningiomaSegmenter._infer_single") + def test_meningioma_segmenter_infer_single_2023_invalid_missing_modalities( + self, mock_infer_single + ): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS23_1) + + with self.assertRaises(ValueError): + segmenter.infer_single( + t1c=self.t1c, + # t1n=self.t1n, # Missing modality + t2f=self.t2f, + t2w=self.t2w, + output_file=self.tmp_data_folder / "output.nii.gz", + ) + + mock_infer_single.assert_not_called() + + @patch("brats.core.segmentation_algorithms.MeningiomaSegmenter._infer_single") + def test_meningioma_segmenter_infer_single_2024_valid(self, mock_infer_single): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS24_1) + + segmenter.infer_single( + t1c=self.t1c, + output_file=self.tmp_data_folder / "output.nii.gz", + ) + + mock_infer_single.assert_called_once() + + @patch("brats.core.segmentation_algorithms.MeningiomaSegmenter._infer_single") + def test_meningioma_segmenter_infer_single_2024_invalid_missing_t1c( + self, mock_infer_single + ): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS24_1) + + with self.assertRaises(ValueError): + segmenter.infer_single( + # t1c=self.t1c, + # t1n=self.t1n, + # t2f=self.t2f, + t2w=self.t2w, + output_file=self.tmp_data_folder / "output.nii.gz", + ) + + mock_infer_single.assert_not_called() + + @patch("brats.core.segmentation_algorithms.MeningiomaSegmenter._infer_single") + def test_meningioma_segmenter_infer_single_2024_invalid_too_many_files( + self, mock_infer_single + ): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS24_1) + + with self.assertRaises(ValueError): + segmenter.infer_single( + t1c=self.t1c, + # t1n=self.t1n, + # t2f=self.t2f, + t2w=self.t2w, + output_file=self.tmp_data_folder / "output.nii.gz", + ) + + mock_infer_single.assert_not_called() + + @patch("brats.core.brats_algorithm.BraTSAlgorithm._process_batch_output") + @patch("brats.core.brats_algorithm.run_container") + @patch( + "brats.core.segmentation_algorithms.MeningiomaSegmenter._standardize_single_inputs" + ) + def test_meningioma_segmenter_infer_batch_2024( + self, + mock_standardize_single_inputs, + mock_run_container, + mock_process_batch_output, + ): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS24_1) + + segmenter.infer_batch( + data_folder=self.data_folder, + output_folder=self.tmp_data_folder, + log_file=self.tmp_data_folder / "log.txt", + ) + + self.assertEqual(mock_standardize_single_inputs.call_count, 1) + # assert algorithms was called with just t1c + args, kwargs = mock_standardize_single_inputs.call_args + input_keys = list(kwargs["inputs"].keys()) + self.assertEqual(input_keys, ["t1c"]) + + self.assertEqual(mock_run_container.call_count, 1) + self.assertEqual(mock_process_batch_output.call_count, 1) + + @patch("brats.core.brats_algorithm.BraTSAlgorithm._process_batch_output") + @patch("brats.core.brats_algorithm.run_container") + @patch( + "brats.core.segmentation_algorithms.MeningiomaSegmenter._standardize_single_inputs" + ) + def test_meningioma_segmenter_infer_batch_2023( + self, + mock_standardize_single_inputs, + mock_run_container, + mock_process_batch_output, + ): + segmenter = MeningiomaSegmenter(algorithm=MeningiomaAlgorithms.BraTS23_1) + + segmenter.infer_batch( + data_folder=self.data_folder, + output_folder=self.tmp_data_folder, + log_file=self.tmp_data_folder / "log.txt", + ) + + self.assertEqual(mock_standardize_single_inputs.call_count, 1) + # assert algorithms was called with all modalities + args, kwargs = mock_standardize_single_inputs.call_args + input_keys = list(kwargs["inputs"].keys()) + self.assertEqual(input_keys, ["t1c", "t1n", "t2f", "t2w"]) + + self.assertEqual(mock_run_container.call_count, 1) + self.assertEqual(mock_process_batch_output.call_count, 1) diff --git a/tests/utils/test_zenodo.py b/tests/utils/test_zenodo.py index 30fa613..05497a6 100644 --- a/tests/utils/test_zenodo.py +++ b/tests/utils/test_zenodo.py @@ -44,13 +44,13 @@ def test_check_additional_files_path( "http://test.url", ) - # Test when local weights are up-to-date + # Test when local additional_files are up-to-date result = check_additional_files_path(mock_record_id) self.assertEqual(result, ADDITIONAL_FILES_FOLDER / f"{mock_record_id}_v1.0.0") mock_rmtree.assert_not_called() mock_download_additional_files.assert_not_called() - # Test when new weights are available + # Test when new additional_files are available mock_get_zenodo_metadata.return_value = ( {"version": "2.0.0"}, "http://test.url",