1515import pickle
1616import time
1717import zipfile
18+ from copy import deepcopy
1819from pathlib import Path
1920from threading import Lock
2021from typing import Any , Dict , List , Optional , Tuple , Type , Union
2930
3031from .inference_operator import InferenceOperator
3132
33+ MONAI_UTILS = "monai.utils"
3234nibabel , _ = optional_import ("nibabel" , "3.2.1" )
33- torch , _ = optional_import ("torch" , "1.10.0 " )
35+ torch , _ = optional_import ("torch" , "1.10.2 " )
3436
37+ NdarrayOrTensor , _ = optional_import ("monai.config" , name = "NdarrayOrTensor" )
38+ MetaTensor , _ = optional_import ("monai.data.meta_tensor" , name = "MetaTensor" )
3539PostFix , _ = optional_import ("monai.utils.enums" , name = "PostFix" ) # For the default meta_key_postfix
3640first , _ = optional_import ("monai.utils.misc" , name = "first" )
37- ensure_tuple , _ = optional_import ("monai.utils" , name = "ensure_tuple" )
41+ ensure_tuple , _ = optional_import (MONAI_UTILS , name = "ensure_tuple" )
42+ convert_to_dst_type , _ = optional_import (MONAI_UTILS , name = "convert_to_dst_type" )
43+ Key , _ = optional_import (MONAI_UTILS , name = "ImageMetaKey" )
44+ MetaKeys , _ = optional_import (MONAI_UTILS , name = "MetaKeys" )
45+ SpaceKeys , _ = optional_import (MONAI_UTILS , name = "SpaceKeys" )
3846Compose_ , _ = optional_import ("monai.transforms" , name = "Compose" )
3947ConfigParser_ , _ = optional_import ("monai.bundle" , name = "ConfigParser" )
4048MapTransform_ , _ = optional_import ("monai.transforms" , name = "MapTransform" )
4553MapTransform : Any = MapTransform_
4654ConfigParser : Any = ConfigParser_
4755
56+
4857__all__ = ["MonaiBundleInferenceOperator" , "IOMapping" , "BundleConfigNames" ]
4958
5059
@@ -198,7 +207,7 @@ def _ensure_str_list(config_names):
198207# operator may choose to pass in a accessible bundle path at development and packaging stage. Ideally,
199208# the bundle path should be passed in by the Packager, e.g. via env var, when the App is initialized.
200209# As of now, the Packager only passes in the model path after the App including all operators are init'ed.
201- @md .env (pip_packages = ["monai==0.9 .0" , "torch>=1.10.02" , "numpy>=1.21" , "nibabel>=3.2.1" ])
210+ @md .env (pip_packages = ["monai>=1.0 .0" , "torch>=1.10.02" , "numpy>=1.21" , "nibabel>=3.2.1" ])
202211class MonaiBundleInferenceOperator (InferenceOperator ):
203212 """This inference operator automates the inference operation for a given MONAI Bundle.
204213
@@ -477,14 +486,19 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
477486
478487 start = time .time ()
479488 for name in self ._inputs .keys ():
480- value , metadata = self ._receive_input (name , op_input , context )
489+ # Input MetaTensor creation is based on the same logic in monai LoadImage
490+ # value: NdarrayOrTensor # MyPy complaints
491+ value , meta_data = self ._receive_input (name , op_input , context )
492+ value = convert_to_dst_type (value , dst = value )[0 ]
493+ if not isinstance (meta_data , dict ):
494+ raise ValueError ("`meta_data` must be a dict." )
495+ value = MetaTensor .ensure_torch_and_prune_meta (value , meta_data )
481496 inputs [name ] = value
482- if metadata :
483- inputs [(f"{ name } _{ self ._meta_key_postfix } " )] = metadata
497+ # Named metadata dict not needed any more, as it is in the MetaTensor
484498
485499 inputs = self .pre_process (inputs )
486- first_input = inputs . pop ( first_input_name )[ None ]. to ( self . _device ) # select first input
487- input_metadata = inputs .get ( f" { first_input_name } _ { self ._meta_key_postfix } " , None )
500+ first_input_v = inputs [ first_input_name ] # keep a copy of value for later use
501+ first_input = inputs .pop ( first_input_name )[ None ]. to ( self ._device )
488502
489503 # select other tensor inputs
490504 other_inputs = {k : v [None ].to (self ._device ) for k , v in inputs .items () if isinstance (v , torch .Tensor )}
@@ -496,9 +510,10 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
496510 outputs : Any = self .predict (data = first_input , ** other_inputs ) # Use type Any to quiet MyPy complaints.
497511 logging .debug (f"Inference elapsed time (seconds): { time .time () - start } " )
498512
499- # TODO: Does this work for models where multiple outputs are returned?
500- # Note that the inputs are needed because the invert transform requires it .
513+ # Note that the `inputs` are needed because the `invert` transform requires it. With metadata being
514+ # in the keyed MetaTensors of inputs, e.g. `image`, the whole inputs are needed .
501515 start = time .time ()
516+ inputs [first_input_name ] = first_input_v
502517 kw_args = {self .kw_preprocessed_inputs : inputs }
503518 outputs = self .post_process (ensure_tuple (outputs )[0 ], ** kw_args )
504519 logging .debug (f"Post-processing elapsed time (seconds): { time .time () - start } " )
@@ -512,7 +527,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
512527 for name in self ._outputs .keys ():
513528 # Note that the input metadata needs to be passed.
514529 # Please see the comments in the called function for the reasons.
515- self ._send_output (output_dict [name ], name , input_metadata , op_output , context )
530+ self ._send_output (output_dict [name ], name , first_input_v . meta , op_output , context )
516531
517532 def predict (self , data : Any , * args , ** kwargs ) -> Union [Image , Any , Tuple [Any , ...], Dict [Any , Any ]]:
518533 """Predicts output using the inferer."""
@@ -698,7 +713,7 @@ def _convert_from_image_dicom_source(self, img: Image) -> Tuple[np.ndarray, Dict
698713 """
699714
700715 img_meta_dict : Dict = img .metadata ()
701- meta_dict = { key : img_meta_dict [ key ] for key in img_meta_dict . keys ()}
716+ meta_dict = deepcopy ( img_meta_dict )
702717
703718 # The MONAI ImageReader, e.g. the ITKReader, arranges the image spatial dims in WHD,
704719 # so the "spacing" needs to be expressed in such an order too, as expected by the transforms.
@@ -709,18 +724,20 @@ def _convert_from_image_dicom_source(self, img: Image) -> Tuple[np.ndarray, Dict
709724 img_meta_dict ["depth_pixel_spacing" ],
710725 ]
711726 )
712- meta_dict ["original_affine" ] = np .asarray (img_meta_dict .get ("nifti_affine_transform" , None ))
713- meta_dict ["affine" ] = meta_dict ["original_affine" ]
727+ # Use defines MetaKeys directly
728+ meta_dict [MetaKeys .ORIGINAL_AFFINE ] = np .asarray (img_meta_dict .get ("nifti_affine_transform" , None ))
729+ meta_dict [MetaKeys .AFFINE ] = meta_dict [MetaKeys .ORIGINAL_AFFINE ].copy ()
730+ meta_dict [MetaKeys .SPACE ] = SpaceKeys .LPS # not using SpaceKeys.RAS or affine_lps_to_ras
714731
715732 # Similarly the Image ndarray has dim order DHW, to be rearranged to WHD.
716733 # TODO: Need to revisit this once multi-channel image is supported and the Image class itself
717734 # is enhanced to provide attributes or functions for channel and dim order details.
718735 converted_image = np .swapaxes (img .asnumpy (), 0 , 2 )
719736
720737 # The spatial shape is then that of the converted image, in WHD
721- meta_dict ["spatial_shape" ] = np .asarray (converted_image .shape )
738+ meta_dict [MetaKeys . SPATIAL_SHAPE ] = np .asarray (converted_image .shape )
722739
723740 # Well, now channel for now.
724- meta_dict ["original_channel_dim" ] = "no_channel"
741+ meta_dict [MetaKeys . ORIGINAL_CHANNEL_DIM ] = "no_channel"
725742
726743 return converted_image , meta_dict
0 commit comments