Skip to content

Commit 13bdec5

Browse files
Add nnUNet integration and corresponding unit tests
1 parent 8ac8e0d commit 13bdec5

3 files changed

Lines changed: 451 additions & 0 deletions

File tree

monai/bundle/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313

1414
from .config_item import ComponentLocator, ConfigComponent, ConfigExpression, ConfigItem, Instantiable
1515
from .config_parser import ConfigParser
16+
from .nnunet import get_nnunet_monai_predictor, get_nnunet_trainer, nnUNetMONAIModelWrapper
1617
from .properties import InferProperties, MetaProperties, TrainProperties
1718
from .reference_resolver import ReferenceResolver
1819
from .scripts import (

monai/bundle/nnunet.py

Lines changed: 338 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,338 @@
1+
# Copyright (c) MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
from __future__ import annotations
12+
13+
import os
14+
15+
import numpy as np
16+
import torch
17+
from torch._dynamo import OptimizedModule
18+
from torch.backends import cudnn
19+
20+
from monai.data.meta_tensor import MetaTensor
21+
from monai.utils import optional_import
22+
23+
join, _ = optional_import("batchgenerators.utilities.file_and_folder_operations", name="join")
24+
load_json, _ = optional_import("batchgenerators.utilities.file_and_folder_operations", name="load_json")
25+
26+
__all__ = ["get_nnunet_trainer", "get_nnunet_monai_predictor", "nnUNetMONAIModelWrapper"]
27+
28+
29+
def get_nnunet_trainer(
30+
dataset_name_or_id,
31+
configuration,
32+
fold,
33+
trainer_class_name="nnUNetTrainer",
34+
plans_identifier="nnUNetPlans",
35+
pretrained_weights=None,
36+
num_gpus=1,
37+
use_compressed_data=False,
38+
export_validation_probabilities=False,
39+
continue_training=False,
40+
only_run_validation=False,
41+
disable_checkpointing=False,
42+
val_with_best=False,
43+
device=torch.device("cuda"),
44+
pretrained_model=None,
45+
):
46+
"""
47+
Get the nnUNet trainer instance based on the provided configuration.
48+
The returned nnUNet trainer can be used to initialize the SupervisedTrainer for training, including the network,
49+
optimizer, loss function, DataLoader, etc.
50+
51+
```python
52+
from monai.apps import SupervisedTrainer
53+
from monai.bundle.nnunet import get_nnunet_trainer
54+
55+
dataset_name_or_id = 'Task101_PROSTATE'
56+
fold = 0
57+
configuration = '3d_fullres'
58+
nnunet_trainer = get_nnunet_trainer(dataset_name_or_id, configuration, fold)
59+
60+
trainer = SupervisedTrainer(
61+
device=nnunet_trainer.device,
62+
max_epochs=nnunet_trainer.num_epochs,
63+
train_data_loader=nnunet_trainer.dataloader_train,
64+
network=nnunet_trainer.network,
65+
optimizer=nnunet_trainer.optimizer,
66+
loss_function=nnunet_trainer.loss_function,
67+
epoch_length=nnunet_trainer.num_iterations_per_epoch,
68+
69+
```
70+
71+
Parameters
72+
----------
73+
dataset_name_or_id : Union[str, int]
74+
The name or ID of the dataset to be used.
75+
configuration : str
76+
The configuration name for the training.
77+
fold : Union[int, str]
78+
The fold number or 'all' for cross-validation.
79+
trainer_class_name : str, optional
80+
The class name of the trainer to be used. Default is 'nnUNetTrainer'.
81+
plans_identifier : str, optional
82+
Identifier for the plans to be used. Default is 'nnUNetPlans'.
83+
pretrained_weights : str, optional
84+
Path to the pretrained weights file.
85+
num_gpus : int, optional
86+
Number of GPUs to be used. Default is 1.
87+
use_compressed_data : bool, optional
88+
Whether to use compressed data. Default is False.
89+
export_validation_probabilities : bool, optional
90+
Whether to export validation probabilities. Default is False.
91+
continue_training : bool, optional
92+
Whether to continue training from a checkpoint. Default is False.
93+
only_run_validation : bool, optional
94+
Whether to only run validation. Default is False.
95+
disable_checkpointing : bool, optional
96+
Whether to disable checkpointing. Default is False.
97+
val_with_best : bool, optional
98+
Whether to validate with the best model. Default is False.
99+
device : torch.device, optional
100+
The device to be used for training. Default is 'cuda'.
101+
pretrained_model : str, optional
102+
Path to the pretrained model file.
103+
Returns
104+
-------
105+
nnunet_trainer
106+
The nnUNet trainer instance.
107+
"""
108+
# From nnUNet/nnunetv2/run/run_training.py#run_training
109+
if isinstance(fold, str):
110+
if fold != "all":
111+
try:
112+
fold = int(fold)
113+
except ValueError as e:
114+
print(
115+
f'Unable to convert given value for fold to int: {fold}. fold must bei either "all" or an integer!'
116+
)
117+
raise e
118+
119+
if int(num_gpus) > 1:
120+
... # Disable for now
121+
else:
122+
from nnunetv2.run.run_training import get_trainer_from_args, maybe_load_checkpoint
123+
124+
nnunet_trainer = get_trainer_from_args(
125+
str(dataset_name_or_id),
126+
configuration,
127+
fold,
128+
trainer_class_name,
129+
plans_identifier,
130+
use_compressed_data,
131+
device=device,
132+
)
133+
if disable_checkpointing:
134+
nnunet_trainer.disable_checkpointing = disable_checkpointing
135+
136+
assert not (continue_training and only_run_validation), "Cannot set --c and --val flag at the same time. Dummy."
137+
138+
maybe_load_checkpoint(nnunet_trainer, continue_training, only_run_validation, pretrained_weights)
139+
nnunet_trainer.on_train_start() # Added to Initialize Trainer
140+
if torch.cuda.is_available():
141+
cudnn.deterministic = False
142+
cudnn.benchmark = True
143+
144+
if pretrained_model is not None:
145+
state_dict = torch.load(pretrained_model)
146+
if "network_weights" in state_dict:
147+
nnunet_trainer.network._orig_mod.load_state_dict(state_dict["network_weights"])
148+
return nnunet_trainer
149+
150+
151+
class nnUNetMONAIModelWrapper(torch.nn.Module):
152+
"""
153+
A wrapper class for nnUNet model integration with MONAI framework.
154+
The wrapper can be use to integrate the nnUNet Bundle within MONAI framework for inference.
155+
156+
Parameters
157+
----------
158+
predictor : object
159+
The nnUNet predictor object used for inference.
160+
model_folder : str
161+
The folder path where the model and related files are stored.
162+
model_name : str, optional
163+
The name of the model file, by default "model.pt".
164+
Attributes
165+
----------
166+
predictor : object
167+
The predictor object used for inference.
168+
network_weights : torch.nn.Module
169+
The network weights of the model.
170+
Methods
171+
-------
172+
forward(x)
173+
Perform forward pass and prediction on the input data.
174+
Notes
175+
-----
176+
This class integrates nnUNet model with MONAI framework by loading necessary configurations,
177+
restoring network architecture, and setting up the predictor for inference.
178+
"""
179+
180+
def __init__(self, predictor, model_folder, model_name="model.pt"):
181+
super().__init__()
182+
self.predictor = predictor
183+
184+
model_training_output_dir = model_folder
185+
use_folds = "0"
186+
187+
from nnunetv2.utilities.plans_handling.plans_handler import PlansManager
188+
189+
## Block Added from nnUNet/nnunetv2/inference/predict_from_raw_data.py#nnUNetPredictor
190+
dataset_json = load_json(join(model_training_output_dir, "dataset.json"))
191+
plans = load_json(join(model_training_output_dir, "plans.json"))
192+
plans_manager = PlansManager(plans)
193+
194+
if isinstance(use_folds, str):
195+
use_folds = [use_folds]
196+
197+
parameters = []
198+
for i, f in enumerate(use_folds):
199+
f = int(f) if f != "all" else f
200+
checkpoint = torch.load(
201+
join(model_training_output_dir, "nnunet_checkpoint.pth"), map_location=torch.device("cpu")
202+
)
203+
monai_checkpoint = torch.load(join(model_training_output_dir, model_name), map_location=torch.device("cpu"))
204+
if i == 0:
205+
trainer_name = checkpoint["trainer_name"]
206+
configuration_name = checkpoint["init_args"]["configuration"]
207+
inference_allowed_mirroring_axes = (
208+
checkpoint["inference_allowed_mirroring_axes"]
209+
if "inference_allowed_mirroring_axes" in checkpoint.keys()
210+
else None
211+
)
212+
213+
parameters.append(monai_checkpoint["network_weights"])
214+
215+
configuration_manager = plans_manager.get_configuration(configuration_name)
216+
# restore network
217+
import nnunetv2
218+
from nnunetv2.utilities.find_class_by_name import recursive_find_python_class
219+
from nnunetv2.utilities.label_handling.label_handling import determine_num_input_channels
220+
221+
num_input_channels = determine_num_input_channels(plans_manager, configuration_manager, dataset_json)
222+
trainer_class = recursive_find_python_class(
223+
join(nnunetv2.__path__[0], "training", "nnUNetTrainer"), trainer_name, "nnunetv2.training.nnUNetTrainer"
224+
)
225+
if trainer_class is None:
226+
raise RuntimeError(
227+
f"Unable to locate trainer class {trainer_name} in nnunetv2.training.nnUNetTrainer. "
228+
f"Please place it there (in any .py file)!"
229+
)
230+
network = trainer_class.build_network_architecture(
231+
configuration_manager.network_arch_class_name,
232+
configuration_manager.network_arch_init_kwargs,
233+
configuration_manager.network_arch_init_kwargs_req_import,
234+
num_input_channels,
235+
plans_manager.get_label_manager(dataset_json).num_segmentation_heads,
236+
enable_deep_supervision=False,
237+
)
238+
239+
predictor.plans_manager = plans_manager
240+
predictor.configuration_manager = configuration_manager
241+
predictor.list_of_parameters = parameters
242+
predictor.network = network
243+
predictor.dataset_json = dataset_json
244+
predictor.trainer_name = trainer_name
245+
predictor.allowed_mirroring_axes = inference_allowed_mirroring_axes
246+
predictor.label_manager = plans_manager.get_label_manager(dataset_json)
247+
if (
248+
("nnUNet_compile" in os.environ.keys())
249+
and (os.environ["nnUNet_compile"].lower() in ("true", "1", "t"))
250+
and not isinstance(predictor.network, OptimizedModule)
251+
):
252+
print("Using torch.compile")
253+
predictor.network = torch.compile(self.network)
254+
## End Block
255+
self.network_weights = self.predictor.network
256+
257+
def forward(self, x):
258+
if type(x) is tuple:
259+
input_files = [img.meta["filename_or_obj"][0] for img in x]
260+
else:
261+
input_files = x.meta["filename_or_obj"]
262+
if type(input_files) is str:
263+
input_files = [input_files]
264+
265+
output = self.predictor.predict_from_files(
266+
[input_files],
267+
None,
268+
save_probabilities=False,
269+
overwrite=True,
270+
num_processes_preprocessing=2,
271+
num_processes_segmentation_export=2,
272+
folder_with_segs_from_prev_stage=None,
273+
num_parts=1,
274+
part_id=0,
275+
)
276+
277+
out_tensors = []
278+
for out in output:
279+
out_tensors.append(torch.from_numpy(np.expand_dims(np.expand_dims(out, 0), 0)))
280+
out_tensor = torch.cat(out_tensors, 0)
281+
282+
if type(x) is tuple:
283+
return MetaTensor(out_tensor, meta=x[0].meta)
284+
else:
285+
return MetaTensor(out_tensor, meta=x.meta)
286+
287+
288+
def get_nnunet_monai_predictor(model_folder, model_name="model.pt"):
289+
"""
290+
Initializes and returns a nnUNetMONAIModelWrapper with a nnUNetPredictor.
291+
The model folder should contain the following files, created during training:
292+
- dataset.json: from the nnUNet results folder.
293+
- plans.json: from the nnUNet results folder.
294+
- nnunet_checkpoint.pth: The nnUNet checkpoint file, containing the nnUNet training configuration
295+
(`init_kwargs`, `trainer_name`, `inference_allowed_mirroring_axes`).
296+
- model.pt: The checkpoint file containing the model weights.
297+
298+
The returned wrapper object can be used for inference with MONAI framework:
299+
```python
300+
from monai.bundle.nnunet import get_nnunet_monai_predictor
301+
302+
model_folder = 'path/to/monai_bundle/model'
303+
model_name = 'model.pt'
304+
wrapper = get_nnunet_monai_predictor(model_folder, model_name)
305+
306+
# Perform inference
307+
input_data = ...
308+
output = wrapper(input_data)
309+
310+
```
311+
312+
Parameters
313+
----------
314+
model_folder : str
315+
The folder where the model is stored.
316+
model_name : str, optional
317+
The name of the model file, by default "model.pt".
318+
319+
Returns
320+
-------
321+
nnUNetMONAIModelWrapper
322+
A wrapper object that contains the nnUNetPredictor and the loaded model.
323+
"""
324+
325+
from nnunetv2.inference.predict_from_raw_data import nnUNetPredictor
326+
327+
predictor = nnUNetPredictor(
328+
tile_step_size=0.5,
329+
use_gaussian=True,
330+
use_mirroring=False,
331+
device=torch.device("cuda", 0),
332+
verbose=False,
333+
verbose_preprocessing=False,
334+
allow_tqdm=True,
335+
)
336+
# initializes the network architecture, loads the checkpoint
337+
wrapper = nnUNetMONAIModelWrapper(predictor, model_folder, model_name)
338+
return wrapper

0 commit comments

Comments
 (0)