Skip to content

Added DICOM text SR assets and updated all operator test functions. #201

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 12 commits into from
Nov 23, 2021
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
6 changes: 6 additions & 0 deletions docs/source/release_notes/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@

```

## Version 0.2

```{toctree}
:maxdepth: 1

v0.2.0
## Version 0.1

```{toctree}
Expand Down
33 changes: 33 additions & 0 deletions docs/source/release_notes/v0.2.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Version 0.2.0 (November 23, 2021)

This is a new and enhanced version of MONAI Deploy App SDK, just in time for Thanksgiving and RSNA 2021!🎉

Please visit [GETTING STARTED](/getting_started/index) guide and follow tutorials.

You can learn more about SDK usage through [DEVELOPING WITH SDK](/developing_with_sdk/index).

Please let us know how you like it and what could be improved by [submitting an issue](https://github.com/Project-MONAI/monai-deploy-app-sdk/issues/new/choose) or [asking questions](https://github.com/Project-MONAI/monai-deploy-app-sdk/discussions)😀

## What's new in this version 0.2.0
### Series Selection Operator
This is to support the use case where whole DICOM studies are used as input to an AI inference application even though only specific series are applicable.

The selection rules are defined in JSON, allowing multiple selections, each with a set of matching conditions. The rules processing engine is implemented in the `DICOMSeriesSelectorOperator`, which itself is regarded as a base class with a default implementation. More advanced rules and processing engines can be implemented in the derived classes.

Multiple instances of the series selection operators, each having its own rules, can be chained in a MONAI Deploy application. In part this is made possible by the new App SDK Domain classes which encapsulate the selected series in a DICOM study, and are used as the output of each series selection operator.

### DICOM Comprehensive Structured Report Writer
This is introduced to support generating DICOM SR SOP instances for AI classification results, and as such, the DICOM SR writer is limited to supporting textual results only.

The DICOM SR writer is implemented in `DICOMTextSRWriterOperator`, it
- loads the AI result from a in-memory object as well as from a file path, with the in-memory object taking precedence
- copies applicable DICOM tags from the original DICOM series used as input for the inference application, as well as generating tags anew when there is no DICOM series provided.
- supports assigning DICOM tags via a dictionary with DICOM keywords and their respective values, so that an application can customize the tags in the DICOM SR instance
- provides classes for an application to encapsulate the AI model information as well as DICOM equipment information, per [IHE Radiology Technical Framework Supplement AI Results (AIR)](https://www.ihe.net/uploadedFiles/Documents/Radiology/IHE_RAD_Suppl_AIR.pdf)

### Updated example applications
- [The AI Spleen Segmentation](https://github.com/Project-MONAI/monai-deploy-app-sdk/tree/main/examples/apps/ai_spleen_seg_app) application updated to demonstrate the use of series selection rules
- [The MedNIST Classifier application](https://github.com/Project-MONAI/monai-deploy-app-sdk/tree/main/examples/apps/mednist_classifier_monaideploy) updated to demonstrate the use of DCIOM SR writing (without initial DICOM input)
- Updated are the main functions of the built-in [operators](https://github.com/Project-MONAI/monai-deploy-app-sdk/tree/main/monai/deploy/operators), which serve as examples on how to parse the output objects
- Updated also are [Jupyter notebook tutorials](https://github.com/Project-MONAI/monai-deploy-app-sdk/tree/main/notebooks/tutorials)
- Multiple [issues](https://github.com/Project-MONAI/monai-deploy-app-sdk/issues?q=is%3Aissue+is%3Aclosed) were fixed and closed
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Text

import monai.deploy.core as md
from monai.deploy.core import (
Application,
Expand All @@ -20,6 +22,7 @@
Operator,
OutputContext,
)
from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
from monai.transforms import AddChannel, Compose, EnsureType, ScaleIntensity

MEDNIST_CLASSES = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]
Expand Down Expand Up @@ -48,7 +51,7 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe


@md.input("image", Image, IOType.IN_MEMORY)
@md.output("output", DataPath, IOType.DISK)
@md.output("result_text", Text, IOType.IN_MEMORY)
@md.env(pip_packages=["monai"])
class MedNISTClassifierOperator(Operator):
"""Classifies the given image and returns the class name."""
Expand Down Expand Up @@ -80,9 +83,11 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe

result = MEDNIST_CLASSES[output_classes[0]] # get the class name
print(result)
op_output.set(result, "result_text")

# Get output (folder) path and create the folder if not exists
output_folder = op_output.get().path
# The following gets the App context's output path, instead the operator's.
output_folder = context.output.get().path
output_folder.mkdir(parents=True, exist_ok=True)

# Write result to "output.json"
Expand All @@ -99,7 +104,15 @@ def compose(self):
load_pil_op = LoadPILOperator()
classifier_op = MedNISTClassifierOperator()

my_model_info = ModelInfo("MONAI WG Trainer", "MEDNIST Classifier", "0.1", "xyz")
my_equipment = EquipmentInfo(manufacturer="MOANI Deploy App SDK", manufacturer_model="DICOM SR Writer")
my_special_tags = {"SeriesDescription": "Not for clinical use. The result is for research use only."}
dicom_sr_operator = DICOMTextSRWriterOperator(
copy_tags=False, model_info=my_model_info, equipment_info=my_equipment, custom_tags=my_special_tags
)

self.add_flow(load_pil_op, classifier_op)
self.add_flow(classifier_op, dicom_sr_operator, {"result_text": "classification_result"})


if __name__ == "__main__":
Expand Down
6 changes: 3 additions & 3 deletions monai/deploy/core/domain/dicom_sop_instance.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@
Dataset_, dataset_ok_ = optional_import("pydicom", name="Dataset")
# Dynamic class is not handled so make it Any for now: https://github.com/python/mypy/issues/2477
Dataset: Any = Dataset_ if dataset_ok_ else Any
TagType_, tagtype_ok_ = optional_import("pydicom.tag", name="TagType")
Tag_, tag_ok_ = optional_import("pydicom.tag", name="Tag")
# Dynamic class is not handled so make it Any for now: https://github.com/python/mypy/issues/2477
TagType: Any = TagType_ if tagtype_ok_ else Any
Tag: Any = Tag_ if tag_ok_ else Any


class DICOMSOPInstance(Domain):
Expand All @@ -39,7 +39,7 @@ def __init__(self, native_sop):
def get_native_sop_instance(self):
return self._sop

def __getitem__(self, key: Union[int, slice, TagType]) -> Union[Dataset, DataElement]:
def __getitem__(self, key: Union[int, slice, Tag]) -> Union[Dataset, DataElement]:
return self._sop.__getitem__(key)

def get_pixel_array(self):
Expand Down
1 change: 1 addition & 0 deletions monai/deploy/operators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
from .dicom_seg_writer_operator import DICOMSegmentationWriterOperator
from .dicom_series_selector_operator import DICOMSeriesSelectorOperator
from .dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
from .dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
from .inference_operator import InferenceOperator
from .monai_seg_inference_operator import MonaiSegInferenceOperator
from .png_converter_operator import PNGConverterOperator
Expand Down
38 changes: 36 additions & 2 deletions monai/deploy/operators/dicom_data_loader_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,16 +245,50 @@ def populate_series_attributes(self, series, sop_instance):


def test():
data_path = "../../../examples/ai_spleen_seg_data/dcm"
current_file_dir = Path(__file__).parent.resolve()
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm")

loader = DICOMDataLoaderOperator()
study_list = loader.load_data_to_studies(Path(data_path).absolute())
study_list = loader.load_data_to_studies(data_path.absolute())

for study in study_list:
print("###############################")
print(study)
for series in study.get_all_series():
print(series)
for series in study.get_all_series():
for sop in series.get_sop_instances():
print("Demonstrating ways to access DICOM attributes in a SOP instance.")
# No need to get the native_ds = sop.get_native_sop_instance()
# sop = sop.get_native_sop_instance()
print(f" 'StudyInstanceUID': {sop['StudyInstanceUID'].repval}")
print(f" (0x0020, 0x000D): {sop[0x0020, 0x000D].repval}")
print(f" 'SeriesInstanceUID': {sop['SeriesInstanceUID'].value.name}")
print(f" (0x0020, 0x000E): {sop[0x0020, 0x000E].value.name}")
print(f" 'SOPInstanceUID': {sop['SOPInstanceUID'].value.name}")
print(f" (0008,0018): {sop[0x0008, 0x0018].value.name}")
try:
print(f" 'InstanceNumber': {sop['InstanceNumber'].repval}")
print(f" (0020, 0013): {sop[0x0020, 0x0013].repval}")
except KeyError:
pass
# Need to get pydicom dataset to use properties and get method of a dict.
ds = sop.get_native_sop_instance()
print(f" 'StudyInstanceUID': {ds.StudyInstanceUID if ds.StudyInstanceUID else ''}")
print(f" 'SeriesDescription': {ds.SeriesDescription if ds.SeriesDescription else ''}")
print(
f" 'IssuerOfPatientID': {ds.get('IssuerOfPatientID', '').repval if ds.get('IssuerOfPatientID', '') else '' }"
)
try:
print(f" 'IssuerOfPatientID': {ds.IssuerOfPatientID if ds.IssuerOfPatientID else '' }")
except AttributeError:
print(
" If the IssuerOfPatientID does not exist, ds.IssuerOfPatientID would throw AttributeError."
)
print(" Use ds.get('IssuerOfPatientID', '') instead.")

break
break


if __name__ == "__main__":
Expand Down
14 changes: 7 additions & 7 deletions monai/deploy/operators/dicom_seg_writer_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -660,17 +660,17 @@ def test():
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator

data_path = "../../../examples/ai_spleen_seg_data/dcm"
out_path = "../../../examples/output_seg_op/dcm_seg_test.dcm"
current_file_dir = Path(__file__).parent.resolve()
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm")
out_path = current_file_dir.joinpath("../../../examples/output_seg_op/dcm_seg_test.dcm")

files = []
loader = DICOMDataLoaderOperator()
series_selector = DICOMSeriesSelectorOperator()
dcm_to_volume_op = DICOMSeriesToVolumeOperator()
seg_writer = DICOMSegmentationWriterOperator()

# Testing with more granular functions
study_list = loader.load_data_to_studies(Path(data_path).absolute())
study_list = loader.load_data_to_studies(data_path.absolute())
series = study_list[0].get_all_series()[0]

dcm_to_volume_op.prepare_series(series)
Expand All @@ -682,10 +682,10 @@ def test():
seg_writer.create_dicom_seg(image_numpy, series, Path(out_path).absolute())

# Testing with the main entry functions
study_list = loader.load_data_to_studies(Path(data_path).absolute())
_, study_selected_series_list = series_selector.filter(None, study_list)
study_list = loader.load_data_to_studies(data_path.absolute())
study_selected_series_list = series_selector.filter(None, study_list)
image = dcm_to_volume_op.convert_to_image(study_selected_series_list)
seg_writer.process_images(image, study_selected_series_list, Path(out_path).parent.absolute())
seg_writer.process_images(image, study_selected_series_list, out_path.parent.absolute())


if __name__ == "__main__":
Expand Down
9 changes: 5 additions & 4 deletions monai/deploy/operators/dicom_series_selector_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -277,14 +277,15 @@ def test():

from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator

data_path = "../../../examples/ai_spleen_seg_data/dcm-multi"
current_file_dir = Path(__file__).parent.resolve()
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm-multi")

loader = DICOMDataLoaderOperator()
study_list = loader.load_data_to_studies(Path(data_path).absolute())
study_list = loader.load_data_to_studies(data_path.absolute())
selector = DICOMSeriesSelectorOperator()
sample_selection_rule = json_loads(Sample_Rules_Text)
print(f"Selection rules in JSON:\n{sample_selection_rule}")
series_list, study_selected_seriee_list = selector.filter(sample_selection_rule, study_list)
study_selected_seriee_list = selector.filter(sample_selection_rule, study_list)

for sss_obj in study_selected_seriee_list:
_print_instance_properties(sss_obj, pre_fix="", print_val=False)
Expand Down Expand Up @@ -314,7 +315,7 @@ def test():
_print_instance_properties(ss_obj, pre_fix)
print(f"{pre_fix}===============================")

print(f"Total # of series selected: {len(series_list)}")
print(f" A total of {len(sss_obj.selected_series)} series selected for study {study.StudyInstanceUID}")


# Sample rule used for testing
Expand Down
5 changes: 3 additions & 2 deletions monai/deploy/operators/dicom_series_to_volume_operator.py
Original file line number Diff line number Diff line change
Expand Up @@ -346,12 +346,13 @@ def test():
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator

data_path = "../../../examples/ai_spleen_seg_data/dcm"
current_file_dir = Path(__file__).parent.resolve()
data_path = current_file_dir.joinpath("../../../examples/ai_spleen_seg_data/dcm")
loader = DICOMDataLoaderOperator()
study_list = loader.load_data_to_studies(Path(data_path).absolute())

series_selector = DICOMSeriesSelectorOperator()
_, study_selected_series_list = series_selector.filter(None, study_list)
study_selected_series_list = series_selector.filter(None, study_list)

op = DICOMSeriesToVolumeOperator()
image = op.convert_to_image(study_selected_series_list)
Expand Down
Loading