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.
- brats/__init__.py +16 -0
- brats/constants.py +268 -0
- brats/core/__init__.py +0 -0
- brats/core/brats_algorithm.py +264 -0
- brats/core/docker.py +474 -0
- brats/core/inpainting_algorithms.py +155 -0
- brats/core/missing_mri_algorithms.py +185 -0
- brats/core/segmentation_algorithms.py +491 -0
- brats/core/singularity.py +268 -0
- brats/data/meta/adult_glioma_pre_and_post_treatment.yml +123 -0
- brats/data/meta/adult_glioma_pre_treatment.yml +64 -0
- brats/data/meta/africa.yml +185 -0
- brats/data/meta/goat.yml +84 -0
- brats/data/meta/inpainting.yml +163 -0
- brats/data/meta/meningioma.yml +96 -0
- brats/data/meta/meningioma_rt.yml +113 -0
- brats/data/meta/metastases.yml +94 -0
- brats/data/meta/missing_mri.yml +125 -0
- brats/data/meta/pediatric.yml +168 -0
- brats/data/parameters/brats23_africa_sparkunn.yml +63 -0
- brats/data/parameters/brats23_inpainting_domaso.yml +44 -0
- brats/data/parameters/brats23_missing_mri_onembyte.yml +5 -0
- brats/data/parameters/brats24_adult_glioma_mist.yml +6 -0
- brats/data/parameters/brats24_inpainting_smint.yml +37 -0
- brats/data/parameters/brats24_missing_mri_cgv.yml +72 -0
- brats/data/parameters/dummy.yml +0 -0
- brats/preprocessing.py +435 -0
- brats/utils/__init__.py +0 -0
- brats/utils/algorithm_config.py +102 -0
- brats/utils/data_handling.py +116 -0
- brats/utils/exceptions.py +25 -0
- brats/utils/logging.py +73 -0
- brats/utils/zenodo.py +217 -0
- brats-0.0.0.dev0.dist-info/METADATA +581 -0
- brats-0.0.0.dev0.dist-info/RECORD +37 -0
- brats-0.0.0.dev0.dist-info/WHEEL +4 -0
- 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
|