Skip to content

Commit f7176c0

Browse files
authored
Merge pull request #368 from Project-MONAI/pin_integration_tweaks
Various fixes to PIN integration example
2 parents 3c86430 + 3416261 commit f7176c0

File tree

4 files changed

+63
-55
lines changed

4 files changed

+63
-55
lines changed

integrations/nuance_pin/README.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ with minimal code changes.
88

99
## Prerequisites
1010

11-
Before setting up and running the example MONAI spleen segmentation app to run as a Nuance PIN App, the user will need to install/download the following libraries.
11+
Before setting up and running the example MONAI lung nodule detection app to run as a Nuance PIN App, the user will need to install/download the following libraries.
1212
It is optional to use a GPU for the example app, however, it is recommended that a GPU is used for inference as it is very computationally intensive.
1313

1414
Minimum software requirements:
@@ -30,9 +30,8 @@ cd integrations/nuance_pin
3030
In this folder you will see the following directory structure
3131
```bash
3232
nuance_pin
33-
├── app # directory with MONAI app code
34-
├── lib # directory where we will place Nuance PIN wheels
35-
├── model # directory where we will place the model used by our MONAI app
33+
├── app/ # directory with MONAI app code ├── lib/ # you should create this directory where we will place Nuance PIN wheels
34+
├── model/ # directory where we will place the model used by our MONAI app
3635
├── app_wrapper.py # Nuance PIN wrapper code
3736
├── docker-compose.yml # docker compose runtime script
3837
├── Dockerfile # container image build script
@@ -48,7 +47,7 @@ To download the test data you may follow the instructions in the [Lund Nodule De
4847

4948
### Download Nuance PIN SDK
5049

51-
Place the Nuance PIN `ai_service` wheel in the `nuance_pin/lib` folder. This can be obtained in the link provided in step 3 of of the [prerequisites](#prerequisites).
50+
Place the Nuance PIN `ai_service` wheel in the `nuance_pin/lib` folder. This can be obtained in the link provided in step 4 of of the [prerequisites](#prerequisites).
5251

5352
### Running the Example App in the Container
5453

@@ -57,7 +56,7 @@ Now we are ready to build and start the container that runs our MONAI app as a N
5756
docker-compose up --build
5857
```
5958

60-
If the build is successful the a service will start on `localhost:5000`. We can verify the service is running
59+
If the build is successful the service will start on `localhost:5000`. We can verify the service is running
6160
by issuing a "live" request such as
6261
```bash
6362
curl -v http://localhost:5000/aiservice/2/live && echo ""
@@ -136,12 +135,12 @@ python -m AiSvcTest -i ~/Downloads/dcm -o ~/Downloads/dcm/out -s http://localhos
136135

137136
### Bring Your Own MONAI App
138137

139-
This example integration may be modified to fit any existing MONAI app, however, there may be caveats.
138+
This example integration may be modified to fit any existing MONAI app by tailoring the files within the `app/` directory, however, there may be caveats.
140139

141140
Nuance PIN requires all artifacts present in the output folder to be also added into the `resultManifest.json` output file
142141
to consider the run successful. To see what this means in practical terms, check the `resultManifest.json` output from the
143142
example app we ran the in previous sections. You will notice an entry in `resultManifest.json` that corresponds to the DICOM
144-
SEG output generated by the underlying MONAI app
143+
GSPS output generated by the underlying MONAI app
145144
```json
146145
"study": {
147146
"uid": "1.2.826.0.1.3680043.2.1125.1.67295333199898911264201812221946213",
@@ -153,17 +152,16 @@ SEG output generated by the underlying MONAI app
153152
{
154153
"documentType": "application/dicom",
155154
"groupCode": "default",
156-
"name": "dicom_seg-DICOMSEG.dcm",
155+
"name": "gsps.dcm",
157156
"trackingUids": []
158157
}
159158
]
160159
}
161160
]
162161
},
163162
```
164-
This entry is generated by `app_wrapper.py`, which takes care of adding any DICOM present in the output folder in the `resultManifest.json`
165-
to ensure that existing MONAI apps complete successfully when deployed in Nuance. In general, however, the developer may need to tailor some
166-
of the code in `app_wrapper.py` to provide more insight to Nuance's network, such as adding findings, conclusions, etc. and generating more insight
163+
This entry is generated automatically by Nuance's `ai_service` library as a result of uploading the DICOM GSPS object in `app/post_inference_ops.py`.
164+
In general, however, the developer may need to tailor some of the code in `app_wrapper.py` to provide more insight to Nuance's network, such as adding findings, conclusions, etc. and generating more insight
167165
using SNOMED codes. All of this is handled within the Nuance PIN SDK libraries - for more information please consult Nuance PIN [documentation](https://www.nuance.com/healthcare/diagnostics-solutions/precision-imaging-network.html).
168166

169167
In simpler cases, the developer will need to place their code and model under `nuance_pin`. Placing the model under `model` is optional as the model may be placed

integrations/nuance_pin/app/inference.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ def pre_process(self, img_reader) -> Compose:
177177
keys=[image_key, f"{image_key}_meta_dict"],
178178
names=[orig_image_key, f"{orig_image_key}_meta_dict"],
179179
),
180-
ToDeviced(keys=image_key, device="cuda"),
180+
ToDeviced(keys=image_key, device=self.device),
181181
EnsureChannelFirstd(keys=image_key),
182182
Spacingd(keys=image_key, pixdim=(0.703125, 0.703125, 1.25)),
183183
Orientationd(

integrations/nuance_pin/app/post_inference_ops.py

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,23 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
4646
series_uid = hd.UID()
4747
series_number = randint(1, 100000)
4848

49+
# One graphic layer to contain all detections
50+
layer = hd.pr.GraphicLayer(
51+
layer_name="LUNG_NODULES",
52+
order=1,
53+
description="Lung Nodule Detections",
54+
)
55+
56+
annotations = []
57+
58+
all_ref_images = [ins.get_native_sop_instance() for ins in selected_series.series.get_sop_instances()]
59+
accession = all_ref_images[0].AccessionNumber
60+
4961
for inst_num, (box_data, box_score) in enumerate(zip(detection_result.box_data, detection_result.score_data)):
5062

63+
tracking_id = f"{accession}_nodule_{inst_num}" # site-specific ID
64+
tracking_uid = hd.UID()
65+
5166
polyline = hd.pr.GraphicObject(
5267
graphic_type=hd.pr.GraphicTypeValues.POLYLINE,
5368
graphic_data=np.array(
@@ -60,8 +75,8 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
6075
]
6176
), # coordinates of polyline vertices
6277
units=hd.pr.AnnotationUnitsValues.PIXEL, # units for graphic data
63-
tracking_id="lung_nodule_MONAI", # site-specific ID
64-
tracking_uid=hd.UID(), # highdicom will generate a unique ID
78+
tracking_id=tracking_id,
79+
tracking_uid=tracking_uid,
6580
)
6681

6782
self.logger.info(f"Box: {[box_data[0], box_data[1], box_data[3], box_data[4]]}")
@@ -70,14 +85,8 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
7085
text_value=f"{box_score:.2f}",
7186
bounding_box=(box_data[0], box_data[1], box_data[3], box_data[4]), # left, top, right, bottom
7287
units=hd.pr.AnnotationUnitsValues.PIXEL, # units for bounding box
73-
tracking_id="LungNoduleMONAI", # site-specific ID
74-
tracking_uid=hd.UID(), # highdicom will generate a unique ID
75-
)
76-
77-
layer = hd.pr.GraphicLayer(
78-
layer_name="LUNG_NODULE",
79-
order=1,
80-
description="Lung Nodule Detection",
88+
tracking_id=tracking_id,
89+
tracking_uid=tracking_uid,
8190
)
8291

8392
affected_slice_idx = [
@@ -103,37 +112,40 @@ def compute(self, op_input: InputContext, op_output: OutputContext, context: Exe
103112
graphic_objects=[polyline],
104113
)
105114

106-
# Assemble the components into a GSPS object
107-
gsps = hd.pr.GrayscaleSoftcopyPresentationState(
108-
referenced_images=ref_images,
109-
series_instance_uid=series_uid,
110-
series_number=series_number,
111-
sop_instance_uid=hd.UID(),
112-
instance_number=inst_num + 1,
113-
manufacturer="MONAI",
114-
manufacturer_model_name="lung_nodule_ct_detection",
115-
software_versions="v0.2.0",
116-
device_serial_number="",
117-
content_label="ANNOTATIONS",
118-
graphic_layers=[layer],
119-
graphic_annotations=[annotation],
120-
institution_name="MONAI",
121-
institutional_department_name="Deploy",
122-
voi_lut_transformations=[
123-
hd.pr.SoftcopyVOILUTTransformation(
124-
window_center=-550.0,
125-
window_width=1350.0,
126-
)
127-
],
128-
)
115+
annotations.append(annotation)
116+
117+
# Assemble the components into a GSPS object
118+
gsps = hd.pr.GrayscaleSoftcopyPresentationState(
119+
referenced_images=all_ref_images,
120+
series_instance_uid=series_uid,
121+
series_number=series_number,
122+
sop_instance_uid=hd.UID(),
123+
instance_number=1,
124+
manufacturer="MONAI",
125+
manufacturer_model_name="lung_nodule_ct_detection",
126+
software_versions="v0.2.0",
127+
device_serial_number="",
128+
content_label="ANNOTATIONS",
129+
graphic_layers=[layer],
130+
graphic_annotations=annotations,
131+
institution_name="MONAI",
132+
institutional_department_name="Deploy",
133+
voi_lut_transformations=[
134+
hd.pr.SoftcopyVOILUTTransformation(
135+
window_center=-550.0,
136+
window_width=1350.0,
137+
)
138+
],
139+
)
129140

130-
gsps.save_as(os.path.join(output_path, f"gsps-{inst_num:04d}.dcm"))
141+
gsps_filename = os.path.join(output_path, "gsps.dcm")
142+
gsps.save_as(gsps_filename)
131143

132-
self.upload_gsps(
133-
file=os.path.join(output_path, f"gsps-{inst_num:04d}.dcm"),
134-
document_detail="MONAI Lung Nodule Detection v0.2.0",
135-
series_uid=series_uid,
136-
)
144+
self.upload_gsps(
145+
file=gsps_filename,
146+
document_detail="MONAI Lung Nodule Detection v0.2.0",
147+
series_uid=series_uid,
148+
)
137149

138150

139151
@md.input("original_dicom", List[StudySelectedSeries], IOType.IN_MEMORY)

integrations/nuance_pin/app_wrapper.py

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,7 @@ def initialize_class(cls):
7272
monai_app_class_module = cls.monai_app_module.rsplit(".", 1)[0]
7373
monai_app_class_name = cls.monai_app_module.rsplit(".", 1)[1]
7474
if not cls.monai_app_module:
75-
raise ValueError(
76-
"MONAI App to be run has not been specificed in `MONAI_APP_CLASSPATH` environment variable"
77-
)
75+
raise ValueError("MONAI App to be run has not been specified in `MONAI_APP_CLASSPATH` environment variable")
7876

7977
monai_app_class = getattr(import_module(monai_app_class_module), monai_app_class_name)
8078
if monai_app_class is None:

0 commit comments

Comments
 (0)