Deploying a MedNIST Classifier App with MONAI Deploy App SDK (Prebuilt Model)

This tutorial demos the process of packaging up a trained model using MONAI Deploy App SDK into an deployable inference application which can be run as a local program, as well as an MONAI Application Package (MAP) for containerized workflow execution.

Clone the github project (the latest version of the main branch only)

!rm -rf source \
 && git clone --branch main --depth 1 https://github.com/Project-MONAI/monai-deploy-app-sdk.git source \
 && rm -rf source/.git
Cloning into 'source'...
remote: Enumerating objects: 276, done.
remote: Counting objects: 100% (276/276), done.
remote: Compressing objects: 100% (223/223), done.
remote: Total 276 (delta 56), reused 143 (delta 31), pack-reused 0
Receiving objects: 100% (276/276), 1.41 MiB | 1.83 MiB/s, done.
Resolving deltas: 100% (56/56), done.
!ls source/examples/apps/mednist_classifier_monaideploy/
app.yaml  mednist_classifier_monaideploy.py  requirements.txt

Install monai-deploy-app-sdk package

!pip install monai-deploy-app-sdk
Requirement already satisfied: monai-deploy-app-sdk in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (0.6.0)
Requirement already satisfied: numpy>=1.21.6 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai-deploy-app-sdk) (1.24.4)
Requirement already satisfied: holoscan~=0.6.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai-deploy-app-sdk) (0.6.0)
Requirement already satisfied: colorama>=0.4.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai-deploy-app-sdk) (0.4.6)
Requirement already satisfied: typeguard>=3.0.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai-deploy-app-sdk) (4.1.5)
Requirement already satisfied: cloudpickle~=2.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (2.2.1)
Requirement already satisfied: python-on-whales~=0.60 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (0.67.0)
Requirement already satisfied: Jinja2~=3.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (3.1.2)
Requirement already satisfied: packaging~=23.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (23.2)
Requirement already satisfied: pyyaml~=6.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (6.0.1)
Requirement already satisfied: requests~=2.28 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (2.31.0)
Requirement already satisfied: pip>=20.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (23.3.1)
Requirement already satisfied: wheel-axle-runtime<1.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from holoscan~=0.6.0->monai-deploy-app-sdk) (0.0.5)
Requirement already satisfied: importlib-metadata>=3.6 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from typeguard>=3.0.0->monai-deploy-app-sdk) (6.8.0)
Requirement already satisfied: typing-extensions>=4.7.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from typeguard>=3.0.0->monai-deploy-app-sdk) (4.8.0)
Requirement already satisfied: zipp>=0.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from importlib-metadata>=3.6->typeguard>=3.0.0->monai-deploy-app-sdk) (3.17.0)
Requirement already satisfied: MarkupSafe>=2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from Jinja2~=3.1->holoscan~=0.6.0->monai-deploy-app-sdk) (2.1.3)
Requirement already satisfied: pydantic!=2.0.*,<3,>=1.9 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (2.5.1)
Requirement already satisfied: tqdm in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (4.66.1)
Requirement already satisfied: typer>=0.4.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (0.9.0)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests~=2.28->holoscan~=0.6.0->monai-deploy-app-sdk) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests~=2.28->holoscan~=0.6.0->monai-deploy-app-sdk) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests~=2.28->holoscan~=0.6.0->monai-deploy-app-sdk) (2.1.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests~=2.28->holoscan~=0.6.0->monai-deploy-app-sdk) (2023.7.22)
Requirement already satisfied: filelock in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from wheel-axle-runtime<1.0->holoscan~=0.6.0->monai-deploy-app-sdk) (3.13.1)
Requirement already satisfied: annotated-types>=0.4.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from pydantic!=2.0.*,<3,>=1.9->python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (0.6.0)
Requirement already satisfied: pydantic-core==2.14.3 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from pydantic!=2.0.*,<3,>=1.9->python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (2.14.3)
Requirement already satisfied: click<9.0.0,>=7.1.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from typer>=0.4.1->python-on-whales~=0.60->holoscan~=0.6.0->monai-deploy-app-sdk) (8.1.7)

Install necessary packages for the app

!pip install monai Pillow # for MONAI transforms and Pillow
!python -c "import pydicom" || pip install -q "pydicom>=1.4.2"
!python -c "import highdicom" || pip install -q "highdicom>=0.18.2" # for the use of DICOM Writer operators
Requirement already satisfied: monai in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (1.3.0)
Requirement already satisfied: Pillow in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (10.0.1)
Requirement already satisfied: numpy>=1.20 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai) (1.24.4)
Requirement already satisfied: torch>=1.9 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from monai) (2.1.1)
Requirement already satisfied: filelock in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (3.13.1)
Requirement already satisfied: typing-extensions in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (4.8.0)
Requirement already satisfied: sympy in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (1.12)
Requirement already satisfied: networkx in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (3.1)
Requirement already satisfied: jinja2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (3.1.2)
Requirement already satisfied: fsspec in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (2023.10.0)
Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.1.105 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.105)
Requirement already satisfied: nvidia-cuda-runtime-cu12==12.1.105 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.105)
Requirement already satisfied: nvidia-cuda-cupti-cu12==12.1.105 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.105)
Requirement already satisfied: nvidia-cudnn-cu12==8.9.2.26 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (8.9.2.26)
Requirement already satisfied: nvidia-cublas-cu12==12.1.3.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.3.1)
Requirement already satisfied: nvidia-cufft-cu12==11.0.2.54 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (11.0.2.54)
Requirement already satisfied: nvidia-curand-cu12==10.3.2.106 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (10.3.2.106)
Requirement already satisfied: nvidia-cusolver-cu12==11.4.5.107 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (11.4.5.107)
Requirement already satisfied: nvidia-cusparse-cu12==12.1.0.106 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.0.106)
Requirement already satisfied: nvidia-nccl-cu12==2.18.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (2.18.1)
Requirement already satisfied: nvidia-nvtx-cu12==12.1.105 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (12.1.105)
Requirement already satisfied: triton==2.1.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from torch>=1.9->monai) (2.1.0)
Requirement already satisfied: nvidia-nvjitlink-cu12 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from nvidia-cusolver-cu12==11.4.5.107->torch>=1.9->monai) (12.3.101)
Requirement already satisfied: MarkupSafe>=2.0 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from jinja2->torch>=1.9->monai) (2.1.3)
Requirement already satisfied: mpmath>=0.19 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from sympy->torch>=1.9->monai) (1.3.0)

Download/Extract mednist_classifier_data.zip from Google Drive

# Download mednist_classifier_data.zip
!pip install gdown 
!gdown "https://drive.google.com/uc?id=1yJ4P-xMNEfN6lIOq_u6x1eMAq1_MJu-E"
Requirement already satisfied: gdown in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (4.7.1)
Requirement already satisfied: filelock in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from gdown) (3.13.1)
Requirement already satisfied: requests[socks] in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from gdown) (2.31.0)
Requirement already satisfied: six in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from gdown) (1.16.0)
Requirement already satisfied: tqdm in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from gdown) (4.66.1)
Requirement already satisfied: beautifulsoup4 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from gdown) (4.12.2)
Requirement already satisfied: soupsieve>1.2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from beautifulsoup4->gdown) (2.5)
Requirement already satisfied: charset-normalizer<4,>=2 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests[socks]->gdown) (3.3.2)
Requirement already satisfied: idna<4,>=2.5 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests[socks]->gdown) (3.4)
Requirement already satisfied: urllib3<3,>=1.21.1 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests[socks]->gdown) (2.1.0)
Requirement already satisfied: certifi>=2017.4.17 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests[socks]->gdown) (2023.7.22)
Requirement already satisfied: PySocks!=1.5.7,>=1.5.6 in /home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages (from requests[socks]->gdown) (1.7.1)
Downloading...
From (uriginal): https://drive.google.com/uc?id=1yJ4P-xMNEfN6lIOq_u6x1eMAq1_MJu-E
From (redirected): https://drive.google.com/uc?id=1yJ4P-xMNEfN6lIOq_u6x1eMAq1_MJu-E&confirm=t&uuid=d9974e09-6ccd-4416-9f41-2c3702a3bea7
To: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/mednist_classifier_data.zip
100%|██████████████████████████████████████| 28.6M/28.6M [00:00<00:00, 64.3MB/s]
# Unzip the downloaded mednist_classifier_data.zip from the web browser or using gdown, and set up folders
input_folder = "input"
output_folder = "output"
models_folder = "models"
!rm -rf {input_folder}
!unzip -o "mednist_classifier_data.zip"

# Need to copy the model file to its own clean subfolder for pacakging, to workaround an issue in the Packager
models_folder = "models"
!rm -rf {models_folder} && mkdir -p {models_folder}/model && cp classifier.zip {models_folder}/model && ls {models_folder}/model
Archive:  mednist_classifier_data.zip
 extracting: classifier.zip          
 extracting: input/AbdomenCT_007000.jpeg  
classifier.zip

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.

Set the environment variables corresponding to the extracted data path.

%env HOLOSCAN_INPUT_PATH {input_folder}
%env HOLOSCAN_OUTPUT_PATH {output_folder}
%env HOLOSCAN_MODEL_PATH {models_folder}
env: HOLOSCAN_INPUT_PATH=input
env: HOLOSCAN_OUTPUT_PATH=output
env: HOLOSCAN_MODEL_PATH=models

Package app (creating MAP container image)

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

Use -l DEBUG option to see progress.

Note

This assumes that NVIDIA Container Toolkit or nvidia docker is installed on the local machine.

tag_prefix = "mednist_app"

!monai-deploy package "source/examples/apps/mednist_classifier_monaideploy/mednist_classifier_monaideploy.py" -m {models_folder} -c "source/examples/apps/mednist_classifier_monaideploy/app.yaml" -t {tag_prefix}:1.0 --platform x64-workstation -l DEBUG
[2023-11-15 18:37:06,027] [INFO] (packager.parameters) - Application: /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/source/examples/apps/mednist_classifier_monaideploy/mednist_classifier_monaideploy.py
[2023-11-15 18:37:06,027] [INFO] (packager.parameters) - Detected application type: Python File
[2023-11-15 18:37:06,027] [INFO] (packager) - Scanning for models in {models_path}...
[2023-11-15 18:37:06,027] [DEBUG] (packager) - Model model=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models/model added.
[2023-11-15 18:37:06,027] [INFO] (packager) - Reading application configuration from /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/source/examples/apps/mednist_classifier_monaideploy/app.yaml...
[2023-11-15 18:37:06,030] [INFO] (packager) - Generating app.json...
[2023-11-15 18:37:06,030] [INFO] (packager) - Generating pkg.json...
[2023-11-15 18:37:06,033] [DEBUG] (common) - 
=============== Begin app.json ===============
{
    "apiVersion": "1.0.0",
    "command": "[\"python3\", \"/opt/holoscan/app/mednist_classifier_monaideploy.py\"]",
    "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-11-15 18:37:06,033] [DEBUG] (common) - 
=============== Begin pkg.json ===============
{
    "apiVersion": "1.0.0",
    "applicationRoot": "/opt/holoscan/app",
    "modelRoot": "/opt/holoscan/models",
    "models": {
        "model": "/opt/holoscan/models"
    },
    "resources": {
        "cpu": 1,
        "gpu": 1,
        "memory": "1Gi",
        "gpuMemory": "1Gi"
    },
    "version": 1.0
}
================ End pkg.json ================
                 
[2023-11-15 18:37:06,061] [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="mednist_app:1.0"
LABEL org.opencontainers.image.title="MONAI Deploy App Package - MedNIST Classifier 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


# Install MONAI Deploy from PyPI org
RUN pip install monai-deploy-app-sdk==0.6.0




COPY ./models  /opt/holoscan/models

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-11-15 18:37:06,061] [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:   pypi.org
    gRPC Health Probe:              N/A
    SDK Version:                    0.6.0
    SDK:                            monai-deploy
    Tag:                            mednist_app-x64-workstation-dgpu-linux-amd64:1.0
    
[2023-11-15 18:37:06,311] [INFO] (common) - Using existing Docker BuildKit builder `holoscan_app_builder`
[2023-11-15 18:37:06,312] [DEBUG] (packager.builder) - Building Holoscan Application Package: tag=mednist_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.49kB done
#1 DONE 0.1s

#2 [internal] load .dockerignore
#2 transferring context: 1.79kB 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:12435489437730595250
#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.1s done
#6 DONE 0.1s

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

#4 [internal] load build context
#4 transferring context: 28.60MB 0.3s done
#4 DONE 0.3s

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

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

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

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

#12 [ 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
#12 CACHED

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

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

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

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

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

#18 [17/21] COPY ./models  /opt/holoscan/models
#18 CACHED

#19 [16/21] RUN pip install monai-deploy-app-sdk==0.6.0
#19 CACHED

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

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

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

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

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

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

#26 [19/21] COPY ./app.config /var/holoscan/app.yaml
#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:19203b8c5bb9a13da3c03963a62723b9f330ff4b2d37776a26d1316d03da1b0b done
#28 exporting config sha256:f980243cd5d80a64490eda352560b92681a08191a30d0a585cdbbf0c409e8abc done
#28 sending tarball
#28 ...

#29 importing to docker
#29 DONE 0.9s

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

#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:19c20b65326c1511f8ab02f4a41453f8c0b6d9f2bdea8bb25038b628cef67ab9 done
#30 writing layer sha256:1de965777e2e37c7fabe00bdbf3d0203ca83ed30a71a5479c3113fe4fc48c4bb done
#30 writing layer sha256:22b384cd1e678fc56dc95c82f42e7a540d055418d0f1eef8d908c88305e23a88 done
#30 writing layer sha256:2369548ddf79fa9fd07c5f1c4b226da97c9cc7991c4b3bd57a97796c31ff0648 done
#30 writing layer sha256:24b5aa2448e920814dd67d7d3c0169b2cdacb13c4048d74ded3b4317843b13ff done
#30 writing layer sha256:2d42104dbf0a7cc962b791f6ab4f45a803f8a36d296f996aca180cfb2f3e30d0 done
#30 writing layer sha256:2fa1ce4fa3fec6f9723380dc0536b7c361d874add0baaddc4bbf2accac82d2ff done
#30 writing layer sha256:361eb07c24550c859aa0f62bebfcaabde6373eeee0c16ae66e7c7053bf1f1e42 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
#30 preparing build cache for export 0.6s 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:4e78baa7922aa440fcba2b268af798b094fdb64e4450a916c15027abc06a1123 done
#30 writing layer sha256:4f4fb700ef54461cfa02571ae0db9a0dc1e0cdb5577484a6d75e68dc38e8acc1 done
#30 writing layer sha256:5150182f1ff123399b300ca469e00f6c4d82e1b9b72652fb8ee7eab370245236 done
#30 writing layer sha256:595c38fa102c61c3dda19bdab70dcd26a0e50465b986d022a84fa69023a05d0f done
#30 writing layer sha256:59d451175f6950740e26d38c322da0ef67cb59da63181eb32996f752ba8a2f17 done
#30 writing layer sha256:5ad1f2004580e415b998124ea394e9d4072a35d70968118c779f307204d6bd17 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:81ab55ca8bce88347661e1c1e6d58975b998017ad2e91a1040bd3f5017741d2b done
#30 writing layer sha256:886c886d8a09d8befb92df75dd461d4f97b77d7cff4144c4223b0d2f6f2c17f2 done
#30 writing layer sha256:8a7451db9b4b817b3b33904abddb7041810a4ffe8ed4a034307d45d9ae9b3f2a done
#30 writing layer sha256:8ac1aede1873b9cad7d72492d19240df0510086e92ac4243bc5119ec85678931 done
#30 writing layer sha256:916f4054c6e7f10de4fd7c08ffc75fa23ebecca4eceb8183cb1023b33b1696c9 done
#30 writing layer sha256:9463aa3f56275af97693df69478a2dc1d171f4e763ca6f7b6f370a35e605c154 done
#30 writing layer sha256:955fd173ed884230c2eded4542d10a97384b408537be6bbb7c4ae09ccd6fb2d0 done
#30 writing layer sha256:9c42a4ee99755f441251e6043b2cbba16e49818a88775e7501ec17e379ce3cfd done
#30 writing layer sha256:9c63be0a86e3dc4168db3814bf464e40996afda0031649d9faa8ff7568c3154f 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:b092e71fced852d79e047f907a83ab32a129dd40cd0e9879dd507367a505dbd6 done
#30 writing layer sha256:b2fa40114a4a0725c81b327df89c0c3ed5c05ca9aa7f1157394d5096cf5460ce done
#30 writing layer sha256:b41f1b7d06dc0a3cea605edc5a074d982aa2f1b75ac808229b9dcb8d6744e17a done
#30 writing layer sha256:b48a5fafcaba74eb5d7e7665601509e2889285b50a04b5b639a23f8adc818157 done
#30 writing layer sha256:c84aee89fb491faea0b4112d4bb2cddb4a0b8fd0fb32e886aed7902a1626f4d2 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: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:fd849d9bd8889edd43ae38e9f21a912430c8526b2c18f3057a3b2cd74eb27b31 done
#30 writing config sha256:2efdd8bd5fee272f11add678820bb36fd73b0d8b589200720e3a2b1f2014c239 done
#30 writing manifest sha256:3e52de4375cea72779be5447ac509152ce31588970ac87f44cfce9a7de19df7e done
#30 DONE 0.6s
[2023-11-15 18:37:49,607] [INFO] (packager) - Build Summary:

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

We can see that the MAP Docker image is created

!docker image ls | grep {tag_prefix}
mednist_app-x64-workstation-dgpu-linux-amd64                                              1.0                        f980243cd5d8   About an hour ago   15.4GB

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/mednist_classifier_monaideploy.py\"]",
  "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": {
    "model": "/opt/holoscan/models"
  },
  "resources": {
    "cpu": 1,
    "gpu": 1,
    "memory": "1Gi",
    "gpuMemory": "1Gi"
  },
  "version": 1
}

2023-11-16 02:37:55 [INFO] Copying application from /opt/holoscan/app to /var/run/holoscan/export/app

2023-11-16 02:37:55 [INFO] Copying application manifest file from /etc/holoscan/app.json to /var/run/holoscan/export/config/app.json
2023-11-16 02:37:55 [INFO] Copying pkg manifest file from /etc/holoscan/pkg.json to /var/run/holoscan/export/config/pkg.json
2023-11-16 02:37:55 [INFO] Copying application configuration from /var/holoscan/app.yaml to /var/run/holoscan/export/config/app.yaml

2023-11-16 02:37:55 [INFO] Copying models from /opt/holoscan/models to /var/run/holoscan/export/models

2023-11-16 02:37:55 [INFO] Copying documentation from /opt/holoscan/docs/ to /var/run/holoscan/export/docs
2023-11-16 02:37:55 [INFO] '/opt/holoscan/docs/' cannot be found.

app  config  models

Executing packaged app locally

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

# Clear the output folder and run the MAP. The input is expected to be a folder.
!rm -rf {ouput_folder}
!monai-deploy run -i $HOLOSCAN_INPUT_PATH -o $HOLOSCAN_OUTPUT_PATH mednist_app-x64-workstation-dgpu-linux-amd64:1.0
[2023-11-15 18:37:58,606] [INFO] (runner) - Checking dependencies...
[2023-11-15 18:37:58,606] [INFO] (runner) - --> Verifying if "docker" is installed...

[2023-11-15 18:37:58,606] [INFO] (runner) - --> Verifying if "docker-buildx" is installed...

[2023-11-15 18:37:58,606] [INFO] (runner) - --> Verifying if "mednist_app-x64-workstation-dgpu-linux-amd64:1.0" is available...

[2023-11-15 18:37:58,679] [INFO] (runner) - Reading HAP/MAP manifest...
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.56kB to /tmp/tmp89s5qkz8/app.json
Preparing to copy...?25lCopying from container - 0B?25hSuccessfully copied 2.05kB to /tmp/tmp89s5qkz8/pkg.json
[2023-11-15 18:37:58,873] [INFO] (runner) - --> Verifying if "nvidia-ctk" is installed...

[2023-11-15 18:37:59,068] [INFO] (common) - Launching container (e8950f0a463a) using image 'mednist_app-x64-workstation-dgpu-linux-amd64:1.0'...
    container name:      priceless_ptolemy
    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-11-16 02:37:59 [INFO] Launching application python3 /opt/holoscan/app/mednist_classifier_monaideploy.py ...

[2023-11-16 02:38:01,849] [INFO] (root) - Parsed args: Namespace(argv=['/opt/holoscan/app/mednist_classifier_monaideploy.py'], input=None, log_level=None, model=None, output=None, workdir=None)

[2023-11-16 02:38:01,857] [INFO] (root) - AppContext object: AppContext(input_path=/var/holoscan/input, output_path=/var/holoscan/output, model_path=/opt/holoscan/models, workdir=/var/holoscan)

[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

/home/holoscan/.local/lib/python3.8/site-packages/monai/utils/deprecate_utils.py:111: FutureWarning: <class 'monai.transforms.utility.array.AddChannel'>: Class `AddChannel` has been deprecated since version 0.8. It will be removed in version 1.3. please use MetaTensor data type and monai.transforms.EnsureChannelFirst instead with `channel_dim='no_channel'`.

  warn_deprecated(obj, msg, warning_category)

/home/holoscan/.local/lib/python3.8/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at ../torch/csrc/utils/tensor_numpy.cpp:206.)

  return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)  # type: ignore

/home/holoscan/.local/lib/python3.8/site-packages/pydicom/valuerep.py:443: UserWarning: Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.

  warnings.warn(msg)

[2023-11-16 02:38:04,038] [INFO] (root) - Finished writing DICOM instance to file /var/holoscan/output/1.2.826.0.1.3680043.8.498.16497775401758865936247607314643258908.dcm

[2023-11-16 02:38:04,039] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in /var/holoscan/output/1.2.826.0.1.3680043.8.498.16497775401758865936247607314643258908.dcm

[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

AbdomenCT

[2023-11-15 18:38:04,955] [INFO] (common) - Container 'priceless_ptolemy'(e8950f0a463a) exited.
!cat {output_folder}/output.json
"AbdomenCT"

Implementing and Packaging Application with MONAI Deploy App SDK

In the following sections we will discuss the details of buildng the application that was packaged and run above.

Based on the Torchscript model(classifier.zip), we will implement an application that process an input Jpeg image and write the prediction(classification) result as JSON file(output.json).

In our inference application, we will define two operators:

  1. LoadPILOperator - Load a JPEG image from the input path and pass the loaded image object to the next operator.

    • Input: a file path (Path)

    • Output: an image object in memory (Image)

  2. MedNISTClassifierOperator - Pre-transform the given image by using MONAI’s Compose class, feed to the Torchscript model (classifier.zip), and write the prediction into JSON file(output.json)

    • Pre-transforms consist of three transforms – EnsureChannelFirst, ScaleIntensity, and EnsureType.

    • Input: an image object in memory (Image)

    • Output: a folder path that the prediction result(output.json) would be written (Path)

The workflow of the application would look like this.

Workflow

Setup imports

Let’s import necessary classes/decorators and define MEDNIST_CLASSES.

import logging
import os
from pathlib import Path
from typing import Optional

import torch

from monai.deploy.conditions import CountCondition
from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec
from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity

MEDNIST_CLASSES = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]

Creating Operator classes

LoadPILOperator

class LoadPILOperator(Operator):
    """Load image from the given input (DataPath) and set numpy array to the output (Image)."""

    DEFAULT_INPUT_FOLDER = Path.cwd() / "input"
    DEFAULT_OUTPUT_NAME = "image"

    # For now, need to have the input folder as an instance attribute, set on init.
    # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the
    # value of the input folder, which is then emitted by a upstream operator.
    def __init__(
        self,
        fragment: Fragment,
        *args,
        input_folder: Path = DEFAULT_INPUT_FOLDER,
        output_name: str = DEFAULT_OUTPUT_NAME,
        **kwargs,
    ):
        """Creates an loader object with the input folder and the output port name overrides as needed.

        Args:
            fragment (Fragment): An instance of the Application class which is derived from Fragment.
            input_folder (Path): Folder from which to load input file(s).
                                 Defaults to `input` in the current working directory.
            output_name (str): Name of the output port, which is an image object. Defaults to `image`.
        """

        self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
        self.input_path = input_folder
        self.index = 0
        self.output_name_image = (
            output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME
        )

        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        """Set up the named input and output port(s)"""
        spec.output(self.output_name_image)

    def compute(self, op_input, op_output, context):
        import numpy as np
        from PIL import Image as PILImage

        # Input path is stored in the object attribute, but could change to use a named port if need be.
        input_path = self.input_path
        if input_path.is_dir():
            input_path = next(self.input_path.glob("*.*"))  # take the first file

        image = PILImage.open(input_path)
        image = image.convert("L")  # convert to greyscale image
        image_arr = np.asarray(image)

        output_image = Image(image_arr)  # create Image domain object with a numpy array
        op_output.emit(output_image, self.output_name_image)  # cannot omit the name even if single output.

MedNISTClassifierOperator

class MedNISTClassifierOperator(Operator):
    """Classifies the given image and returns the class name.

    Named inputs:
        image: Image object for which to generate the classification.
        output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__

    Named output:
        result_text: The classification results in text.
    """

    DEFAULT_OUTPUT_FOLDER = Path.cwd() / "classification_results"
    # For testing the app directly, the model should be at the following path.
    MODEL_LOCAL_PATH = Path(os.environ.get("HOLOSCAN_MODEL_PATH", Path.cwd() / "model/model.ts"))

    def __init__(
        self,
        frament: Fragment,
        *args,
        app_context: AppContext,
        model_name: Optional[str] = "",
        model_path: Path = MODEL_LOCAL_PATH,
        output_folder: Path = DEFAULT_OUTPUT_FOLDER,
        **kwargs,
    ):
        """Creates an instance with the reference back to the containing application/fragment.

        fragment (Fragment): An instance of the Application class which is derived from Fragment.
        model_name (str, optional): Name of the model. Default to "" for single model app.
        model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.
        output_folder (Path, optional): output folder for saving the classification results JSON file.
        """

        # the names used for the model inference input and output
        self._input_dataset_key = "image"
        self._pred_dataset_key = "pred"

        # The names used for the operator input and output
        self.input_name_image = "image"
        self.output_name_result = "result_text"

        # The name of the optional input port for passing data to override the output folder path.
        self.input_name_output_folder = "output_folder"

        # The output folder set on the object can be overriden at each compute by data in the optional named input
        self.output_folder = output_folder

        # Need the name when there are multiple models loaded
        self._model_name = model_name.strip() if isinstance(model_name, str) else ""
        # Need the path to load the models when they are not loaded in the execution context
        self.model_path = model_path
        self.app_context = app_context
        self.model = self._get_model(self.app_context, self.model_path, self._model_name)

        # This needs to be at the end of the constructor.
        super().__init__(frament, *args, **kwargs)

    def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):
        """Load the model with the given name from context or model path

        Args:
            app_context (AppContext): The application context object holding the model(s)
            model_path (Path): The path to the model file, as a backup to load model directly
            model_name (str): The name of the model, when multiples are loaded in the context
        """

        if app_context.models:
            # `app_context.models.get(model_name)` returns a model instance if exists.
            # If model_name is not specified and only one model exists, it returns that model.
            model = app_context.models.get(model_name)
        else:
            model = torch.jit.load(
                MedNISTClassifierOperator.MODEL_LOCAL_PATH,
                map_location=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
            )

        return model

    def setup(self, spec: OperatorSpec):
        """Set up the operator named input and named output, both are in-memory objects."""

        spec.input(self.input_name_image)
        spec.input(self.input_name_output_folder).condition(ConditionType.NONE)  # Optional for overriding.
        spec.output(self.output_name_result).condition(ConditionType.NONE)  # Not forcing a downstream receiver.

    @property
    def transform(self):
        return Compose([EnsureChannelFirst(channel_dim="no_channel"), ScaleIntensity(), EnsureType()])

    def compute(self, op_input, op_output, context):
        import json

        import torch

        img = op_input.receive(self.input_name_image).asnumpy()  # (64, 64), uint8. Input validation can be added.
        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64
        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32

        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        image_tensor = image_tensor.to(device)

        with torch.no_grad():
            outputs = self.model(image_tensor)

        _, output_classes = outputs.max(dim=1)

        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name
        print(result)
        op_output.emit(result, self.output_name_result)

        # Get output folder, with value in optional input port overriding the obj attribute
        output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder
        Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True)  # Let exception bubble up if raised.
        output_path = output_folder_on_compute / "output.json"
        with open(output_path, "w") as fp:
            json.dump(result, fp)

Creating Application class

Our application class would look like below.

It defines App class inheriting Application class.

LoadPILOperator is connected to MedNISTClassifierOperator by using self.add_flow() in compose() method of App.

class App(Application):
    """Application class for the MedNIST classifier."""

    def compose(self):
        app_context = Application.init_app_context({})  # Do not pass argv in Jupyter Notebook
        app_input_path = Path(app_context.input_path)
        app_output_path = Path(app_context.output_path)
        model_path = Path(app_context.model_path)
        load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name="pil_loader_op")
        classifier_op = MedNISTClassifierOperator(
            self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name="classifier_op"
        )

        my_model_info = ModelInfo("MONAI WG Trainer", "MEDNIST Classifier", "0.1", "xyz")
        my_equipment = EquipmentInfo(manufacturer="MOANI Deploy App SDK", manufacturer_model="DICOM SR Writer")
        my_special_tags = {"SeriesDescription": "Not for clinical use. The result is for research use only."}
        dicom_sr_operator = DICOMTextSRWriterOperator(
            self,
            copy_tags=False,
            model_info=my_model_info,
            equipment_info=my_equipment,
            custom_tags=my_special_tags,
            output_folder=app_output_path,
        )

        self.add_flow(load_pil_op, classifier_op, {("image", "image")})
        self.add_flow(classifier_op, dicom_sr_operator, {("result_text", "text")})

Executing app locally

We can execute the app in the Jupyter notebook. Before doing so, we also need to clean the output folder which was created by running the packaged containerizd app in the previous cell.

!rm -rf $HOLOSCAN_OUTPUT_PATH
app = App().run()
[2023-11-15 18:38:10,416] [INFO] (root) - Parsed args: Namespace(argv=[], input=None, log_level=None, model=None, output=None, workdir=None)
[2023-11-15 18:38:10,440] [INFO] (root) - AppContext object: AppContext(input_path=input, output_path=output, model_path=models, workdir=)
[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
/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at ../torch/csrc/utils/tensor_numpy.cpp:206.)
  return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)
/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages/pydicom/valuerep.py:443: UserWarning: Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.
  warnings.warn(msg)
[2023-11-15 18:38:11,717] [INFO] (root) - Finished writing DICOM instance to file output/1.2.826.0.1.3680043.8.498.12540892677700748616860638452010005160.dcm
[2023-11-15 18:38:11,720] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in output/1.2.826.0.1.3680043.8.498.12540892677700748616860638452010005160.dcm
AbdomenCT
[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
!cat $HOLOSCAN_OUTPUT_PATH/output.json
"AbdomenCT"

Once the application is verified inside Jupyter notebook, we can write the whole application as a file(mednist_classifier_monaideploy.py) by concatenating code above, then add the following lines:

if __name__ == "__main__":
    App().run()

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

# Create an application folder
!mkdir -p mednist_app && rm -rf mednist_app/*
%%writefile mednist_app/mednist_classifier_monaideploy.py

# Copyright 2021-2023 MONAI Consortium
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#     http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import logging
import os
from pathlib import Path
from typing import Optional

import torch

from monai.deploy.conditions import CountCondition
from monai.deploy.core import AppContext, Application, ConditionType, Fragment, Image, Operator, OperatorSpec
from monai.deploy.operators.dicom_text_sr_writer_operator import DICOMTextSRWriterOperator, EquipmentInfo, ModelInfo
from monai.transforms import EnsureChannelFirst, Compose, EnsureType, ScaleIntensity

MEDNIST_CLASSES = ["AbdomenCT", "BreastMRI", "CXR", "ChestCT", "Hand", "HeadCT"]


# @md.env(pip_packages=["pillow"])
class LoadPILOperator(Operator):
    """Load image from the given input (DataPath) and set numpy array to the output (Image)."""

    DEFAULT_INPUT_FOLDER = Path.cwd() / "input"
    DEFAULT_OUTPUT_NAME = "image"

    # For now, need to have the input folder as an instance attribute, set on init.
    # If dynamically changing the input folder, per compute, then use a (optional) input port to convey the
    # value of the input folder, which is then emitted by a upstream operator.
    def __init__(
        self,
        fragment: Fragment,
        *args,
        input_folder: Path = DEFAULT_INPUT_FOLDER,
        output_name: str = DEFAULT_OUTPUT_NAME,
        **kwargs,
    ):
        """Creates an loader object with the input folder and the output port name overrides as needed.

        Args:
            fragment (Fragment): An instance of the Application class which is derived from Fragment.
            input_folder (Path): Folder from which to load input file(s).
                                 Defaults to `input` in the current working directory.
            output_name (str): Name of the output port, which is an image object. Defaults to `image`.
        """

        self._logger = logging.getLogger("{}.{}".format(__name__, type(self).__name__))
        self.input_path = input_folder
        self.index = 0
        self.output_name_image = (
            output_name.strip() if output_name and len(output_name.strip()) > 0 else LoadPILOperator.DEFAULT_OUTPUT_NAME
        )

        super().__init__(fragment, *args, **kwargs)

    def setup(self, spec: OperatorSpec):
        """Set up the named input and output port(s)"""
        spec.output(self.output_name_image)

    def compute(self, op_input, op_output, context):
        import numpy as np
        from PIL import Image as PILImage

        # Input path is stored in the object attribute, but could change to use a named port if need be.
        input_path = self.input_path
        if input_path.is_dir():
            input_path = next(self.input_path.glob("*.*"))  # take the first file

        image = PILImage.open(input_path)
        image = image.convert("L")  # convert to greyscale image
        image_arr = np.asarray(image)

        output_image = Image(image_arr)  # create Image domain object with a numpy array
        op_output.emit(output_image, self.output_name_image)  # cannot omit the name even if single output.


# @md.env(pip_packages=["monai"])
class MedNISTClassifierOperator(Operator):
    """Classifies the given image and returns the class name.

    Named inputs:
        image: Image object for which to generate the classification.
        output_folder: Optional, the path to save the results JSON file, overridingthe the one set on __init__

    Named output:
        result_text: The classification results in text.
    """

    DEFAULT_OUTPUT_FOLDER = Path.cwd() / "classification_results"
    # For testing the app directly, the model should be at the following path.
    MODEL_LOCAL_PATH = Path(os.environ.get("HOLOSCAN_MODEL_PATH", Path.cwd() / "model/model.ts"))

    def __init__(
        self,
        frament: Fragment,
        *args,
        app_context: AppContext,
        model_name: Optional[str] = "",
        model_path: Path = MODEL_LOCAL_PATH,
        output_folder: Path = DEFAULT_OUTPUT_FOLDER,
        **kwargs,
    ):
        """Creates an instance with the reference back to the containing application/fragment.

        fragment (Fragment): An instance of the Application class which is derived from Fragment.
        model_name (str, optional): Name of the model. Default to "" for single model app.
        model_path (Path): Path to the model file. Defaults to model/models.ts of current working dir.
        output_folder (Path, optional): output folder for saving the classification results JSON file.
        """

        # the names used for the model inference input and output
        self._input_dataset_key = "image"
        self._pred_dataset_key = "pred"

        # The names used for the operator input and output
        self.input_name_image = "image"
        self.output_name_result = "result_text"

        # The name of the optional input port for passing data to override the output folder path.
        self.input_name_output_folder = "output_folder"

        # The output folder set on the object can be overriden at each compute by data in the optional named input
        self.output_folder = output_folder

        # Need the name when there are multiple models loaded
        self._model_name = model_name.strip() if isinstance(model_name, str) else ""
        # Need the path to load the models when they are not loaded in the execution context
        self.model_path = model_path
        self.app_context = app_context
        self.model = self._get_model(self.app_context, self.model_path, self._model_name)

        # This needs to be at the end of the constructor.
        super().__init__(frament, *args, **kwargs)

    def _get_model(self, app_context: AppContext, model_path: Path, model_name: str):
        """Load the model with the given name from context or model path

        Args:
            app_context (AppContext): The application context object holding the model(s)
            model_path (Path): The path to the model file, as a backup to load model directly
            model_name (str): The name of the model, when multiples are loaded in the context
        """

        if app_context.models:
            # `app_context.models.get(model_name)` returns a model instance if exists.
            # If model_name is not specified and only one model exists, it returns that model.
            model = app_context.models.get(model_name)
        else:
            model = torch.jit.load(
                MedNISTClassifierOperator.MODEL_LOCAL_PATH,
                map_location=torch.device("cuda" if torch.cuda.is_available() else "cpu"),
            )

        return model

    def setup(self, spec: OperatorSpec):
        """Set up the operator named input and named output, both are in-memory objects."""

        spec.input(self.input_name_image)
        spec.input(self.input_name_output_folder).condition(ConditionType.NONE)  # Optional for overriding.
        spec.output(self.output_name_result).condition(ConditionType.NONE)  # Not forcing a downstream receiver.

    @property
    def transform(self):
        return Compose([EnsureChannelFirst(channel_dim="no_channel"), ScaleIntensity(), EnsureType()])

    def compute(self, op_input, op_output, context):
        import json

        import torch

        img = op_input.receive(self.input_name_image).asnumpy()  # (64, 64), uint8. Input validation can be added.
        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64
        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32

        device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
        image_tensor = image_tensor.to(device)

        with torch.no_grad():
            outputs = self.model(image_tensor)

        _, output_classes = outputs.max(dim=1)

        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name
        print(result)
        op_output.emit(result, self.output_name_result)

        # Get output folder, with value in optional input port overriding the obj attribute
        output_folder_on_compute = op_input.receive(self.input_name_output_folder) or self.output_folder
        Path.mkdir(output_folder_on_compute, parents=True, exist_ok=True)  # Let exception bubble up if raised.
        output_path = output_folder_on_compute / "output.json"
        with open(output_path, "w") as fp:
            json.dump(result, fp)


# @md.resource(cpu=1, gpu=1, memory="1Gi")
class App(Application):
    """Application class for the MedNIST classifier."""

    def compose(self):
        # Use Commandline options over environment variables to init context.
        app_context = Application.init_app_context(self.argv)
        app_input_path = Path(app_context.input_path)
        app_output_path = Path(app_context.output_path)
        model_path = Path(app_context.model_path)
        load_pil_op = LoadPILOperator(self, CountCondition(self, 1), input_folder=app_input_path, name="pil_loader_op")
        classifier_op = MedNISTClassifierOperator(
            self, app_context=app_context, output_folder=app_output_path, model_path=model_path, name="classifier_op"
        )

        my_model_info = ModelInfo("MONAI WG Trainer", "MEDNIST Classifier", "0.1", "xyz")
        my_equipment = EquipmentInfo(manufacturer="MOANI Deploy App SDK", manufacturer_model="DICOM SR Writer")
        my_special_tags = {"SeriesDescription": "Not for clinical use. The result is for research use only."}
        dicom_sr_operator = DICOMTextSRWriterOperator(
            self,
            copy_tags=False,
            model_info=my_model_info,
            equipment_info=my_equipment,
            custom_tags=my_special_tags,
            output_folder=app_output_path,
        )

        self.add_flow(load_pil_op, classifier_op, {("image", "image")})
        self.add_flow(classifier_op, dicom_sr_operator, {("result_text", "text")})


if __name__ == "__main__":
    App().run()
Writing mednist_app/mednist_classifier_monaideploy.py

This time, let’s execute the app on 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.

!python "mednist_app/mednist_classifier_monaideploy.py" -i {input_folder} -o {output_folder} -m {models_folder} -l DEBUG
[2023-11-15 18:38:17,975] [INFO] (root) - Parsed args: Namespace(argv=['mednist_app/mednist_classifier_monaideploy.py', '-i', 'input', '-o', 'output', '-m', 'models', '-l', 'DEBUG'], input=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/input'), log_level='DEBUG', model=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models'), output=PosixPath('/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output'), workdir=None)
[2023-11-15 18:38:17,979] [INFO] (root) - AppContext object: AppContext(input_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/input, output_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output, model_path=/home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/models, workdir=)
[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
/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages/monai/data/meta_tensor.py:116: UserWarning: The given NumPy array is not writable, and PyTorch does not support non-writable tensors. This means writing to this tensor will result in undefined behavior. You may want to copy the array to protect its data or make it writable before converting it to a tensor. This type of warning will be suppressed for the rest of this program. (Triggered internally at ../torch/csrc/utils/tensor_numpy.cpp:206.)
  return torch.as_tensor(x, *args, **_kwargs).as_subclass(cls)
AbdomenCT
[2023-11-15 18:38:19,019] [DEBUG] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - Writing DICOM object...

[2023-11-15 18:38:19,019] [DEBUG] (root) - Writing DICOM common modules...
/home/mqin/src/monai-deploy-app-sdk/.venv/lib/python3.8/site-packages/pydicom/valuerep.py:443: UserWarning: Invalid value for VR UI: 'xyz'. Please see <https://dicom.nema.org/medical/dicom/current/output/html/part05.html#table_6.2-1> for allowed values for each VR.
  warnings.warn(msg)
[2023-11-15 18:38:19,021] [DEBUG] (root) - DICOM common modules written:
Dataset.file_meta -------------------------------
(0002, 0000) File Meta Information Group Length  UL: 198
(0002, 0001) File Meta Information Version       OB: b'01'
(0002, 0002) Media Storage SOP Class UID         UI: Basic Text SR Storage
(0002, 0003) Media Storage SOP Instance UID      UI: 1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558
(0002, 0010) Transfer Syntax UID                 UI: Implicit VR Little Endian
(0002, 0012) Implementation Class UID            UI: 1.2.40.0.13.1.1.1
(0002, 0013) Implementation Version Name         SH: '0.6.0'
-------------------------------------------------
(0008, 0005) Specific Character Set              CS: 'ISO_IR 100'
(0008, 0012) Instance Creation Date              DA: '20231115'
(0008, 0013) Instance Creation Time              TM: '183819'
(0008, 0016) SOP Class UID                       UI: Basic Text SR Storage
(0008, 0018) SOP Instance UID                    UI: 1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558
(0008, 0020) Study Date                          DA: '20231115'
(0008, 0021) Series Date                         DA: '20231115'
(0008, 0023) Content Date                        DA: '20231115'
(0008, 002a) Acquisition DateTime                DT: '20231115183819'
(0008, 0030) Study Time                          TM: '183819'
(0008, 0031) Series Time                         TM: '183819'
(0008, 0033) Content Time                        TM: '183819'
(0008, 0050) Accession Number                    SH: ''
(0008, 0060) Modality                            CS: 'SR'
(0008, 0070) Manufacturer                        LO: 'MOANI Deploy App SDK'
(0008, 0090) Referring Physician's Name          PN: ''
(0008, 0201) Timezone Offset From UTC            SH: '-0800'
(0008, 1030) Study Description                   LO: 'AI results.'
(0008, 103e) Series Description                  LO: 'CAUTION: Not for Diagnostic Use, for research use only.'
(0008, 1090) Manufacturer's Model Name           LO: 'DICOM SR Writer'
(0010, 0010) Patient's Name                      PN: ''
(0010, 0020) Patient ID                          LO: ''
(0010, 0021) Issuer of Patient ID                LO: ''
(0010, 0030) Patient's Birth Date                DA: ''
(0010, 0040) Patient's Sex                       CS: ''
(0018, 0015) Body Part Examined                  CS: ''
(0018, 1020) Software Versions                   LO: '0.6.0'
(0018, a001)  Contributing Equipment Sequence  1 item(s) ---- 
   (0008, 0070) Manufacturer                        LO: 'MONAI WG Trainer'
   (0008, 1090) Manufacturer's Model Name           LO: 'MEDNIST Classifier'
   (0018, 1002) Device UID                          UI: xyz
   (0018, 1020) Software Versions                   LO: '0.1'
   (0040, a170)  Purpose of Reference Code Sequence  1 item(s) ---- 
      (0008, 0100) Code Value                          SH: 'Newcode1'
      (0008, 0102) Coding Scheme Designator            SH: '99IHE'
      (0008, 0104) Code Meaning                        LO: '"Processing Algorithm'
      ---------
   ---------
(0020, 000d) Study Instance UID                  UI: 1.2.826.0.1.3680043.8.498.12379609192731250420244173376142712446
(0020, 000e) Series Instance UID                 UI: 1.2.826.0.1.3680043.8.498.73656010492290419012392741095771197196
(0020, 0010) Study ID                            SH: '1'
(0020, 0011) Series Number                       IS: '9783'
(0020, 0013) Instance Number                     IS: '1'
(0040, 1001) Requested Procedure ID              SH: ''
[2023-11-15 18:38:19,022] [DEBUG] (root) - DICOM dataset to be written:Dataset.file_meta -------------------------------
(0002, 0000) File Meta Information Group Length  UL: 198
(0002, 0001) File Meta Information Version       OB: b'01'
(0002, 0002) Media Storage SOP Class UID         UI: Basic Text SR Storage
(0002, 0003) Media Storage SOP Instance UID      UI: 1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558
(0002, 0010) Transfer Syntax UID                 UI: Implicit VR Little Endian
(0002, 0012) Implementation Class UID            UI: 1.2.40.0.13.1.1.1
(0002, 0013) Implementation Version Name         SH: '0.6.0'
-------------------------------------------------
(0008, 0005) Specific Character Set              CS: 'ISO_IR 100'
(0008, 0012) Instance Creation Date              DA: '20231115'
(0008, 0013) Instance Creation Time              TM: '183819'
(0008, 0016) SOP Class UID                       UI: Basic Text SR Storage
(0008, 0018) SOP Instance UID                    UI: 1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558
(0008, 0020) Study Date                          DA: '20231115'
(0008, 0021) Series Date                         DA: '20231115'
(0008, 0023) Content Date                        DA: '20231115'
(0008, 002a) Acquisition DateTime                DT: '20231115183819'
(0008, 0030) Study Time                          TM: '183819'
(0008, 0031) Series Time                         TM: '183819'
(0008, 0033) Content Time                        TM: '183819'
(0008, 0050) Accession Number                    SH: ''
(0008, 0060) Modality                            CS: 'SR'
(0008, 0070) Manufacturer                        LO: 'MOANI Deploy App SDK'
(0008, 0090) Referring Physician's Name          PN: ''
(0008, 0201) Timezone Offset From UTC            SH: '-0800'
(0008, 1030) Study Description                   LO: 'AI results.'
(0008, 103e) Series Description                  LO: 'Not for clinical use. The result is for research use only.'
(0008, 1090) Manufacturer's Model Name           LO: 'DICOM SR Writer'
(0010, 0010) Patient's Name                      PN: ''
(0010, 0020) Patient ID                          LO: ''
(0010, 0021) Issuer of Patient ID                LO: ''
(0010, 0030) Patient's Birth Date                DA: ''
(0010, 0040) Patient's Sex                       CS: ''
(0018, 0015) Body Part Examined                  CS: ''
(0018, 1020) Software Versions                   LO: '0.6.0'
(0018, a001)  Contributing Equipment Sequence  1 item(s) ---- 
   (0008, 0070) Manufacturer                        LO: 'MONAI WG Trainer'
   (0008, 1090) Manufacturer's Model Name           LO: 'MEDNIST Classifier'
   (0018, 1002) Device UID                          UI: xyz
   (0018, 1020) Software Versions                   LO: '0.1'
   (0040, a170)  Purpose of Reference Code Sequence  1 item(s) ---- 
      (0008, 0100) Code Value                          SH: 'Newcode1'
      (0008, 0102) Coding Scheme Designator            SH: '99IHE'
      (0008, 0104) Code Meaning                        LO: '"Processing Algorithm'
      ---------
   ---------
(0020, 000d) Study Instance UID                  UI: 1.2.826.0.1.3680043.8.498.12379609192731250420244173376142712446
(0020, 000e) Series Instance UID                 UI: 1.2.826.0.1.3680043.8.498.73656010492290419012392741095771197196
(0020, 0010) Study ID                            SH: '1'
(0020, 0011) Series Number                       IS: '9783'
(0020, 0013) Instance Number                     IS: '1'
(0040, 1001) Requested Procedure ID              SH: ''
(0040, a040) Value Type                          CS: 'CONTAINER'
(0040, a043)  Concept Name Code Sequence  1 item(s) ---- 
   (0008, 0100) Code Value                          SH: '18748-4'
   (0008, 0102) Coding Scheme Designator            SH: 'LN'
   (0008, 0104) Code Meaning                        LO: 'Diagnostic Imaging Report'
   ---------
(0040, a050) Continuity Of Content               CS: 'SEPARATE'
(0040, a493) Verification Flag                   CS: 'UNVERIFIED'
(0040, a730)  Content Sequence  1 item(s) ---- 
   (0040, a010) Relationship Type                   CS: 'CONTAINS'
   (0040, a040) Value Type                          CS: 'TEXT'
   (0040, a043)  Concept Name Code Sequence  1 item(s) ---- 
      (0008, 0100) Code Value                          SH: '111412'
      (0008, 0102) Coding Scheme Designator            SH: 'DCM'
      (0008, 0104) Code Meaning                        LO: 'Narrative Summary'
      ---------
   (0040, a160) Text Value                          UT: 'AbdomenCT'
   ---------
[2023-11-15 18:38:19,026] [INFO] (root) - Finished writing DICOM instance to file /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output/1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558.dcm
[2023-11-15 18:38:19,027] [INFO] (monai.deploy.operators.dicom_text_sr_writer_operator.DICOMTextSRWriterOperator) - DICOM SOP instance saved in /home/mqin/src/monai-deploy-app-sdk/notebooks/tutorials/output/1.2.826.0.1.3680043.8.498.67182506684910194313532021844767536558.dcm
[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
!cat {output_folder}/output.json
"AbdomenCT"

Additional file required for packaging the app (creating MAP Docker image)

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 mednist_app/app.yaml
%YAML 1.2
---
application:
  title: MONAI Deploy App Package - MedNIST Classifier App
  version: 1.0
  inputFormats: ["file"]
  outputFormats: ["file"]

resources:
  cpu: 1
  gpu: 1
  memory: 1Gi
  gpuMemory: 1Gi
Writing mednist_app/app.yaml
%%writefile mednist_app/requirements.txt
monai>=1.2.0
Pillow>=8.4.0
pydicom>=2.3.0
highdicom>=0.18.2
SimpleITK>=2.0.0
setuptools>=59.5.0 # for pkg_resources
Writing mednist_app/requirements.txt

By now, we have built the application and prepared all necessary files for create the MONAI Application Package (MAP).