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 0x7fbde09a6d60>
../../_images/01_simple_app_3_2.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()
[2023-08-30 00:09:09,515] [INFO] (root) - Parsed args: Namespace(argv=[], input=None, log_level=None, model=None, output=None, workdir=None)
[2023-08-30 00:09:09,523] [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
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:210] Creating context
[info] [gxf_executor.cpp:1595] Loading extensions from configs...
[info] [gxf_executor.cpp:1741] Activating Graph...
[info] [gxf_executor.cpp:1771] Running Graph...
[info] [gxf_executor.cpp:1773] Waiting for completion...
[info] [gxf_executor.cpp:1774] Graph execution waiting. Fragment: 
[info] [greedy_scheduler.cpp:190] Scheduling 3 entities
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
[info] [greedy_scheduler.cpp:369] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.
[info] [greedy_scheduler.cpp:398] Scheduler finished.
[info] [gxf_executor.cpp:1783] Graph execution deactivating. Fragment: 
[info] [gxf_executor.cpp:1784] Deactivating Graph...
[info] [gxf_executor.cpp:1787] Graph execution finished. Fragment: 
[info] [gxf_executor.cpp:229] 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 0x7fbd24bc8760>
../../_images/01_simple_app_19_1.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
[2023-08-30 00:09:17,221] [INFO] (root) - Parsed args: Namespace(argv=['simple_imaging_app', '-i', '/tmp/simple_app', '-o', 'output', '-l', 'DEBUG'], input=PosixPath('/tmp/simple_app'), log_level='DEBUG', model=None, output=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output'), workdir=None)
[2023-08-30 00:09:17,223] [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=)
[2023-08-30 00:09:17,223] [INFO] (root) - sample_data_path: /tmp/simple_app
[info] [gxf_executor.cpp:210] Creating context
[info] [gxf_executor.cpp:1595] Loading extensions from configs...
[info] [gxf_executor.cpp:1741] Activating Graph...
[info] [gxf_executor.cpp:1771] Running Graph...
[info] [gxf_executor.cpp:1773] Waiting for completion...
[info] [gxf_executor.cpp:1774] Graph execution waiting. Fragment: 
[info] [greedy_scheduler.cpp:190] 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
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IHDR' 16 13
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'sRGB' 41 1
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'gAMA' 54 4
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'pHYs' 70 9
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IDAT' 91 65445
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IHDR' 16 13
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'sRGB' 41 1
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'gAMA' 54 4
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'pHYs' 70 9
[2023-08-30 00:09:17,302] [DEBUG] (PIL.PngImagePlugin) - STREAM b'IDAT' 91 65445
[2023-08-30 00:09:17,308] [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
[2023-08-30 00:09:17,513] [DEBUG] (PIL.Image) - Importing BlpImagePlugin
[2023-08-30 00:09:17,514] [DEBUG] (PIL.Image) - Importing BmpImagePlugin
[2023-08-30 00:09:17,514] [DEBUG] (PIL.Image) - Importing BufrStubImagePlugin
[2023-08-30 00:09:17,514] [DEBUG] (PIL.Image) - Importing CurImagePlugin
[2023-08-30 00:09:17,515] [DEBUG] (PIL.Image) - Importing DcxImagePlugin
[2023-08-30 00:09:17,515] [DEBUG] (PIL.Image) - Importing DdsImagePlugin
[2023-08-30 00:09:17,515] [DEBUG] (PIL.Image) - Importing EpsImagePlugin
[2023-08-30 00:09:17,516] [DEBUG] (PIL.Image) - Importing FitsImagePlugin
[2023-08-30 00:09:17,516] [DEBUG] (PIL.Image) - Importing FliImagePlugin
[2023-08-30 00:09:17,516] [DEBUG] (PIL.Image) - Importing FpxImagePlugin
[2023-08-30 00:09:17,516] [DEBUG] (PIL.Image) - Image: failed to import FpxImagePlugin: No module named 'olefile'
[2023-08-30 00:09:17,516] [DEBUG] (PIL.Image) - Importing FtexImagePlugin
[2023-08-30 00:09:17,517] [DEBUG] (PIL.Image) - Importing GbrImagePlugin
[2023-08-30 00:09:17,517] [DEBUG] (PIL.Image) - Importing GifImagePlugin
[2023-08-30 00:09:17,517] [DEBUG] (PIL.Image) - Importing GribStubImagePlugin
[2023-08-30 00:09:17,517] [DEBUG] (PIL.Image) - Importing Hdf5StubImagePlugin
[2023-08-30 00:09:17,517] [DEBUG] (PIL.Image) - Importing IcnsImagePlugin
[2023-08-30 00:09:17,518] [DEBUG] (PIL.Image) - Importing IcoImagePlugin
[2023-08-30 00:09:17,518] [DEBUG] (PIL.Image) - Importing ImImagePlugin
[2023-08-30 00:09:17,519] [DEBUG] (PIL.Image) - Importing ImtImagePlugin
[2023-08-30 00:09:17,519] [DEBUG] (PIL.Image) - Importing IptcImagePlugin
[2023-08-30 00:09:17,519] [DEBUG] (PIL.Image) - Importing JpegImagePlugin
[2023-08-30 00:09:17,519] [DEBUG] (PIL.Image) - Importing Jpeg2KImagePlugin
[2023-08-30 00:09:17,519] [DEBUG] (PIL.Image) - Importing McIdasImagePlugin
[2023-08-30 00:09:17,520] [DEBUG] (PIL.Image) - Importing MicImagePlugin
[2023-08-30 00:09:17,520] [DEBUG] (PIL.Image) - Image: failed to import MicImagePlugin: No module named 'olefile'
[2023-08-30 00:09:17,520] [DEBUG] (PIL.Image) - Importing MpegImagePlugin
[2023-08-30 00:09:17,520] [DEBUG] (PIL.Image) - Importing MpoImagePlugin
[2023-08-30 00:09:17,522] [DEBUG] (PIL.Image) - Importing MspImagePlugin
[2023-08-30 00:09:17,522] [DEBUG] (PIL.Image) - Importing PalmImagePlugin
[2023-08-30 00:09:17,523] [DEBUG] (PIL.Image) - Importing PcdImagePlugin
[2023-08-30 00:09:17,523] [DEBUG] (PIL.Image) - Importing PcxImagePlugin
[2023-08-30 00:09:17,523] [DEBUG] (PIL.Image) - Importing PdfImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing PixarImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing PngImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing PpmImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing PsdImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing QoiImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing SgiImagePlugin
[2023-08-30 00:09:17,528] [DEBUG] (PIL.Image) - Importing SpiderImagePlugin
[2023-08-30 00:09:17,529] [DEBUG] (PIL.Image) - Importing SunImagePlugin
[2023-08-30 00:09:17,529] [DEBUG] (PIL.Image) - Importing TgaImagePlugin
[2023-08-30 00:09:17,529] [DEBUG] (PIL.Image) - Importing TiffImagePlugin
[2023-08-30 00:09:17,529] [DEBUG] (PIL.Image) - Importing WebPImagePlugin
[2023-08-30 00:09:17,530] [DEBUG] (PIL.Image) - Importing WmfImagePlugin
[2023-08-30 00:09:17,530] [DEBUG] (PIL.Image) - Importing XbmImagePlugin
[2023-08-30 00:09:17,531] [DEBUG] (PIL.Image) - Importing XpmImagePlugin
[2023-08-30 00:09:17,531] [DEBUG] (PIL.Image) - Importing XVThumbImagePlugin
[info] [greedy_scheduler.cpp:369] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.
[info] [greedy_scheduler.cpp:398] Scheduler finished.
[info] [gxf_executor.cpp:1783] Graph execution deactivating. Fragment: 
[info] [gxf_executor.cpp:1784] Deactivating Graph...
[info] [gxf_executor.cpp:1787] Graph execution finished. Fragment: 
[info] [gxf_executor.cpp:229] 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 0x7fbd243b00a0>
../../_images/01_simple_app_35_1.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
[2023-08-30 00:09:20,073] [INFO] (packager.parameters) - Application: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/simple_imaging_app
[2023-08-30 00:09:20,073] [INFO] (packager.parameters) - Detected application type: Python Module
[2023-08-30 00:09:20,074] [INFO] (packager) - Reading application configuration from /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/simple_imaging_app/app.yaml...
[2023-08-30 00:09:20,075] [INFO] (packager) - Generating app.json...
[2023-08-30 00:09:20,075] [INFO] (packager) - Generating pkg.json...
[2023-08-30 00:09:20,076] [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.6.0",
    "timeout": 0,
    "version": 1.0,
    "workingDirectory": "/var/holoscan"
}
================ End app.json ================
                 
[2023-08-30 00:09:20,076] [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
}
================ End pkg.json ================
                 
[2023-08-30 00:09:20,088] [DEBUG] (packager.builder) - 
========== Begin Dockerfile ==========


FROM nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.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:v0.6.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="0.6.0"

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 -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

# Install Holoscan from PyPI org
RUN pip install holoscan==0.6.0


# Copy user-specified MONAI Deploy SDK file
COPY ./monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl /tmp/monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl
RUN pip install /tmp/monai_deploy_app_sdk-0.5.1+22.g029f8bc.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 ===========

[2023-08-30 00:09:20,088] [INFO] (packager.builder) - 
===============================================================================
Building image for:                 x64-workstation
    Architecture:                   linux/amd64
    Base Image:                     nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.0-dgpu
    Build Image:                    N/A  
    Cache:                          Enabled
    Configuration:                  dgpu
    Holoiscan SDK Package:          pypi.org
    MONAI Deploy App SDK Package:   /home/mqin/src/monai-deploy-app-sdk/dist/monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl
    gRPC Health Probe:              N/A
    SDK Version:                    0.6.0
    SDK:                            monai-deploy
    Tag:                            simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0
    
[2023-08-30 00:09:20,333] [INFO] (common) - Using existing Docker BuildKit builder `holoscan_app_builder`
[2023-08-30 00:09:20,333] [DEBUG] (packager.builder) - Building Holoscan Application Package: tag=simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0
#1 [internal] load .dockerignore
#1 transferring context: 33B
#1 transferring context: 1.79kB done
#1 DONE 0.1s

#2 [internal] load build definition from Dockerfile
#2 transferring dockerfile: 2.64kB done
#2 DONE 0.1s

#3 [internal] load metadata for nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.0-dgpu
#3 DONE 0.4s

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

#5 importing cache manifest from local:10108727038215150215
#5 DONE 0.0s

#6 [ 1/21] FROM nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.0-dgpu@sha256:9653f80f241fd542f25afbcbcf7a0d02ed7e5941c79763e69def5b1e6d9fb7bc
#6 resolve nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.0-dgpu@sha256:9653f80f241fd542f25afbcbcf7a0d02ed7e5941c79763e69def5b1e6d9fb7bc 0.0s done
#6 DONE 0.0s

#7 importing cache manifest from nvcr.io/nvidia/clara-holoscan/holoscan:v0.6.0-dgpu
#7 DONE 0.9s

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

#8 [ 9/21] WORKDIR /var/holoscan
#8 CACHED

#9 [13/21] RUN pip install --upgrade pip
#9 CACHED

#10 [ 4/21] RUN groupadd -g 1000 holoscan
#10 CACHED

#11 [12/21] COPY ./pip/requirements.txt /tmp/requirements.txt
#11 CACHED

#12 [ 6/21] RUN chown -R holoscan /var/holoscan
#12 CACHED

#13 [15/21] RUN pip install holoscan==0.6.0
#13 CACHED

#14 [16/21] COPY ./monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl /tmp/monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl
#14 CACHED

#15 [ 8/21] RUN chown -R holoscan /var/holoscan/output
#15 CACHED

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

#17 [11/21] RUN chmod +x /var/holoscan/tools
#17 CACHED

#18 [ 7/21] RUN chown -R holoscan /var/holoscan/input
#18 CACHED

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

#20 [ 2/21] 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
#20 CACHED

#21 [20/21] COPY ./map/pkg.json /etc/holoscan/pkg.json
#21 CACHED

#22 [17/21] RUN pip install /tmp/monai_deploy_app_sdk-0.5.1+22.g029f8bc.dirty-py3-none-any.whl
#22 CACHED

#23 [19/21] COPY ./app.config /var/holoscan/app.yaml
#23 CACHED

#24 [10/21] COPY ./tools /var/holoscan/tools
#24 CACHED

#25 [18/21] COPY ./map/app.json /etc/holoscan/app.json
#25 CACHED

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

#27 [21/21] COPY ./app /opt/holoscan/app
#27 CACHED

#28 exporting to docker image format
#28 exporting layers done
#28 exporting manifest sha256:7a414ac96708c13dd2e15e2134807075b96eb40f1e16a0c9caf433f415792140 done
#28 exporting config sha256:1e2e576a3d23da01c03efa3c866cf22ecbcb75fa628e7edb6f2e0ffa7f6900b3 done
#28 sending tarball
#28 ...

#29 importing to docker
#29 DONE 0.5s

#28 exporting to docker image format
#28 sending tarball 40.7s done
#28 DONE 40.7s

#30 exporting content cache
#30 preparing build cache for export
#30 writing layer sha256:0709800848b4584780b40e7e81200689870e890c38b54e96b65cd0a3b1942f2d done
#30 writing layer sha256:0ce020987cfa5cd1654085af3bb40779634eb3d792c4a4d6059036463ae0040d done
#30 writing layer sha256:0f65089b284381bf795d15b1a186e2a8739ea957106fa526edef0d738e7cda70 done
#30 writing layer sha256:12a47450a9f9cc5d4edab65d0f600dbbe8b23a1663b0b3bb2c481d40e074b580 done
#30 writing layer sha256:1de965777e2e37c7fabe00bdbf3d0203ca83ed30a71a5479c3113fe4fc48c4bb done
#30 writing layer sha256:24b5aa2448e920814dd67d7d3c0169b2cdacb13c4048d74ded3b4317843b13ff done
#30 writing layer sha256:268c96d4e21881b9b02957ef94da0eeb249c2b670d35bd1099347ae0f15b7a9b done
#30 writing layer sha256:2d42104dbf0a7cc962b791f6ab4f45a803f8a36d296f996aca180cfb2f3e30d0 done
#30 writing layer sha256:2fa1ce4fa3fec6f9723380dc0536b7c361d874add0baaddc4bbf2accac82d2ff done
#30 writing layer sha256:38794be1b5dc99645feabf89b22cd34fb5bdffb5164ad920e7df94f353efe9c0 done
#30 writing layer sha256:38f963dc57c1e7b68a738fe39ed9f9345df7188111a047e2163a46648d7f1d88 done
#30 writing layer sha256:3e7e4c9bc2b136814c20c04feb4eea2b2ecf972e20182d88759931130cfb4181 done
#30 writing layer sha256:3fd77037ad585442cd82d64e337f49a38ddba50432b2a1e563a48401d25c79e6 done
#30 writing layer sha256:41814ed91034b30ac9c44dfc604a4bade6138005ccf682372c02e0bead66dbc0 done
#30 writing layer sha256:45893188359aca643d5918c9932da995364dc62013dfa40c075298b1baabece3 done
#30 writing layer sha256:49bc651b19d9e46715c15c41b7c0daa007e8e25f7d9518f04f0f06592799875a done
#30 writing layer sha256:4c12db5118d8a7d909e4926d69a2192d2b3cd8b110d49c7504a4f701258c1ccc done
#30 writing layer sha256:4cc43a803109d6e9d1fd35495cef9b1257035f5341a2db54f7a1940815b6cc65 done
#30 writing layer sha256:4d32b49e2995210e8937f0898327f196d3fcc52486f0be920e8b2d65f150a7ab done
#30 writing layer sha256:4d6fe980bad9cd7b2c85a478c8033cae3d098a81f7934322fb64658b0c8f9854 done
#30 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1
#30 preparing build cache for export 0.5s done
#30 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 done
#30 writing layer sha256:5150182f1ff123399b300ca469e00f6c4d82e1b9b72652fb8ee7eab370245236 done
#30 writing layer sha256:51d9df4b19947155e6a9772cfc44d4bde1be74b8e3b3019c525ccfb894d43ef4 done
#30 writing layer sha256:593b5820e1f0928ae3939148a0a3b9c58ed117b33fc61ffd184393d2e6c42cb4 done
#30 writing layer sha256:595c38fa102c61c3dda19bdab70dcd26a0e50465b986d022a84fa69023a05d0f done
#30 writing layer sha256:59d451175f6950740e26d38c322da0ef67cb59da63181eb32996f752ba8a2f17 done
#30 writing layer sha256:5ad1f2004580e415b998124ea394e9d4072a35d70968118c779f307204d6bd17 done
#30 writing layer sha256:5eb65183a3d11174b5abe72026da705ac8258f9056e8e11ac3ca28ed6db3fbc5 done
#30 writing layer sha256:62598eafddf023e7f22643485f4321cbd51ff7eee743b970db12454fd3c8c675 done
#30 writing layer sha256:63d7e616a46987136f4cc9eba95db6f6327b4854cfe3c7e20fed6db0c966e380 done
#30 writing layer sha256:6939d591a6b09b14a437e5cd2d6082a52b6d76bec4f72d960440f097721da34f done
#30 writing layer sha256:698318e5a60e5e0d48c45bf992f205a9532da567fdfe94bd59be2e192975dd6f done
#30 writing layer sha256:6ddc1d0f91833b36aac1c6f0c8cea005c87d94bab132d46cc06d9b060a81cca3 done
#30 writing layer sha256:74ac1f5a47c0926bff1e997bb99985a09926f43bd0895cb27ceb5fa9e95f8720 done
#30 writing layer sha256:7577973918dd30e764733a352a93f418000bc3181163ca451b2307492c1a6ba9 done
#30 writing layer sha256:839f7088ab6d6cdaab613aab7f1ec078aa2a909559f4592f9dcb34ad4c8a1e42 done
#30 writing layer sha256:886c886d8a09d8befb92df75dd461d4f97b77d7cff4144c4223b0d2f6f2c17f2 done
#30 writing layer sha256:8a7451db9b4b817b3b33904abddb7041810a4ffe8ed4a034307d45d9ae9b3f2a done
#30 writing layer sha256:916f4054c6e7f10de4fd7c08ffc75fa23ebecca4eceb8183cb1023b33b1696c9 done
#30 writing layer sha256:9463aa3f56275af97693df69478a2dc1d171f4e763ca6f7b6f370a35e605c154 done
#30 writing layer sha256:94c4f534ca3de906e1661bb9bccbfa9f9748d875d1d92778697a61e3e762ffde done
#30 writing layer sha256:955fd173ed884230c2eded4542d10a97384b408537be6bbb7c4ae09ccd6fb2d0 done
#30 writing layer sha256:9c42a4ee99755f441251e6043b2cbba16e49818a88775e7501ec17e379ce3cfd done
#30 writing layer sha256:9c63be0a86e3dc4168db3814bf464e40996afda0031649d9faa8ff7568c3154f done
#30 writing layer sha256:9d989b2672cdb90093e998e0b94c689332d25e7494c1dfff74bb086cc7dab05a done
#30 writing layer sha256:9e04bda98b05554953459b5edef7b2b14d32f1a00b979a23d04b6eb5c191e66b done
#30 writing layer sha256:a4a0c690bc7da07e592514dccaa26098a387e8457f69095e922b6d73f7852502 done
#30 writing layer sha256:a4aafbc094d78a85bef41036173eb816a53bcd3e2564594a32f542facdf2aba6 done
#30 writing layer sha256:ae36a4d38b76948e39a5957025c984a674d2de18ce162a8caaa536e6f06fccea done
#30 writing layer sha256:b2fa40114a4a0725c81b327df89c0c3ed5c05ca9aa7f1157394d5096cf5460ce done
#30 writing layer sha256:b48a5fafcaba74eb5d7e7665601509e2889285b50a04b5b639a23f8adc818157 done
#30 writing layer sha256:c86976a083599e36a6441f36f553627194d05ea82bb82a78682e718fe62fccf6 done
#30 writing layer sha256:cb506fbdedc817e3d074f609e2edbf9655aacd7784610a1bbac52f2d7be25438 done
#30 writing layer sha256:d2a6fe65a1f84edb65b63460a75d1cac1aa48b72789006881b0bcfd54cd01ffd done
#30 writing layer sha256:d8d16d6af76dc7c6b539422a25fdad5efb8ada5a8188069fcd9d113e3b783304 done
#30 writing layer sha256:da250bc94d2969d5caed175341870b944c3b4a74c9af0bae1c1660fac13b7990 done
#30 writing layer sha256:ddc2ade4f6fe866696cb638c8a102cb644fa842c2ca578392802b3e0e5e3bcb7 done
#30 writing layer sha256:e2cfd7f6244d6f35befa6bda1caa65f1786cecf3f00ef99d7c9a90715ce6a03c done
#30 writing layer sha256:e94a4481e9334ff402bf90628594f64a426672debbdfb55f1290802e52013907 done
#30 writing layer sha256:eaf45e9f32d1f5a9983945a1a9f8dedbb475bc0f578337610e00b4dedec87c20 done
#30 writing layer sha256:eb411bef39c013c9853651e68f00965dbd826d829c4e478884a2886976e9c989 done
#30 writing layer sha256:edfe4a95eb6bd3142aeda941ab871ffcc8c19cf50c33561c210ba8ead2424759 done
#30 writing layer sha256:ef4466d6f927d29d404df9c5af3ef5733c86fa14e008762c90110b963978b1e7 done
#30 writing layer sha256:f346e3ecdf0bee048fa1e3baf1d3128ff0283b903f03e97524944949bd8882e5 done
#30 writing layer sha256:f3f9a00a1ce9aadda250aacb3e66a932676badc5d8519c41517fdf7ea14c13ed done
#30 writing layer sha256:f60a35c82433f4cfc4167240d52d87341204f29196716db10d5d1efceca61ea4 done
#30 writing layer sha256:fd849d9bd8889edd43ae38e9f21a912430c8526b2c18f3057a3b2cd74eb27b31 done
#30 writing layer sha256:ff1c6faca98dc5db7491d4ab471109c5932a09be6c6f87c1b375993719052c7c done
#30 writing config sha256:f998b87c5b1c42b3bd8b024c5ab2c7fb2e501296bef12002c76edd9f0f2904ed done
#30 writing manifest sha256:b7c4a839b7badff031c8a720d93abcb07ac1c0afe26d6a64a94cae167afa97d8 done
#30 DONE 0.5s
[2023-08-30 00:10:04,252] [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                        1e2e576a3d23   45 minutes ago      10.8GB

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.6.0",
  "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
}

2023-08-30 07:10:10 [INFO] Copying application from /opt/holoscan/app to /var/run/holoscan/export/app

2023-08-30 07:10:10 [INFO] Copying application manifest file from /etc/holoscan/app.json to /var/run/holoscan/export/config/app.json
2023-08-30 07:10:10 [INFO] Copying pkg manifest file from /etc/holoscan/pkg.json to /var/run/holoscan/export/config/pkg.json
2023-08-30 07:10:10 [INFO] Copying application configuration from /var/holoscan/app.yaml to /var/run/holoscan/export/config/app.yaml

2023-08-30 07:10:10 [INFO] Copying models from /opt/holoscan/models to /var/run/holoscan/export/models
2023-08-30 07:10:10 [INFO] '/opt/holoscan/models' cannot be found.

2023-08-30 07:10:10 [INFO] Copying documentation from /opt/holoscan/docs/ to /var/run/holoscan/export/docs
2023-08-30 07:10:10 [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
[2023-08-30 00:10:13,473] [INFO] (runner) - Checking dependencies...
[2023-08-30 00:10:13,473] [INFO] (runner) - --> Verifying if "docker" is installed...

[2023-08-30 00:10:13,473] [INFO] (runner) - --> Verifying if "docker-buildx" is installed...

[2023-08-30 00:10:13,473] [INFO] (runner) - --> Verifying if "simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0" is available...

[2023-08-30 00:10:13,539] [INFO] (runner) - Reading HAP/MAP manifest...
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.56kB to /tmp/tmpzdvb7nut/app.json
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.05kB to /tmp/tmpzdvb7nut/pkg.json
[2023-08-30 00:10:13,748] [INFO] (runner) - --> Verifying if "nvidia-ctk" is installed...

[2023-08-30 00:10:13,942] [INFO] (common) - Launching container (0fbb33cb469e) using image 'simple_imaging_app-x64-workstation-dgpu-linux-amd64:1.0'...
    container name:      determined_leavitt
    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:             
2023-08-30 07:10:14 [INFO] Launching application python3 /opt/holoscan/app ...

[2023-08-30 07:10:15,097] [INFO] (root) - Parsed args: Namespace(argv=['/opt/holoscan/app'], input=None, log_level=None, model=None, output=None, workdir=None)

[2023-08-30 07:10:15,097] [INFO] (root) - AppContext object: AppContext(input_path=/var/holoscan/input, output_path=/var/holoscan/output, model_path=/opt/holoscan/models, workdir=/var/holoscan)

[2023-08-30 07:10:15,098] [INFO] (root) - sample_data_path: /var/holoscan/input

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

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

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

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

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

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

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

[info] [gxf_executor.cpp:1774] Graph execution waiting. Fragment: 

[info] [greedy_scheduler.cpp:190] Scheduling 3 entities

[info] [greedy_scheduler.cpp:369] Scheduler stopped: Some entities are waiting for execution, but there are no periodic or async entities to get out of the deadlock.

[info] [greedy_scheduler.cpp:398] Scheduler finished.

[info] [gxf_executor.cpp:1783] Graph execution deactivating. Fragment: 

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

[info] [gxf_executor.cpp:1787] Graph execution finished. Fragment: 

[info] [gxf_executor.cpp:229] 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

[2023-08-30 00:10:16,538] [INFO] (common) - Container 'determined_leavitt'(0fbb33cb469e) 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 0x7fbddd078220>
../../_images/01_simple_app_48_1.png