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/core/docker.py
ADDED
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
import time
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict, List, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import docker
|
|
10
|
+
import nibabel as nib
|
|
11
|
+
import numpy as np
|
|
12
|
+
from docker.errors import DockerException
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from rich.console import Console
|
|
15
|
+
from rich.progress import Progress
|
|
16
|
+
from rich.table import Table
|
|
17
|
+
from rich import box
|
|
18
|
+
|
|
19
|
+
from brats.constants import DUMMY_PARAMETERS, PARAMETERS_DIR, PACKAGE_CITATION
|
|
20
|
+
from brats.utils.algorithm_config import AlgorithmData
|
|
21
|
+
from brats.utils.exceptions import (
|
|
22
|
+
AlgorithmNotCPUCompatibleException,
|
|
23
|
+
BraTSContainerException,
|
|
24
|
+
)
|
|
25
|
+
from brats.utils.zenodo import check_additional_files_path, get_dummy_path
|
|
26
|
+
|
|
27
|
+
try:
|
|
28
|
+
client = docker.from_env()
|
|
29
|
+
except DockerException as e:
|
|
30
|
+
logger.error(
|
|
31
|
+
f"Failed to connect to docker daemon. Please make sure docker is installed and running. Error: {e}"
|
|
32
|
+
)
|
|
33
|
+
# not aborting since this happens during read the docs builds. not a great solution tbf
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _show_docker_pull_progress(tasks: Dict, progress: Progress, line: Dict):
|
|
37
|
+
"""Show the progress of a docker pull operation.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
tasks (Dict): The tasks to update
|
|
41
|
+
progress (Progress): The progress bar to update
|
|
42
|
+
line (Dict): the next line from docker.client.api.pull stream
|
|
43
|
+
"""
|
|
44
|
+
status = line.get("status")
|
|
45
|
+
layer_id = line.get("id", "unknown")
|
|
46
|
+
|
|
47
|
+
if status in {"Downloading", "Extracting"}:
|
|
48
|
+
task_key = f"[{status} {layer_id}]"
|
|
49
|
+
|
|
50
|
+
progress_detail = line.get("progressDetail") or {}
|
|
51
|
+
total = progress_detail.get("total")
|
|
52
|
+
current = progress_detail.get("current")
|
|
53
|
+
|
|
54
|
+
# Case 1: we know total size -> normal progress bar
|
|
55
|
+
if total:
|
|
56
|
+
if task_key not in tasks:
|
|
57
|
+
tasks[task_key] = progress.add_task(f"{task_key}", total=total)
|
|
58
|
+
progress.update(tasks[task_key], completed=current or 0)
|
|
59
|
+
|
|
60
|
+
# Case 2: no total/current -> animated bar showing indefinite progress
|
|
61
|
+
else:
|
|
62
|
+
if task_key not in tasks:
|
|
63
|
+
tasks[task_key] = progress.add_task(
|
|
64
|
+
f"{task_key}", total=None # total=None means indeterminate
|
|
65
|
+
)
|
|
66
|
+
progress.update(tasks[task_key], advance=0.1)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _ensure_image(image: str):
|
|
70
|
+
"""Ensure the docker image is present on the system. If not, pull it.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
image (str): The docker image to pull
|
|
74
|
+
"""
|
|
75
|
+
if not client.images.list(name=image):
|
|
76
|
+
logger.info(f"Pulling docker image {image}")
|
|
77
|
+
tasks = {}
|
|
78
|
+
with Progress() as progress:
|
|
79
|
+
resp = client.api.pull(image, stream=True, decode=True)
|
|
80
|
+
for line in resp:
|
|
81
|
+
_show_docker_pull_progress(tasks=tasks, progress=progress, line=line)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _is_cuda_available() -> bool:
|
|
85
|
+
"""Check if CUDA is available on the system by trying to run nvidia-smi."""
|
|
86
|
+
try:
|
|
87
|
+
# Attempt to run `nvidia-smi` to check for CUDA.
|
|
88
|
+
# This command should run successfully if NVIDIA drivers are installed and GPUs are present.
|
|
89
|
+
subprocess.run(
|
|
90
|
+
["nvidia-smi"],
|
|
91
|
+
stdout=subprocess.DEVNULL,
|
|
92
|
+
stderr=subprocess.DEVNULL,
|
|
93
|
+
check=True,
|
|
94
|
+
)
|
|
95
|
+
return True
|
|
96
|
+
except:
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _handle_device_requests(
|
|
101
|
+
algorithm: AlgorithmData, cuda_devices: str, force_cpu: bool
|
|
102
|
+
) -> List[docker.types.DeviceRequest]:
|
|
103
|
+
"""Handle the device requests for the docker container (request cuda or cpu).
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
algorithm (AlgorithmData): Algorithm data
|
|
107
|
+
cuda_devices (str): The CUDA devices to use
|
|
108
|
+
force_cpu (bool): Whether to force CPU execution
|
|
109
|
+
"""
|
|
110
|
+
cuda_available = _is_cuda_available()
|
|
111
|
+
if not cuda_available or force_cpu:
|
|
112
|
+
if not algorithm.run_args.cpu_compatible:
|
|
113
|
+
cause = (
|
|
114
|
+
"User tried to force CPU execution but"
|
|
115
|
+
if cuda_available
|
|
116
|
+
else "No Cuda installation/ GPU was found and"
|
|
117
|
+
)
|
|
118
|
+
raise AlgorithmNotCPUCompatibleException(
|
|
119
|
+
f"{cause} the chosen algorithm is not CPU-compatible. Aborting..."
|
|
120
|
+
)
|
|
121
|
+
# empty device requests => run on CPU
|
|
122
|
+
logger.info("Forcing CPU execution")
|
|
123
|
+
return []
|
|
124
|
+
# request gpu with chosen devices
|
|
125
|
+
return [
|
|
126
|
+
docker.types.DeviceRequest(device_ids=[cuda_devices], capabilities=[["gpu"]])
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _get_additional_files_path(algorithm: AlgorithmData) -> Path:
|
|
131
|
+
"""Get the path to the additional files for @algorithm.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
algorithm (AlgorithmData): The algorithm data
|
|
135
|
+
|
|
136
|
+
Returns:
|
|
137
|
+
Path to the additional files
|
|
138
|
+
"""
|
|
139
|
+
# ensure additional_files are present and get path
|
|
140
|
+
if algorithm.additional_files is not None:
|
|
141
|
+
return check_additional_files_path(
|
|
142
|
+
record_id=algorithm.additional_files.record_id
|
|
143
|
+
)
|
|
144
|
+
else:
|
|
145
|
+
# if no additional_files are directly specified a dummy additional_files folder will be mounted
|
|
146
|
+
return get_dummy_path()
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _get_volume_mappings_mlcube(
|
|
150
|
+
data_path: Path,
|
|
151
|
+
additional_files_path: Path,
|
|
152
|
+
output_path: Path,
|
|
153
|
+
parameters_path: Path,
|
|
154
|
+
) -> Dict:
|
|
155
|
+
"""Get the volume mappings for the docker container.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
data_path (Path): The path to the input data
|
|
159
|
+
additional_files_path (Path): The path to the additional files
|
|
160
|
+
output_path (Path): The path to save the output
|
|
161
|
+
parameters_path (Path): The path to mount for the parameters file
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dict: The volume mappings
|
|
165
|
+
"""
|
|
166
|
+
# TODO: add support for recommended "ro" mount mode for input data
|
|
167
|
+
# data = mlcube_io0, additional files = mlcube_io1, output = mlcube_io2, parameters = mlcube_io3
|
|
168
|
+
return {
|
|
169
|
+
volume.absolute(): {
|
|
170
|
+
"bind": f"/mlcube_io{i}",
|
|
171
|
+
"mode": "rw",
|
|
172
|
+
}
|
|
173
|
+
for i, volume in enumerate(
|
|
174
|
+
[data_path, additional_files_path, output_path, parameters_path]
|
|
175
|
+
)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _get_volume_mappings_docker_only(
|
|
180
|
+
data_path: Path,
|
|
181
|
+
output_path: Path,
|
|
182
|
+
) -> Dict[str, Dict[str, str]]:
|
|
183
|
+
"""Get the volume mappings for the docker container.
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
data_path (Path): The path to the input data
|
|
187
|
+
output_path (Path): The path to save the output
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
Dict: The volume mappings
|
|
191
|
+
"""
|
|
192
|
+
return {
|
|
193
|
+
str(data_path.absolute()): {"bind": "/input", "mode": "rw"},
|
|
194
|
+
str(output_path.absolute()): {"bind": "/output", "mode": "rw"},
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _get_parameters_arg(algorithm: AlgorithmData) -> Optional[str]:
|
|
199
|
+
"""Get the parameters argument for the docker container.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
algorithm (AlgorithmData): The algorithm data
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Optional[str]: The parameters argument for the docker container or None if a parameter file is not required
|
|
206
|
+
"""
|
|
207
|
+
if algorithm.run_args.parameters_file:
|
|
208
|
+
# Docker image name is used as the identifier for the param file
|
|
209
|
+
identifier = algorithm.run_args.docker_image.split(":")[0].split("/")[-1]
|
|
210
|
+
file = PARAMETERS_DIR / f"{identifier}.yml"
|
|
211
|
+
# Some algorithms do require a param file to be present but don't actually use it
|
|
212
|
+
# In this case we simply use a dummy file
|
|
213
|
+
param_file = file if file.exists() else DUMMY_PARAMETERS
|
|
214
|
+
return f" --parameters_file=/mlcube_io3/{param_file.name}"
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _build_command_args(
|
|
219
|
+
algorithm: AlgorithmData,
|
|
220
|
+
) -> str:
|
|
221
|
+
"""Build the command arguments for the docker container.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
algorithm (AlgorithmData): The algorithm data
|
|
225
|
+
|
|
226
|
+
Returns:
|
|
227
|
+
command_args: The command arguments
|
|
228
|
+
"""
|
|
229
|
+
# Build command that will be run in the docker container
|
|
230
|
+
command_args = f"--data_path=/mlcube_io0 --output_path=/mlcube_io2"
|
|
231
|
+
if algorithm.additional_files is not None:
|
|
232
|
+
for i, param in enumerate(algorithm.additional_files.param_name):
|
|
233
|
+
additional_files_arg = f"--{param}=/mlcube_io1"
|
|
234
|
+
if algorithm.additional_files.param_path:
|
|
235
|
+
additional_files_arg += f"/{algorithm.additional_files.param_path[i]}"
|
|
236
|
+
command_args += f" {additional_files_arg}"
|
|
237
|
+
|
|
238
|
+
# Add parameters file arg if required
|
|
239
|
+
params_arg = _get_parameters_arg(algorithm=algorithm)
|
|
240
|
+
if params_arg:
|
|
241
|
+
command_args += params_arg
|
|
242
|
+
|
|
243
|
+
return command_args
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _get_container_user(
|
|
247
|
+
algorithm: AlgorithmData,
|
|
248
|
+
) -> Optional[str]:
|
|
249
|
+
"""Build extra arguments for the docker container.
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
algorithm (AlgorithmData): The algorithm data
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Optional[str]: The user to run the container as or None if root is required
|
|
256
|
+
|
|
257
|
+
"""
|
|
258
|
+
|
|
259
|
+
if not algorithm.run_args.requires_root:
|
|
260
|
+
# run the container as the current user to ensure written files are always owned by the user
|
|
261
|
+
# also overall better security-wise
|
|
262
|
+
return f"{os.getuid()}:{os.getgid()}"
|
|
263
|
+
return None
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def _observe_docker_output(container: docker.models.containers.Container) -> str:
|
|
267
|
+
"""Observe the output of a running docker container and display a spinner. On Errors log container output.
|
|
268
|
+
|
|
269
|
+
Args:
|
|
270
|
+
container (docker.models.containers.Container): The container to observe
|
|
271
|
+
"""
|
|
272
|
+
# capture the output
|
|
273
|
+
container_output = container.attach(
|
|
274
|
+
stdout=True, stderr=True, stream=True, logs=True
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
# Display spinner while the container is running
|
|
278
|
+
with Console().status("Running inference..."):
|
|
279
|
+
# Wait for the container to finish
|
|
280
|
+
exit_code = container.wait()
|
|
281
|
+
container_output = "\n\r".join(
|
|
282
|
+
[line.decode("utf-8", errors="replace") for line in container_output]
|
|
283
|
+
)
|
|
284
|
+
# Check if the container exited with an error
|
|
285
|
+
if exit_code["StatusCode"] != 0:
|
|
286
|
+
logger.error(f">> {container_output}")
|
|
287
|
+
raise BraTSContainerException(
|
|
288
|
+
"Container finished with an error:\n"
|
|
289
|
+
f"{'-'*80}\n{container_output}\n {'-'*80}\n"
|
|
290
|
+
"For further debugging please specify a log file to capture the output or enable console logging (brats.utils.logging.enable())"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return container_output
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _sanity_check_output(
|
|
297
|
+
data_path: Path,
|
|
298
|
+
output_path: Path,
|
|
299
|
+
container_output: str,
|
|
300
|
+
internal_external_name_map: Optional[Dict[str, str]] = None,
|
|
301
|
+
) -> None:
|
|
302
|
+
"""Sanity check that the number of output files matches the number of input files and the output is not empty.
|
|
303
|
+
|
|
304
|
+
Args:
|
|
305
|
+
data_path (Path): The path to the input data
|
|
306
|
+
output_path (Path): The path to the output data
|
|
307
|
+
container_output (str): The output of the docker container
|
|
308
|
+
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)
|
|
309
|
+
|
|
310
|
+
Raises:
|
|
311
|
+
BraTSContainerException: If not enough output files exist
|
|
312
|
+
"""
|
|
313
|
+
|
|
314
|
+
# some algorithms create extra files in the data folder, so we only check for files starting with "BraTS"
|
|
315
|
+
# (should result in only counting actual inputs)
|
|
316
|
+
inputs = [e for e in data_path.iterdir() if e.name.startswith("BraTS")]
|
|
317
|
+
outputs = list(output_path.iterdir())
|
|
318
|
+
if len(outputs) < len(inputs):
|
|
319
|
+
logger.error(f"Docker container output: \n\r{container_output}")
|
|
320
|
+
raise BraTSContainerException(
|
|
321
|
+
f"Not enough output files were created by the algorithm. Expected: {len(inputs)} Got: {len(outputs)}. Please check the logging output of the docker container for more information."
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
for i, output in enumerate(outputs, start=1):
|
|
325
|
+
content = nib.load(output).get_fdata()
|
|
326
|
+
if np.count_nonzero(content) == 0:
|
|
327
|
+
name = "<unknown>"
|
|
328
|
+
if internal_external_name_map is not None:
|
|
329
|
+
name_key = [
|
|
330
|
+
k for k in internal_external_name_map.keys() if k in output.name
|
|
331
|
+
]
|
|
332
|
+
if name_key:
|
|
333
|
+
name = internal_external_name_map[name_key[0]]
|
|
334
|
+
|
|
335
|
+
logger.warning(
|
|
336
|
+
f"""Output file for subject {name} contains only zeros.
|
|
337
|
+
Potentially the selected algorithm might not work properly with your data unless this behavior is correct for your use case.
|
|
338
|
+
If this seems wrong please try to use one of the other provided algorithms and file an issue on GitHub if the problem persists."""
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
def _log_algorithm_info(algorithm: AlgorithmData):
|
|
343
|
+
"""Log information about the algorithm and citation reminder.
|
|
344
|
+
|
|
345
|
+
Args:
|
|
346
|
+
algorithm (AlgorithmData): algorithm data
|
|
347
|
+
"""
|
|
348
|
+
|
|
349
|
+
# intentional prints! (to always display them even when logging is disabled)
|
|
350
|
+
console = Console()
|
|
351
|
+
console.rule("[bold red]Citation Reminder[/bold red]")
|
|
352
|
+
console.print(
|
|
353
|
+
"Please support our development by citing the relevant manuscripts for the used algorithm:\n"
|
|
354
|
+
)
|
|
355
|
+
table = Table(
|
|
356
|
+
show_header=False,
|
|
357
|
+
show_lines=True,
|
|
358
|
+
show_edge=False,
|
|
359
|
+
box=box.ASCII,
|
|
360
|
+
)
|
|
361
|
+
table.add_column("type", justify="right", style="cyan")
|
|
362
|
+
table.add_column("url", justify="left", style="white")
|
|
363
|
+
table.add_row("BraTS Package", PACKAGE_CITATION)
|
|
364
|
+
table.add_row(
|
|
365
|
+
f"Challenge ({algorithm.meta.challenge} {algorithm.meta.year})",
|
|
366
|
+
algorithm.meta.challenge_manuscript,
|
|
367
|
+
)
|
|
368
|
+
table.add_row(f"Algorithm ({algorithm.meta.authors})", algorithm.meta.paper)
|
|
369
|
+
if algorithm.meta.dataset_manuscript:
|
|
370
|
+
table.add_row(f"Dataset", algorithm.meta.dataset_manuscript)
|
|
371
|
+
|
|
372
|
+
console.print(table)
|
|
373
|
+
console.rule()
|
|
374
|
+
|
|
375
|
+
logger.opt(colors=True).info(
|
|
376
|
+
f"Running algorithm: <light-green> BraTS {algorithm.meta.year} {algorithm.meta.challenge} [{algorithm.meta.rank} place]</>"
|
|
377
|
+
)
|
|
378
|
+
logger.debug(f"Docker image: {algorithm.run_args.docker_image}")
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def run_container(
|
|
382
|
+
algorithm: AlgorithmData,
|
|
383
|
+
data_path: Path,
|
|
384
|
+
output_path: Path,
|
|
385
|
+
cuda_devices: str,
|
|
386
|
+
force_cpu: bool,
|
|
387
|
+
internal_external_name_map: Optional[Dict[str, str]] = None,
|
|
388
|
+
):
|
|
389
|
+
"""Run a docker container for the provided algorithm.
|
|
390
|
+
|
|
391
|
+
Args:
|
|
392
|
+
algorithm (AlgorithmData): The data of the algorithm to run
|
|
393
|
+
data_path (Path | str): The path to the input data
|
|
394
|
+
output_path (Path | str): The path to save the output
|
|
395
|
+
cuda_devices (str): The CUDA devices to use
|
|
396
|
+
force_cpu (bool): Whether to force CPU execution
|
|
397
|
+
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)
|
|
398
|
+
"""
|
|
399
|
+
_log_algorithm_info(algorithm=algorithm)
|
|
400
|
+
|
|
401
|
+
# ensure image is present, if not pull it
|
|
402
|
+
_ensure_image(image=algorithm.run_args.docker_image)
|
|
403
|
+
|
|
404
|
+
additional_files_path = _get_additional_files_path(algorithm)
|
|
405
|
+
|
|
406
|
+
# ensure output folder exists
|
|
407
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
408
|
+
|
|
409
|
+
# device setup
|
|
410
|
+
device_requests = _handle_device_requests(
|
|
411
|
+
algorithm=algorithm,
|
|
412
|
+
cuda_devices=cuda_devices,
|
|
413
|
+
force_cpu=force_cpu,
|
|
414
|
+
)
|
|
415
|
+
logger.debug(f"GPU Device requests: {device_requests}")
|
|
416
|
+
|
|
417
|
+
user = _get_container_user(algorithm=algorithm)
|
|
418
|
+
logger.debug(f"Container user: {user if user else 'root (required by algorithm)'}")
|
|
419
|
+
|
|
420
|
+
logger.info(f"{'Starting inference'}")
|
|
421
|
+
start_time = time.time()
|
|
422
|
+
|
|
423
|
+
if algorithm.meta.year <= 2024:
|
|
424
|
+
volume_mappings = _get_volume_mappings_mlcube(
|
|
425
|
+
data_path=data_path,
|
|
426
|
+
additional_files_path=additional_files_path,
|
|
427
|
+
output_path=output_path,
|
|
428
|
+
parameters_path=PARAMETERS_DIR,
|
|
429
|
+
)
|
|
430
|
+
logger.debug(f"Volume mappings: {volume_mappings}")
|
|
431
|
+
|
|
432
|
+
command_args = _build_command_args(algorithm=algorithm)
|
|
433
|
+
logger.debug(f"Command args: {command_args}")
|
|
434
|
+
|
|
435
|
+
# Run the container
|
|
436
|
+
container = client.containers.run(
|
|
437
|
+
image=algorithm.run_args.docker_image,
|
|
438
|
+
volumes=volume_mappings,
|
|
439
|
+
device_requests=device_requests,
|
|
440
|
+
command=f"infer {command_args}",
|
|
441
|
+
network_mode="none",
|
|
442
|
+
detach=True,
|
|
443
|
+
remove=True,
|
|
444
|
+
shm_size=algorithm.run_args.shm_size,
|
|
445
|
+
user=user,
|
|
446
|
+
)
|
|
447
|
+
else:
|
|
448
|
+
volume_mappings = _get_volume_mappings_docker_only(
|
|
449
|
+
data_path=data_path,
|
|
450
|
+
output_path=output_path,
|
|
451
|
+
)
|
|
452
|
+
logger.debug(f"Volume mappings: {volume_mappings}")
|
|
453
|
+
container = client.containers.run(
|
|
454
|
+
image=algorithm.run_args.docker_image,
|
|
455
|
+
volumes=volume_mappings,
|
|
456
|
+
device_requests=device_requests,
|
|
457
|
+
network_mode="none",
|
|
458
|
+
detach=True,
|
|
459
|
+
remove=True,
|
|
460
|
+
shm_size=algorithm.run_args.shm_size,
|
|
461
|
+
user=user,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
container_output = _observe_docker_output(container=container)
|
|
465
|
+
_sanity_check_output(
|
|
466
|
+
data_path=data_path,
|
|
467
|
+
output_path=output_path,
|
|
468
|
+
container_output=container_output,
|
|
469
|
+
internal_external_name_map=internal_external_name_map,
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
logger.debug(f"Docker container output: \n\r{container_output}")
|
|
473
|
+
|
|
474
|
+
logger.info(f"Finished inference in {time.time() - start_time:.2f} seconds")
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import shutil
|
|
5
|
+
import sys
|
|
6
|
+
from typing import Dict, Optional
|
|
7
|
+
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
from brats.core.brats_algorithm import BraTSAlgorithm
|
|
11
|
+
from brats.constants import INPAINTING_ALGORITHMS, InpaintingAlgorithms, Task, Backends
|
|
12
|
+
from brats.utils.data_handling import input_sanity_check
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Inpainter(BraTSAlgorithm):
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
algorithm: InpaintingAlgorithms = InpaintingAlgorithms.BraTS23_1,
|
|
20
|
+
cuda_devices: str | None = "0",
|
|
21
|
+
force_cpu: bool = False,
|
|
22
|
+
):
|
|
23
|
+
super().__init__(
|
|
24
|
+
algorithm=algorithm,
|
|
25
|
+
algorithms_file_path=INPAINTING_ALGORITHMS,
|
|
26
|
+
task=Task.INPAINTING,
|
|
27
|
+
cuda_devices=cuda_devices,
|
|
28
|
+
force_cpu=force_cpu,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def _standardize_single_inputs(
|
|
32
|
+
self,
|
|
33
|
+
data_folder: Path,
|
|
34
|
+
subject_id: str,
|
|
35
|
+
inputs: dict[str, Path | str],
|
|
36
|
+
subject_modality_separator: str,
|
|
37
|
+
) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Standardize the input data to match the requirements of the selected algorithm.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
data_folder (Path): Path to the data folder
|
|
43
|
+
subject_id (str): Subject ID
|
|
44
|
+
inputs (dict[str, Path | str]): Dictionary with the input data
|
|
45
|
+
subject_modality_separator (str): Separator between the subject ID and the modality
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
subject_folder = data_folder / subject_id
|
|
49
|
+
subject_folder.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
# TODO: investigate usage of symlinks (might cause issues on windows and would probably require different volume handling)
|
|
51
|
+
t1n, mask = inputs["t1n"], inputs["mask"]
|
|
52
|
+
try:
|
|
53
|
+
shutil.copy(
|
|
54
|
+
t1n,
|
|
55
|
+
subject_folder
|
|
56
|
+
/ f"{subject_id}{subject_modality_separator}t1n-voided.nii.gz",
|
|
57
|
+
)
|
|
58
|
+
shutil.copy(
|
|
59
|
+
mask,
|
|
60
|
+
subject_folder / f"{subject_id}{subject_modality_separator}mask.nii.gz",
|
|
61
|
+
)
|
|
62
|
+
except FileNotFoundError as e:
|
|
63
|
+
logger.error(f"Error while standardizing files: {e}")
|
|
64
|
+
raise
|
|
65
|
+
|
|
66
|
+
# sanity check inputs
|
|
67
|
+
input_sanity_check(t1n=t1n, mask=mask)
|
|
68
|
+
|
|
69
|
+
def _standardize_batch_inputs(
|
|
70
|
+
self, data_folder: Path, subjects: list[Path], input_name_schema: str
|
|
71
|
+
) -> Dict[str, str]:
|
|
72
|
+
"""Standardize the input images for a list of subjects to match requirements of all algorithms and save them in @tmp_data_folder/@subject_id.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
subjects (List[Path]): List of subject folders, each with a voided t1n and a mask image
|
|
76
|
+
data_folder (Path): Parent folder where the subject folders will be created
|
|
77
|
+
input_name_schema (str): Schema to be used for the subject folder and filenames depending on the BraTS Challenge
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
Dict[str, str]: Dictionary mapping internal name (in standardized format) to external subject name provided by user
|
|
81
|
+
"""
|
|
82
|
+
internal_external_name_map = {}
|
|
83
|
+
for i, subject in enumerate(subjects):
|
|
84
|
+
internal_name = input_name_schema.format(id=i)
|
|
85
|
+
internal_external_name_map[internal_name] = subject.name
|
|
86
|
+
# TODO Add support for .nii files
|
|
87
|
+
|
|
88
|
+
self._standardize_single_inputs(
|
|
89
|
+
data_folder=data_folder,
|
|
90
|
+
subject_id=internal_name,
|
|
91
|
+
inputs={
|
|
92
|
+
"t1n": subject / f"{subject.name}-t1n-voided.nii.gz",
|
|
93
|
+
"mask": subject / f"{subject.name}-mask.nii.gz",
|
|
94
|
+
},
|
|
95
|
+
subject_modality_separator=self.algorithm.run_args.subject_modality_separator,
|
|
96
|
+
)
|
|
97
|
+
return internal_external_name_map
|
|
98
|
+
|
|
99
|
+
def infer_single(
|
|
100
|
+
self,
|
|
101
|
+
t1n: Path | str,
|
|
102
|
+
mask: Path | str,
|
|
103
|
+
output_file: Path | str,
|
|
104
|
+
log_file: Optional[Path | str] = None,
|
|
105
|
+
backend: Optional[Backends] = Backends.DOCKER,
|
|
106
|
+
) -> None:
|
|
107
|
+
"""Perform inpainting task on a single subject with the provided images and save the result to the output file.
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
t1n (Path | str): Path to the voided T1n image
|
|
111
|
+
mask (Path | str): Path to the mask image
|
|
112
|
+
output_file (Path | str): Path to save the segmentation
|
|
113
|
+
log_file (Path | str, optional): Save logs to this file
|
|
114
|
+
backend (Backends, optional): Backend to use for inference. Defaults to Backends.DOCKER.
|
|
115
|
+
"""
|
|
116
|
+
|
|
117
|
+
self._infer_single(
|
|
118
|
+
inputs={"t1n": t1n, "mask": mask},
|
|
119
|
+
output_file=output_file,
|
|
120
|
+
log_file=log_file,
|
|
121
|
+
backend=backend,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def infer_batch(
|
|
125
|
+
self,
|
|
126
|
+
data_folder: Path | str,
|
|
127
|
+
output_folder: Path | str,
|
|
128
|
+
log_file: Path | str | None = None,
|
|
129
|
+
backend: Optional[Backends] = Backends.DOCKER,
|
|
130
|
+
) -> None:
|
|
131
|
+
"""Perform inpainting on a batch of subjects with the provided images and save the results to the output folder. \n
|
|
132
|
+
Requires the following structure:\n
|
|
133
|
+
data_folder\n
|
|
134
|
+
┣ A\n
|
|
135
|
+
┃ ┣ A-t1n-voided.nii.gz\n
|
|
136
|
+
┃ ┣ A-mask.nii.gz\n
|
|
137
|
+
┣ B\n
|
|
138
|
+
┃ ┣ B-t1n-voided.nii.gz\n
|
|
139
|
+
┃ ┣ B-mask.nii.gz\n
|
|
140
|
+
┣ C ...\n
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
data_folder (Path | str): Folder containing the subjects with required structure
|
|
145
|
+
output_folder (Path | str): Output folder to save the segmentations
|
|
146
|
+
log_file (Path | str, optional): Save logs to this file
|
|
147
|
+
backend (Backends, optional): Backend to use for inference. Defaults to Backends.DOCKER.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
return self._infer_batch(
|
|
151
|
+
data_folder=data_folder,
|
|
152
|
+
output_folder=output_folder,
|
|
153
|
+
log_file=log_file,
|
|
154
|
+
backend=backend,
|
|
155
|
+
)
|