Skip to content

Commit 22d14f2

Browse files
committed
WIP and initial commit
Signed-off-by: M Q <mingmelvinq@nvidia.com>
1 parent f62199b commit 22d14f2

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+2012
-3452
lines changed
Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1+
import os
2+
13
from app import App
24

35
if __name__ == "__main__":
4-
App(do_run=True)
6+
app = App()
7+
8+
# Config if need to, none for now
9+
app.config(os.path.join(os.path.dirname(__file__), "simple_imaging_app.yaml"))
10+
app.run()

examples/apps/simple_imaging_app/app.py

Lines changed: 45 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,32 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12+
import os
13+
from pathlib import Path
14+
1215
from gaussian_operator import GaussianOperator
1316
from median_operator import MedianOperator
1417
from sobel_operator import SobelOperator
1518

16-
from monai.deploy.core import Application, env, resource
19+
from monai.deploy.conditions import CountCondition
20+
from monai.deploy.core import Application
21+
from monai.deploy.logger import load_env_log_level
22+
23+
# Input path should be taken care of by the base app.
24+
# Without it being done yet, try to refer to MAP spec
25+
# for the well-known env vars
26+
27+
28+
DEFAULT_IN_PATH = Path(os.path.dirname(__file__)) / "input"
29+
DEFAULT_OUT_PATH = Path(os.path.dirname(__file__)) / "output"
1730

31+
sample_data_path = Path(os.environ.get("MONAI_INPUTPATH", DEFAULT_IN_PATH))
32+
output_data_path = Path(os.environ.get("MONAI_OUTPUTPATH", DEFAULT_OUT_PATH))
1833

19-
@resource(cpu=1)
20-
# pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
21-
@env(pip_packages=["scikit-image >= 0.17.2"])
34+
35+
# @resource(cpu=1)
36+
# # pip_packages can be a string that is a path(str) to requirements.txt file or a list of packages.
37+
# @env(pip_packages=["scikit-image >= 0.17.2"])
2238
class App(Application):
2339
"""This is a very basic application.
2440
@@ -38,16 +54,31 @@ def compose(self):
3854
Each operator has a single input and a single output port.
3955
Each operator performs some kind of image processing function.
4056
"""
41-
sobel_op = SobelOperator()
42-
median_op = MedianOperator()
43-
gaussian_op = GaussianOperator()
44-
45-
self.add_flow(sobel_op, median_op)
46-
# self.add_flow(sobel_op, median_op, {"image": "image"})
47-
# self.add_flow(sobel_op, median_op, {"image": {"image"}})
48-
49-
self.add_flow(median_op, gaussian_op)
57+
print(f"sample_data_path: {sample_data_path}")
58+
sobel_op = SobelOperator(self, CountCondition(self, 1), input_folder=sample_data_path, name="sobel_op")
59+
median_op = MedianOperator(self, name="median_op")
60+
gaussian_op = GaussianOperator(self, output_folder=output_data_path, name="gaussian_op")
61+
self.add_flow(
62+
sobel_op,
63+
median_op,
64+
{
65+
("out1", "in1"),
66+
},
67+
) # Identifing port optional for single port cases
68+
self.add_flow(
69+
median_op,
70+
gaussian_op,
71+
{
72+
(
73+
"out1",
74+
"in1",
75+
)
76+
},
77+
)
5078

5179

5280
if __name__ == "__main__":
53-
App(do_run=True)
81+
load_env_log_level()
82+
app = App()
83+
app.config(os.path.join(os.path.dirname(__file__), "simple_imaing_app.yaml"))
84+
app.run()

examples/apps/simple_imaging_app/gaussian_operator.py

Lines changed: 38 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,15 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
import monai.deploy.core as md
13-
from monai.deploy.core import DataPath, ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
12+
import os
13+
from pathlib import Path
14+
15+
from skimage.filters import gaussian
16+
from skimage.io import imsave
17+
18+
from monai.deploy.core import Operator, OperatorSpec
1419

1520

16-
@md.input("image", Image, IOType.IN_MEMORY)
17-
@md.output("image", DataPath, IOType.DISK)
1821
# If `pip_packages` is specified, the definition will be aggregated with the package dependency list of other
1922
# operators and the application in packaging time.
2023
# @md.env(pip_packages=["scikit-image >= 0.17.2"])
@@ -24,13 +27,37 @@ class GaussianOperator(Operator):
2427
It ingests a single input and provides a single output.
2528
"""
2629

27-
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
28-
from skimage.filters import gaussian
29-
from skimage.io import imsave
30+
DEFAULT_OUTPUT_FOLDER = Path(os.path.join(os.path.dirname(__file__))) / "output"
31+
32+
def __init__(self, *args, output_folder: Path, **kwargs):
33+
# If `self.sigma_default` is set here (e.g., `self.sigma_default = 0.2`), then
34+
# the default value by `param()` in `setup()` will be ignored.
35+
# (you can just call `spec.param("sigma_default")` in `setup()` to use the
36+
# default value)
37+
self.output_folder = output_folder if output_folder else GaussianOperator.DEFAULT_OUTPUT_FOLDER
38+
self.index = 0
39+
40+
# Need to call the base class constructor last
41+
super().__init__(*args, **kwargs)
3042

31-
data_in = op_input.get().asnumpy()
32-
data_out = gaussian(data_in, sigma=0.2, channel_axis=2) # Add the param introduced in 0.19.
43+
def setup(self, spec: OperatorSpec):
44+
spec.input("in1")
45+
# spec.output("out1")
46+
spec.param("sigma_default", 0.2)
47+
spec.param("channel_axis", 2)
3348

34-
output_folder = op_output.get().path
35-
output_path = output_folder / "final_output.png"
49+
def compute(self, op_input, op_output, context):
50+
self.index += 1
51+
print(f"# of times pperator {__name__} called: {self.index}")
52+
53+
data_in = op_input.receive("in1")
54+
data_out = gaussian(data_in, sigma=self.sigma_default, channel_axis=self.channel_axis)
55+
56+
# Where to set the Application's output folder, in the context?
57+
# For now, use attribute.
58+
output_path = self.output_folder / "final_output.png"
3659
imsave(output_path, data_out)
60+
61+
# Let's also emit the output, even though not sure what the receiver would be
62+
# CANNOT set dangling out!!!
63+
# op_input.emit(data_out, "out1")

examples/apps/simple_imaging_app/median_operator.py

Lines changed: 18 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -9,59 +9,34 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
import monai.deploy.core as md
13-
from monai.deploy.core import ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
12+
from skimage.filters import median
13+
14+
from monai.deploy.core import Operator, OperatorSpec
1415

1516

16-
@md.input("image", Image, IOType.IN_MEMORY)
17-
@md.output("image", Image, IOType.IN_MEMORY)
1817
# If `pip_packages` is specified, the definition will be aggregated with the package dependency list of other
1918
# operators and the application in packaging time.
2019
# @md.env(pip_packages=["scikit-image >= 0.17.2"])
21-
class MedianOperatorBase(Operator):
20+
class MedianOperator(Operator):
2221
"""This Operator implements a noise reduction.
2322
2423
The algorithm is based on the median operator.
2524
It ingests a single input and provides a single output.
2625
"""
2726

2827
# Define __init__ method with super().__init__() if you want to override the default behavior.
29-
def __init__(self):
30-
super().__init__()
31-
# Do something
32-
33-
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
34-
print("Executing base operator...")
35-
36-
37-
class MedianOperator(MedianOperatorBase):
38-
"""This operator is a subclass of the base operator to demonstrate the usage of inheritance."""
39-
40-
# Define __init__ method with super().__init__() if you want to override the default behavior.
41-
def __init__(self):
42-
super().__init__()
43-
# Do something
44-
45-
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
46-
# Execute the base operator's compute method.
47-
super().compute(op_input, op_output, context)
48-
49-
from skimage.filters import median
50-
51-
# `context.input.get().path` (Path) is the file/folder path of the input data from the application's context.
52-
# `context.output.get().path` (Path) is the file/folder path of the output data from the application's context.
53-
# `context.models.get(model_name)` returns a model instance
54-
# (a null model would be returned if model is not available)
55-
# If model_name is not specified and only one model exists, it returns that model.
56-
model = context.models.get() # a model object that inherits Model class
57-
58-
# Get a model instance if exists
59-
if model: # if model is not a null model
60-
print(model.items())
61-
# # model.path for accessing the model's path
62-
# # model.name for accessing the model's name
63-
# result = model(input.get().asnumpy())
64-
65-
data_in = op_input.get().asnumpy()
28+
def __init__(self, *args, **kwargs):
29+
# Need to call the base class constructor last
30+
super().__init__(*args, **kwargs)
31+
self.index = 0
32+
33+
def setup(self, spec: OperatorSpec):
34+
spec.input("in1")
35+
spec.output("out1")
36+
37+
def compute(self, op_input, op_output, context):
38+
self.index += 1
39+
print(f"# of times pperator {__name__} called: {self.index}")
40+
data_in = op_input.receive("in1")
6641
data_out = median(data_in)
67-
op_output.set(Image(data_out))
42+
op_output.emit(data_out, "out1") # TODO: change this to use Image class

examples/apps/simple_imaging_app/sobel_operator.py

Lines changed: 42 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -9,29 +9,58 @@
99
# See the License for the specific language governing permissions and
1010
# limitations under the License.
1111

12-
import monai.deploy.core as md
13-
from monai.deploy.core import DataPath, ExecutionContext, Image, InputContext, IOType, Operator, OutputContext
12+
import os
13+
from pathlib import Path
1414

15+
from monai.deploy.core import Operator, OperatorSpec
1516

16-
@md.input("image", DataPath, IOType.DISK)
17-
@md.output("image", Image, IOType.IN_MEMORY)
18-
# If `pip_packages` is specified, the definition will be aggregated with the package dependency list of other
19-
# operators and the application in packaging time.
20-
# @md.env(pip_packages=["scikit-image >= 0.17.2"])
17+
18+
# @md.input("image", DataPath, IOType.DISK)
19+
# @md.output("image", Image, IOType.IN_MEMORY)
20+
# # If `pip_packages` is specified, the definition will be aggregated with the package dependency list of other
21+
# # operators and the application in packaging time.
22+
# # @md.env(pip_packages=["scikit-image >= 0.17.2"])
2123
class SobelOperator(Operator):
2224
"""This Operator implements a Sobel edge detector.
2325
2426
It has a single input and single output.
2527
"""
2628

27-
def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):
29+
DEFAULT_INPUT_FOLDER = Path(os.path.join(os.path.dirname(__file__))) / "input"
30+
31+
def __init__(self, *args, input_folder: Path, **kwargs):
32+
# TODO: what is this for? Many examples use this. Related to CountConditions?
33+
self.index = 0
34+
35+
# May want to validate the path, but should really be validate when the compute
36+
# is called as the input path can set for each compute call
37+
self.input_folder = (
38+
input_folder if (input_folder and input_folder.is_dir()) else SobelOperator.DEFAULT_INPUT_FOLDER
39+
)
40+
41+
# Need to call the base class constructor last
42+
super().__init__(*args, **kwargs)
43+
44+
def setup(self, spec: OperatorSpec):
45+
spec.output("out1")
46+
# spec.param("input_folder", Path("."))
47+
48+
def compute(self, op_input, op_output, context):
2849
from skimage import filters, io
2950

30-
input_path = op_input.get().path
31-
if input_path.is_dir():
32-
input_path = next(input_path.glob("*.*")) # take the first file
51+
self.index += 1
52+
print(f"# of times pperator {__name__} called: {self.index}")
53+
54+
# TODO: Ideally the op_input or execution context should provide the file path
55+
# to read data from, for operators that are File input based.
56+
#
57+
# Need to use a temporary way to get input path. e.g. value set on init
58+
input_folder = self.input_folder # op_input.get().path
59+
print(f"Input from: {input_folder}")
60+
if input_folder.is_dir():
61+
input_file = next(input_folder.glob("*.*")) # take the first file
3362

34-
data_in = io.imread(input_path)[:, :, :3] # discard alpha channel if exists
63+
data_in = io.imread(input_file)[:, :, :3] # discard alpha channel if exists
3564
data_out = filters.sobel(data_in)
3665

37-
op_output.set(Image(data_out))
66+
op_output.emit(data_out, "out1")

monai/deploy/cli/exec_command.py

Lines changed: 18 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@
1616
from argparse import ArgumentParser, Namespace, _SubParsersAction
1717
from typing import List
1818

19-
from monai.deploy.core.datastores.factory import DatastoreFactory
20-
from monai.deploy.core.executors.factory import ExecutorFactory
21-
from monai.deploy.core.graphs.factory import GraphFactory
19+
# from monai.deploy.core.datastores.factory import DatastoreFactory
20+
# from monai.deploy.core.executors.factory import ExecutorFactory
21+
# from monai.deploy.core.graphs.factory import GraphFactory
2222

2323

2424
def create_exec_parser(subparser: _SubParsersAction, command: str, parents: List[ArgumentParser]) -> ArgumentParser:
@@ -37,21 +37,21 @@ def create_exec_parser(subparser: _SubParsersAction, command: str, parents: List
3737
type=str,
3838
help="Path to workspace folder (default: A temporary '.monai_workdir' folder in the current folder)",
3939
)
40-
parser.add_argument(
41-
"--graph",
42-
help=f"Set Graph engine (default: {GraphFactory.DEFAULT})",
43-
choices=GraphFactory.NAMES,
44-
)
45-
parser.add_argument(
46-
"--datastore",
47-
help=f"Set Datastore (default: {DatastoreFactory.DEFAULT})",
48-
choices=DatastoreFactory.NAMES,
49-
)
50-
parser.add_argument(
51-
"--executor",
52-
help=f"Set Executor (default: {ExecutorFactory.DEFAULT})",
53-
choices=ExecutorFactory.NAMES,
54-
)
40+
# parser.add_argument(
41+
# "--graph",
42+
# help=f"Set Graph engine (default: {GraphFactory.DEFAULT})",
43+
# choices=GraphFactory.NAMES,
44+
# )
45+
# parser.add_argument(
46+
# "--datastore",
47+
# help=f"Set Datastore (default: {DatastoreFactory.DEFAULT})",
48+
# choices=DatastoreFactory.NAMES,
49+
# )
50+
# parser.add_argument(
51+
# "--executor",
52+
# help=f"Set Executor (default: {ExecutorFactory.DEFAULT})",
53+
# choices=ExecutorFactory.NAMES,
54+
# )
5555
parser.add_argument("remaining", nargs="*")
5656

5757
return parser

monai/deploy/core/__init__.py

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,17 @@
2424
OutputContext
2525
"""
2626

27-
from .application import Application
27+
from holoscan.core import *
28+
29+
# from .application import Application
2830
from .domain.datapath import DataPath
2931
from .domain.image import Image
30-
from .env import env
31-
from .execution_context import ExecutionContext
32-
from .io_context import InputContext, OutputContext
33-
from .io_type import IOType
32+
33+
# from .env import env
34+
# from .execution_context import ExecutionContext
35+
# from .io_context import InputContext, OutputContext
36+
# from .io_type import IOType
3437
from .models import Model, ModelFactory, NamedModel, TorchScriptModel, TritonModel
35-
from .operator import Operator, input, output
36-
from .resource import resource
38+
39+
# from .operator import Operator, input, output
40+
# from .resource import resource

0 commit comments

Comments
 (0)