Skip to content

A breast density monai application package #384

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 3 commits 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
29 changes: 29 additions & 0 deletions examples/apps/breast_density_classifer_app/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
## A MONAI Application Package to deploy breast density classification algorithm
This MAP is based on the Breast Density Model in MONAI [Model-Zoo](https://github.com/Project-MONAI/model-zoo). This model is developed at the Center for Augmented Intelligence in Imaging at the Mayo Clinic, Florida.
For any questions, feel free to contact Vikash Gupta ([email protected])
Sample data and a torchscript model can be downloaded from https://drive.google.com/drive/folders/1Dryozl2MwNunpsGaFPVoaKBLkNbVM3Hu?usp=sharing


## Run the application package
### Python CLI
```
python app.py -i <input_dir> -o <out_dir> -m <breast_density_model>
```

### MONAI Deploy CLI
```
monai-deploy exec app.py -i <input_dir> -o <out_dir> -m <breast_density_model>
```
Alternatively, you can go a level higher and execute
```
monai-deploy exec breast_density_classification_app -i <input_dir> -o <out_dir> -m <breast_density_model>
```


### Packaging the monai app
In order to build the monai app, Go a level up and execute the following command.
```
monai-deploy package -b nvcr.io/nvidia/pytorch:21.12-py3 breast_density_classification_app --tag breast_density:0.1.0 -m $breast_density_model
```


7 changes: 7 additions & 0 deletions examples/apps/breast_density_classifer_app/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import os
import sys

_current_dir = os.path.abspath(os.path.dirname(__file__))
if sys.path and os.path.abspath(sys.path[0]) != _current_dir:
sys.path.insert(0, _current_dir)
del _current_dir
4 changes: 4 additions & 0 deletions examples/apps/breast_density_classifer_app/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
from app import BreastClassificationApp

if __name__ == "__main__":
BreastClassificationApp(do_run=True)
48 changes: 48 additions & 0 deletions examples/apps/breast_density_classifer_app/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from breast_density_classifier_operator import ClassifierOperator

from monai.deploy.core import Application
from monai.deploy.operators.dicom_data_loader_operator import DICOMDataLoaderOperator
from monai.deploy.operators.dicom_series_selector_operator import DICOMSeriesSelectorOperator
from monai.deploy.operators.dicom_series_to_volume_operator import DICOMSeriesToVolumeOperator
from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo


class BreastClassificationApp(Application):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

def compose(self):
model_info = ModelInfo(
"MONAI Model for Breast Density",
"BreastDensity",
"0.1",
"Center for Augmented Intelligence in Imaging, Mayo Clinic, Florida",
)
my_equipment = EquipmentInfo(manufacturer="MONAI Deploy App SD", manufacturer_model="DICOM SR Writer")
my_special_tags = {"SeriesDescription": "Not for clinical use"}
study_loader_op = DICOMDataLoaderOperator()
series_selector_op = DICOMSeriesSelectorOperator(rules="")
series_to_vol_op = DICOMSeriesToVolumeOperator()
classifier_op = ClassifierOperator()
sr_writer_op = DICOMTextSRWriterOperator(
copy_tags=False, model_info=model_info, equipment_info=my_equipment, custom_tags=my_special_tags
)

self.add_flow(study_loader_op, series_selector_op, {"dicom_study_list": "dicom_study_list"})
self.add_flow(
series_selector_op, series_to_vol_op, {"study_selected_series_list": "study_selected_series_list"}
)
self.add_flow(series_to_vol_op, classifier_op, {"image": "image"})
self.add_flow(classifier_op, sr_writer_op, {"result_text": "classification_result"})


def main():
app = BreastClassificationApp()
image_dir = "./sampleDICOMs/1/BI_BREAST_SCREENING_BILATERAL_WITH_TOMOSYNTHESIS-2019-07-08/1/L_CC_C-View"

model_path = "./model/traced_ts_model.pt"
app.run(input=image_dir, output="./output", model=model_path)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
import json
from typing import Dict, Text

import torch

import monai.deploy.core as md
from monai.data import DataLoader, Dataset
from monai.deploy.core import ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
from monai.deploy.operators.monai_seg_inference_operator import InMemImageReader
from monai.transforms import (
Activations,
Compose,
EnsureChannelFirst,
EnsureType,
LoadImage,
NormalizeIntensity,
RepeatChannel,
Resize,
SqueezeDim,
)


@md.input("image", Image, IOType.IN_MEMORY)
@md.output("result_text", Text, IOType.IN_MEMORY)
class ClassifierOperator(Operator):
def __init__(self):
super().__init__()
self._input_dataset_key = "image"
self._pred_dataset_key = "pred"

def _convert_dicom_metadata_datatype(self, metadata: Dict):
if not metadata:
return metadata

# Try to convert data type for the well knowned attributes. Add more as needed.
if metadata.get("SeriesInstanceUID", None):
try:
metadata["SeriesInstanceUID"] = str(metadata["SeriesInstanceUID"])
except Exception:
pass
if metadata.get("row_pixel_spacing", None):
try:
metadata["row_pixel_spacing"] = float(metadata["row_pixel_spacing"])
except Exception:
pass
if metadata.get("col_pixel_spacing", None):
try:
metadata["col_pixel_spacing"] = float(metadata["col_pixel_spacing"])
except Exception:
pass

print("Converted Image object metadata:")
for k, v in metadata.items():
print(f"{k}: {v}, type {type(v)}")

return metadata

def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
input_image = op_input.get("image")
_reader = InMemImageReader(input_image)
input_img_metadata = self._convert_dicom_metadata_datatype(input_image.metadata())
img_name = str(input_img_metadata.get("SeriesInstanceUID", "Img_in_context"))

output_path = context.output.get().path

device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model = context.models.get()

pre_transforms = self.pre_process(_reader)
post_transforms = self.post_process()

dataset = Dataset(data=[{self._input_dataset_key: img_name}], transform=pre_transforms)
dataloader = DataLoader(dataset, batch_size=10, shuffle=False, num_workers=0)

with torch.no_grad():
for d in dataloader:
image = d[0].to(device)
outputs = model(image)
out = post_transforms(outputs).data.cpu().numpy()[0]
print(out)

result_dict = (
"A " + ":" + str(out[0]) + " B " + ":" + str(out[1]) + " C " + ":" + str(out[2]) + " D " + ":" + str(out[3])
)
result_dict_out = {"A": str(out[0]), "B": str(out[1]), "C": str(out[2]), "D": str(out[3])}
output_folder = context.output.get().path
output_folder.mkdir(parents=True, exist_ok=True)

output_path = output_folder / "output.json"
with open(output_path, "w") as fp:
json.dump(result_dict, fp)

op_output.set(result_dict, "result_text")

def pre_process(self, image_reader) -> Compose:
return Compose(
[
LoadImage(reader=image_reader, image_only=True),
EnsureChannelFirst(),
SqueezeDim(dim=3),
NormalizeIntensity(),
Resize(spatial_size=(299, 299)),
RepeatChannel(repeats=3),
EnsureChannelFirst(),
]
)

def post_process(self) -> Compose:
return Compose([EnsureType(), Activations(sigmoid=True)])