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/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
+ )