Skip to content

Add MONAI Bundle inference operator and update example app #303

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Jul 11, 2022

Conversation

MMelQin
Copy link
Collaborator

@MMelQin MMelQin commented Jun 22, 2022

This PR is based on the bundle_addtion branch that @ericspod created for automating the parsing of MONAI Bundle and execution of inference, with some notable changes:

  • The base Application uses ModelFactory to load model files, e.g. MONAI Bundle TorchScript file, and makes the model network and model file path available through the ExecutionContext. The bundle operator will rely on this mechanism to retrieve the bundle path, and completes the bundle parsing when the operator is executed. The operator does support an optional arg to passing in the bundle path on its constructor so that the parsing can be completed on object creation. The models folder is known to the App at its creation time, but the logic for selecting the specific named model out of potentially multiple models is within the ModelFactory. The App's creation and execution sequence needs to be looked at in future releases so that named model and its path can be retrieved during App creation time so that the bundle path can be reliably passed to the bundle operator on its creation.
  • Similar related to the sequence of creation and execution, the bundle operator needs to have its input and output defined on creation so that the App's executor can validate their compatibilities with connected operators. Also, the operator input and output can have IN_MEMORY and DISK storage type, which is information not coming from the bundle, so the application needs to define it on creating the bundle operator instance.
  • Added support for parsing metadata from input image and preserving the metadata. This is required so that the inversion in post-processing can work. Also added are special treatment for parsing image object generated from DICOM instances as the order of dims and the names of some metadata are different from what are expected by the transforms.
  • Although the bundle operator will typically be used in between other operators, e.g. DICOM image converter and seg image to DICOM writer operators, a user might use the bundle operator as the first or leaf operator. For such cases, the app executor does have restrictions that the storage type needs to be DISK, so the input or output needs to be files. In order not to have many file formats to support in this initial release of the bundle operator, only Python pickle file is supported. Pickle file does have some security concerns, though content is only used as pure data object, validated after loading, thus mitigating the risk.
  • The bundle operator can be used in an application that has multiple models, by specifying the model name (the ModelFactory has a specification on model folder structure when multiple models are used).
  • The bundle operator and the example app have been tested with the Spleen_CT_Segmentation bundle from MONAI Model Zoo.

@vikashg
Copy link
Collaborator

vikashg commented Jun 28, 2022

Hey @MMelQin So I am going through the code to test the functions. I think it will be helpful if you can also share any test code that you might have written. For example in monai_bundle_inference_operator.py I am looking at the inputs


    def __init__(
        self,
        input_mapping: List[IOMapping],
        output_mapping: List[IOMapping],
        model_name: Optional[str] = "",
        bundle_path: Optional[str] = "",
        bundle_config_names: Optional[BundleConfigNames] = DEFAULT_BundleConfigNames,
        *args,
        **kwargs,
    ):

and then I have to guess the inputs. I am looking at the spleen_ct_segmentation as the model for testing.
A small example will really help peering into the code.
Thanks

@vikashg
Copy link
Collaborator

vikashg commented Jun 28, 2022

Actually nevermind. I was looking at the diffs and I see the operator being using here

I always find the answers after I already post about it 😞

Copy link
Collaborator

@gigony gigony left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @MMelQin !
Looks good to me!
Left some comments on things I would like to know more.

@@ -94,6 +94,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
pre_transforms,
post_transforms,
overlap=0.6,
model_name=" ",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder why model_name has " " instead of "".

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch! I was testing the model names and may have left the blank str. Will make it a empty str, even though the arg default is already empty str.

return self.known_io_data_types[ctype]
elif isinstance(ctype, type): # type object
return ctype
else: # don't know, something that hasn't been figured out
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to add some logging message in case this path is executed?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do.

value = op_input.get(name)

metadata = None
if isinstance(value, DataPath):
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great to see it is compatible with any storage type!

Just wonder if we were also able to make it work with ordinary image files, instead of pickle files.
For example, If the type of value is DataPath (especially for folder path), instead of removing load operator from bundle's transforms, we may be able to pass the file/folder paths to bundle's load transform?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question! I should have left more comments in the code because I myself thought about this for quite a bit.

  • Was considering loading other, and a limited set of, files, e.g. NIfTI/MHD which we already know exactly what to do, but on second thought, decided to clamp down on only pickle file, and will have more discussion on the list of file types to b supported in the future releases (we may end up with reusing the MONAI Core ImageReader impl if need be).
  • In my earlier versions of the bundle operator, as well as in the previous release, I actually did customize the LoadImage transform in the pre-process Compose, albeit with the IN_MEM image reader/converter. But now with the bundle API, it makes more sense to treat the loadImage transform opaquely, i.e. either in or out and no need to customize it when compute is called for a specific payload. The also will allow us to have a more flexible loadimage impl in another operator, e.g. I was considering a multi-modal use case where a JSON file is one of the inputs, beside an image file (see the first point).

MMelQin added 9 commits July 6, 2022 16:25
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
The base inference operator needs to adjust the function signatures due to the expanded
use of types resulting from the intro if bundle inference operator.
The new version of MyPy also seems to be stricter on types.

Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
@MMelQin MMelQin force-pushed the mqin/Issue286_Inference_Operator_for_MONAI_Bundle branch from 64ac711 to 17aa29e Compare July 6, 2022 23:33
…ts but for consistency.

Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
@vikashg
Copy link
Collaborator

vikashg commented Jul 7, 2022

Hi @MMelQin,
So because my installation was always conflicting with the existing python install. I created a docker and installed monai-deploy-app-sdk. This installation is clean. After that I went inside 'examples/app/ai_spleen_seg_app'. As in the example I issued the following command

python app.py -m /workspace/app/model-zoo/models/spleen_ct_segmentation/ -i /workspace/app/data/dcm/ -o ./out 

This model-zoo was downloaded from the github repo but I realised that there is no model in it and so I get the following error

INFO:root:Begin compose
DEBUG:root:Bundle path, None, not valid. Will get it in the execution context.
INFO:root:End compose
INFO:__main__.AISpleenSegApp:Begin run
Going to initiate execution of operator DICOMDataLoaderOperator
Executing operator DICOMDataLoaderOperator (Process ID: 797, Operator ID: 7c9ae4f5-d491-4c55-a06b-caf4002a6190)
Traceback (most recent call last):
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 116, in _load_data
    sop_instances.append(dcmread(file))
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/utils/importutil.py", line 269, in __call__
    raise self._exception
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/utils/importutil.py", line 221, in optional_import
    pkg = __import__(module)  # top level module
monai.deploy.utils.importutil.OptionalImportError: from pydicom import dcmread (No module named 'pydicom').

For details about installing the optional dependencies, please visit:
    https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "app.py", line 114, in <module>
    app_instance = AISpleenSegApp(do_run=True)
  File "app.py", line 33, in __init__
    super().__init__(*args, **kwargs)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/application.py", line 129, in __init__
    self.run(log_level=args.log_level)
  File "app.py", line 38, in run
    super().run(*args, **kwargs)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/application.py", line 429, in run
    executor_obj.run()
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/executors/single_process_executor.py", line 125, in run
    op.compute(op_exec_context.input_context, op_exec_context.output_context, op_exec_context)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 55, in compute
    dicom_study_list = self.load_data_to_studies(input_path)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 81, in load_data_to_studies
    dicom_studies = self._load_data(files)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 117, in _load_data
    except InvalidDicomError as ex:
TypeError: catching classes that do not inherit from BaseException is not allowed

Can you post a link to the monai bundle you are using, so I can test ?

Thanks

@MMelQin
Copy link
Collaborator Author

MMelQin commented Jul 8, 2022

@vikashg The large_files.yml for the bundle has the download path for the bundle TorchScript file itself, and once downloaded this file is used for -m arg.

MONAI does provide the utility to download a bundle, though I had already done the download before knowing the utility exists.

Also, for the bundle operator tutorial Jupyter notebook, I plan to package both the DICOM files AND the bundle TorchScript file in zip shared in GDrive, similar to earlier examples, to make running the example simpler for the users.

@vikashg
Copy link
Collaborator

vikashg commented Jul 8, 2022

Hey @MMelQin,
So isnt the whole point of a bundle operator to be able to run the model by giving the path to the appropriate model-zoo folder and not the model. The operator should parse the "config" file and apply appropriate transformations.

However, I tried to use the model as you said. That is download the torchscript file and then issued the following command

python app.py -m /workspace/app/data/model.ts -i /workspace/app/data/dcm/ -o ./out

I still get this error. I feel I am doing something wrong 😢 Am I ?

INFO:root:Begin compose
DEBUG:root:Bundle path, None, not valid. Will get it in the execution context.
INFO:root:End compose
INFO:__main__.AISpleenSegApp:Begin run
Going to initiate execution of operator DICOMDataLoaderOperator
Executing operator DICOMDataLoaderOperator (Process ID: 963, Operator ID: 066b0d4e-1d3f-4be7-b6d3-3efde32a622f)
Traceback (most recent call last):
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 116, in _load_data
    sop_instances.append(dcmread(file))
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/utils/importutil.py", line 269, in __call__
    raise self._exception
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/utils/importutil.py", line 221, in optional_import
    pkg = __import__(module)  # top level module
monai.deploy.utils.importutil.OptionalImportError: from pydicom import dcmread (No module named 'pydicom').

For details about installing the optional dependencies, please visit:
    https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "app.py", line 114, in <module>
    app_instance = AISpleenSegApp(do_run=True)
  File "app.py", line 33, in __init__
    super().__init__(*args, **kwargs)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/application.py", line 129, in __init__
    self.run(log_level=args.log_level)
  File "app.py", line 38, in run
    super().run(*args, **kwargs)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/application.py", line 429, in run
    executor_obj.run()
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/core/executors/single_process_executor.py", line 125, in run
    op.compute(op_exec_context.input_context, op_exec_context.output_context, op_exec_context)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 55, in compute
    dicom_study_list = self.load_data_to_studies(input_path)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 81, in load_data_to_studies
    dicom_studies = self._load_data(files)
  File "/opt/conda/lib/python3.8/site-packages/monai_deploy_app_sdk-0.3.0+18.gb564836-py3.8.egg/monai/deploy/operators/dicom_data_loader_operator.py", line 117, in _load_data
    except InvalidDicomError as ex:
TypeError: catching classes that do not inherit from BaseException is not allowed

@MMelQin
Copy link
Collaborator Author

MMelQin commented Jul 8, 2022

@vikashg

Yes with a path to the TorchScript file, which is the portable form of a MONAI Bundle. This TS file can be used as any other TS file, with the added extra data that can be consumed by MONAI Bundle aware/compliant applications or even human users after unzipping the TS.

No to path to the Model Zoo URL. An app developer with see the info on Model Zoo, and use the MONAI utility to download the TS file to be used in the app and then MAP.

@vikashg
Copy link
Collaborator

vikashg commented Jul 8, 2022

Okay i will try again tomorrow morning

Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
@vikashg
Copy link
Collaborator

vikashg commented Jul 8, 2022

So, I have been trying to make the notebooks work but

---------------------------------------------------------------------------
ModuleNotFoundError                       Traceback (most recent call last)
/tmp/ipykernel_2011689/3696571223.py in <module>
      1 import logging
----> 2 import monai.deploy
      3 from monai.deploy.core import Application, resource
      4 from monai.deploy.core.domain import Image
      5 from monai.deploy.core.io_type import IOType

ModuleNotFoundError: No module named 'monai.deploy'

Still trying to figure out.

@MMelQin
Copy link
Collaborator Author

MMelQin commented Jul 8, 2022

Thanks @vikashg I DM'ed you directly on some known issues on setting up App SDK on edit mode with required MONAI core dependency; need to make sure both the core and App SDK modules are under the same monai root that Python interpreter sees. BTW, I've created a placeholder discussion topic and we can collect all the pain points on setting up dev env so as to find ways to smooth out the process and improve dev experience.

@vikashg
Copy link
Collaborator

vikashg commented Jul 8, 2022

Hey @MMelQin Thanks for the detailed message and I will follow along and update you by the end of the date

MMelQin added 2 commits July 8, 2022 12:22
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
Signed-off-by: mmelqin <mingmelvinq@nvidia.com>
@sonarqubecloud
Copy link

Kudos, SonarCloud Quality Gate passed!    Quality Gate passed

Bug A 0 Bugs
Vulnerability A 0 Vulnerabilities
Security Hotspot A 0 Security Hotspots
Code Smell A 6 Code Smells

No Coverage information No Coverage information
0.0% 0.0% Duplication

@MMelQin
Copy link
Collaborator Author

MMelQin commented Jul 11, 2022

A discussion topic has been created for improving the developer experience for creating/testing App SDK operators that also requires MONAI core package. When the current process is followed, the setup also works correctly to allow testing the App SDK in edit mode with MONAI Core dependency.

@MMelQin MMelQin merged commit 913b392 into main Jul 11, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
3 participants