Skip to content

Commit 0b75f65

Browse files
Add Clara Viz operator.
Signed-off-by: Andreas Heumann <aheumann@nvidia.com>
1 parent 0aa7ef0 commit 0b75f65

File tree

3 files changed

+1896
-1
lines changed

3 files changed

+1896
-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: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
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+
interact, _ = optional_import("ipywidgets", name="interact")
22+
Dropdown, _ = optional_import("ipywidgets", name="Dropdown")
23+
24+
25+
@md.input("image", Image, IOType.IN_MEMORY)
26+
@md.input("seg_image", Image, IOType.IN_MEMORY)
27+
@md.env(pip_packages=["clara.viz.core", "clara.viz.widgets", "IPython"])
28+
class ClaraVizOperator(Operator):
29+
"""
30+
This operator uses Clara Viz to provide interactive view of a 3D volume including segmentation mask.
31+
"""
32+
33+
def __init__(self):
34+
"""Constructor of the operator."""
35+
super().__init__()
36+
37+
@staticmethod
38+
def _build_array(image, order):
39+
if order == "MXYZ":
40+
# @todo mask is transposed, see issue #171
41+
numpy_array = np.transpose(image.asnumpy(), (2, 1, 0))
42+
else:
43+
numpy_array = image.asnumpy()
44+
45+
array = DataDefinition.Array(array=numpy_array, order=order)
46+
array.element_size = [1.0]
47+
array.element_size.append(image.metadata().get("col_pixel_spacing", 1.0))
48+
array.element_size.append(image.metadata().get("row_pixel_spacing", 1.0))
49+
array.element_size.append(image.metadata().get("depth_pixel_spacing", 1.0))
50+
51+
# the renderer is expecting data in RIP order (Right Inferior Posterior) which results in
52+
# this matrix
53+
target_affine_transform = [
54+
[0.0, 1.0, 0.0, 0.0],
55+
[1.0, 0.0, 0.0, 0.0],
56+
[0.0, 0.0, -1.0, 0.0],
57+
[0.0, 0.0, 0.0, 1.0],
58+
]
59+
60+
dicom_affine_transform = image.metadata().get("dicom_affine_transform", np.identity(4))
61+
62+
affine_transform = np.matmul(target_affine_transform, dicom_affine_transform)
63+
64+
array.permute_axes = [
65+
0,
66+
max(range(3), key=lambda k: abs(affine_transform[0][k])) + 1,
67+
max(range(3), key=lambda k: abs(affine_transform[1][k])) + 1,
68+
max(range(3), key=lambda k: abs(affine_transform[2][k])) + 1,
69+
]
70+
71+
array.flip_axes = [
72+
False,
73+
affine_transform[0][array.permute_axes[1] - 1] < 0.0,
74+
affine_transform[1][array.permute_axes[2] - 1] < 0.0,
75+
affine_transform[2][array.permute_axes[3] - 1] < 0.0,
76+
]
77+
78+
return array
79+
80+
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
81+
"""Displays the input image and segmentation mask
82+
83+
Args:
84+
op_input (InputContext): An input context for the operator.
85+
op_output (OutputContext): An output context for the operator.
86+
context (ExecutionContext): An execution context for the operator.
87+
"""
88+
input_image = op_input.get("image")
89+
if not input_image:
90+
raise ValueError("Input image is not found.")
91+
input_seg_image = op_input.get("seg_image")
92+
if not input_seg_image:
93+
raise ValueError("Input segmentation image is not found.")
94+
95+
# build the data definition
96+
data_definition = DataDefinition()
97+
98+
data_definition.arrays.append(self._build_array(input_image, "DXYZ"))
99+
100+
data_definition.arrays.append(self._build_array(input_seg_image, "MXYZ"))
101+
102+
widget = Widget()
103+
widget.select_data_definition(data_definition)
104+
# default view mode is 'CINEMATIC' switch to 'SLICE_SEGMENTATION' since we have no transfer functions defined
105+
widget.settings["Views"][0]["mode"] = "SLICE_SEGMENTATION"
106+
widget.settings["Views"][0]["cameraName"] = "Top"
107+
widget.set_settings()
108+
display(widget)
109+
110+
# add controls
111+
def set_view_mode(view_mode):
112+
widget.settings["Views"][0]["mode"] = view_mode
113+
if view_mode == "CINEMATIC":
114+
widget.settings["Views"][0]["cameraName"] = "Perspective"
115+
elif widget.settings["Views"][0]["cameraName"] == "Perspective":
116+
widget.settings["Views"][0]["cameraName"] = "Top"
117+
widget.set_settings()
118+
119+
interact(
120+
set_view_mode,
121+
view_mode=Dropdown(
122+
options=[("Cinematic", "CINEMATIC"), ("Slice", "SLICE"), ("Slice Segmentation", "SLICE_SEGMENTATION")],
123+
value="SLICE_SEGMENTATION",
124+
description="View mode",
125+
),
126+
)
127+
128+
def set_camera(camera):
129+
if widget.settings["Views"][0]["mode"] != "CINEMATIC":
130+
widget.settings["Views"][0]["cameraName"] = camera
131+
widget.set_settings()
132+
133+
interact(set_camera, camera=Dropdown(options=["Top", "Right", "Front"], value="Top", description="Camera"))

0 commit comments

Comments
 (0)