Skip to content

Added a Nifti data loader for issue #270 #361

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 1 commit into from
Nov 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions monai/deploy/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
PublisherOperator
STLConversionOperator
STLConverter
NiftiDataLoader
"""

from .clara_viz_operator import ClaraVizOperator
Expand All @@ -38,6 +39,7 @@
from .inference_operator import InferenceOperator
from .monai_bundle_inference_operator import BundleConfigNames, IOMapping, MonaiBundleInferenceOperator
from .monai_seg_inference_operator import MonaiSegInferenceOperator
from .nii_data_loader_operator import NiftiDataLoader
from .png_converter_operator import PNGConverterOperator
from .publisher_operator import PublisherOperator
from .stl_conversion_operator import STLConversionOperator, STLConverter
53 changes: 53 additions & 0 deletions monai/deploy/operators/nii_data_loader_operator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Copyright 2021-2022 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np
import SimpleITK

import monai.deploy.core as md
from monai.deploy.core import DataPath, ExecutionContext, InputContext, IOType, Operator, OutputContext


@md.input("image_path", DataPath, IOType.DISK)
@md.output("image", np.ndarray, IOType.IN_MEMORY)
class NiftiDataLoader(Operator):
"""
This operator reads a nifti image, extracts the numpy array and forwards it to the next operator
"""

def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
input_path = op_input.get().path
image_np = self.convert_and_save(input_path)
Copy link
Collaborator

Choose a reason for hiding this comment

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

The op_input path is typically a folder, even though file path is also possible depending on how the input is set up. It is important that the path itself is checked for what type it is.

Also, in the case of folder, there has to logic to scan the file(s) for supported types, e.g. nii, nii.gz, mhd, etc. The monai core has the LoadImage class that has a set of supported readers, including nii, and wonder why not simple use it in the application, or make use of it here.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes monai core has the logic for LoadImage. But, it is also very specific in terms of how it wants to handle images. Now it is using metadata, there is this idea of channel first etc. I wanted a very raw example, which someone can look and manipulate more easily if needed. We will talk about it today.
Thanks @MMelQin

Copy link
Member

Choose a reason for hiding this comment

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

LoadImage shouldn't manipulate the image data at all and leaves that to subsequent transforms. It should preserve the Nifti header in the metadata so you can use it later especially for saving output images back to the same format as the input. It doesn't however take as input single file names so to scan a directory we typically use glob to get a list of files.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@MMelQin I understand that the op_input path is typically a folder. But that is a valid point for dicom images. The nifti images are just files. In a typical scenario, all the nifti images will be contained in a single folder so it makes sense to use op_input path for a file instead of a folder.

@ericspod and @MMelQin
Yes LoadImage can be used for loading the images. However, in the monai environment the channels are considered first by default. and there might be use cases where that is not the case. In addition, I thought it will be nice to have an example where you can show that monai deploy operators do not necessarily depend on the monai core components and it is possible to play around with other readers and demonstrate the flexibility it offers.
But if you think, it is not particularly useful please go ahead with the changes.

Copy link
Member

Choose a reason for hiding this comment

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

Channels are added first because that's what Pytorch wants when considering what the dimensions of a tensor represent. What we often do is use Orientation/Orientationd transforms to change the orientation of the image based on what the Nifti header says so that the spatial dimensions always represent the same thing, like here. If that's the kind of situation in which you wouldn't want to add a channel dimension then I'd say the use of these transforms together is the correct solution.

I'm obviously biased but I'd always recommend using MONAI code rather than other libraries for doing the same thing.

op_output.set(image_np)

def convert_and_save(self, nii_path):
"""
reads the nifti image and
"""
image_reader = SimpleITK.ImageFileReader()
image_reader.SetFileName(str(nii_path))
image = image_reader.Execute()
image_np = np.transpose(SimpleITK.GetArrayFromImage(image), [2, 1, 0])
return image_np
Copy link
Collaborator

Choose a reason for hiding this comment

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

So this will only return the ndarray, how would the metadata be preserved? Metadata, e.g. spacing and affine are important and needed for the inference transforms.

In App SDK there is a Image domain class, very primitive with only ndarray and a list of metadata in a dict. We used this object to preserve image ndarray as well as a list of required metadata from the DICOM Series to vol image. The built-in inference operators are expecting Image type as input.

Also, with NIfTI image only, unless the original slices are converted to a DICOM series, the DICOM Seg Writer operator can not be used, simply because the Seg needs to refer back to the original DICOM instances.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I wrote it before metatensor became a norm in MONAI. I wanted an operator which just reads an images and hands off the numpy array to the next operator for inference. I do not think we can or need to worry about DICOM Seg operator if someone is doing inference on Nifti operators. It is not expected. I will look into this class and ensure that we can output metadata from the operator. I will try to tie in this operator with an example to make a complete use case.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Unless the input images in NIfTI have already been resampled to the spacing/orientation to fit the model, resampling and orientation are needed in the pre-processing step, granted in many cases and in competitions the training and validation images have been pre-prepared.
The MONAI built-in readers simply returns the ndarray and a metadata dict containing certain entries common/required by the transforms. The LoadImage class then convert both into a MetaTensor object.



def test():
filepath = "/home/gupta/Documents/mni_icbm152_nlin_sym_09a/mni_icbm152_gm_tal_nlin_sym_09a.nii"
nii_operator = NiftiDataLoader()
image_np = nii_operator.convert_and_save(filepath)


def main():
test()


if __name__ == "__main__":
main()