BraTS 0.0.0.dev0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. brats/__init__.py +16 -0
  2. brats/constants.py +268 -0
  3. brats/core/__init__.py +0 -0
  4. brats/core/brats_algorithm.py +264 -0
  5. brats/core/docker.py +474 -0
  6. brats/core/inpainting_algorithms.py +155 -0
  7. brats/core/missing_mri_algorithms.py +185 -0
  8. brats/core/segmentation_algorithms.py +491 -0
  9. brats/core/singularity.py +268 -0
  10. brats/data/meta/adult_glioma_pre_and_post_treatment.yml +123 -0
  11. brats/data/meta/adult_glioma_pre_treatment.yml +64 -0
  12. brats/data/meta/africa.yml +185 -0
  13. brats/data/meta/goat.yml +84 -0
  14. brats/data/meta/inpainting.yml +163 -0
  15. brats/data/meta/meningioma.yml +96 -0
  16. brats/data/meta/meningioma_rt.yml +113 -0
  17. brats/data/meta/metastases.yml +94 -0
  18. brats/data/meta/missing_mri.yml +125 -0
  19. brats/data/meta/pediatric.yml +168 -0
  20. brats/data/parameters/brats23_africa_sparkunn.yml +63 -0
  21. brats/data/parameters/brats23_inpainting_domaso.yml +44 -0
  22. brats/data/parameters/brats23_missing_mri_onembyte.yml +5 -0
  23. brats/data/parameters/brats24_adult_glioma_mist.yml +6 -0
  24. brats/data/parameters/brats24_inpainting_smint.yml +37 -0
  25. brats/data/parameters/brats24_missing_mri_cgv.yml +72 -0
  26. brats/data/parameters/dummy.yml +0 -0
  27. brats/preprocessing.py +435 -0
  28. brats/utils/__init__.py +0 -0
  29. brats/utils/algorithm_config.py +102 -0
  30. brats/utils/data_handling.py +116 -0
  31. brats/utils/exceptions.py +25 -0
  32. brats/utils/logging.py +73 -0
  33. brats/utils/zenodo.py +217 -0
  34. brats-0.0.0.dev0.dist-info/METADATA +581 -0
  35. brats-0.0.0.dev0.dist-info/RECORD +37 -0
  36. brats-0.0.0.dev0.dist-info/WHEEL +4 -0
  37. brats-0.0.0.dev0.dist-info/licenses/LICENSE +201 -0
brats/utils/logging.py ADDED
@@ -0,0 +1,73 @@
1
+ import sys
2
+ import threading
3
+ from typing import Optional, Union
4
+
5
+ from loguru import logger
6
+
7
+ # Singleton handler reference
8
+ _console_handler_id: Optional[int] = None
9
+ _console_handler_lock = threading.Lock()
10
+
11
+
12
+ def _reset_logging_state_for_tests():
13
+ """
14
+ Reset internal logging state. Only intended for use in tests.
15
+ """
16
+ global _console_handler_id
17
+ logger.remove()
18
+ _console_handler_id = None
19
+
20
+
21
+ def disable():
22
+ """
23
+ Disable the logging for the brats package.
24
+ """
25
+
26
+ logger.disable("brats")
27
+
28
+
29
+ def enable():
30
+ """
31
+ Enable the logging for the brats package.
32
+ """
33
+
34
+ logger.enable("brats")
35
+
36
+
37
+ def add_console_handler(level: Union[str, int] = "WARNING"):
38
+ """
39
+ Add a console handler to the logger for the brats package.
40
+
41
+ Follows the singleton pattern, ensuring only one console handler is present at any time.
42
+
43
+ Args:
44
+ level (str | int): The logging level for the console handler. Defaults to "WARNING".
45
+ """
46
+ global _console_handler_id
47
+
48
+ with _console_handler_lock:
49
+ if _console_handler_id is None:
50
+ _console_handler_id = logger.add(sys.stderr, level=level)
51
+ else:
52
+ try:
53
+ logger.remove(_console_handler_id)
54
+ except ValueError:
55
+ # If the handler was already removed or doesn't exist, we can safely ignore this.
56
+ pass
57
+ _console_handler_id = logger.add(sys.stderr, level=level)
58
+
59
+
60
+ def remove_console_handler():
61
+ """
62
+ Remove the console handler if it was added.
63
+ """
64
+ global _console_handler_id
65
+
66
+ with _console_handler_lock:
67
+ if _console_handler_id is not None:
68
+ try:
69
+ logger.remove(_console_handler_id)
70
+ except ValueError:
71
+ # If the handler was already removed or doesn't exist, we can safely ignore this.
72
+ pass
73
+ _console_handler_id = None
brats/utils/zenodo.py ADDED
@@ -0,0 +1,217 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ import sys
5
+ import zipfile
6
+ from io import BytesIO
7
+ from pathlib import Path
8
+ from typing import Dict, List, Tuple
9
+
10
+ import requests
11
+ from loguru import logger
12
+ from rich.progress import Progress, SpinnerColumn, TextColumn
13
+
14
+ from brats.constants import ADDITIONAL_FILES_FOLDER, ZENODO_RECORD_BASE_URL
15
+ from brats.utils.exceptions import ZenodoException
16
+
17
+
18
+ def get_dummy_path() -> Path:
19
+ dummy = ADDITIONAL_FILES_FOLDER / "dummy"
20
+ dummy.mkdir(exist_ok=True, parents=True)
21
+ return dummy
22
+
23
+
24
+ def check_additional_files_path(record_id: str) -> Path:
25
+ """Check if latest additional files are present and download them otherwise.
26
+
27
+ Args:
28
+ record_id (str): Zenodo record ID.
29
+
30
+ Raises:
31
+ ZenodoException: If the additional files are not found locally and Zenodo is unreachable.
32
+
33
+ Returns:
34
+ Path: Path to the additional files folder.
35
+ """
36
+
37
+ zenodo_response = _get_zenodo_metadata_and_archive_url(record_id=record_id)
38
+
39
+ record_additional_files_pattern = f"{record_id}_v*.*.*"
40
+ matching_folders = list(
41
+ ADDITIONAL_FILES_FOLDER.glob(record_additional_files_pattern)
42
+ )
43
+ # Get the latest downloaded additional_files
44
+ latest_downloaded_additional_files = _get_latest_version_folder_name(
45
+ matching_folders
46
+ )
47
+
48
+ if not latest_downloaded_additional_files:
49
+ if not zenodo_response:
50
+ msg = "Additional files not found locally and Zenodo could not be reached. Exiting..."
51
+ logger.error(msg)
52
+ raise ZenodoException(msg)
53
+ logger.info(f"Additional files not found locally")
54
+
55
+ zenodo_metadata, archive_url = zenodo_response
56
+ return _download_additional_files(
57
+ zenodo_metadata=zenodo_metadata,
58
+ record_id=record_id,
59
+ archive_url=archive_url,
60
+ )
61
+
62
+ logger.info(
63
+ f"Found downloaded local additional_files: {latest_downloaded_additional_files}"
64
+ )
65
+
66
+ if not zenodo_response:
67
+ logger.warning(
68
+ "Zenodo server could not be reached. Using the latest downloaded additional files."
69
+ )
70
+ return ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files
71
+
72
+ zenodo_metadata, archive_url = zenodo_response
73
+ # Compare the latest downloaded additional files with the latest Zenodo version
74
+ if zenodo_metadata["version"] == latest_downloaded_additional_files.split("_v")[1]:
75
+ logger.info(
76
+ f"Latest additional files ({latest_downloaded_additional_files}) are already present."
77
+ )
78
+ return ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files
79
+
80
+ logger.info(
81
+ f"New additional files available on Zenodo ({zenodo_metadata['version']}). Deleting old and fetching new additional files..."
82
+ )
83
+ # delete old additional files
84
+ shutil.rmtree(
85
+ ADDITIONAL_FILES_FOLDER / latest_downloaded_additional_files,
86
+ onerror=lambda func, path, excinfo: logger.warning(
87
+ f"Failed to delete {path}: {excinfo}"
88
+ ),
89
+ )
90
+ return _download_additional_files(
91
+ zenodo_metadata=zenodo_metadata, record_id=record_id, archive_url=archive_url
92
+ )
93
+
94
+
95
+ def _get_latest_version_folder_name(folders: List[Path]) -> str | None:
96
+ """Get the latest (non empty) version folder name from the list of folders.
97
+
98
+ Args:
99
+ folders (List[Path]): List of folders matching the pattern.
100
+
101
+ Returns:
102
+ str | None: Latest version folder name if one exists, else None.
103
+ """
104
+ if not folders:
105
+ return None
106
+ latest_downloaded_folder = sorted(
107
+ folders,
108
+ reverse=True,
109
+ key=lambda x: tuple(map(int, str(x).split("_v")[1].split("."))),
110
+ )[0]
111
+ # check folder is not empty
112
+ if not list(latest_downloaded_folder.glob("*")):
113
+ return None
114
+ return latest_downloaded_folder.name
115
+
116
+
117
+ def _get_zenodo_metadata_and_archive_url(record_id: str) -> Tuple[Dict, str] | None:
118
+ """Get the metadata for the Zenodo record and the files archive url.
119
+
120
+ Args:
121
+ record_id (str): Zenodo record ID.
122
+
123
+ Returns:
124
+ Union[Tuple[Dict,str], None]: Tuple with Metadata dict and archive URL if the record exists, else None.
125
+ """
126
+ try:
127
+ response = requests.get(f"{ZENODO_RECORD_BASE_URL}/{record_id}")
128
+ if response.status_code != 200:
129
+ error_msg = f"Can not find additional files for record_id '{record_id}' on Zenodo ({response.status_code=}). Exiting..."
130
+ logger.error(error_msg)
131
+ raise ZenodoException(error_msg)
132
+
133
+ data = response.json()
134
+ return data["metadata"], data["links"]["archive"]
135
+
136
+ except requests.exceptions.RequestException as e:
137
+ logger.warning(f"Failed to fetch Zenodo metadata: {e}")
138
+ return None
139
+
140
+
141
+ def _download_additional_files(
142
+ zenodo_metadata: Dict, record_id: str, archive_url: str
143
+ ) -> Path:
144
+ """Download the latest additional files from Zenodo for the requested record and extract them to the target folder.
145
+
146
+ Args:
147
+ zenodo_metadata (Dict): Metadata for the Zenodo record.
148
+ record_id (str): Zenodo record ID.
149
+ archive_url (str): URL to the archive file.
150
+
151
+ Raises:
152
+ ZenodoException: If the download fails or the response is not successful.
153
+
154
+ Returns:
155
+ Path: Path to the folder where the additional files were extracted.
156
+ """
157
+ record_folder = (
158
+ ADDITIONAL_FILES_FOLDER / f"{record_id}_v{zenodo_metadata['version']}"
159
+ )
160
+ # ensure folder exists
161
+ record_folder.mkdir(parents=True, exist_ok=True)
162
+
163
+ logger.info(f"Downloading additional files from Zenodo. This might take a while...")
164
+ # Make a GET request to the URL
165
+ response = requests.get(archive_url, stream=True)
166
+ # Ensure the request was successful
167
+ if response.status_code != 200:
168
+ error_msg = (
169
+ f"Failed to download additional files. Status code: {response.status_code}"
170
+ )
171
+ logger.error(error_msg)
172
+ raise ZenodoException(error_msg)
173
+
174
+ _extract_archive(response=response, record_folder=record_folder)
175
+
176
+ logger.info(f"Zip file extracted successfully to {record_folder}")
177
+ return record_folder
178
+
179
+
180
+ def _extract_archive(response: requests.Response, record_folder: Path):
181
+ # Download with progress bar
182
+ chunk_size = 1024 # 1KB
183
+ bytes_io = BytesIO()
184
+
185
+ with Progress(
186
+ SpinnerColumn(),
187
+ TextColumn("[cyan]Downloading additional files..."),
188
+ TextColumn("[cyan]{task.completed:.2f} MB"),
189
+ transient=True,
190
+ ) as progress:
191
+ task = progress.add_task("", total=None) # Indeterminate progress
192
+
193
+ for data in response.iter_content(chunk_size=chunk_size):
194
+ bytes_io.write(data)
195
+ progress.update(
196
+ task, advance=len(data) / (chunk_size**2)
197
+ ) # Convert bytes to MB
198
+
199
+ # Extract the downloaded zip file to the target folder
200
+ with zipfile.ZipFile(bytes_io) as zip_ref:
201
+ zip_ref.extractall(record_folder)
202
+
203
+ # check if the extracted file is still a zip
204
+ for f in record_folder.iterdir():
205
+ if f.is_file() and f.suffix == ".zip":
206
+ with zipfile.ZipFile(f) as zip_ref:
207
+ files = zip_ref.namelist()
208
+ with Progress(transient=True) as progress:
209
+ task = progress.add_task(
210
+ "[cyan]Extracting files...", total=len(files)
211
+ )
212
+ # Iterate over the files and extract them
213
+ for i, file in enumerate(files):
214
+ zip_ref.extract(file, record_folder)
215
+ # Update the progress bar
216
+ progress.update(task, completed=i + 1)
217
+ f.unlink() # remove zip after extraction