Creating a Simple Image Processing App with MONAI Deploy App SDK

This tutorial shows how a simple image processing application can be created with MONAI Deploy App SDK.

Creating Operators and connecting them in Application class

We will implement an application that consists of three Operators:

  • SobelOperator: Apply a Sobel edge detector.

    • Input: a file path (Path)

    • Output: an image object in memory (Image)

  • MedianOperator: Apply a Median filter for noise reduction.

    • Input: an image object in memory (Image)

    • Output: an image object in memory (Image)

  • GaussianOperator: Apply a Gaussian filter for smoothening.

    • Input: an image object in memory (Image)

    • Output: a file path (Path)

The workflow of the application would look like this.

%%{init: {"theme": "base", "themeVariables": { "fontSize": "16px"}} }%% classDiagram direction LR SobelOperator --|> MedianOperator : image...image MedianOperator --|> GaussianOperator : image...image class SobelOperator { <in>image : Path image(out) : IN_MEMORY } class MedianOperator { <in>image : IN_MEMORY image(out) : IN_MEMORY } class GaussianOperator { <in>image : IN_MEMORY image(out) : Path }

Setup environment

# Install necessary image loading/processing packages for the application
!python -c "import PIL" || pip install -q "Pillow"
!python -c "import skimage" || pip install -q "scikit-image"
!python -c "import matplotlib" || pip install -q "matplotlib"
%matplotlib inline

# Install MONAI Deploy App SDK package
!python -c "import monai.deploy" || pip install -q "monai-deploy-app-sdk"

Download test input

We will use a test input from the following.

Case courtesy of Dr Bruno Di Muzio, Radiopaedia.org. From the case rID: 41113

test_input_folder = "/tmp/simple_app"
test_input_path = test_input_folder + "/normal-brain-mri-4.png"

!python -c "import wget" || pip install -q "wget"
!mkdir -p {test_input_folder}

from skimage import io
import wget


wget.download("https://user-images.githubusercontent.com/1928522/133383228-2357d62d-316c-46ad-af8a-359b56f25c87.png", test_input_path)

print(f"Test input file path: {test_input_path!r}")

test_image = io.imread(test_input_path)
io.imshow(test_image)
Test input file path: '/tmp/simple_app/normal-brain-mri-4.png'
<matplotlib.image.AxesImage at 0x7fd82fbd9810>
../../_images/d7dfe5abb9901169be7708b774990a7126d10b79f61e019082ad1ac7b6d939d0.png

Set up environment variables

The application uses well-known enviornment variables for the input/output data path, working dir, as well as AI model file path if applicable. Defaults are used if these environment variable are absent.

In this example, only the input data path and output path need to be set.

output_path = "output"
%env HOLOSCAN_INPUT_FOLDER {test_input_folder}
%env HOLOSCAN_INPUT_PATH {test_input_path}
%env HOLOSCAN_OUTPUT_PATH {output_path}
%ls $HOLOSCAN_INPUT_PATH
env: HOLOSCAN_INPUT_FOLDER=/tmp/simple_app
env: HOLOSCAN_INPUT_PATH=/tmp/simple_app/normal-brain-mri-4.png
env: HOLOSCAN_OUTPUT_PATH=output
/tmp/simple_app/normal-brain-mri-4.png

Setup imports

Let’s import necessary classes/decorators to define the application and operators.

from pathlib import Path

from monai.deploy.conditions import CountCondition
from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Operator, OperatorSpec

Creating Operator classes

Each Operator class inherits from the Operator class, with the input and output ports of the operator specified using the setup method. Business logic would be implemented in the compute method.

Note

  • the way to specify operator input and output in this version of the App SDK is different from versions, up to and including V0.5, where Python decorators are used. Decorator support will be re-introduced in future releases

  • the first operator(SobelOperator)’s input and the last operator(GaussianOperator)’s output are data paths, which are not data types supported by operator ports but as object can be used as optional input and output. In the example, these paths are passed in as arguments to the constructor and the operator classes have defined logic on using the paths, e.g. reading from or writing to the path. The application class is responsible for setting the path by parsing the well-known environment variables

SobelOperator

SobelOperator is the first operator (the root operator in the workflow graph). It reads from the input file/folder path, which is passed in as an argument on the constructor and assigned to an attribute.

Once loaded and processed, the image data (as a Numpy array) is set to the output (op_output.emit(value, label)).

class SobelOperator(Operator):
    """This Operator implements a Sobel edge detector.

    It has the following input and output:
        single input:
          a image file, first one found in the input folder
        single output:
          array object in memory
    """

    DEFAULT_INPUT_FOLDER = Path.cwd() / "input"

    def __init__(self, fragment: Fragment, *args, input_path: Path, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): An instance of the Application class which is derived from Fragment
            input_path (Path): The path of the input image file or folder containing the image file
        """
        self.index = 0

        # May want to validate the path, but it should really be validated when the compute function is called, also,
        # when file path as input is supported in the operator or execution context, input_folder needs not an attribute.
        self.input_path = (
            input_path if input_path else SobelOperator.DEFAULT_INPUT_FOLDER
        )

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.output("out1")

    def compute(self, op_input, op_output, context):
        from skimage import filters, io

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")

        # Ideally the op_input or execution context should provide the file path
        # to read data from, for operators that are file input based.
        # For now, use a temporary way to get input path. e.g. value set on init
        input_path = self.input_path
        print(f"Input from: {input_path}, whose absolute path: {input_path.absolute()}")
        if input_path.is_dir():
            input_path = next(input_path.glob("*.*"))  # take the first file

        data_in = io.imread(input_path)[:, :, :3]  # discard alpha channel if exists
        data_out = filters.sobel(data_in)

        op_output.emit(data_out, "out1")

MedianOperator

MedianOperator is a middle operator that accepts data from SobelOperator and passes the processed image data to GaussianOperator.

Its input data type is image in Numpy array. Once received at the input (op_input.receive(label)), the image is transformed and set to the output (op_output.emit(value, label)).

class MedianOperator(Operator):
    """This Operator implements a noise reduction.

    The algorithm is based on the median operator.
    It ingests a single input and provides a single output, both are in-memory image arrays
    """

    # Define __init__ method with super().__init__() if you want to override the default behavior.
    def __init__(self, fragment: Fragment, *args, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): The instance of Application class which is derived from Fragment
        """

        self.index = 0

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.input("in1")
        spec.output("out1")

    def compute(self, op_input, op_output, context):
        from skimage.filters import median

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")
        data_in = op_input.receive("in1")
        data_out = median(data_in)
        op_output.emit(data_out, "out1")

GaussianOperator

GaussianOperator is the last operator (a leaf operator in the workflow graph) and saves the processed image to a file, whose path is provided via an argument on the constructor.

This operator can also output the image in Numpy array in memory without requiring a receiver for it. This can be set up by using the optional output condition in the function setup.

class GaussianOperator(Operator):
    """This Operator implements a smoothening based on Gaussian.

    It has the following input and output:
        single input:
          an image array object
        single output:
          an image arrary object, without enforcing a downsteam receiver

    Besides, this operator also saves the image file in the given output folder.
    """

    DEFAULT_OUTPUT_FOLDER = Path.cwd() / "output"

    def __init__(self, fragment: Fragment, *args, output_folder: Path, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): The instance of Application class which is derived from Fragment
            output_folder (Path): The folder to save the output file.
        """
        self.output_folder = output_folder if output_folder else GaussianOperator.DEFAULT_OUTPUT_FOLDER
        self.index = 0

        # If `self.sigma_default` is set here (e.g., `self.sigma_default = 0.2`), then
        # the default value by `param()` in `setup()` will be ignored.
        # (you can just call `spec.param("sigma_default")` in `setup()` to use the
        # default value)
        self.sigma_default = 0.2
        self.channel_axis = 2

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.input("in1")
        spec.output("out1").condition(ConditionType.NONE)  # Condition is for no or not-ready receiver ports.
        spec.param("sigma_default", 0.2)
        spec.param("channel_axis", 2)

    def compute(self, op_input, op_output, context):
        from skimage.filters import gaussian
        from skimage.io import imsave
        import numpy as np

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")

        data_in = op_input.receive("in1")
        data_out = gaussian(data_in, sigma=self.sigma_default, channel_axis=self.channel_axis)

        # Make sure the data type is what PIL Image can support, as the imsave function calls PIL Image fromarray()
        # Some details can be found at https://stackoverflow.com/questions/55319949/pil-typeerror-cannot-handle-this-data-type
        print(f"Data type of output: {type(data_out)!r}, max = {np.max(data_out)!r}")
        if np.max(data_out) <= 1:
            data_out = (data_out*255).astype(np.uint8)
        print(f"Data type of output post conversion: {type(data_out)!r}, max = {np.max(data_out)!r}")

        # For now, use attribute of self to find the output path.
        self.output_folder.mkdir(parents=True, exist_ok=True)
        output_path = self.output_folder / "final_output.png"
        imsave(output_path, data_out)

        op_output.emit(data_out, "out1")

Creating Application class

Our application class would look like below.

It defines App class, inheriting Application class.

In compose() method, objects of SobelOperator, MedianOperator, and GaussianOperator classes are created and connected through self.add_flow().

add_flow(source_op, destination_op, io_map=None)

io_map is a dictionary of mapping from the source operator’s label to the destination operator’s label(s) and its type is Set[Tuple[str, str]].

We can skip specifying io_map if both the number of source_op’s outputs and the number of destination_op’s inputs are one so self.add_flow(sobel_op, median_op) is same with self.add_flow(sobel_op, median_op, {"image": "image"}) or self.add_flow(sobel_op, median_op, {"image": {"image"}}).

class App(Application):
    """This is a very basic application.

    This showcases the MONAI Deploy application framework.
    """

    # App's name. <class name>('App') if not specified.
    name = "simple_imaging_app"
    # App's description. <class docstring> if not specified.
    description = "This is a very simple application."
    # App's version. <git version tag> or '0.0.0' if not specified.
    version = "0.1.0"

    def compose(self):
        """This application has three operators.

        Each operator has a single input and a single output port.
        Each operator performs some kind of image processing function.
        """
        app_context = Application.init_app_context({})  # Do not pass argv in Jupyter notebook
        sample_data_path = Path(app_context.input_path)
        output_data_path = Path(app_context.output_path)
        print(f"sample_data_path: {sample_data_path}")

        # Please note that the Application object, self, is passed as the first positonal argument
        # and the others as kwargs.
        # Also note the CountCondition of 1 on the first operator, indicating to the application executor
        # to invoke this operator, hence the pipleline, only once.
        sobel_op = SobelOperator(self, CountCondition(self, 1), input_path=sample_data_path, name="sobel_op")
        median_op = MedianOperator(self, name="median_op")
        gaussian_op = GaussianOperator(self, output_folder=output_data_path, name="gaussian_op")
        self.add_flow(
            sobel_op,
            median_op,
            {
                ("out1", "in1"),
            },
        )
        self.add_flow(
            median_op,
            gaussian_op,
            {
                (
                    "out1",
                    "in1",
                )
            },
        )  # Using port name is optional for single port cases


if __name__ == "__main__":
    print("The statement, App().run(), is needed when this is run directly by the interpreter.")
    # App().run()
The statement, App().run(), is needed when this is run directly by the interpreter.

Executing app locally

We can execute the app in the Jupyter notebook.

!rm -rf {output_path}
App().run()
[2024-04-23 15:26:29,737] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, argv=[])
[2024-04-23 15:26:29,745] [INFO] (root) - AppContext object: AppContext(input_path=/tmp/simple_app/normal-brain-mri-4.png, output_path=output, model_path=models, workdir=)
sample_data_path: /tmp/simple_app/normal-brain-mri-4.png
2024-04-23 15:26:29.768 INFO  gxf/std/greedy_scheduler.cpp@191: Scheduling 3 entities
Number of times operator sobel_op whose class is defined in __main__ called: 1
Input from: /tmp/simple_app/normal-brain-mri-4.png, whose absolute path: /tmp/simple_app/normal-brain-mri-4.png
[info] [gxf_executor.cpp:247] Creating context
[info] [gxf_executor.cpp:1672] Loading extensions from configs...
[info] [gxf_executor.cpp:1842] Activating Graph...
[info] [gxf_executor.cpp:1874] Running Graph...
[info] [gxf_executor.cpp:1876] Waiting for completion...
Number of times operator median_op whose class is defined in __main__ called: 1
Number of times operator gaussian_op whose class is defined in __main__ called: 1
Data type of output: <class 'numpy.ndarray'>, max = 0.35821119421406195
Data type of output post conversion: <class 'numpy.ndarray'>, max = 91
2024-04-23 15:26:30.023 INFO  gxf/std/greedy_scheduler.cpp@372: Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.
2024-04-23 15:26:30.023 INFO  gxf/std/greedy_scheduler.cpp@401: Scheduler finished.
[info] [gxf_executor.cpp:1879] Deactivating Graph...
[info] [gxf_executor.cpp:1887] Graph execution finished.
[info] [gxf_executor.cpp:275] Destroying context
!ls {output_path}
final_output.png
output_image_path = output_path + "/final_output.png"
output_image = io.imread(output_image_path)
io.imshow(output_image)
<matplotlib.image.AxesImage at 0x7fd82f735f90>
../../_images/414d7ac9802534e3b377644bf1cc2a17c279fcec50bc3fed60b2361db8731d8f.png

Once the application is verified inside Jupyter notebook, we can write the above Python code into Python files in an application folder.

The application folder structure would look like below:

simple_imaging_app
├── __main__.py
├── app.py
├── gaussian_operator.py
├── median_operator.py
└── sobel_operator.py

Note

We can create a single application Python file (such as simple_imaging_app.py) that includes the content of the files, instead of creating multiple files. You will see such example in MedNist Classifier Tutorial.

# Create an application folder
!mkdir -p simple_imaging_app

sobel_operator.py

%%writefile simple_imaging_app/sobel_operator.py

from pathlib import Path
from monai.deploy.core import Fragment, Operator, OperatorSpec

class SobelOperator(Operator):
    """This Operator implements a Sobel edge detector.

    It has the following input and output:
        single input:
          a image file, first one found in the input folder
        single output:
          array object in memory
    """

    DEFAULT_INPUT_FOLDER = Path.cwd() / "input"

    def __init__(self, fragment: Fragment, *args, input_path: Path, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): An instance of the Application class which is derived from Fragment
            input_path (Path): The path of the input image file or folder containing the image file
        """
        self.index = 0

        # May want to validate the path, but it should really be validated when the compute function is called, also,
        # when file path as input is supported in the operator or execution context, input_folder needs not an attribute.
        self.input_path = (
            input_path if input_path else SobelOperator.DEFAULT_INPUT_FOLDER
        )

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.output("out1")

    def compute(self, op_input, op_output, context):
        from skimage import filters, io

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")

        # Ideally the op_input or execution context should provide the file path
        # to read data from, for operators that are file input based.
        # For now, use a temporary way to get input path. e.g. value set on init
        input_path = self.input_path
        print(f"Input from: {input_path}, whose absolute path: {input_path.absolute()}")
        if input_path.is_dir():
            input_path = next(input_path.glob("*.*"))  # take the first file

        data_in = io.imread(input_path)[:, :, :3]  # discard alpha channel if exists
        data_out = filters.sobel(data_in)

        op_output.emit(data_out, "out1")
Overwriting simple_imaging_app/sobel_operator.py

median_operator.py

%%writefile simple_imaging_app/median_operator.py
from monai.deploy.core import Fragment, Operator, OperatorSpec


# Decorator support is not available in this version of the SDK, to be re-introduced later
# @md.env(pip_packages=["scikit-image >= 0.17.2"])
class MedianOperator(Operator):
    """This Operator implements a noise reduction.

    The algorithm is based on the median operator.
    It ingests a single input and provides a single output, both are in-memory image arrays
    """

    # Define __init__ method with super().__init__() if you want to override the default behavior.
    def __init__(self, fragment: Fragment, *args, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): The instance of Application class which is derived from Fragment
        """

        self.index = 0

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.input("in1")
        spec.output("out1")

    def compute(self, op_input, op_output, context):
        from skimage.filters import median

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")
        data_in = op_input.receive("in1")
        data_out = median(data_in)
        op_output.emit(data_out, "out1")
Overwriting simple_imaging_app/median_operator.py

gaussian_operator.py

%%writefile simple_imaging_app/gaussian_operator.py
from pathlib import Path

from monai.deploy.core import ConditionType, Fragment, Operator, OperatorSpec


# Decorator support is not available in this version of the SDK, to be re-introduced later
# @md.env(pip_packages=["scikit-image >= 0.17.2"])
class GaussianOperator(Operator):
    """This Operator implements a smoothening based on Gaussian.

    It has the following input and output:
        single input:
          an image array object
        single output:
          an image arrary object, without enforcing a downsteam receiver

    Besides, this operator also saves the image file in the given output folder.
    """

    DEFAULT_OUTPUT_FOLDER = Path.cwd() / "output"

    def __init__(self, fragment: Fragment, *args, output_folder: Path, **kwargs):
        """Create an instance to be part of the given application (fragment).

        Args:
            fragment (Fragment): The instance of Application class which is derived from Fragment
            output_folder (Path): The folder to save the output file.
        """
        self.output_folder = output_folder if output_folder else GaussianOperator.DEFAULT_OUTPUT_FOLDER
        self.index = 0

        # If `self.sigma_default` is set here (e.g., `self.sigma_default = 0.2`), then
        # the default value by `param()` in `setup()` will be ignored.
        # (you can just call `spec.param("sigma_default")` in `setup()` to use the
        # default value)
        self.sigma_default = 0.2
        self.channel_axis = 2

        # Need to call the base class constructor last
        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        spec.input("in1")
        spec.output("out1").condition(ConditionType.NONE)  # Condition is for no or not-ready receiver ports.
        spec.param("sigma_default", 0.2)
        spec.param("channel_axis", 2)

    def compute(self, op_input, op_output, context):
        from skimage.filters import gaussian
        from skimage.io import imsave
        import numpy as np

        self.index += 1
        print(f"Number of times operator {self.name} whose class is defined in {__name__} called: {self.index}")

        data_in = op_input.receive("in1")
        data_out = gaussian(data_in, sigma=self.sigma_default, channel_axis=self.channel_axis)

        # Make sure the data type is what PIL Image can support, as the imsave function calls PIL Image fromarray()
        # Some details can be found at https://stackoverflow.com/questions/55319949/pil-typeerror-cannot-handle-this-data-type
        print(f"Data type of output: {type(data_out)!r}, max = {np.max(data_out)!r}")
        if np.max(data_out) <= 1:
            data_out = (data_out*255).astype(np.uint8)
        print(f"Data type of output post conversion: {type(data_out)!r}, max = {np.max(data_out)!r}")

        # For now, use attribute of self to find the output path.
        self.output_folder.mkdir(parents=True, exist_ok=True)
        output_path = self.output_folder / "final_output.png"
        imsave(output_path, data_out)

        op_output.emit(data_out, "out1")
Overwriting simple_imaging_app/gaussian_operator.py

app.py

%%writefile simple_imaging_app/app.py
import logging
from pathlib import Path

from gaussian_operator import GaussianOperator
from median_operator import MedianOperator
from sobel_operator import SobelOperator

from monai.deploy.conditions import CountCondition
from monai.deploy.core import AppContext, Application


# Decorator support is not available in this version of the SDK, to be re-introduced later
# @resource(cpu=1)
class App(Application):
    """This is a very basic application.

    This showcases the MONAI Deploy application framework.
    """

    # App's name. <class name>('App') if not specified.
    name = "simple_imaging_app"
    # App's description. <class docstring> if not specified.
    description = "This is a very simple application."
    # App's version. <git version tag> or '0.0.0' if not specified.
    version = "0.1.0"

    def compose(self):
        """This application has three operators.

        Each operator has a single input and a single output port.
        Each operator performs some kind of image processing function.
        """
        # Use Commandline options over environment variables to init context.
        app_context = Application.init_app_context(self.argv)
        sample_data_path = Path(app_context.input_path)
        output_data_path = Path(app_context.output_path)
        logging.info(f"sample_data_path: {sample_data_path}")

        # Please note that the Application object, self, is passed as the first positonal argument
        # and the others as kwargs.
        # Also note the CountCondition of 1 on the first operator, indicating to the application executor
        # to invoke this operator, hence the pipleline, only once.
        sobel_op = SobelOperator(self, CountCondition(self, 1), input_path=sample_data_path, name="sobel_op")
        median_op = MedianOperator(self, name="median_op")
        gaussian_op = GaussianOperator(self, output_folder=output_data_path, name="gaussian_op")
        self.add_flow(
            sobel_op,
            median_op,
            {
                ("out1", "in1"),
            },
        )
        self.add_flow(
            median_op,
            gaussian_op,
            {
                (
                    "out1",
                    "in1",
                )
            },
        )


if __name__ == "__main__":
    logging.info(f"Begin {__name__}")
    App().run()
    logging.info(f"End {__name__}")
Overwriting simple_imaging_app/app.py
if __name__ == "__main__":
    App().run()

The above lines are needed to execute the application code by using python interpreter.

__main__.py

__main__.py is needed for MONAI Application Packager to detect the main application code (app.py) when the application is executed with the application folder path (e.g., python simple_imaging_app).

%%writefile simple_imaging_app/__main__.py
from app import App

if __name__ == "__main__":
    App().run()
Overwriting simple_imaging_app/__main__.py
!ls simple_imaging_app
app.py	  gaussian_operator.py	median_operator.py  requirements.txt
app.yaml  __main__.py		__pycache__	    sobel_operator.py

This time, let’s execute the app in the command line.

Note

Since the environment variables have been set and contain the correct paths, it is not necessary to provide the command line options on running the application, though the following demonstrates the use of the options.

!rm -rf {output_path}
!python simple_imaging_app -i {test_input_folder} -o {output_path} -l DEBUG
[2024-04-23 15:26:34,943] [INFO] (root) - Parsed args: Namespace(log_level='DEBUG', input=PosixPath('/tmp/simple_app'), output=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output'), model=None, workdir=None, argv=['simple_imaging_app', '-i', '/tmp/simple_app', '-o', 'output', '-l', 'DEBUG'])
[2024-04-23 15:26:34,945] [INFO] (root) - AppContext object: AppContext(input_path=/tmp/simple_app, output_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output, model_path=models, workdir=)
[2024-04-23 15:26:34,945] [INFO] (root) - sample_data_path: /tmp/simple_app
[info] [gxf_executor.cpp:247] Creating context
[info] [gxf_executor.cpp:1672] Loading extensions from configs...
[info] [gxf_executor.cpp:1842] Activating Graph...
[info] [gxf_executor.cpp:1874] Running Graph...
[info] [gxf_executor.cpp:1876] Waiting for completion...
2024-04-23 15:26:34.968 INFO  gxf/std/greedy_scheduler.cpp@191: Scheduling 3 entities
Number of times operator sobel_op whose class is defined in sobel_operator called: 1
Input from: /tmp/simple_app, whose absolute path: /tmp/simple_app
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IHDR' 16 13
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'sRGB' 41 1
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'gAMA' 54 4
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'pHYs' 70 9
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IDAT' 91 65445
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IHDR' 16 13
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'sRGB' 41 1
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'gAMA' 54 4
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'pHYs' 70 9
[2024-04-23 15:26:34,996] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IDAT' 91 65445
[2024-04-23 15:26:35,002] [DEBUG] (PIL.Image) - Error closing: Operation on closed image
Number of times operator median_op whose class is defined in median_operator called: 1
Number of times operator gaussian_op whose class is defined in gaussian_operator called: 1
Data type of output: <class 'numpy.ndarray'>, max = 0.35821119421406195
Data type of output post conversion: <class 'numpy.ndarray'>, max = 91
[2024-04-23 15:26:35,225] [DEBUG] (PIL.Image) - Importing BlpImagePlugin
[2024-04-23 15:26:35,226] [DEBUG] (PIL.Image) - Importing BmpImagePlugin
[2024-04-23 15:26:35,226] [DEBUG] (PIL.Image) - Importing BufrStubImagePlugin
[2024-04-23 15:26:35,227] [DEBUG] (PIL.Image) - Importing CurImagePlugin
[2024-04-23 15:26:35,227] [DEBUG] (PIL.Image) - Importing DcxImagePlugin
[2024-04-23 15:26:35,227] [DEBUG] (PIL.Image) - Importing DdsImagePlugin
[2024-04-23 15:26:35,230] [DEBUG] (PIL.Image) - Importing EpsImagePlugin
[2024-04-23 15:26:35,231] [DEBUG] (PIL.Image) - Importing FitsImagePlugin
[2024-04-23 15:26:35,231] [DEBUG] (PIL.Image) - Importing FliImagePlugin
[2024-04-23 15:26:35,231] [DEBUG] (PIL.Image) - Importing FpxImagePlugin
[2024-04-23 15:26:35,231] [DEBUG] (PIL.Image) - Image: failed to import FpxImagePlugin: No module named 'olefile'
[2024-04-23 15:26:35,231] [DEBUG] (PIL.Image) - Importing FtexImagePlugin
[2024-04-23 15:26:35,232] [DEBUG] (PIL.Image) - Importing GbrImagePlugin
[2024-04-23 15:26:35,232] [DEBUG] (PIL.Image) - Importing GifImagePlugin
[2024-04-23 15:26:35,232] [DEBUG] (PIL.Image) - Importing GribStubImagePlugin
[2024-04-23 15:26:35,232] [DEBUG] (PIL.Image) - Importing Hdf5StubImagePlugin
[2024-04-23 15:26:35,232] [DEBUG] (PIL.Image) - Importing IcnsImagePlugin
[2024-04-23 15:26:35,233] [DEBUG] (PIL.Image) - Importing IcoImagePlugin
[2024-04-23 15:26:35,233] [DEBUG] (PIL.Image) - Importing ImImagePlugin
[2024-04-23 15:26:35,234] [DEBUG] (PIL.Image) - Importing ImtImagePlugin
[2024-04-23 15:26:35,234] [DEBUG] (PIL.Image) - Importing IptcImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing JpegImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing Jpeg2KImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing McIdasImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing MicImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Image: failed to import MicImagePlugin: No module named 'olefile'
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing MpegImagePlugin
[2024-04-23 15:26:35,235] [DEBUG] (PIL.Image) - Importing MpoImagePlugin
[2024-04-23 15:26:35,236] [DEBUG] (PIL.Image) - Importing MspImagePlugin
[2024-04-23 15:26:35,237] [DEBUG] (PIL.Image) - Importing PalmImagePlugin
[2024-04-23 15:26:35,237] [DEBUG] (PIL.Image) - Importing PcdImagePlugin
[2024-04-23 15:26:35,237] [DEBUG] (PIL.Image) - Importing PcxImagePlugin
[2024-04-23 15:26:35,237] [DEBUG] (PIL.Image) - Importing PdfImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing PixarImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing PngImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing PpmImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing PsdImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing QoiImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing SgiImagePlugin
[2024-04-23 15:26:35,242] [DEBUG] (PIL.Image) - Importing SpiderImagePlugin
[2024-04-23 15:26:35,243] [DEBUG] (PIL.Image) - Importing SunImagePlugin
[2024-04-23 15:26:35,243] [DEBUG] (PIL.Image) - Importing TgaImagePlugin
[2024-04-23 15:26:35,243] [DEBUG] (PIL.Image) - Importing TiffImagePlugin
[2024-04-23 15:26:35,243] [DEBUG] (PIL.Image) - Importing WebPImagePlugin
[2024-04-23 15:26:35,244] [DEBUG] (PIL.Image) - Importing WmfImagePlugin
[2024-04-23 15:26:35,245] [DEBUG] (PIL.Image) - Importing XbmImagePlugin
[2024-04-23 15:26:35,245] [DEBUG] (PIL.Image) - Importing XpmImagePlugin
[2024-04-23 15:26:35,245] [DEBUG] (PIL.Image) - Importing XVThumbImagePlugin
2024-04-23 15:26:35.273 INFO  gxf/std/greedy_scheduler.cpp@372: Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.
2024-04-23 15:26:35.273 INFO  gxf/std/greedy_scheduler.cpp@401: Scheduler finished.
[info] [gxf_executor.cpp:1879] Deactivating Graph...
[info] [gxf_executor.cpp:1887] Graph execution finished.
[info] [gxf_executor.cpp:275] Destroying context
#output_image_path was set as before, output_image_path = output_path + "/final_output.png"
output_image = io.imread(output_image_path)
io.imshow(output_image)
<matplotlib.image.AxesImage at 0x7fd82f7c2800>
../../_images/414d7ac9802534e3b377644bf1cc2a17c279fcec50bc3fed60b2361db8731d8f.png

Packaging app

Let’s package the app with MONAI Application Packager.

In this version of the App SDK, we need to write out the configuration yaml file as well as the package requirements file, in the application folder.

%%writefile simple_imaging_app/app.yaml
%YAML 1.2
---
application:
  title: MONAI Deploy App Package - Simple Imaging App
  version: 1.0
  inputFormats: ["file"]
  outputFormats: ["file"]

resources:
  cpu: 1
  gpu: 1
  memory: 1Gi
  gpuMemory: 1Gi
Overwriting simple_imaging_app/app.yaml
%%writefile simple_imaging_app/requirements.txt
scikit-image
setuptools>=59.5.0 # for pkg_resources
Overwriting simple_imaging_app/requirements.txt

Now we can use the CLI package command to build the MONAI Application Package (MAP) container image based on a supported base image.

tag_prefix = "simple_imaging_app"

!monai-deploy package simple_imaging_app -c simple_imaging_app/app.yaml -t {tag_prefix}:1.0 --platform x64-workstation -l DEBUG
[2024-04-23 15:26:37,078] [INFO] (common) - Downloading CLI manifest file...
[2024-04-23 15:26:37,629] [DEBUG] (common) - Validating CLI manifest file...
[2024-04-23 15:26:37,631] [INFO] (packager.parameters) - Application: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/simple_imaging_app
[2024-04-23 15:26:37,632] [INFO] (packager.parameters) - Detected application type: Python Module
[2024-04-23 15:26:37,632] [INFO] (packager) - Reading application configuration from /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/simple_imaging_app/app.yaml...
[2024-04-23 15:26:37,636] [INFO] (packager) - Generating app.json...
[2024-04-23 15:26:37,636] [INFO] (packager) - Generating pkg.json...
[2024-04-23 15:26:37,649] [DEBUG] (common) - 
=============== Begin app.json ===============
{
    "apiVersion": "1.0.0",
    "command": "[\"python3\", \"/opt/holoscan/app\"]",
    "environment": {
        "HOLOSCAN_APPLICATION": "/opt/holoscan/app",
        "HOLOSCAN_INPUT_PATH": "input/",
        "HOLOSCAN_OUTPUT_PATH": "output/",
        "HOLOSCAN_WORKDIR": "/var/holoscan",
        "HOLOSCAN_MODEL_PATH": "/opt/holoscan/models",
        "HOLOSCAN_CONFIG_PATH": "/var/holoscan/app.yaml",
        "HOLOSCAN_APP_MANIFEST_PATH": "/etc/holoscan/app.json",
        "HOLOSCAN_PKG_MANIFEST_PATH": "/etc/holoscan/pkg.json",
        "HOLOSCAN_DOCS_PATH": "/opt/holoscan/docs",
        "HOLOSCAN_LOGS_PATH": "/var/holoscan/logs"
    },
    "input": {
        "path": "input/",
        "formats": null
    },
    "liveness": null,
    "output": {
        "path": "output/",
        "formats": null
    },
    "readiness": null,
    "sdk": "monai-deploy",
    "sdkVersion": "0.5.1",
    "timeout": 0,
    "version": 1.0,
    "workingDirectory": "/var/holoscan"
}
================ End app.json ================
                 
[2024-04-23 15:26:37,650] [DEBUG] (common) - 
=============== Begin pkg.json ===============
{
    "apiVersion": "1.0.0",
    "applicationRoot": "/opt/holoscan/app",
    "modelRoot": "/opt/holoscan/models",
    "models": {},
    "resources": {
        "cpu": 1,
        "gpu": 1,
        "memory": "1Gi",
        "gpuMemory": "1Gi"
    },
    "version": 1.0,
    "platformConfig": "dgpu"
}
================ End pkg.json ================
                 
[2024-04-23 15:26:37,673] [DEBUG] (packager.builder) - 
========== Begin Dockerfile ==========


FROM nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu

ENV DEBIAN_FRONTEND=noninteractive
ENV TERM=xterm-256color

ARG UNAME
ARG UID
ARG GID

RUN mkdir -p /etc/holoscan/ \
        && mkdir -p /opt/holoscan/ \
        && mkdir -p /var/holoscan \
        && mkdir -p /opt/holoscan/app \
        && mkdir -p /var/holoscan/input \
        && mkdir -p /var/holoscan/output

LABEL base="nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu"
LABEL tag="simple_imaging_app:1.0"
LABEL org.opencontainers.image.title="MONAI Deploy App Package - Simple Imaging App"
LABEL org.opencontainers.image.version="1.0"
LABEL org.nvidia.holoscan="2.0.0"
LABEL org.monai.deploy.app-sdk="0.5.1"


ENV HOLOSCAN_ENABLE_HEALTH_CHECK=true
ENV HOLOSCAN_INPUT_PATH=/var/holoscan/input
ENV HOLOSCAN_OUTPUT_PATH=/var/holoscan/output
ENV HOLOSCAN_WORKDIR=/var/holoscan
ENV HOLOSCAN_APPLICATION=/opt/holoscan/app
ENV HOLOSCAN_TIMEOUT=0
ENV HOLOSCAN_MODEL_PATH=/opt/holoscan/models
ENV HOLOSCAN_DOCS_PATH=/opt/holoscan/docs
ENV HOLOSCAN_CONFIG_PATH=/var/holoscan/app.yaml
ENV HOLOSCAN_APP_MANIFEST_PATH=/etc/holoscan/app.json
ENV HOLOSCAN_PKG_MANIFEST_PATH=/etc/holoscan/pkg.json
ENV HOLOSCAN_LOGS_PATH=/var/holoscan/logs
ENV PATH=/root/.local/bin:/opt/nvidia/holoscan:$PATH
ENV LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/opt/libtorch/1.13.1/lib/:/opt/nvidia/holoscan/lib

RUN apt-get update \
    && apt-get install -y curl jq \
    && rm -rf /var/lib/apt/lists/*

ENV PYTHONPATH="/opt/holoscan/app:$PYTHONPATH"


RUN groupadd -f -g $GID $UNAME
RUN useradd -rm -d /home/$UNAME -s /bin/bash -g $GID -G sudo -u $UID $UNAME
RUN chown -R holoscan /var/holoscan 
RUN chown -R holoscan /var/holoscan/input 
RUN chown -R holoscan /var/holoscan/output 

# Set the working directory
WORKDIR /var/holoscan

# Copy HAP/MAP tool script
COPY ./tools /var/holoscan/tools
RUN chmod +x /var/holoscan/tools


# Copy gRPC health probe

USER $UNAME

ENV PATH=/root/.local/bin:/home/holoscan/.local/bin:/opt/nvidia/holoscan:$PATH

COPY ./pip/requirements.txt /tmp/requirements.txt

RUN pip install --upgrade pip
RUN pip install --no-cache-dir --user -r /tmp/requirements.txt

 
# MONAI Deploy

# Copy user-specified MONAI Deploy SDK file
COPY ./monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl /tmp/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl
RUN pip install /tmp/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl



COPY ./map/app.json /etc/holoscan/app.json
COPY ./app.config /var/holoscan/app.yaml
COPY ./map/pkg.json /etc/holoscan/pkg.json

COPY ./app /opt/holoscan/app

ENTRYPOINT ["/var/holoscan/tools"]
=========== End Dockerfile ===========

[2024-04-23 15:26:37,673] [INFO] (packager.builder) - 
===============================================================================
Building image for:                 x64-workstation
    Architecture:                   linux/amd64
    Base Image:                     nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu
    Build Image:                    N/A
    Cache:                          Enabled
    Configuration:                  dgpu
    Holoscan SDK Package:           pypi.org
    MONAI Deploy App SDK Package:   /home/mqin/src/monai-deploy-app-sdk/dist/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl
    gRPC Health Probe:              N/A
    SDK Version:                    2.0.0
    SDK:                            monai-deploy
    Tag:                            simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0
    
[2024-04-23 15:26:38,231] [INFO] (common) - Using existing Docker BuildKit builder `holoscan_app_builder`
[2024-04-23 15:26:38,231] [DEBUG] (packager.builder) - Building Holoscan Application Package: tag=simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0
#0 building with "holoscan_app_builder" instance using docker-container driver

#1 [internal] load build definition from Dockerfile
#1 transferring dockerfile: 2.63kB done
#1 DONE 0.0s

#2 [internal] load metadata for nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu
#2 DONE 0.5s

#3 [internal] load .dockerignore
#3 transferring context: 1.79kB done
#3 DONE 0.0s

#4 [internal] load build context
#4 DONE 0.0s

#5 importing cache manifest from local:10678196058931023490
#5 inferred cache manifest type: application/vnd.oci.image.index.v1+json done
#5 DONE 0.0s

#6 [ 1/20] FROM nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu@sha256:20adbccd2c7b12dfb1798f6953f071631c3b85cd337858a7506f8e420add6d4a
#6 resolve nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu@sha256:20adbccd2c7b12dfb1798f6953f071631c3b85cd337858a7506f8e420add6d4a 0.0s done
#6 DONE 0.0s

#7 importing cache manifest from nvcr.io/nvidia/clara-holoscan/holoscan:v2.0.0-dgpu
#7 inferred cache manifest type: application/vnd.docker.distribution.manifest.list.v2+json done
#7 DONE 0.9s

#4 [internal] load build context
#4 transferring context: 157.77kB 0.0s done
#4 DONE 0.0s

#8 [ 5/20] RUN useradd -rm -d /home/holoscan -s /bin/bash -g 1000 -G sudo -u 1000 holoscan
#8 CACHED

#9 [ 3/20] RUN apt-get update     && apt-get install -y curl jq     && rm -rf /var/lib/apt/lists/*
#9 CACHED

#10 [ 6/20] RUN chown -R holoscan /var/holoscan
#10 CACHED

#11 [13/20] RUN pip install --upgrade pip
#11 CACHED

#12 [ 8/20] RUN chown -R holoscan /var/holoscan/output
#12 CACHED

#13 [ 7/20] RUN chown -R holoscan /var/holoscan/input
#13 CACHED

#14 [ 2/20] RUN mkdir -p /etc/holoscan/         && mkdir -p /opt/holoscan/         && mkdir -p /var/holoscan         && mkdir -p /opt/holoscan/app         && mkdir -p /var/holoscan/input         && mkdir -p /var/holoscan/output
#14 CACHED

#15 [12/20] COPY ./pip/requirements.txt /tmp/requirements.txt
#15 CACHED

#16 [11/20] RUN chmod +x /var/holoscan/tools
#16 CACHED

#17 [ 9/20] WORKDIR /var/holoscan
#17 CACHED

#18 [ 4/20] RUN groupadd -f -g 1000 holoscan
#18 CACHED

#19 [10/20] COPY ./tools /var/holoscan/tools
#19 CACHED

#20 [14/20] RUN pip install --no-cache-dir --user -r /tmp/requirements.txt
#20 CACHED

#21 [15/20] COPY ./monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl /tmp/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl
#21 DONE 0.1s

#22 [16/20] RUN pip install /tmp/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl
#22 0.725 Defaulting to user installation because normal site-packages is not writeable
#22 0.802 Processing /tmp/monai_deploy_app_sdk-0.5.1+20.gb869749.dirty-py3-none-any.whl
#22 0.815 Requirement already satisfied: numpy>=1.21.6 in /usr/local/lib/python3.10/dist-packages (from monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (1.23.5)
#22 1.004 Collecting holoscan~=2.0 (from monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.074   Downloading holoscan-2.0.0-cp310-cp310-manylinux_2_35_x86_64.whl.metadata (6.7 kB)
#22 1.118 Collecting colorama>=0.4.1 (from monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.122   Downloading colorama-0.4.6-py2.py3-none-any.whl.metadata (17 kB)
#22 1.172 Collecting typeguard>=3.0.0 (from monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.176   Downloading typeguard-4.2.1-py3-none-any.whl.metadata (3.7 kB)
#22 1.192 Requirement already satisfied: pip>=20.3 in /home/holoscan/.local/lib/python3.10/site-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (24.0)
#22 1.193 Requirement already satisfied: cupy-cuda12x==12.2 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (12.2.0)
#22 1.193 Requirement already satisfied: cloudpickle==2.2.1 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2.2.1)
#22 1.194 Requirement already satisfied: python-on-whales==0.60.1 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (0.60.1)
#22 1.195 Requirement already satisfied: Jinja2==3.1.3 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (3.1.3)
#22 1.195 Requirement already satisfied: packaging==23.1 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (23.1)
#22 1.196 Requirement already satisfied: pyyaml==6.0 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (6.0)
#22 1.197 Requirement already satisfied: requests==2.31.0 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2.31.0)
#22 1.197 Requirement already satisfied: psutil==5.9.6 in /usr/local/lib/python3.10/dist-packages (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (5.9.6)
#22 1.296 Collecting wheel-axle-runtime<1.0 (from holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.302   Downloading wheel_axle_runtime-0.0.5-py3-none-any.whl.metadata (7.7 kB)
#22 1.326 Requirement already satisfied: fastrlock>=0.5 in /usr/local/lib/python3.10/dist-packages (from cupy-cuda12x==12.2->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (0.8.2)
#22 1.329 Requirement already satisfied: MarkupSafe>=2.0 in /usr/local/lib/python3.10/dist-packages (from Jinja2==3.1.3->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2.1.3)
#22 1.342 Requirement already satisfied: pydantic<2,>=1.5 in /usr/local/lib/python3.10/dist-packages (from python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (1.10.15)
#22 1.342 Requirement already satisfied: tqdm in /usr/local/lib/python3.10/dist-packages (from python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (4.66.2)
#22 1.343 Requirement already satisfied: typer>=0.4.1 in /usr/local/lib/python3.10/dist-packages (from python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (0.12.3)
#22 1.343 Requirement already satisfied: typing-extensions in /usr/local/lib/python3.10/dist-packages (from python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (4.7.1)
#22 1.352 Requirement already satisfied: charset-normalizer<4,>=2 in /usr/local/lib/python3.10/dist-packages (from requests==2.31.0->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (3.3.2)
#22 1.352 Requirement already satisfied: idna<4,>=2.5 in /usr/local/lib/python3.10/dist-packages (from requests==2.31.0->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (3.7)
#22 1.353 Requirement already satisfied: urllib3<3,>=1.21.1 in /usr/local/lib/python3.10/dist-packages (from requests==2.31.0->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2.2.1)
#22 1.353 Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.10/dist-packages (from requests==2.31.0->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2024.2.2)
#22 1.389 Collecting typing-extensions (from python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.392   Downloading typing_extensions-4.11.0-py3-none-any.whl.metadata (3.0 kB)
#22 1.451 Collecting filelock (from wheel-axle-runtime<1.0->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty)
#22 1.455   Downloading filelock-3.13.4-py3-none-any.whl.metadata (2.8 kB)
#22 1.486 Requirement already satisfied: click>=8.0.0 in /usr/local/lib/python3.10/dist-packages (from typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (8.1.7)
#22 1.490 Requirement already satisfied: shellingham>=1.3.0 in /usr/local/lib/python3.10/dist-packages (from typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (1.5.4)
#22 1.491 Requirement already satisfied: rich>=10.11.0 in /usr/local/lib/python3.10/dist-packages (from typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (13.7.1)
#22 1.533 Requirement already satisfied: markdown-it-py>=2.2.0 in /usr/local/lib/python3.10/dist-packages (from rich>=10.11.0->typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (3.0.0)
#22 1.533 Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /usr/local/lib/python3.10/dist-packages (from rich>=10.11.0->typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (2.17.2)
#22 1.556 Requirement already satisfied: mdurl~=0.1 in /usr/local/lib/python3.10/dist-packages (from markdown-it-py>=2.2.0->rich>=10.11.0->typer>=0.4.1->python-on-whales==0.60.1->holoscan~=2.0->monai-deploy-app-sdk==0.5.1+20.gb869749.dirty) (0.1.2)
#22 1.570 Downloading colorama-0.4.6-py2.py3-none-any.whl (25 kB)
#22 1.585 Downloading holoscan-2.0.0-cp310-cp310-manylinux_2_35_x86_64.whl (33.2 MB)
#22 3.441    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 33.2/33.2 MB 25.5 MB/s eta 0:00:00
#22 3.447 Downloading typeguard-4.2.1-py3-none-any.whl (34 kB)
#22 3.465 Downloading typing_extensions-4.11.0-py3-none-any.whl (34 kB)
#22 3.484 Downloading wheel_axle_runtime-0.0.5-py3-none-any.whl (12 kB)
#22 3.499 Downloading filelock-3.13.4-py3-none-any.whl (11 kB)
#22 3.799 Installing collected packages: typing-extensions, filelock, colorama, wheel-axle-runtime, typeguard, holoscan, monai-deploy-app-sdk
#22 4.520 Successfully installed colorama-0.4.6 filelock-3.13.4 holoscan-2.0.0 monai-deploy-app-sdk-0.5.1+20.gb869749.dirty typeguard-4.2.1 typing-extensions-4.11.0 wheel-axle-runtime-0.0.5
#22 DONE 5.2s

#23 [17/20] COPY ./map/app.json /etc/holoscan/app.json
#23 DONE 0.1s

#24 [18/20] COPY ./app.config /var/holoscan/app.yaml
#24 DONE 0.0s

#25 [19/20] COPY ./map/pkg.json /etc/holoscan/pkg.json
#25 DONE 0.0s

#26 [20/20] COPY ./app /opt/holoscan/app
#26 DONE 0.0s

#27 exporting to docker image format
#27 exporting layers
#27 exporting layers 4.7s done
#27 exporting manifest sha256:b2f214d50ea4107e85a01c007f24375e996d9dc954742f585907f47d96cc3c1a 0.0s done
#27 exporting config sha256:6485f181da93a3c0fc12abe1cf367c89306b7e8f7b110993be48f38b56164b89 0.0s done
#27 sending tarball
#27 ...

#28 importing to docker
#28 loading layer d16586f61a51 32.77kB / 125.82kB
#28 loading layer bb2ce52783d3 557.06kB / 67.52MB
#28 loading layer 5f42a2d4a17c 492B / 492B
#28 loading layer 93299cfbc42b 312B / 312B
#28 loading layer 222cc4dc3d9f 294B / 294B
#28 loading layer 4dbebd69efec 3.18kB / 3.18kB
#28 loading layer 5f42a2d4a17c 492B / 492B 1.0s done
#28 loading layer d16586f61a51 32.77kB / 125.82kB 2.9s done
#28 loading layer bb2ce52783d3 557.06kB / 67.52MB 2.9s done
#28 loading layer 93299cfbc42b 312B / 312B 1.0s done
#28 loading layer 222cc4dc3d9f 294B / 294B 1.0s done
#28 loading layer 4dbebd69efec 3.18kB / 3.18kB 0.9s done
#28 DONE 2.9s

#27 exporting to docker image format
#27 sending tarball 39.7s done
#27 DONE 44.4s

#29 exporting cache to client directory
#29 preparing build cache for export
#29 writing layer sha256:014cff740c9ec6e9a30d0b859219a700ae880eb385d62095d348f5ea136d6015
#29 writing layer sha256:014cff740c9ec6e9a30d0b859219a700ae880eb385d62095d348f5ea136d6015 done
#29 writing layer sha256:0487800842442c7a031a39e1e1857bc6dae4b4f7e5daf3d625f7a8a4833fb364 done
#29 writing layer sha256:06c6aee94862daf0603783db4e1de6f8524b30ac9fbe0374ab3f1d85b2f76f7f done
#29 writing layer sha256:0a1756432df4a4350712d8ae5c003f1526bd2180800b3ae6301cfc9ccf370254 done
#29 writing layer sha256:0a77dcbd0e648ddc4f8e5230ade8fdb781d99e24fa4f13ca96a360c7f7e6751f done
#29 writing layer sha256:0ec682bf99715a9f88631226f3749e2271b8b9f254528ef61f65ed829984821c done
#29 writing layer sha256:18ff8faa5fb62bc13ea7af97d3ed57fd4dbffbcb15b37606935aa20a1b2b9879
#29 writing layer sha256:18ff8faa5fb62bc13ea7af97d3ed57fd4dbffbcb15b37606935aa20a1b2b9879 1.2s done
#29 writing layer sha256:1c5c3aa9c2c8bfd1b9eb36248f5b6d67b3db73ef43440f9dd897615771974b39
#29 writing layer sha256:1c5c3aa9c2c8bfd1b9eb36248f5b6d67b3db73ef43440f9dd897615771974b39 done
#29 writing layer sha256:1f73278b7f17492ce1a8b28b139d54596961596d6790dc20046fa6d5909f3e9c done
#29 writing layer sha256:20d331454f5fb557f2692dfbdbe092c718fd2cb55d5db9d661b62228dacca5c2 done
#29 writing layer sha256:238f69a43816e481f0295995fcf5fe74d59facf0f9f99734c8d0a2fb140630e0 done
#29 writing layer sha256:2ad84487f9d4d31cd1e0a92697a5447dd241935253d036b272ef16d31620c1e7 done
#29 writing layer sha256:2e367cdca270a2175c84537f99cfd3f00f286e5977884762ba2b7045637f7eb8 done
#29 writing layer sha256:2f65750928993b5b31fe572d9e085b53853c5a344feeb0e8615898e285a8c256 done
#29 writing layer sha256:3777c6498f08c0400339c243e827d465075b7296eb2526e38d9b01c84f8764d8 done
#29 writing layer sha256:3e3e04011ebdba380ab129f0ee390626cb2a600623815ca756340c18bedb9517 done
#29 writing layer sha256:42619ce4a0c9e54cfd0ee41a8e5f27d58b3f51becabd1ac6de725fbe6c42b14a done
#29 writing layer sha256:44b45fba2f5d69895a3b96828a250406b216add7940feab849e3bf18bc864780 0.0s done
#29 writing layer sha256:49bdc9abf8a437ccff67cc11490ba52c976577992909856a86be872a34d3b950 done
#29 writing layer sha256:4b691ba9f48b41eaa0c754feba8366f1c030464fcbc55eeffa6c86675990933a done
#29 writing layer sha256:4d04a8db404f16c2704fa10739cb6745a0187713a21a6ef0deb34b48629b54c1 done
#29 writing layer sha256:4dde81c0149c8207008e2b45ce8533f41589094e1b998390a2d39fb50b337992 0.0s done
#29 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 done
#29 writing layer sha256:542bc8c8d18fbc95e6794122c3593a4a693f8ab6dda4460406f4d7b1ae64a2bc done
#29 writing layer sha256:57f244836ad318f9bbb3b29856ae1a5b31038bfbb9b43d2466d51c199eb55041 done
#29 writing layer sha256:5b5b131e0f20db4cb8e568b623a95f8fc16ed1c6b322a9366df70b59a881f24f done
#29 writing layer sha256:5b90d17b5048adcadefd0b1e4dba9a99247a8827a887e1ca042df375c85b518d done
#29 writing layer sha256:62452179df7c18e292f141d4aec29e6aba9ff8270c893731169fc6f41dc07631 done
#29 writing layer sha256:6630c387f5f2115bca2e646fd0c2f64e1f3d5431c2e050abe607633883eda230 done
#29 writing layer sha256:6661e0146e77a8bcb03edbfda95bf7780c8bb4c4f98bc03a398c88f4b2403d12 done
#29 writing layer sha256:717ebf8c9c66ae393ad01e50dbac4413d7b026b9c97d4d348b22ad17052a1a35 done
#29 writing layer sha256:758ac279497f68ba566bbdb5686aaaa9e362ca063c817c725de96fd205aeaa53 0.0s done
#29 writing layer sha256:773c6815e5e7d6855a62f8c5e2fabce3d939ded36c5420f15b54dd7908cdbcfa done
#29 writing layer sha256:7852b73ea931e3a8d3287ee7ef3cf4bad068e44f046583bfc2b81336fb299284 done
#29 writing layer sha256:7f8ec130348bcdac81c295e37fe82b4a6e5e9a3ca980a6343809c561020d82d7 done
#29 writing layer sha256:80885adcad6b5d021bb9f68b6c952018085bb4ce72011bdc0cf7fe8178b5960b done
#29 writing layer sha256:82a3436133b2b17bb407c7fe488932aa0ca55411f23ab55c34a6134b287c6a27 done
#29 writing layer sha256:8371d15eb4d69b1d98174dd098b8ddd5c4f19ec6f8d8b67e72dfa9891dc454b4 done
#29 writing layer sha256:85713f9b166b5add777c524ee807f6265d88b967cbeb9f961d6b09bf220c9a65 done
#29 writing layer sha256:8fe00505006a09966e763918147ef6ed55bb6695b26e4940c780ee430dc5da8e done
#29 writing layer sha256:90eae6faa5cc5ba62f12c25915cdfb1a7a51abfba0d05cb5818c3f908f4e345f done
#29 writing layer sha256:9205d97d9d3e906698bcc6c42d45727c2fa6ec2622abf953d46778c3b8c78edc done
#29 writing layer sha256:993369dbcc13162a6654d2a3e990b8d8b5f37963564d25710e12764337261ae3 done
#29 writing layer sha256:99e42a4adebadb39bf55bf94bbd9fb8034230ee19b6b0a42e6ff96f2e7794f30 done
#29 writing layer sha256:9ac855545fa90ed2bf3b388fdff9ef06ac9427b0c0fca07c9e59161983d8827e done
#29 writing layer sha256:9d19ee268e0d7bcf6716e6658ee1b0384a71d6f2f9aa1ae2085610cf7c7b316f done
#29 writing layer sha256:9fafbd4203c4fefe007a462e0d2cd4c1c7c41db2cfdc58d212279e1b9b4b230c done
#29 writing layer sha256:a1748eee9d376f97bd19225ba61dfada9986f063f4fc429e435f157abb629fc6 done
#29 writing layer sha256:a251fe5ae6c6d2d5034e4ca88b5dfe5d4827ff90b18e9b143a073232a32bb18d done
#29 writing layer sha256:a68f4e0ec09ec3b78cb4cf8e4511d658e34e7b6f676d7806ad9703194ff17604 done
#29 writing layer sha256:a8e4decc8f7289623b8fd7b9ba1ca555b5a755ebdbf81328d68209f148d9e602 done
#29 writing layer sha256:afde1c269453ce68a0f2b54c1ba8c5ecddeb18a19e5618a4acdef1f0fe3921af done
#29 writing layer sha256:b406feb20a37b8c87ef4f5ef814039e3adc90473d50c366b7d9bb6ded4e94a2e done
#29 writing layer sha256:b48a5fafcaba74eb5d7e7665601509e2889285b50a04b5b639a23f8adc818157
#29 preparing build cache for export 1.7s done
#29 writing layer sha256:b48a5fafcaba74eb5d7e7665601509e2889285b50a04b5b639a23f8adc818157 done
#29 writing layer sha256:b98f5f4d1c99ef6c3b4ecce24d32753787f67982a13b216d17115c13fee3aa58 done
#29 writing layer sha256:ba9f7c75e4dd7942b944679995365aab766d3677da2e69e1d74472f471a484dd done
#29 writing layer sha256:bdc13166216ae226fa6976f9ce91f4f259d43972f1e0a9b723e436919534b2f4 done
#29 writing layer sha256:c815f0be64eded102822d81e029bd23b0d8d9a0fbfeb492ec0b4b0bc4ee777bf done
#29 writing layer sha256:c98533d2908f36a5e9b52faae83809b3b6865b50e90e2817308acfc64cd3655f done
#29 writing layer sha256:d7da5c5e9a40c476c4b3188a845e3276dedfd752e015ea5113df5af64d4d43f7 done
#29 writing layer sha256:db20521a869adda8244cb64b783c65e1a911efaae0e73ae00e4a34ea6213d6ce done
#29 writing layer sha256:df4fd0ac710d7af949afbc6d25b5b4daf3f0596dabf3dec36fa7ca8fa6e1d049 done
#29 writing layer sha256:e16d56ac40a253ee3622e4a00c4d5f0cc28effdef15ac4b2c51b78f4732abcef done
#29 writing layer sha256:e291ddecfbe16b95ee9e90b5e90b1a3d0cfd53dc5e720d6b0f3d28e4a47cf5ac done
#29 writing layer sha256:e2cb45922077b1926bf021323ddd666dbc92d2c9daea0acf78515292da5000c8 done
#29 writing layer sha256:e79b8737ab7fc2e26d2e35f3e6572c9ababcbaaa232c1ff5987cdbf9ccd55b88 done
#29 writing layer sha256:e8acb678f16bc0c369d5cf9c184f2d3a1c773986816526e5e3e9c0354f7e757f done
#29 writing layer sha256:e9225f7ab6606813ec9acba98a064826ebfd6713a9645a58cd068538af1ecddb done
#29 writing layer sha256:f249faf9663a96b0911a903f8803b11a553c59b698013fb8343492fefdaaea90 done
#29 writing layer sha256:f608e2fbff86e98627b7e462057e7d2416522096d73fe4664b82fe6ce8a4047d done
#29 writing layer sha256:f65d191416580d6c38e3d95eee12377b75a4df548be1492618ce2a8c3c41b99e done
#29 writing config sha256:339862140e56654739f221a003e85d34c38ecd35b385c81ec1172b98fc0def66 0.0s done
#29 writing cache manifest sha256:5120653f7a3d293475e31a259659d8eb2d8883ab243f43924a2d526e754e676c 0.0s done
#29 DONE 1.7s
[2024-04-23 15:27:32,621] [INFO] (packager) - Build Summary:

Platform: x64-workstation/dgpu
    Status:     Succeeded
    Docker Tag: simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0
    Tarball:    None

Note

Building a MONAI Application Package (Docker image) can take time. Use -l DEBUG option if you want to see the progress.

We can see that the MAP Docker image is created.

!docker image ls | grep {tag_prefix}
simple_imaging_app-x64-workstation-dgpu-linux-amd64                                       1.0                 6485f181da93   46 seconds ago   12.5GB

We can choose to display and inspect the MAP manifests by running the container with the show command. Furthermore, we can also extract the manifests and other contents in the MAP by using the extract command while mapping specific folder to the host’s (we know that our MAP is compliant and supports these commands).

Note

The host folder for storing the extracted content must first be created by the user, and if it has been created by Docker on running the container, the folder needs to be deleted and re-created.

!echo "Display manifests and extract MAP contents to the host folder, ./export"
!docker run --rm {tag_prefix}-x64-workstation-dgpu-linux-amd64:1.0 show
!rm -rf `pwd`/export && mkdir -p `pwd`/export
!docker run --rm -v `pwd`/export/:/var/run/holoscan/export/ {tag_prefix}-x64-workstation-dgpu-linux-amd64:1.0 extract
!ls `pwd`/export
Display manifests and extract MAP contents to the host folder, ./export

============================== app.json ==============================
{
  "apiVersion": "1.0.0",
  "command": "[\"python3\", \"/opt/holoscan/app\"]",
  "environment": {
    "HOLOSCAN_APPLICATION": "/opt/holoscan/app",
    "HOLOSCAN_INPUT_PATH": "input/",
    "HOLOSCAN_OUTPUT_PATH": "output/",
    "HOLOSCAN_WORKDIR": "/var/holoscan",
    "HOLOSCAN_MODEL_PATH": "/opt/holoscan/models",
    "HOLOSCAN_CONFIG_PATH": "/var/holoscan/app.yaml",
    "HOLOSCAN_APP_MANIFEST_PATH": "/etc/holoscan/app.json",
    "HOLOSCAN_PKG_MANIFEST_PATH": "/etc/holoscan/pkg.json",
    "HOLOSCAN_DOCS_PATH": "/opt/holoscan/docs",
    "HOLOSCAN_LOGS_PATH": "/var/holoscan/logs"
  },
  "input": {
    "path": "input/",
    "formats": null
  },
  "liveness": null,
  "output": {
    "path": "output/",
    "formats": null
  },
  "readiness": null,
  "sdk": "monai-deploy",
  "sdkVersion": "0.5.1",
  "timeout": 0,
  "version": 1,
  "workingDirectory": "/var/holoscan"
}

============================== pkg.json ==============================
{
  "apiVersion": "1.0.0",
  "applicationRoot": "/opt/holoscan/app",
  "modelRoot": "/opt/holoscan/models",
  "models": {},
  "resources": {
    "cpu": 1,
    "gpu": 1,
    "memory": "1Gi",
    "gpuMemory": "1Gi"
  },
  "version": 1,
  "platformConfig": "dgpu"
}

2024-04-23 22:27:35 [INFO] Copying application from /opt/holoscan/app to /var/run/holoscan/export/app

2024-04-23 22:27:35 [INFO] Copying application manifest file from /etc/holoscan/app.json to /var/run/holoscan/export/config/app.json
2024-04-23 22:27:35 [INFO] Copying pkg manifest file from /etc/holoscan/pkg.json to /var/run/holoscan/export/config/pkg.json
2024-04-23 22:27:35 [INFO] Copying application configuration from /var/holoscan/app.yaml to /var/run/holoscan/export/config/app.yaml

2024-04-23 22:27:35 [INFO] Copying models from /opt/holoscan/models to /var/run/holoscan/export/models
2024-04-23 22:27:35 [INFO] '/opt/holoscan/models' cannot be found.

2024-04-23 22:27:35 [INFO] Copying documentation from /opt/holoscan/docs/ to /var/run/holoscan/export/docs
2024-04-23 22:27:35 [INFO] '/opt/holoscan/docs/' cannot be found.

app  config

Executing packaged app locally

The packaged app can be run locally through MONAI Application Runner.

# Clear the output folder and run the MAP container. The input is expected to be a folder
!rm -rf {output_path}
!monai-deploy run -i {test_input_folder} -o {output_path} {tag_prefix}-x64-workstation-dgpu-linux-amd64:1.0
[2024-04-23 15:27:36,245] [INFO] (runner) - Checking dependencies...
[2024-04-23 15:27:36,245] [INFO] (runner) - --> Verifying if "docker" is installed...

[2024-04-23 15:27:36,245] [INFO] (runner) - --> Verifying if "docker-buildx" is installed...

[2024-04-23 15:27:36,245] [INFO] (runner) - --> Verifying if "simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0" is available...

[2024-04-23 15:27:36,326] [INFO] (runner) - Reading HAP/MAP manifest...
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.56kB to /tmp/tmpv5e8cb_i/app.json
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.05kB to /tmp/tmpv5e8cb_i/pkg.json
[2024-04-23 15:27:36,492] [INFO] (runner) - --> Verifying if "nvidia-ctk" is installed...

[2024-04-23 15:27:36,492] [INFO] (runner) - --> Verifying "nvidia-ctk" version...

[2024-04-23 15:27:36,761] [INFO] (common) - Launching container (94febd6eabce) using image 'simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0'...
    container name:      dazzling_kowalevski
    host name:           mingq-dt
    network:             host
    user:                1000:1000
    ulimits:             memlock=-1:-1, stack=67108864:67108864
    cap_add:             CAP_SYS_PTRACE
    ipc mode:            host
    shared memory size:  67108864
    devices:             
    group_add:           44
2024-04-23 22:27:37 [INFO] Launching application python3 /opt/holoscan/app ...

[2024-04-23 22:27:37,772] [INFO] (root) - Parsed args: Namespace(log_level=None, input=None, output=None, model=None, workdir=None, argv=['/opt/holoscan/app'])

[2024-04-23 22:27:37,772] [INFO] (root) - AppContext object: AppContext(input_path=/var/holoscan/input, output_path=/var/holoscan/output, model_path=/opt/holoscan/models, workdir=/var/holoscan)

[2024-04-23 22:27:37,772] [INFO] (root) - sample_data_path: /var/holoscan/input

[info] [app_driver.cpp:1161] Launching the driver/health checking service

[info] [gxf_executor.cpp:247] Creating context

[info] [server.cpp:87] Health checking server listening on 0.0.0.0:8777

[info] [gxf_executor.cpp:1672] Loading extensions from configs...

[info] [gxf_executor.cpp:1842] Activating Graph...

[info] [gxf_executor.cpp:1874] Running Graph...

[info] [gxf_executor.cpp:1876] Waiting for completion...

2024-04-23 22:27:37.797 INFO  gxf/std/greedy_scheduler.cpp@191: Scheduling 3 entities

2024-04-23 22:27:38.257 INFO  gxf/std/greedy_scheduler.cpp@372: Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.

2024-04-23 22:27:38.257 INFO  gxf/std/greedy_scheduler.cpp@401: Scheduler finished.

[info] [gxf_executor.cpp:1879] Deactivating Graph...

[info] [gxf_executor.cpp:1887] Graph execution finished.

[info] [gxf_executor.cpp:275] Destroying context

Number of times operator sobel_op whose class is defined in sobel_operator called: 1

Input from: /var/holoscan/input, whose absolute path: /var/holoscan/input

Number of times operator median_op whose class is defined in median_operator called: 1

Number of times operator gaussian_op whose class is defined in gaussian_operator called: 1

Data type of output: <class 'numpy.ndarray'>, max = 0.35821119421406195

Data type of output post conversion: <class 'numpy.ndarray'>, max = 91

[2024-04-23 15:27:38,526] [INFO] (common) - Container 'dazzling_kowalevski'(94febd6eabce) exited.
#output_image_path was set as before, output_image_path = output_path + "/final_output.png"
output_image = io.imread(output_image_path)
io.imshow(output_image)
<matplotlib.image.AxesImage at 0x7fd82f333b20>
../../_images/414d7ac9802534e3b377644bf1cc2a17c279fcec50bc3fed60b2361db8731d8f.png