{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Deploying a MedNIST Classifier App with MONAI Deploy App SDK\n",
    "\n",
    "This tutorial demos the process of packaging up a trained model using MONAI Deploy App SDK into an artifact which can be run as a local program performing inference, a workflow job doing the same, and a Docker containerized workflow execution.\n",
    "\n",
    "In this tutorial, we will train a MedNIST classifier like the [MONAI tutorial here](https://github.com/Project-MONAI/tutorials/blob/master/2d_classification/mednist_tutorial.ipynb) and then implement & package the inference application, executing the application locally.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Train a MedNIST classifier model with MONAI Core"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup environment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Install necessary packages for MONAI Core\n",
    "!python -c \"import monai\" || pip install -q \"monai[pillow, tqdm]\"\n",
    "!python -c \"import ignite\" || pip install -q \"monai[ignite]\"\n",
    "!python -c \"import gdown\" || pip install -q \"monai[gdown]\"\n",
    "\n",
    "# Install MONAI Deploy App SDK package\n",
    "!python -c \"import monai.deploy\" || pip install -q \"monai-deploy-app-sdk\""
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MONAI version: 0.6.0\n",
      "Numpy version: 1.19.5\n",
      "Pytorch version: 1.9.0\n",
      "MONAI flags: HAS_EXT = False, USE_COMPILED = False\n",
      "MONAI rev id: 0ad9e73639e30f4f1af5a1f4a45da9cb09930179\n",
      "\n",
      "Optional dependencies:\n",
      "Pytorch Ignite version: 0.4.5\n",
      "Nibabel version: 3.2.1\n",
      "scikit-image version: 0.17.2\n",
      "Pillow version: 8.3.1\n",
      "Tensorboard version: 2.6.0\n",
      "gdown version: 3.13.0\n",
      "TorchVision version: 0.10.0\n",
      "ITK version: 5.2.0\n",
      "tqdm version: 4.62.1\n",
      "lmdb version: NOT INSTALLED or UNKNOWN VERSION.\n",
      "psutil version: 5.8.0\n",
      "pandas version: 1.1.5\n",
      "einops version: 0.3.2\n",
      "\n",
      "For details about installing the optional dependencies, please visit:\n",
      "    https://docs.monai.io/en/latest/installation.html#installing-the-recommended-dependencies\n",
      "\n"
     ]
    }
   ],
   "source": [
    "# Copyright 2020 MONAI Consortium\n",
    "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#     http://www.apache.org/licenses/LICENSE-2.0\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License.\n",
    "\n",
    "import os\n",
    "import shutil\n",
    "import tempfile\n",
    "import glob\n",
    "import PIL.Image\n",
    "import torch\n",
    "import numpy as np\n",
    "\n",
    "from ignite.engine import Events\n",
    "\n",
    "from monai.apps import download_and_extract\n",
    "from monai.config import print_config\n",
    "from monai.networks.nets import DenseNet121\n",
    "from monai.engines import SupervisedTrainer\n",
    "from monai.transforms import (\n",
    "    AddChannel,\n",
    "    Compose,\n",
    "    LoadImage,\n",
    "    RandFlip,\n",
    "    RandRotate,\n",
    "    RandZoom,\n",
    "    ScaleIntensity,\n",
    "    EnsureType,\n",
    ")\n",
    "from monai.utils import set_determinism\n",
    "\n",
    "set_determinism(seed=0)\n",
    "\n",
    "print_config()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Download dataset\n",
    "\n",
    "The MedNIST dataset was gathered from several sets from [TCIA](https://wiki.cancerimagingarchive.net/display/Public/Data+Usage+Policies+and+Restrictions),\n",
    "the RSNA Bone Age Challenge(https://www.rsna.org/education/ai-resources-and-training/ai-image-challenge/rsna-pediatric-bone-age-challenge-2017),\n",
    "and [the NIH Chest X-ray dataset](https://cloud.google.com/healthcare/docs/resources/public-datasets/nih-chest).\n",
    "\n",
    "The dataset is kindly made available by [Dr. Bradley J. Erickson M.D., Ph.D.](https://www.mayo.edu/research/labs/radiology-informatics/overview) (Department of Radiology, Mayo Clinic)\n",
    "under the Creative Commons [CC BY-SA 4.0 license](https://creativecommons.org/licenses/by-sa/4.0/).\n",
    "\n",
    "If you use the MedNIST dataset, please acknowledge the source."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "metadata": {
    "tags": []
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "/tmp/tmpgh08b1ks\n"
     ]
    },
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Downloading...\n",
      "From: https://drive.google.com/uc?id=1QsnnkvZyJPcbRoV_ArW8SnE1OTuoVbKE\n",
      "To: /tmp/tmpthbz6o8r/MedNIST.tar.gz\n",
      "61.8MB [00:05, 10.7MB/s]\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Downloaded: /tmp/tmpgh08b1ks/MedNIST.tar.gz\n",
      "Verified 'MedNIST.tar.gz', md5: 0bc7306e7427e00ad1c5526a6677552d.\n",
      "Writing into directory: /tmp/tmpgh08b1ks.\n"
     ]
    }
   ],
   "source": [
    "directory = os.environ.get(\"MONAI_DATA_DIRECTORY\")\n",
    "root_dir = tempfile.mkdtemp() if directory is None else directory\n",
    "print(root_dir)\n",
    "\n",
    "resource = \"https://drive.google.com/uc?id=1QsnnkvZyJPcbRoV_ArW8SnE1OTuoVbKE\"\n",
    "md5 = \"0bc7306e7427e00ad1c5526a6677552d\"\n",
    "\n",
    "compressed_file = os.path.join(root_dir, \"MedNIST.tar.gz\")\n",
    "data_dir = os.path.join(root_dir, \"MedNIST\")\n",
    "if not os.path.exists(data_dir):\n",
    "    download_and_extract(resource, compressed_file, root_dir, md5)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Label names: ['AbdomenCT', 'BreastMRI', 'CXR', 'ChestCT', 'Hand', 'HeadCT']\n",
      "Label counts: [10000, 8954, 10000, 10000, 10000, 10000]\n",
      "Total image count: 58954\n",
      "Image dimensions: 64 x 64\n"
     ]
    }
   ],
   "source": [
    "subdirs = sorted(glob.glob(f\"{data_dir}/*/\"))\n",
    "\n",
    "class_names = [os.path.basename(sd[:-1]) for sd in subdirs]\n",
    "image_files = [glob.glob(f\"{sb}/*\") for sb in subdirs]\n",
    "\n",
    "image_files_list = sum(image_files, [])\n",
    "image_class = sum(([i] * len(f) for i, f in enumerate(image_files)), [])\n",
    "image_width, image_height = PIL.Image.open(image_files_list[0]).size\n",
    "\n",
    "print(f\"Label names: {class_names}\")\n",
    "print(f\"Label counts: {list(map(len, image_files))}\")\n",
    "print(f\"Total image count: {len(image_class)}\")\n",
    "print(f\"Image dimensions: {image_width} x {image_height}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Setup and train\n",
    "\n",
    "Here we'll create a transform sequence and train the network, omitting validation and testing since we know this does indeed work and it's not needed here:\n",
    "\n",
    "(train_transforms)="
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "metadata": {},
   "outputs": [],
   "source": [
    "train_transforms = Compose(\n",
    "    [\n",
    "        LoadImage(image_only=True),\n",
    "        AddChannel(),\n",
    "        ScaleIntensity(),\n",
    "        RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),\n",
    "        RandFlip(spatial_axis=0, prob=0.5),\n",
    "        RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),\n",
    "        EnsureType(),\n",
    "    ]\n",
    ")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "metadata": {},
   "outputs": [],
   "source": [
    "class MedNISTDataset(torch.utils.data.Dataset):\n",
    "    def __init__(self, image_files, labels, transforms):\n",
    "        self.image_files = image_files\n",
    "        self.labels = labels\n",
    "        self.transforms = transforms\n",
    "\n",
    "    def __len__(self):\n",
    "        return len(self.image_files)\n",
    "\n",
    "    def __getitem__(self, index):\n",
    "        return self.transforms(self.image_files[index]), self.labels[index]\n",
    "\n",
    "\n",
    "# just one dataset and loader, we won't bother with validation or testing \n",
    "train_ds = MedNISTDataset(image_files_list, image_class, train_transforms)\n",
    "train_loader = torch.utils.data.DataLoader(train_ds, batch_size=300, shuffle=True, num_workers=10)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "metadata": {},
   "outputs": [],
   "source": [
    "device = torch.device(\"cuda:0\")\n",
    "net = DenseNet121(spatial_dims=2, in_channels=1, out_channels=len(class_names)).to(device)\n",
    "loss_function = torch.nn.CrossEntropyLoss()\n",
    "opt = torch.optim.Adam(net.parameters(), 1e-5)\n",
    "max_epochs = 5"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "metadata": {},
   "outputs": [
    {
     "name": "stderr",
     "output_type": "stream",
     "text": [
      "Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /opt/conda/conda-bld/pytorch_1623448272031/work/c10/core/TensorImpl.h:1156.)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Epoch 1/5 Loss: 0.1811893731355667\n",
      "Epoch 2/5 Loss: 0.08026652783155441\n",
      "Epoch 3/5 Loss: 0.05008228123188019\n",
      "Epoch 4/5 Loss: 0.01724996417760849\n",
      "Epoch 5/5 Loss: 0.029151903465390205\n"
     ]
    }
   ],
   "source": [
    "def _prepare_batch(batch, device, non_blocking):\n",
    "    return tuple(b.to(device) for b in batch)\n",
    "\n",
    "\n",
    "trainer = SupervisedTrainer(device, max_epochs, train_loader, net, opt, loss_function, prepare_batch=_prepare_batch)\n",
    "\n",
    "\n",
    "@trainer.on(Events.EPOCH_COMPLETED)\n",
    "def _print_loss(engine):\n",
    "    print(f\"Epoch {engine.state.epoch}/{engine.state.max_epochs} Loss: {engine.state.output[0]['loss']}\")\n",
    "\n",
    "\n",
    "trainer.run()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "The network will be saved out here as a Torchscript object named `classifier.zip`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "metadata": {},
   "outputs": [],
   "source": [
    "torch.jit.script(net).save(\"classifier.zip\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Implementing and Packaging Application with MONAI Deploy App SDK\n",
    "\n",
    "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`).\n",
    "\n",
    "### Creating Operators and connecting them in Application class\n",
    "\n",
    "We used the following [train transforms](train_transforms) as pre-transforms during the training.\n",
    "\n",
    "```{code-block} python\n",
    "---\n",
    "lineno-start: 1\n",
    "emphasize-lines: 3,4,5,9\n",
    "caption: |\n",
    "    Train transforms used in training\n",
    "---\n",
    "train_transforms = Compose(\n",
    "    [\n",
    "        LoadImage(image_only=True),\n",
    "        AddChannel(),\n",
    "        ScaleIntensity(),\n",
    "        RandRotate(range_x=np.pi / 12, prob=0.5, keep_size=True),\n",
    "        RandFlip(spatial_axis=0, prob=0.5),\n",
    "        RandZoom(min_zoom=0.9, max_zoom=1.1, prob=0.5),\n",
    "        EnsureType(),\n",
    "    ]\n",
    ")\n",
    "```\n",
    "\n",
    "`RandRotate`, `RandFlip`, and `RandZoom` transforms are used only for training and those are not necessary during the inference.\n",
    "\n",
    "In our inference application, we will define two operators:\n",
    "\n",
    "1. `LoadPILOperator` - Load a JPEG image from the input path and pass the loaded image object to the next operator.\n",
    "    - This Operator does similar job with `LoadImage(image_only=True)` transform in *train_transforms*, but handles only one image.\n",
    "    - **Input**: a file path ([`DataPath`](/modules/_autosummary/monai.deploy.core.domain.DataPath))\n",
    "    - **Output**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "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`)\n",
    "    - Pre-transforms consist of three transforms -- `AddChannel`, `ScaleIntensity`, and `EnsureType`.\n",
    "    - **Input**: an image object in memory ([`Image`](/modules/_autosummary/monai.deploy.core.domain.Image))\n",
    "    - **Output**: a folder path that the prediction result(`output.json`) would be written ([`DataPath`](/modules/_autosummary/monai.deploy.core.domain.DataPath))\n",
    "\n",
    "The workflow of the application would look like this.\n",
    "\n",
    "```{mermaid}\n",
    "%%{init: {\"theme\": \"base\", \"themeVariables\": { \"fontSize\": \"16px\"}} }%%\n",
    "\n",
    "classDiagram\n",
    "    direction LR\n",
    "\n",
    "    LoadPILOperator --|> MedNISTClassifierOperator : image...image\n",
    "\n",
    "\n",
    "    class LoadPILOperator {\n",
    "        <in>image : DISK\n",
    "        image(out) IN_MEMORY\n",
    "    }\n",
    "    class MedNISTClassifierOperator {\n",
    "        <in>image : IN_MEMORY\n",
    "        output(out) DISK\n",
    "    }\n",
    "```\n",
    "\n",
    "\n",
    "#### Setup imports\n",
    "\n",
    "Let's import necessary classes/decorators and define `MEDNIST_CLASSES`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "metadata": {},
   "outputs": [],
   "source": [
    "import monai.deploy.core as md  # 'md' stands for MONAI Deploy (or can use 'core' instead)\n",
    "from monai.deploy.core import (\n",
    "    Application,\n",
    "    DataPath,\n",
    "    ExecutionContext,\n",
    "    Image,\n",
    "    InputContext,\n",
    "    IOType,\n",
    "    Operator,\n",
    "    OutputContext,\n",
    ")\n",
    "from monai.transforms import AddChannel, Compose, EnsureType, ScaleIntensity\n",
    "\n",
    "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Creating Operator classes\n",
    "\n",
    "##### LoadPILOperator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "metadata": {},
   "outputs": [],
   "source": [
    "@md.input(\"image\", DataPath, IOType.DISK)\n",
    "@md.output(\"image\", Image, IOType.IN_MEMORY)\n",
    "@md.env(pip_packages=[\"pillow\"])\n",
    "class LoadPILOperator(Operator):\n",
    "    \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n",
    "\n",
    "    def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):\n",
    "        import numpy as np\n",
    "        from PIL import Image as PILImage\n",
    "\n",
    "        input_path = op_input.get().path\n",
    "        if input_path.is_dir():\n",
    "            input_path = next(input_path.glob(\"*.*\"))  # take the first file\n",
    "\n",
    "        image = PILImage.open(input_path)\n",
    "        image = image.convert(\"L\")  # convert to greyscale image\n",
    "        image_arr = np.asarray(image)\n",
    "\n",
    "        output_image = Image(image_arr)  # create Image domain object with a numpy array\n",
    "        op_output.set(output_image)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "##### MedNISTClassifierOperator"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "metadata": {},
   "outputs": [],
   "source": [
    "@md.input(\"image\", Image, IOType.IN_MEMORY)\n",
    "@md.output(\"output\", DataPath, IOType.DISK)\n",
    "@md.env(pip_packages=[\"monai\"])\n",
    "class MedNISTClassifierOperator(Operator):\n",
    "    \"\"\"Classifies the given image and returns the class name.\"\"\"\n",
    "\n",
    "    @property\n",
    "    def transform(self):\n",
    "        return Compose([AddChannel(), ScaleIntensity(), EnsureType()])\n",
    "\n",
    "    def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):\n",
    "        import json\n",
    "\n",
    "        import torch\n",
    "\n",
    "        img = op_input.get().asnumpy()  # (64, 64), uint8\n",
    "        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64\n",
    "        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32\n",
    "\n",
    "        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "        image_tensor = image_tensor.to(device)\n",
    "\n",
    "        model = context.models.get()  # get a TorchScriptModel object\n",
    "\n",
    "        with torch.no_grad():\n",
    "            outputs = model(image_tensor)\n",
    "\n",
    "        _, output_classes = outputs.max(dim=1)\n",
    "\n",
    "        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name\n",
    "        print(result)\n",
    "\n",
    "        # Get output (folder) path and create the folder if not exists\n",
    "        output_folder = op_output.get().path\n",
    "        output_folder.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "        # Write result to \"output.json\"\n",
    "        output_path = output_folder / \"output.json\"\n",
    "        with open(output_path, \"w\") as fp:\n",
    "            json.dump(result, fp)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "#### Creating Application class\n",
    "\n",
    "Our application class would look like below.\n",
    "\n",
    "It defines `App` class inheriting `Application` class.\n",
    "\n",
    "`LoadPILOperator` is connected to `MedNISTClassifierOperator` by using `self.add_flow()` in `compose()` method of `App`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "metadata": {},
   "outputs": [],
   "source": [
    "@md.resource(cpu=1, gpu=1, memory=\"1Gi\")\n",
    "class App(Application):\n",
    "    \"\"\"Application class for the MedNIST classifier.\"\"\"\n",
    "\n",
    "    def compose(self):\n",
    "        load_pil_op = LoadPILOperator()\n",
    "        classifier_op = MedNISTClassifierOperator()\n",
    "\n",
    "        self.add_flow(load_pil_op, classifier_op)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Executing app locally\n",
    "\n",
    "Let's find a test input file path to use."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Test input file path: /tmp/tmpgh08b1ks/MedNIST/AbdomenCT/007000.jpeg\n"
     ]
    }
   ],
   "source": [
    "test_input_path = image_files[0][0]\n",
    "print(f\"Test input file path: {test_input_path}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "We can execute the app in the Jupyter notebook."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "metadata": {},
   "outputs": [],
   "source": [
    "app = App()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[34mGoing to initiate execution of operator LoadPILOperator\u001b[39m\n",
      "\u001b[32mExecuting operator LoadPILOperator \u001b[33m(Process ID: 14835, Operator ID: dd5dee72-9764-458a-9719-dc89f3cd14ea)\u001b[39m\n",
      "\u001b[34mDone performing execution of operator LoadPILOperator\n",
      "\u001b[39m\n",
      "\u001b[34mGoing to initiate execution of operator MedNISTClassifierOperator\u001b[39m\n",
      "\u001b[32mExecuting operator MedNISTClassifierOperator \u001b[33m(Process ID: 14835, Operator ID: 9b032f84-6a73-4f59-9c56-d04efed5bdb5)\u001b[39m\n",
      "AbdomenCT\n",
      "\u001b[34mDone performing execution of operator MedNISTClassifierOperator\n",
      "\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "app.run(input=test_input_path, output=\"output\", model=\"classifier.zip\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"AbdomenCT\""
     ]
    }
   ],
   "source": [
    "!cat output/output.json"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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:\n",
    "\n",
    "```python\n",
    "if __name__ == \"__main__\":\n",
    "    App(do_run=True)\n",
    "```\n",
    "\n",
    "The above lines are needed to execute the application code by using `python` interpreter."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Writing mednist_classifier_monaideploy.py\n"
     ]
    }
   ],
   "source": [
    "%%writefile mednist_classifier_monaideploy.py\n",
    "\n",
    "# Copyright 2021 MONAI Consortium\n",
    "# Licensed under the Apache License, Version 2.0 (the \"License\");\n",
    "# you may not use this file except in compliance with the License.\n",
    "# You may obtain a copy of the License at\n",
    "#     http://www.apache.org/licenses/LICENSE-2.0\n",
    "# Unless required by applicable law or agreed to in writing, software\n",
    "# distributed under the License is distributed on an \"AS IS\" BASIS,\n",
    "# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n",
    "# See the License for the specific language governing permissions and\n",
    "# limitations under the License.\n",
    "\n",
    "import monai.deploy.core as md  # 'md' stands for MONAI Deploy (or can use 'core' instead)\n",
    "from monai.deploy.core import (\n",
    "    Application,\n",
    "    DataPath,\n",
    "    ExecutionContext,\n",
    "    Image,\n",
    "    InputContext,\n",
    "    IOType,\n",
    "    Operator,\n",
    "    OutputContext,\n",
    ")\n",
    "from monai.transforms import AddChannel, Compose, EnsureType, ScaleIntensity\n",
    "\n",
    "MEDNIST_CLASSES = [\"AbdomenCT\", \"BreastMRI\", \"CXR\", \"ChestCT\", \"Hand\", \"HeadCT\"]\n",
    "\n",
    "\n",
    "@md.input(\"image\", DataPath, IOType.DISK)\n",
    "@md.output(\"image\", Image, IOType.IN_MEMORY)\n",
    "@md.env(pip_packages=[\"pillow\"])\n",
    "class LoadPILOperator(Operator):\n",
    "    \"\"\"Load image from the given input (DataPath) and set numpy array to the output (Image).\"\"\"\n",
    "\n",
    "    def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):\n",
    "        import numpy as np\n",
    "        from PIL import Image as PILImage\n",
    "\n",
    "        input_path = op_input.get().path\n",
    "        if input_path.is_dir():\n",
    "            input_path = next(input_path.glob(\"*.*\"))  # take the first file\n",
    "\n",
    "        image = PILImage.open(input_path)\n",
    "        image = image.convert(\"L\")  # convert to greyscale image\n",
    "        image_arr = np.asarray(image)\n",
    "\n",
    "        output_image = Image(image_arr)  # create Image domain object with a numpy array\n",
    "        op_output.set(output_image)\n",
    "\n",
    "\n",
    "@md.input(\"image\", Image, IOType.IN_MEMORY)\n",
    "@md.output(\"output\", DataPath, IOType.DISK)\n",
    "@md.env(pip_packages=[\"monai\"])\n",
    "class MedNISTClassifierOperator(Operator):\n",
    "    \"\"\"Classifies the given image and returns the class name.\"\"\"\n",
    "\n",
    "    @property\n",
    "    def transform(self):\n",
    "        return Compose([AddChannel(), ScaleIntensity(), EnsureType()])\n",
    "\n",
    "    def compute(self, op_input: InputContext, op_output: OutputContext, context: ExecutionContext):\n",
    "        import json\n",
    "\n",
    "        import torch\n",
    "\n",
    "        img = op_input.get().asnumpy()  # (64, 64), uint8\n",
    "        image_tensor = self.transform(img)  # (1, 64, 64), torch.float64\n",
    "        image_tensor = image_tensor[None].float()  # (1, 1, 64, 64), torch.float32\n",
    "\n",
    "        device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n",
    "        image_tensor = image_tensor.to(device)\n",
    "\n",
    "        model = context.models.get()  # get a TorchScriptModel object\n",
    "\n",
    "        with torch.no_grad():\n",
    "            outputs = model(image_tensor)\n",
    "\n",
    "        _, output_classes = outputs.max(dim=1)\n",
    "\n",
    "        result = MEDNIST_CLASSES[output_classes[0]]  # get the class name\n",
    "        print(result)\n",
    "\n",
    "        # Get output (folder) path and create the folder if not exists\n",
    "        output_folder = op_output.get().path\n",
    "        output_folder.mkdir(parents=True, exist_ok=True)\n",
    "\n",
    "        # Write result to \"output.json\"\n",
    "        output_path = output_folder / \"output.json\"\n",
    "        with open(output_path, \"w\") as fp:\n",
    "            json.dump(result, fp)\n",
    "\n",
    "\n",
    "@md.resource(cpu=1, gpu=1, memory=\"1Gi\")\n",
    "class App(Application):\n",
    "    \"\"\"Application class for the MedNIST classifier.\"\"\"\n",
    "\n",
    "    def compose(self):\n",
    "        load_pil_op = LoadPILOperator()\n",
    "        classifier_op = MedNISTClassifierOperator()\n",
    "\n",
    "        self.add_flow(load_pil_op, classifier_op)\n",
    "\n",
    "\n",
    "if __name__ == \"__main__\":\n",
    "    App(do_run=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "In this time, let's execute the app in the command line."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[34mGoing to initiate execution of operator LoadPILOperator\u001b[39m\n",
      "\u001b[32mExecuting operator LoadPILOperator \u001b[33m(Process ID: 18193, Operator ID: de9a33aa-0abb-4e64-88af-90b27617ff63)\u001b[39m\n",
      "\u001b[34mDone performing execution of operator LoadPILOperator\n",
      "\u001b[39m\n",
      "\u001b[34mGoing to initiate execution of operator MedNISTClassifierOperator\u001b[39m\n",
      "\u001b[32mExecuting operator MedNISTClassifierOperator \u001b[33m(Process ID: 18193, Operator ID: 73bfa497-459c-4ef3-998a-8d162be57687)\u001b[39m\n",
      "Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /opt/conda/conda-bld/pytorch_1623448272031/work/c10/core/TensorImpl.h:1156.)\n",
      "AbdomenCT\n",
      "\u001b[34mDone performing execution of operator MedNISTClassifierOperator\n",
      "\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "!python mednist_classifier_monaideploy.py -i {test_input_path} -o output -m classifier.zip"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Above command is same with the following command line:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\u001b[34mGoing to initiate execution of operator LoadPILOperator\u001b[39m\n",
      "\u001b[32mExecuting operator LoadPILOperator \u001b[33m(Process ID: 18328, Operator ID: 70e92517-e6ad-4d0a-aaff-2141c672d587)\u001b[39m\n",
      "\u001b[34mDone performing execution of operator LoadPILOperator\n",
      "\u001b[39m\n",
      "\u001b[34mGoing to initiate execution of operator MedNISTClassifierOperator\u001b[39m\n",
      "\u001b[32mExecuting operator MedNISTClassifierOperator \u001b[33m(Process ID: 18328, Operator ID: a9a7fc21-b180-4981-b775-ea8736e805a2)\u001b[39m\n",
      "Named tensors and all their associated APIs are an experimental feature and subject to change. Please do not use them for anything important until they are released as stable. (Triggered internally at  /opt/conda/conda-bld/pytorch_1623448272031/work/c10/core/TensorImpl.h:1156.)\n",
      "AbdomenCT\n",
      "\u001b[34mDone performing execution of operator MedNISTClassifierOperator\n",
      "\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "!monai-deploy exec mednist_classifier_monaideploy.py -i {test_input_path} -o output -m classifier.zip"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"AbdomenCT\""
     ]
    }
   ],
   "source": [
    "!cat output/output.json"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Packaging app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Let's package the app with <a href=\"../../developing_with_sdk/packaging_app.html\">MONAI Application Packager</a>."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Building MONAI Application Package... Done\n",
      "[2021-09-20 17:01:24,898] [INFO] (app_packager) - Successfully built mednist_app:latest\n"
     ]
    }
   ],
   "source": [
    "!monai-deploy package mednist_classifier_monaideploy.py --tag mednist_app:latest --model classifier.zip  # -l DEBUG"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    ":::{note}\n",
    "Building a MONAI Application Package (Docker image) can take time. Use `-l DEBUG` option if you want to see the progress.\n",
    "\n",
    ":::\n",
    "\n",
    "We can see that the Docker image is created."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mednist_app                                                             latest                                   8c78cc6e0966        3 seconds ago       15.3GB\n"
     ]
    }
   ],
   "source": [
    "!docker image ls | grep mednist_app"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Executing packaged app locally\n",
    "\n",
    "The packaged app can be run locally through <a href=\"../../developing_with_sdk/executing_packaged_app_locally.html\">MONAI Application Runner</a>."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Checking dependencies...\n",
      "--> Verifying if \"docker\" is installed...\n",
      "\n",
      "--> Verifying if \"mednist_app:latest\" is available...\n",
      "\n",
      "Checking for MAP \"mednist_app:latest\" locally\n",
      "\"mednist_app:latest\" found.\n",
      "\n",
      "Reading MONAI App Package manifest...\n",
      " > export '/var/run/monai/export/' detected\n",
      "--> Verifying if \"nvidia-docker\" is installed...\n",
      "\n",
      "\u001b[34mGoing to initiate execution of operator LoadPILOperator\u001b[39m\n",
      "\u001b[32mExecuting operator LoadPILOperator \u001b[33m(Process ID: 1, Operator ID: 7bb4824c-ebc7-4801-a0c3-1c5525b132cf)\u001b[39m\n",
      "\u001b[34mDone performing execution of operator LoadPILOperator\n",
      "\u001b[39m\n",
      "\u001b[34mGoing to initiate execution of operator MedNISTClassifierOperator\u001b[39m\n",
      "\u001b[32mExecuting operator MedNISTClassifierOperator \u001b[33m(Process ID: 1, Operator ID: d27f4a05-e557-49c3-8adf-08f83a860d14)\u001b[39m\n",
      "AbdomenCT\n",
      "\u001b[34mDone performing execution of operator MedNISTClassifierOperator\n",
      "\u001b[39m\n"
     ]
    }
   ],
   "source": [
    "# Copy a test input file to 'input' folder\n",
    "!mkdir -p input && rm -rf input/*\n",
    "!cp {test_input_path} input/\n",
    "\n",
    "# Launch the app\n",
    "!monai-deploy run mednist_app:latest input output"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"AbdomenCT\""
     ]
    }
   ],
   "source": [
    "!cat output/output.json"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Note**: Please execute the following script once the exercise is done."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Remove data files which is in the temporary folder\n",
    "if directory is None:\n",
    "    shutil.rmtree(root_dir)"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.6.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
