Skip to content

Commit 32a9d10

Browse files
Add Clara Viz operator.
Signed-off-by: Andreas Heumann <aheumann@nvidia.com>
1 parent 819977a commit 32a9d10

File tree

3 files changed

+1870
-1
lines changed

3 files changed

+1870
-1
lines changed

monai/deploy/operators/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright 2021 MONAI Consortium
1+
# Copyright 2021-2022 MONAI Consortium
22
# Licensed under the Apache License, Version 2.0 (the "License");
33
# you may not use this file except in compliance with the License.
44
# You may obtain a copy of the License at
@@ -12,6 +12,7 @@
1212
.. autosummary::
1313
:toctree: _autosummary
1414
15+
ClaraVizOperator
1516
DICOMDataLoaderOperator
1617
DICOMSegmentationWriterOperator
1718
DICOMSeriesSelectorOperator
@@ -22,6 +23,7 @@
2223
PublisherOperator
2324
"""
2425

26+
from .clara_viz_operator import ClaraVizOperator
2527
from .dicom_data_loader_operator import DICOMDataLoaderOperator
2628
from .dicom_seg_writer_operator import DICOMSegmentationWriterOperator
2729
from .dicom_series_selector_operator import DICOMSeriesSelectorOperator
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
# Copyright 2022 MONAI Consortium
2+
# Licensed under the Apache License, Version 2.0 (the "License");
3+
# you may not use this file except in compliance with the License.
4+
# You may obtain a copy of the License at
5+
# http://www.apache.org/licenses/LICENSE-2.0
6+
# Unless required by applicable law or agreed to in writing, software
7+
# distributed under the License is distributed on an "AS IS" BASIS,
8+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
9+
# See the License for the specific language governing permissions and
10+
# limitations under the License.
11+
12+
import numpy as np
13+
14+
import monai.deploy.core as md
15+
from monai.deploy.core import ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
16+
from monai.deploy.utils.importutil import optional_import
17+
18+
DataDefinition, _ = optional_import("clara.viz.core", name="DataDefinition")
19+
Widget, _ = optional_import("clara.viz.widgets", name="Widget")
20+
display, _ = optional_import("IPython.display", name="display")
21+
interactive, _ = optional_import("ipywidgets", name="interactive")
22+
Dropdown, _ = optional_import("ipywidgets", name="Dropdown")
23+
Box, _ = optional_import("ipywidgets", name="Box")
24+
VBox, _ = optional_import("ipywidgets", name="VBox")
25+
26+
27+
@md.input("image", Image, IOType.IN_MEMORY)
28+
@md.input("seg_image", Image, IOType.IN_MEMORY)
29+
@md.env(pip_packages=["clara.viz.core", "clara.viz.widgets", "IPython"])
30+
class ClaraVizOperator(Operator):
31+
"""
32+
This operator uses Clara Viz to provide interactive view of a 3D volume including segmentation mask.
33+
"""
34+
35+
def __init__(self):
36+
"""Constructor of the operator."""
37+
super().__init__()
38+
39+
@staticmethod
40+
def _build_array(image, order):
41+
numpy_array = image.asnumpy()
42+
43+
array = DataDefinition.Array(array=numpy_array, order=order)
44+
array.element_size = [1.0]
45+
array.element_size.append(image.metadata().get("col_pixel_spacing", 1.0))
46+
array.element_size.append(image.metadata().get("row_pixel_spacing", 1.0))
47+
array.element_size.append(image.metadata().get("depth_pixel_spacing", 1.0))
48+
49+
# the renderer is expecting data in RIP order (Right Inferior Posterior) which results in
50+
# this matrix
51+
target_affine_transform = [
52+
[-1.0, 0.0, 0.0, 0.0],
53+
[0.0, 0.0, 1.0, 0.0],
54+
[0.0, -1.0, 0.0, 0.0],
55+
[0.0, 0.0, 0.0, 1.0],
56+
]
57+
58+
dicom_affine_transform = image.metadata().get("dicom_affine_transform", np.identity(4))
59+
60+
affine_transform = np.matmul(target_affine_transform, dicom_affine_transform)
61+
62+
array.permute_axes = [
63+
0,
64+
max(range(3), key=lambda k: abs(affine_transform[0][k])) + 1,
65+
max(range(3), key=lambda k: abs(affine_transform[1][k])) + 1,
66+
max(range(3), key=lambda k: abs(affine_transform[2][k])) + 1,
67+
]
68+
69+
array.flip_axes = [
70+
False,
71+
affine_transform[0][array.permute_axes[1] - 1] < 0.0,
72+
affine_transform[1][array.permute_axes[2] - 1] < 0.0,
73+
affine_transform[2][array.permute_axes[3] - 1] < 0.0,
74+
]
75+
76+
return array
77+
78+
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
79+
"""Displays the input image and segmentation mask
80+
81+
Args:
82+
op_input (InputContext): An input context for the operator.
83+
op_output (OutputContext): An output context for the operator.
84+
context (ExecutionContext): An execution context for the operator.
85+
"""
86+
input_image = op_input.get("image")
87+
if not input_image:
88+
raise ValueError("Input image is not found.")
89+
input_seg_image = op_input.get("seg_image")
90+
if not input_seg_image:
91+
raise ValueError("Input segmentation image is not found.")
92+
93+
# build the data definition
94+
data_definition = DataDefinition()
95+
96+
data_definition.arrays.append(self._build_array(input_image, "DXYZ"))
97+
98+
data_definition.arrays.append(self._build_array(input_seg_image, "MXYZ"))
99+
100+
widget = Widget()
101+
widget.select_data_definition(data_definition)
102+
# default view mode is 'CINEMATIC' switch to 'SLICE_SEGMENTATION' since we have no transfer functions defined
103+
widget.settings["Views"][0]["mode"] = "SLICE_SEGMENTATION"
104+
widget.settings["Views"][0]["cameraName"] = "Top"
105+
widget.set_settings()
106+
107+
# add controls
108+
def set_view_mode(view_mode):
109+
widget.settings["Views"][0]["mode"] = view_mode
110+
if view_mode == "CINEMATIC":
111+
widget.settings["Views"][0]["cameraName"] = "Perspective"
112+
elif widget.settings["Views"][0]["cameraName"] == "Perspective":
113+
widget.settings["Views"][0]["cameraName"] = "Top"
114+
widget.set_settings()
115+
116+
widget_view_mode = interactive(
117+
set_view_mode,
118+
view_mode=Dropdown(
119+
options=[("Cinematic", "CINEMATIC"), ("Slice", "SLICE"), ("Slice Segmentation", "SLICE_SEGMENTATION")],
120+
value="SLICE_SEGMENTATION",
121+
description="View mode",
122+
),
123+
)
124+
125+
def set_camera(camera):
126+
if widget.settings["Views"][0]["mode"] != "CINEMATIC":
127+
widget.settings["Views"][0]["cameraName"] = camera
128+
widget.set_settings()
129+
130+
widget_camera = interactive(
131+
set_camera, camera=Dropdown(options=["Top", "Right", "Front"], value="Top", description="Camera")
132+
)
133+
134+
display(Box([widget, VBox([widget_view_mode, widget_camera])]))

0 commit comments

Comments
 (0)