From 46af07c4b05c722f5492866b2ad293b320888b26 Mon Sep 17 00:00:00 2001 From: Pradyumna Elavarthi Date: Mon, 9 Mar 2026 13:32:23 -0700 Subject: [PATCH 1/4] Restructure segmentation containers: add leaf_segmentation container - Moved existing Dockerfile/docker-compose.yml/vtk_trial.ipynb into general_model/ - Added new leaf_segmentation/ container with: - Dockerfile: Ubuntu + Miniforge + PyTorch (CUDA 12.4) + segmentation-models-pytorch, albumentations, wandb - docker-compose.yml: JupyterLab on port 8001 - Notebook: 12225_UPDATE_TRAINING_fullbigleaf_optionsunetplusplus_batch2.ipynb --- segmentation/{ => general_model}/Dockerfile | 2 +- .../{ => general_model}/docker-compose.yml | 0 .../{ => general_model}/vtk_trial.ipynb | 0 ...llbigleaf_optionsunetplusplus_batch2.ipynb | 2892 +++++++++++++++++ segmentation/leaf_segmentation/Dockerfile | 45 + .../leaf_segmentation/docker-compose.yml | 13 + 6 files changed, 2951 insertions(+), 1 deletion(-) rename segmentation/{ => general_model}/Dockerfile (99%) rename segmentation/{ => general_model}/docker-compose.yml (100%) rename segmentation/{ => general_model}/vtk_trial.ipynb (100%) create mode 100644 segmentation/leaf_segmentation/12225_UPDATE_TRAINING_fullbigleaf_optionsunetplusplus_batch2.ipynb create mode 100644 segmentation/leaf_segmentation/Dockerfile create mode 100644 segmentation/leaf_segmentation/docker-compose.yml diff --git a/segmentation/Dockerfile b/segmentation/general_model/Dockerfile similarity index 99% rename from segmentation/Dockerfile rename to segmentation/general_model/Dockerfile index 5e4adf0..271db0b 100644 --- a/segmentation/Dockerfile +++ b/segmentation/general_model/Dockerfile @@ -58,5 +58,5 @@ ENV VTK_DEFAULT_EGL_DEVICE=0 # expected by entrypoint script RUN mkdir -p /alsuser /alsdata - + WORKDIR /alsuser diff --git a/segmentation/docker-compose.yml b/segmentation/general_model/docker-compose.yml similarity index 100% rename from segmentation/docker-compose.yml rename to segmentation/general_model/docker-compose.yml diff --git a/segmentation/vtk_trial.ipynb b/segmentation/general_model/vtk_trial.ipynb similarity index 100% rename from segmentation/vtk_trial.ipynb rename to segmentation/general_model/vtk_trial.ipynb diff --git a/segmentation/leaf_segmentation/12225_UPDATE_TRAINING_fullbigleaf_optionsunetplusplus_batch2.ipynb b/segmentation/leaf_segmentation/12225_UPDATE_TRAINING_fullbigleaf_optionsunetplusplus_batch2.ipynb new file mode 100644 index 0000000..afa579e --- /dev/null +++ b/segmentation/leaf_segmentation/12225_UPDATE_TRAINING_fullbigleaf_optionsunetplusplus_batch2.ipynb @@ -0,0 +1,2892 @@ +{ + "cells": [ + { + "cell_type": "raw", + "metadata": { + "id": "wVf81n_-qGAe" + }, + "source": [ + "# ***Welcome to the Training and Inference Pipeline ***\n", + "\n", + "# ***Step 1: Mount google drive***\n", + "\n", + "# ***Input code from google to access your drive***\n", + "\n", + "*** Consider running your instance locally. This will require modification of the file name paths, but will allow use on computers with more resources.***\n", + "\n", + "### *To run a local instance:*\n", + "\n", + "jupyter notebook --NotebookApp.allow_origin='https://colab.research.google.com' --port=8080 --NotebookApp.port_retries=0" + ] + }, + { + "cell_type": "code", + "source": [], + "metadata": { + "id": "JvU-ANS1rp-W" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "source": [], + "metadata": { + "id": "ggWo7e92rG5t" + } + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "_-bM_bUM1xUm" + }, + "outputs": [], + "source": [ + "import os\n", + "os.environ[\"CUDA_VISIBLE_DEVICES\"] = \"0\"\n", + "import torch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "9wIucgH-C0od", + "outputId": "4998043b-ab43-415f-e987-bb8f7c95ab1b", + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Thu Dec 4 14:20:03 2025 \n", + "+-----------------------------------------------------------------------------------------+\n", + "| NVIDIA-SMI 550.163.01 Driver Version: 550.163.01 CUDA Version: 12.4 |\n", + "|-----------------------------------------+------------------------+----------------------+\n", + "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |\n", + "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |\n", + "| | | MIG M. |\n", + "|=========================================+========================+======================|\n", + "| 0 NVIDIA A100-SXM4-80GB On | 00000000:03:00.0 Off | 0 |\n", + "| N/A 28C P0 61W / 500W | 1MiB / 81920MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "| 1 NVIDIA A100-SXM4-80GB On | 00000000:41:00.0 Off | 0 |\n", + "| N/A 27C P0 62W / 500W | 1MiB / 81920MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "| 2 NVIDIA A100-SXM4-80GB On | 00000000:82:00.0 Off | 0 |\n", + "| N/A 28C P0 60W / 500W | 1MiB / 81920MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + "| 3 NVIDIA A100-SXM4-80GB On | 00000000:C1:00.0 Off | 0 |\n", + "| N/A 26C P0 60W / 500W | 1MiB / 81920MiB | 0% Default |\n", + "| | | Disabled |\n", + "+-----------------------------------------+------------------------+----------------------+\n", + " \n", + "+-----------------------------------------------------------------------------------------+\n", + "| Processes: |\n", + "| GPU GI CI PID Type Process name GPU Memory |\n", + "| ID ID Usage |\n", + "|=========================================================================================|\n", + "| No running processes found |\n", + "+-----------------------------------------------------------------------------------------+\n" + ] + } + ], + "source": [ + "#Code Box 2\n", + "!nvidia-smi" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "MTtktpgSWaQ6" + }, + "source": [ + "#**Materials**\n", + " Input the material mask name and information below.\n", + "\n", + " Specifically:\n", + "\n", + " **name** - The name for the material. This is pretty arbitrary, but it will be\n", + " used to label output folders and images.\n", + "\n", + " **input_rbg_vals** - The rbg values of the material in the input mask image.\n", + "\n", + " **output_val** - The greyscale value of the mask when you output the images.\n", + " This is arbitrary, but every material should have its own output color\n", + " so they can be differentiated\n", + "\n", + " **confidence_threshold** - The lower this number, the more voxels will be labled a specific material. Essentially, the ML algorith outptus a confdience value (centered on 0.5) for every voxel and every material. By default, voxels with a confidence of 0.5 or greater are determined to be the material in question. But we can labled voxles with a lower condience level by changing this parameter\n", + " \n", + " **training_image_directory /training_mask_directory**: Input the directory where your training images and masks are located.\n", + "\n", + " **validation_fraction**: Input the fraction of images you want to validate your model during training. These are not a independent validation, but are part of the training process.\n", + "\n", + " **num_models**: Enter the number of models you want to iteratively train. Because these are statistical models, the performance of any given model will vary. Training more models will allow you to select the model that best fits your data.\n", + " \n", + " **num_epochs**: Enter number of epochs that you want to use to train your model. More is generally better, but takes more time.\n", + "\n", + " **batch_size**: Input your batch size. Larger batch sizes allow for faster training, but take up more VRAM. If you are running out of VRAM during training, decrease your batch size.\n", + "\n", + " **scale**: Input how you want your images scaled during model training and inference. When the scale is 1, your images will be used at full size for training. When the scale is less than 1, your images will be downsized according to the scale you set for training and inference, decreasing VRAM usage. If you run out of VRAM during training, consider rescaling your images.\n", + "\n", + " **models_directory**: Directory where your models are saved.\n", + "\n", + " **model_group**: Name for the group models you iteratively generate.\n", + "\n", + " **current_model_name**: Name for each individual model you generate; will automatically be labeled 1 through n for the number of models you specify above.\n", + "\n", + " **val_images/val_masks**: Input the directory where your independent validation images and masks are located. These images are not used for training and are used as an independent validation of your model.\n", + "\n", + " **csv_directory**: Directory where a CSV file of your validation results will be saved.\n", + "\n", + " **inference_directory**: Directory where the images you want analyzed are located.\n", + "\n", + " **output_directory**: Directory where you want your analysis results to be saved.\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "l8dr3wwmq9RT", + "outputId": "d0806513-0c95-4d2f-d026-c497a473b46a", + "tags": [] + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/pscratch/sd/d/drippner/walnut leaves/General Model Start Images_unetplusplus efficientnetb4 200 epoch batchsize 2 scale point8 12225 11326 test/\n" + ] + } + ], + "source": [ + "class Material:\n", + "\n", + " def __init__(self, name, input_rgb_vals, output_val, confidence_threshold=0):\n", + " self.name = name\n", + " self.input_rgb_vals = input_rgb_vals\n", + " self.output_val = output_val\n", + " self.confidence_threshold = confidence_threshold\n", + "\n", + "#Creating a list of materials so we can iterate through it\n", + "materials = [\n", + " Material(\"background\", [170,170,170], 170 , 0.5),\n", + " Material(\"epidermis\", [85,85,85], 85, 0.6),\n", + " Material(\"mesophyll\", [0,0,0], 1, 0.4),\n", + " Material(\"air_space\", [255,255,255], 255, 0.6),\n", + " # Material(\"bundle_sheath_extension\", [152,152,152], 152, 0.5),\n", + " Material('vein', (180,180,180), 180, 0.2)\n", + " ]\n", + "\n", + "num_materials =len(materials)\n", + "#Various input/output directories\n", + "training_image_directory = \"/pscratch/sd/d/drippner/General Model Start Images/\"\n", + "training_mask_directory = \"/pscratch/sd/d/drippner/General model 5 class masks/\"\n", + "#Fraction of total annotations you want to leave for validating the model.\n", + "validation_fraction=0.2\n", + "#Model Performance varies, make multiple models to have the best chance at success.\n", + "num_models=1\n", + "#Model Performance improves with increasing epochs, to a point.\n", + "num_epochs=200\n", + "\"\"\"Increasing batch size increase model training speed, but also eats up VRAM on the GPU. Find a balance between scale and batch size\n", + "that best suits your needs\"\"\"\n", + "batch_size=2\n", + "#Decrease scale to decrease VRAM usage; if you run out of VRAM during traing, restart your runtime and down scale your images\n", + "scale=0.8\n", + "#Input model directory\n", + "models_directory = \"/pscratch/sd/d/drippner/walnut leaves/\"\n", + "#Input the name you want to use for your group of models\n", + "model_group='General Model Start Images_unetplusplus efficientnetb4 200 epoch batchsize 2 scale point8 12225/'\n", + "current_model_name = '200 epoch P'\n", + "\"\"\"Hold images/annotations in reserve to test your model performance. Use this metric to decide which model you want to use\n", + "for your data analysis\"\"\"\n", + "test_images = \"/pscratch/sd/d/drippner/walnut leaves/images/\"\n", + "test_masks= \"/pscratch/sd/d/drippner/walnut leaves/masks/\"\n", + "#test_images=\"/pscratch/sd/d/drippner/walnut leaves/bieu_train_brom/bieu_train_images_brom/\"\n", + "#test_masks=\"/pscratch/sd/d/drippner/walnut leaves/bieu_train_brom/bieu_train_masks_brom/\"\n", + "csv_directory = \"/pscratch/sd/d/drippner/walnut leaves/General Model Start Images_unetplusplus efficientnetb4 200 epoch batchsize 2 scale point8 12225 11326walnut leaves.csv\"\n", + "#Input the directory of the data you want to segment here.\n", + "# inference_directory= 'drive/MyDrive/ALS Workflow/test/New soil/'\n", + "# test_images = \"drive/MyDrive/EMSL Syngenta/Syngenta Test/Syngenta Images/\"\n", + "# inference_directory= \"drive/MyDrive/EMSL Syngenta/Syngenta Test/Syngenta Images/\"\n", + "#inference_directory=\"/pscratch/sd/d/drippner/walnut leaves/grape test/\"\n", + "#inference_directory=\"/pscratch/sd/d/drippner/walnut leaves/bieu_train_brom/bieu_train_images_brom/\"\n", + "inference_directory=\"/pscratch/sd/d/drippner/General Model Start Images/\"\n", + "\n", + "\n", + "#Input the 5 alpha-numeric characters proceding the file number of your images\n", + " #EX. Jmic3111_S0_GRID image_0.tif ----->mage_\n", + "proceeding=\"ce_\"\n", + "#Input the 4 or mor alpha-numeric characters following the file number\n", + " #EX. Jmic3111_S0_GRID image_0.tif ----->.tif\n", + "following=\".png\"\n", + "output_directory = \"/pscratch/sd/d/drippner/walnut leaves/General Model Start Images_unetplusplus efficientnetb4 200 epoch batchsize 2 scale point8 12225 11326 test/\"\n", + "print(output_directory)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "7PrmZUm1p-SF", + "outputId": "a132047b-e3e1-4f42-8e2a-ba8066f143fb" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/torchvision-0.21.0+7af6987-py3.12-linux-x86_64.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/setuptools-75.8.0-py3.12.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/pillow-11.1.0-py3.12-linux-x86_64.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0mRequirement already satisfied: albumentations in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (2.0.8)\n", + "Requirement already satisfied: numpy>=1.24.4 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from albumentations) (2.2.3)\n", + "Requirement already satisfied: scipy>=1.10.0 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from albumentations) (1.15.2)\n", + "Requirement already satisfied: PyYAML in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from albumentations) (6.0.2)\n", + "Requirement already satisfied: pydantic>=2.9.2 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from albumentations) (2.10.6)\n", + "Requirement already satisfied: albucore==0.0.24 in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (from albumentations) (0.0.24)\n", + "Requirement already satisfied: opencv-python-headless>=4.9.0.80 in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (from albumentations) (4.12.0.88)\n", + "Requirement already satisfied: stringzilla>=3.10.4 in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (from albucore==0.0.24->albumentations) (4.2.3)\n", + "Requirement already satisfied: simsimd>=5.9.2 in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (from albucore==0.0.24->albumentations) (6.5.3)\n", + "Requirement already satisfied: annotated-types>=0.6.0 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from pydantic>=2.9.2->albumentations) (0.7.0)\n", + "Requirement already satisfied: pydantic-core==2.27.2 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from pydantic>=2.9.2->albumentations) (2.27.2)\n", + "Requirement already satisfied: typing-extensions>=4.12.2 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from pydantic>=2.9.2->albumentations) (4.12.2)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install albumentations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "QmGuEG83p-SF", + "outputId": "bf0f7372-f3f3-4b0f-e4dc-b3b7ad2761e7" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Defaulting to user installation because normal site-packages is not writeable\n", + "\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/torchvision-0.21.0+7af6987-py3.12-linux-x86_64.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/setuptools-75.8.0-py3.12.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0m\u001b[33mDEPRECATION: Loading egg at /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/pillow-11.1.0-py3.12-linux-x86_64.egg is deprecated. pip 25.1 will enforce this behaviour change. A possible replacement is to use pip for package installation. Discussion can be found at https://github.com/pypa/pip/issues/12330\u001b[0m\u001b[33m\n", + "\u001b[0mRequirement already satisfied: segmentation-models-pytorch in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (0.5.0)\n", + "Requirement already satisfied: huggingface-hub>=0.24 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (0.29.1)\n", + "Requirement already satisfied: numpy>=1.19.3 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (2.2.3)\n", + "Requirement already satisfied: pillow>=8 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/pillow-11.1.0-py3.12-linux-x86_64.egg (from segmentation-models-pytorch) (11.1.0)\n", + "Requirement already satisfied: safetensors>=0.3.1 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (0.5.2)\n", + "Requirement already satisfied: timm>=0.9 in /global/homes/d/drippner/.local/perlmutter/pytorch2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (1.0.22)\n", + "Requirement already satisfied: torch>=1.8 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (2.6.0)\n", + "Requirement already satisfied: torchvision>=0.9 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/torchvision-0.21.0+7af6987-py3.12-linux-x86_64.egg (from segmentation-models-pytorch) (0.21.0+7af6987)\n", + "Requirement already satisfied: tqdm>=4.42.1 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from segmentation-models-pytorch) (4.67.1)\n", + "Requirement already satisfied: filelock in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (3.17.0)\n", + "Requirement already satisfied: fsspec>=2023.5.0 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (2024.12.0)\n", + "Requirement already satisfied: packaging>=20.9 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (24.2)\n", + "Requirement already satisfied: pyyaml>=5.1 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (6.0.2)\n", + "Requirement already satisfied: requests in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (2.32.3)\n", + "Requirement already satisfied: typing-extensions>=3.7.4.3 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from huggingface-hub>=0.24->segmentation-models-pytorch) (4.12.2)\n", + "Requirement already satisfied: setuptools in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages/setuptools-75.8.0-py3.12.egg (from torch>=1.8->segmentation-models-pytorch) (75.8.0)\n", + "Requirement already satisfied: sympy==1.13.1 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from torch>=1.8->segmentation-models-pytorch) (1.13.1)\n", + "Requirement already satisfied: networkx in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from torch>=1.8->segmentation-models-pytorch) (3.4.2)\n", + "Requirement already satisfied: jinja2 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from torch>=1.8->segmentation-models-pytorch) (3.1.5)\n", + "Requirement already satisfied: mpmath<1.4,>=1.1.0 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from sympy==1.13.1->torch>=1.8->segmentation-models-pytorch) (1.3.0)\n", + "Requirement already satisfied: MarkupSafe>=2.0 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from jinja2->torch>=1.8->segmentation-models-pytorch) (3.0.2)\n", + "Requirement already satisfied: charset-normalizer<4,>=2 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from requests->huggingface-hub>=0.24->segmentation-models-pytorch) (3.4.1)\n", + "Requirement already satisfied: idna<4,>=2.5 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from requests->huggingface-hub>=0.24->segmentation-models-pytorch) (3.10)\n", + "Requirement already satisfied: urllib3<3,>=1.21.1 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from requests->huggingface-hub>=0.24->segmentation-models-pytorch) (2.3.0)\n", + "Requirement already satisfied: certifi>=2017.4.17 in /global/common/software/nersc9/pytorch/2.6.0/lib/python3.12/site-packages (from requests->huggingface-hub>=0.24->segmentation-models-pytorch) (2025.1.31)\n", + "Note: you may need to restart the kernel to use updated packages.\n" + ] + } + ], + "source": [ + "pip install segmentation-models-pytorch" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "tags": [], + "id": "0ugrnHNop-SF" + }, + "outputs": [], + "source": [ + "# Standard Library Imports\n", + "import os\n", + "import gc\n", + "import random\n", + "import logging\n", + "import glob\n", + "import torch.distributed as dist\n", + "import torch.multiprocessing as mp\n", + "from functools import partial\n", + "from tqdm import tqdm\n", + "\n", + "# Third-party Library Imports\n", + "from torch.cuda.amp import autocast, GradScaler\n", + "import wandb\n", + "import torch\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image\n", + "import albumentations as A\n", + "import torch.nn as nn\n", + "import torchvision.transforms as transforms\n", + "from torch.utils.data import Dataset, DataLoader, DistributedSampler\n", + "from torch.nn.parallel import DistributedDataParallel as DDP\n", + "\n", + "\n", + "\n", + "# from torchvision.models.segmentation import (\n", + "# fcn_resnet101, fcn_resnet50, deeplabv3_resnet101, deeplabv3_resnet50, deeplabv3_mobilenet_v3_large\n", + "# )\n", + "# from torchvision.models.segmentation.fcn import FCNHead\n", + "# from torchvision.models.segmentation.deeplabv3 import DeepLabHead\n", + "\n", + "\n", + "\n", + "gc.enable()\n", + "logging.basicConfig(format='%(asctime)s: %(levelname)s: %(message)s\\n', level=logging.INFO)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "0F20zRhMZPpq", + "outputId": "cbceaf41-2ad2-4e69-e66e-b39c29410cf2", + "tags": [] + }, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-01-13 15:08:11,701: INFO: Creating dataset with 361 examples\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Raw Data Stats (SAVED for training):\n", + "tensor([0.3335, 0.3335, 0.3335], dtype=torch.float64)\n", + "tensor([0.1426, 0.1426, 0.1426], dtype=torch.float64)\n", + "Normalized Stats Check (Should be closer to 0/1, but affected by padding):\n", + "tensor([0.6070, 0.6070, 0.6070], dtype=torch.float64)\n", + "tensor([0.4448, 0.4448, 0.4448], dtype=torch.float64)\n" + ] + } + ], + "source": [ + "# Code Box 3: Dataset Definition with Pad Collate\n", + "from os.path import splitext\n", + "from os import listdir\n", + "import numpy as np\n", + "from glob import glob\n", + "import torch\n", + "from torch.utils.data import Dataset\n", + "import logging\n", + "from PIL import Image\n", + "import random\n", + "import albumentations as A\n", + "import matplotlib.pyplot as plt\n", + "import torchvision.transforms as transforms\n", + "import torch.nn.functional as F\n", + "from scipy.ndimage import morphology\n", + "from torch.utils.data import DataLoader, random_split\n", + "import pandas as pd\n", + "\n", + "class BasicDataset(Dataset):\n", + " def __init__(self, imgs_dir, masks_dir, scale=scale, transform=False):\n", + " self.imgs_dir = imgs_dir\n", + " self.masks_dir = masks_dir\n", + " self.scale = scale\n", + " self.transform = transform\n", + " self.t_list = A.Compose([A.HorizontalFlip(p=0.4), A.VerticalFlip(p=0.4), A.Rotate(limit=(-50, 50), p=0.4),])\n", + " self.means = [0, 0, 0]\n", + " self.stds = [1, 1, 1]\n", + "\n", + " assert 0 < scale <= 1, 'Scale must be between 0 and 1'\n", + "\n", + " self.ids = [splitext(file)[0] for file in listdir(imgs_dir)\n", + " if not file.startswith('.')]\n", + " logging.info(f'Creating dataset with {len(self.ids)} examples')\n", + "\n", + " def __len__(self):\n", + " return len(self.ids)\n", + "\n", + " @classmethod\n", + " def mask_preprocess(cls, pil_img, scale):\n", + " w, h = pil_img.size\n", + " newW, newH = int(scale * w), int(scale * h)\n", + " assert newW > 0 and newH > 0, 'Scale is too small'\n", + " pil_img = pil_img.resize((newW, newH))\n", + " img_nd = np.array(pil_img)\n", + " if len(img_nd.shape) == 2:\n", + " img_nd = np.expand_dims(img_nd, axis=2)\n", + " return img_nd\n", + "\n", + " def img_preprocess(cls, pil_img, scale):\n", + " w, h = pil_img.size\n", + " newW, newH = int(scale * w), int(scale * h)\n", + " assert newW > 0 and newH > 0, 'Scale is too small'\n", + " pil_img = pil_img.resize((newW, newH))\n", + " img_nd = np.array(pil_img)\n", + " if len(img_nd.shape) == 2:\n", + " img_nd = np.expand_dims(img_nd, axis=2)\n", + " return img_nd\n", + "\n", + " def __getitem__(self, i):\n", + " idx = self.ids[i]\n", + " mask_file = glob(self.masks_dir + idx + '*')\n", + " img_file = glob(self.imgs_dir + idx + '*')\n", + "\n", + " assert len(mask_file) == 1, \\\n", + " f'Either no mask or multiple masks found for the ID {idx}: {mask_file}'\n", + " assert len(img_file) == 1, \\\n", + " f'Either no image or multiple images found for the ID {idx}: {img_file}'\n", + " mask = Image.open(mask_file[0])\n", + " img = Image.open(img_file[0])\n", + "\n", + " # Reshapes from 1 channel to 3 channels in grayscale\n", + " img = self.img_preprocess(img, self.scale)\n", + " mask = self.mask_preprocess(mask, self.scale)\n", + " new_image = np.zeros((img.shape[0], img.shape[1], 3))\n", + " new_image[:, :, 0] = img[:, :, 0]\n", + " new_image[:, :, 1] = img[:, :, 0]\n", + " new_image[:, :, 2] = img[:, :, 0]\n", + "\n", + " img = new_image\n", + "\n", + " # New Code\n", + " masklist = []\n", + " for i, mat in enumerate(materials):\n", + " indices = np.all(mask == mat.input_rgb_vals, axis=-1)\n", + " new_mask = np.zeros((img.shape[0], img.shape[1]))\n", + " new_mask[indices] = 1\n", + " masklist.append(new_mask)\n", + "\n", + " mask = masklist\n", + "\n", + " if img.max() > 1:\n", + " img = img / 255\n", + "\n", + " if self.transform:\n", + " augmented = self.t_list(image=img, masks=mask)\n", + " img = augmented[\"image\"]\n", + " mask = augmented[\"masks\"]\n", + "\n", + " img = img.transpose((2, 0, 1))\n", + " mask = np.array(mask)\n", + "\n", + " img = torch.from_numpy(img)\n", + " mask = torch.from_numpy(mask)\n", + "\n", + " img = transforms.Normalize(mean=self.means, std=self.stds)(img)\n", + " return img, mask\n", + "\n", + "# !!!!!!!! ADDED PAD COLLATE FUNCTION !!!!!!!!\n", + "def pad_collate(batch):\n", + " \"\"\"\n", + " Pads a batch of variable-size images/masks to the largest dimensions in that batch.\n", + " \"\"\"\n", + " images = [item[0] for item in batch]\n", + " masks = [item[1] for item in batch]\n", + "\n", + " max_h = max([img.shape[1] for img in images])\n", + " max_w = max([img.shape[2] for img in images])\n", + "\n", + " images_padded = []\n", + " masks_padded = []\n", + "\n", + " for img, mask in zip(images, masks):\n", + " pad_h = max_h - img.shape[1]\n", + " pad_w = max_w - img.shape[2]\n", + "\n", + " # Pad right and bottom (left, right, top, bottom)\n", + " img_p = F.pad(img, (0, pad_w, 0, pad_h), value=0)\n", + " mask_p = F.pad(mask, (0, pad_w, 0, pad_h), value=0)\n", + "\n", + " images_padded.append(img_p)\n", + " masks_padded.append(mask_p)\n", + "\n", + " return torch.stack(images_padded), torch.stack(masks_padded)\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + "dataset = BasicDataset(training_image_directory, training_mask_directory, scale=scale, transform=False)\n", + "\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!Set batch size here!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "train_loader = DataLoader(dataset, batch_size=batch_size, shuffle=False, num_workers=0, pin_memory=True, collate_fn=pad_collate)\n", + "\n", + "# --- PASS 1: Calculate Raw Stats ---\n", + "nimages = 0\n", + "mean = 0.\n", + "std = 0.\n", + "for batch, _ in train_loader:\n", + " batch = batch.view(batch.size(0), batch.size(1), -1)\n", + " nimages += batch.size(0)\n", + " mean += batch.mean(2).sum(0)\n", + " std += batch.std(2).sum(0)\n", + "\n", + "mean /= nimages\n", + "std /= nimages\n", + "\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "# SAVE THESE GLOBALLY FOR CODE BOX 4\n", + "training_mean = mean\n", + "training_std = std\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + "print(\"Raw Data Stats (SAVED for training):\")\n", + "print(training_mean)\n", + "print(training_std)\n", + "\n", + "# Update dataset for verification pass\n", + "dataset.means = training_mean\n", + "dataset.stds = training_std\n", + "\n", + "# --- PASS 2: Verify Normalization (Do not overwrite training_mean) ---\n", + "nimages = 0\n", + "ver_mean = 0.\n", + "ver_std = 0.\n", + "for batch, _ in train_loader:\n", + " batch = batch.view(batch.size(0), batch.size(1), -1)\n", + " nimages += batch.size(0)\n", + " ver_mean += batch.mean(2).sum(0)\n", + " ver_std += batch.std(2).sum(0)\n", + "\n", + "ver_mean /= nimages\n", + "ver_std /= nimages\n", + "\n", + "print(\"Normalized Stats Check (Should be closer to 0/1, but affected by padding):\")\n", + "print(ver_mean)\n", + "print(ver_std)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "Qamo0PVkJj78" + }, + "source": [ + "Now you are ready to train your model in code block 4.\n", + "If you run out of VRAM, restart the runtime, reload google drive, and try again. Also consider rescaling your images or decreasing your batch size.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "4YVqggi2Wwzl" + }, + "source": [ + "#**Model** **Training**\n", + "Please do not alter this code." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "zzhumQbzp-SG", + "outputId": "488e834c-2bbe-4042-dae1-c1d66e742599" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training Model 1/2\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2025-12-03 16:34:33,683: INFO: Creating dataset with 361 examples\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Auto-Link Success! Using calculated stats from Box 3.\n", + "Mean: tensor([0.3335, 0.3335, 0.3335], dtype=torch.float64), Std: tensor([0.1426, 0.1426, 0.1426], dtype=torch.float64)\n", + "RAM Used: 11.1%\n", + "Epoch: 0\n", + "Train loss: 0.5984659194946289\n", + "Train loss: 0.6104778051376343\n", + "Train loss: 0.5973512530326843\n", + "Train loss: 0.6028390526771545\n", + "Train loss: 0.6046233773231506\n", + "Train loss: 0.5851494073867798\n", + "Train loss: 0.5907465815544128\n", + "Train loss: 0.5924205780029297\n", + "Train loss: 0.571448564529419\n", + "Train loss: 0.5772271752357483\n", + "Train loss: 0.5729486346244812\n", + "Train loss: 0.5629420280456543\n", + "Train loss: 0.5760489106178284\n", + "Train loss: 0.5524650812149048\n", + "Train loss: 0.5691994428634644\n", + "Train loss: 0.555388331413269\n", + "Train loss: 0.5488037467002869\n", + "Train loss: 0.5697166919708252\n", + "Train loss: 0.5527728796005249\n", + "Train loss: 0.5478957891464233\n", + "Train loss: 0.5485062599182129\n", + "Train loss: 0.5395074486732483\n", + "Train loss: 0.5614256858825684\n", + "Train loss: 0.5559452176094055\n", + "Train loss: 0.5395122766494751\n", + "Train loss: 0.5564334988594055\n", + "Train loss: 0.5569146275520325\n", + "Train loss: 0.54538494348526\n", + "Train loss: 0.5364524722099304\n", + "Train loss: 0.5607620477676392\n", + "Train loss: 0.5302472114562988\n", + "Train loss: 0.535891592502594\n", + "Train loss: 0.5293347239494324\n", + "Train loss: 0.5261389017105103\n", + "Train loss: 0.5352516174316406\n", + "Train loss: 0.523641049861908\n", + "Train loss: 0.5270371437072754\n", + "Train loss: 0.5285115838050842\n", + "Train loss: 0.533044159412384\n", + "Train loss: 0.5195791125297546\n", + "Train loss: 0.5294498205184937\n", + "Train loss: 0.5289150476455688\n", + "Train loss: 0.5274163484573364\n", + "Train loss: 0.5264512300491333\n", + "Train loss: 0.518023669719696\n", + "Train loss: 0.5210024118423462\n", + "Train loss: 0.5425032377243042\n", + "Train loss: 0.514063835144043\n", + "Train loss: 0.512790322303772\n", + "Train loss: 0.5384300351142883\n", + "Train loss: 0.5164257884025574\n", + "Train loss: 0.5109089612960815\n", + "Train loss: 0.5125555396080017\n", + "Train loss: 0.5143252015113831\n", + "Train loss: 0.5097662806510925\n", + "Train loss: 0.5430392026901245\n", + "Train loss: 0.5098984837532043\n", + "Train loss: 0.5248419642448425\n", + "Train loss: 0.5061163902282715\n", + "Train loss: 0.4998232126235962\n", + "Train loss: 0.5279181003570557\n", + "Train loss: 0.504570722579956\n", + "Train loss: 0.5158320069313049\n", + "Train loss: 0.5070858001708984\n", + "Train loss: 0.4999786913394928\n", + "Train loss: 0.5039259195327759\n", + "Train loss: 0.5117713212966919\n", + "Train loss: 0.5020225048065186\n", + "Train loss: 0.5075755715370178\n", + "Train loss: 0.49450230598449707\n", + "Train loss: 0.5244110822677612\n", + "Train loss: 0.5151591897010803\n", + "Train loss: 0.5041654706001282\n", + "Train loss: 0.5300232172012329\n", + "Train loss: 0.5137308835983276\n", + "Train loss: 0.5149857997894287\n", + "Train loss: 0.5220301747322083\n", + "Train loss: 0.5002567172050476\n", + "Train loss: 0.510103702545166\n", + "Train loss: 0.5056576728820801\n", + "Train loss: 0.49747955799102783\n", + "Train loss: 0.4950031638145447\n", + "Train loss: 0.5018643140792847\n" + ] + } + ], + "source": [ + "# Code Box 4: Training with UNet++ & Focal/Jaccard Loss (Medical/Rare Class config)\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "# RUN THIS LINE ONCE to install the library if you haven't already\n", + "# !pip install segmentation-models-pytorch\n", + "# !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + "import segmentation_models_pytorch as smp\n", + "import torchvision\n", + "from torch.utils.data import DataLoader, random_split\n", + "import torch\n", + "import torchvision.transforms as T\n", + "import matplotlib.pyplot as plt\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "import os\n", + "import psutil\n", + "import gc\n", + "import random\n", + "import numpy as np\n", + "\n", + "# --- Main Training Logic ---\n", + "\n", + "dir_checkpoint = models_directory\n", + "model_group = model_group\n", + "num_models = num_models\n", + "\n", + "# Ensure directory exists\n", + "if not os.path.exists(dir_checkpoint + model_group):\n", + " os.mkdir(dir_checkpoint + model_group)\n", + "\n", + "seed = 0\n", + "torch.manual_seed(seed)\n", + "random.seed(seed)\n", + "np.random.seed(seed)\n", + "\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "for i in range(num_models):\n", + " print(f\"Training Model {i+1}/{num_models}\")\n", + "\n", + " # !!!!!!!! MODEL: UNet++ with resnet101 !!!!!!!!\n", + " model = smp.UnetPlusPlus(\n", + " encoder_name=\"efficientnet-b4\",\n", + " encoder_weights=None, # NERSC / No Internet\n", + " in_channels=3,\n", + " classes=num_materials\n", + " )\n", + " model.to(device)\n", + "\n", + " # --- Split Data ---\n", + " def trainval_split(dataset, val_fraction=validation_fraction):\n", + " validation_size = int(len(dataset) * val_fraction)\n", + " train_size = len(dataset) - validation_size\n", + " train, val = torch.utils.data.random_split(\n", + " dataset,\n", + " [train_size, validation_size],\n", + " generator=torch.Generator().manual_seed(i)\n", + " )\n", + " return train, val\n", + "\n", + " # Initialize Dataset (Uses class from Code Box 3)\n", + " dataset = BasicDataset(training_image_directory, training_mask_directory, scale=scale, transform=True)\n", + "\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + " # AUTO-LINKING: Use 'training_mean' from Code Box 3 if it exists\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + " if 'training_mean' in globals() and 'training_std' in globals():\n", + " print(f\"Auto-Link Success! Using calculated stats from Box 3.\")\n", + " print(f\"Mean: {training_mean}, Std: {training_std}\")\n", + " dataset.means = training_mean\n", + " dataset.stds = training_std\n", + " else:\n", + " print(\"WARNING: 'training_mean' not found. Defaulting to [0,0,0].\")\n", + " print(\"Did you run Code Box 3?\")\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " dataset_train, dataset_val = trainval_split(dataset, val_fraction=validation_fraction)\n", + "\n", + " # --- DataLoaders ---\n", + " train_loader = DataLoader(dataset_train, batch_size=batch_size, shuffle=True,\n", + " num_workers=0, pin_memory=True, drop_last=True,\n", + " collate_fn=pad_collate)\n", + "\n", + " val_loader = DataLoader(dataset_val, batch_size=batch_size, shuffle=False,\n", + " num_workers=0, pin_memory=True,\n", + " collate_fn=pad_collate)\n", + "\n", + " # Input epochs here\n", + " num_epochs = num_epochs\n", + "\n", + " # Optimizer\n", + " optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)\n", + "\n", + " # Initialize best_loss\n", + " best_loss = 999\n", + "\n", + " # !!!!!!!! HYBRID LOSS FUNCTION !!!!!!!!\n", + " loss_focal = smp.losses.FocalLoss(mode=\"multilabel\")\n", + " loss_jaccard = smp.losses.JaccardLoss(mode=\"multilabel\", smooth=1.0)\n", + "\n", + " def criterion(preds, masks):\n", + " return 0.5 * loss_focal(preds, masks) + 0.5 * loss_jaccard(preds, masks)\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " # --- Training Loop ---\n", + " for epoch in range(num_epochs):\n", + " print(f\"RAM Used: {psutil.virtual_memory().percent}%\")\n", + " print('Epoch: ', str(epoch))\n", + "\n", + " # ================= TRAINING PHASE =================\n", + " train_loader.dataset.dataset.transform = True\n", + " model.train()\n", + "\n", + " for images, masks in train_loader:\n", + " images = images.to(device=device, dtype=torch.float32)\n", + " masks = masks.to(device=device, dtype=torch.float32)\n", + "\n", + " # !!!!!!!! FIX: PAD TO MULTIPLE OF 32 !!!!!!!!\n", + " h, w = images.shape[2], images.shape[3]\n", + " target_h = ((h - 1) // 32 + 1) * 32\n", + " target_w = ((w - 1) // 32 + 1) * 32\n", + "\n", + " pad_bottom = target_h - h\n", + " pad_right = target_w - w\n", + "\n", + " if pad_bottom > 0 or pad_right > 0:\n", + " images = F.pad(images, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " masks = F.pad(masks, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " # Forward pass\n", + " preds = model(images)\n", + "\n", + " # Compute loss\n", + " loss = criterion(preds, masks)\n", + "\n", + " # Backprop\n", + " optimizer.zero_grad()\n", + " loss.backward()\n", + " optimizer.step()\n", + "\n", + " print('Train loss: ' + str(loss.item()))\n", + "\n", + " # ================= VALIDATION PHASE =================\n", + " val_loader.dataset.dataset.transform = False\n", + " current_loss = 0\n", + "\n", + " model.eval()\n", + " with torch.no_grad():\n", + " for images, masks in val_loader:\n", + " images = images.to(device=device, dtype=torch.float32)\n", + " masks = masks.to(device=device, dtype=torch.float32)\n", + "\n", + " # !!!!!!!! FIX: PAD TO MULTIPLE OF 32 !!!!!!!!\n", + " h, w = images.shape[2], images.shape[3]\n", + " target_h = ((h - 1) // 32 + 1) * 32\n", + " target_w = ((w - 1) // 32 + 1) * 32\n", + "\n", + " pad_bottom = target_h - h\n", + " pad_right = target_w - w\n", + "\n", + " if pad_bottom > 0 or pad_right > 0:\n", + " images = F.pad(images, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " masks = F.pad(masks, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " preds = model(images)\n", + "\n", + " loss = criterion(preds, masks)\n", + " current_loss += loss.item()\n", + "\n", + " del images, masks, preds, loss\n", + "\n", + " # Save Best Model\n", + " if best_loss > current_loss:\n", + " best_loss = current_loss\n", + " print('Best Model Saved!, loss: ' + str(best_loss))\n", + " torch.save(model.state_dict(), dir_checkpoint + model_group + current_model_name + str(i + 1) + \".pth\")\n", + " else:\n", + " print('Model is bad!, Current loss: ' + str(current_loss) + ' Best loss: ' + str(best_loss))\n", + " print('\\n')" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "WoZMPaNMWiFh" + }, + "source": [ + "# **Validation**\n", + "Please do not alter this code\n", + "\n", + "Something fucked up is happening when we get nan values; basically, with 0 predictions and 0 occurances of a material, we get nan. This than makes the average nan." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "aJtCfl_Hp-SH" + }, + "source": [ + "# **Validation**\n", + "Please do not alter this code\n", + "\n", + "Something fucked up is happening when we get nan values; basically, with 0 predictions and 0 occurances of a material, we get nan. This than makes the average nan." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "cb7lg24up-SH", + "outputId": "bbd50801-069e-4204-d023-8108ad0cd4c0" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading model: 200 epoch P1\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2026-01-13 15:10:42,460: INFO: Creating dataset with 56 examples\n", + "\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Validation Auto-Link: Using training stats -> Mean: tensor([0.3335, 0.3335, 0.3335], dtype=torch.float64), Std: tensor([0.1426, 0.1426, 0.1426], dtype=torch.float64)\n", + "Processed: 200 epoch P 1\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nameprecisionrecallaccuracyf1model numbermodel name
0background0.97670160.97861960.9885360.97757611200 epoch P
1epidermis0.75166410.80534550.9764910.77495891200 epoch P
2mesophyll0.858371260.90823110.94894070.881469251200 epoch P
3air_space0.82800570.8851930.97590250.852353451200 epoch P
4vein0.90058870.890018050.97261680.893121061200 epoch P
\n", + "
" + ], + "text/plain": [ + " name precision recall accuracy f1 model number \\\n", + "0 background 0.9767016 0.9786196 0.988536 0.9775761 1 \n", + "1 epidermis 0.7516641 0.8053455 0.976491 0.7749589 1 \n", + "2 mesophyll 0.85837126 0.9082311 0.9489407 0.88146925 1 \n", + "3 air_space 0.8280057 0.885193 0.9759025 0.85235345 1 \n", + "4 vein 0.9005887 0.89001805 0.9726168 0.89312106 1 \n", + "\n", + " model name \n", + "0 200 epoch P \n", + "1 200 epoch P \n", + "2 200 epoch P \n", + "3 200 epoch P \n", + "4 200 epoch P " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Code Box 5: Evaluation Metrics (Compatible with UNet++, EfficientNet, and pad_collate)\n", + "import torch.nn as nn\n", + "import torchvision\n", + "import torch\n", + "import torch.nn.functional as F\n", + "from torch.utils.data import DataLoader, random_split\n", + "import numpy as np\n", + "import pandas as pd\n", + "import segmentation_models_pytorch as smp\n", + "\n", + "# Initialize DataFrame\n", + "modeldata = pd.DataFrame(columns=[\"name\", \"precision\", \"recall\", \"accuracy\", \"f1\"])\n", + "\n", + "for s in range(num_models):\n", + "\n", + " # !!!!!!!! MODEL INITIALIZATION (Must match Code Box 4) !!!!!!!!\n", + " model = smp.UnetPlusPlus(\n", + " encoder_name=\"efficientnet-b4\",\n", + " encoder_weights=None, # NERSC / No Internet\n", + " in_channels=3,\n", + " classes=num_materials\n", + " )\n", + "\n", + " device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + " model.to(device)\n", + "\n", + " # !!!!!!!!!!!!!!!!!!!!!Select Correct Model from the best models directory!!!!!!!!!!!!!!!!!!!!!!!!!\n", + " print(f\"Loading model: {current_model_name}{str(s+1)}\")\n", + " path = models_directory + model_group + current_model_name + str(s+1) + \".pth\"\n", + " model.load_state_dict(torch.load(path))\n", + "\n", + " # Set to eval() mode\n", + " model.eval()\n", + "\n", + " # !!!!!!!! DATALOADER SETUP !!!!!!!!\n", + " dataset_val = BasicDataset(test_images, test_masks, scale=scale, transform=False)\n", + "\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + " # AUTO-LINKING: Use 'training_mean' from Code Box 3 if it exists\n", + " # This ensures Validation sees the exact same colors/scaling as Training\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + " if 'training_mean' in globals() and 'training_std' in globals():\n", + " print(f\"Validation Auto-Link: Using training stats -> Mean: {training_mean}, Std: {training_std}\")\n", + " dataset_val.means = training_mean\n", + " dataset_val.stds = training_std\n", + " else:\n", + " print(\"WARNING: 'training_mean' not found! Using defaults [0,0,0]. Evaluation results may be incorrect.\")\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " # We add collate_fn=pad_collate to handle variable image sizes in the batch\n", + " val_loader = DataLoader(dataset_val, batch_size=batch_size, shuffle=False,\n", + " num_workers=0, pin_memory=True,\n", + " collate_fn=pad_collate) # <--- IMPORTANT: Matches Code Box 3/4\n", + "\n", + " prop_list = []\n", + " for mat in materials:\n", + " prop_list.append([[],[],[],[]])\n", + "\n", + " # Disable gradient calculation\n", + " with torch.no_grad():\n", + " for images, target in val_loader:\n", + " images = images.to(device=device, dtype=torch.float32)\n", + " target = target.to(device=device, dtype=torch.float32)\n", + "\n", + " # 1. Calculate Padding (Multiple of 32 for EfficientNet/UNet++)\n", + " h, w = images.shape[2], images.shape[3]\n", + " target_h = ((h - 1) // 32 + 1) * 32\n", + " target_w = ((w - 1) // 32 + 1) * 32\n", + "\n", + " pad_bottom = target_h - h\n", + " pad_right = target_w - w\n", + "\n", + " # 2. Pad Input Image\n", + " if pad_bottom > 0 or pad_right > 0:\n", + " images_padded = F.pad(images, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " else:\n", + " images_padded = images\n", + "\n", + " # 3. Inference\n", + " pred = model(images_padded)\n", + "\n", + " # 4. Crop Prediction back to Original Size\n", + " # This removes the padding so we compare apples-to-apples with the target\n", + " pred = pred[:, :, :h, :w]\n", + "\n", + " pred = nn.Sigmoid()(pred)\n", + "\n", + " for i, mat in enumerate(materials):\n", + " material_target = target[:,i,:,:]\n", + "\n", + " material_pred = pred[:, i, :, :].clone()\n", + "\n", + " # Thresholding\n", + " material_pred[material_pred >= mat.confidence_threshold] = 1\n", + " material_pred[material_pred < mat.confidence_threshold] = 0\n", + "\n", + " # Metrics Calculation\n", + " # Summing over height/width dims 1,2\n", + " material_tp = torch.sum(material_target * material_pred, (1,2))\n", + " material_fp = torch.sum((1-material_target) * material_pred, (1,2))\n", + " material_fn = torch.sum(material_target * (1-material_pred), (1,2))\n", + " material_tn = torch.sum((1-material_target) * (1-material_pred), (1,2))\n", + "\n", + " eps = 1e-9\n", + "\n", + " material_precision = torch.mean((material_tp + eps) / (material_tp + material_fp + eps))\n", + " material_recall = torch.mean((material_tp + eps) / (material_tp + material_fn + eps))\n", + " material_accuracy = torch.mean((material_tp + material_tn + eps) / (material_tp + material_tn + material_fp + material_fn + eps))\n", + " material_f1 = torch.mean((material_tp + eps) / (material_tp + eps + 0.5 * (material_fp + material_fn)))\n", + "\n", + " prop_list[i][0].append(material_precision.cpu().detach().numpy())\n", + " prop_list[i][1].append(material_recall.cpu().detach().numpy())\n", + " prop_list[i][2].append(material_accuracy.cpu().detach().numpy())\n", + " prop_list[i][3].append(material_f1.cpu().detach().numpy())\n", + "\n", + " model_name = current_model_name\n", + " model_number = (str(s+1))\n", + " print(f\"Processed: {model_name} {model_number}\")\n", + "\n", + " # printing with pandas\n", + " properties = {\n", + " \"name\" : [mat.name for mat in materials],\n", + " \"precision\" : [str(np.mean(prop_list[i][0])) for i in range(num_materials)],\n", + " \"recall\" : [str(np.mean(prop_list[i][1])) for i in range(num_materials)],\n", + " \"accuracy\" : [str(np.mean(prop_list[i][2])) for i in range(num_materials)],\n", + " \"f1\" : [str(np.mean(prop_list[i][3])) for i in range(num_materials)]\n", + " }\n", + "\n", + " df = pd.DataFrame(properties, columns = [\"name\", \"precision\", \"recall\", \"accuracy\", \"f1\"])\n", + "\n", + " df_meta = pd.DataFrame(columns=[\"model number\", \"model name\"])\n", + " df = pd.concat([df, df_meta], axis=1)\n", + " df[\"model number\"] = model_number\n", + " df[\"model name\"] = model_name\n", + "\n", + " modeldata = modeldata._append([df], ignore_index=True)\n", + "\n", + "display(modeldata)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "IZmEjhTJ6Qyb" + }, + "source": [ + "#**Save Validation CSV**\n", + "Please do not alter this code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/", + "height": 1000 + }, + "id": "J2uTuPAtHsMm", + "outputId": "f9b693b5-cf56-4c81-9c68-cb8ce34c0f27" + }, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
nameprecisionrecallaccuracyf1model numbermodel name
0background0.97670160.97861960.9885360.97757611200 epoch P
1epidermis0.75166410.80534550.9764910.77495891200 epoch P
2mesophyll0.858371260.90823110.94894070.881469251200 epoch P
3air_space0.82800570.8851930.97590250.852353451200 epoch P
4vein0.90058870.890018050.97261680.893121061200 epoch P
\n", + "
" + ], + "text/plain": [ + " name precision recall accuracy f1 model number \\\n", + "0 background 0.9767016 0.9786196 0.988536 0.9775761 1 \n", + "1 epidermis 0.7516641 0.8053455 0.976491 0.7749589 1 \n", + "2 mesophyll 0.85837126 0.9082311 0.9489407 0.88146925 1 \n", + "3 air_space 0.8280057 0.885193 0.9759025 0.85235345 1 \n", + "4 vein 0.9005887 0.89001805 0.9726168 0.89312106 1 \n", + "\n", + " model name \n", + "0 200 epoch P \n", + "1 200 epoch P \n", + "2 200 epoch P \n", + "3 200 epoch P \n", + "4 200 epoch P " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "display(modeldata)\n", + "modeldata.to_csv(csv_directory)" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "GDdtRtlaW8fX" + }, + "source": [ + "#**Load Model**\n", + "**model_number**: Select the best model number above and input into the model_number line. Dont forget the quotation marks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "1CAux88lvcxS" + }, + "outputs": [], + "source": [ + "\"\"\"Input model number here\"\"\"\n", + "model_number='1'" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "EUB79IBEvg5m" + }, + "source": [ + "#Run" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "id": "IdM4pMb3p-SH", + "outputId": "4a5a3d89-42c4-4a94-8647-389a42a2159b" + }, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loading model weights: 200 epoch P1\n", + "Checking Inference Directory: /pscratch/sd/d/drippner/General Model Start Images/\n", + "Found 362 files in directory.\n", + "\n", + "Processing Image 1: LaCa1_Bottom_Slide_90.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 2: 18_118-3_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 3: LaCa18_12h_Slide_364.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 4: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 5: 260_R2_Oct_slice_5.png\n", + "Original Size: 1527x471\n", + "\n", + "Processing Image 6: 377_R4_Oct_slice_304.png\n", + "Original Size: 1500x405\n", + "\n", + "Processing Image 7: LaCa21_Slide_208.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 8: 20_152-3_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 9: LaCa3_5x_Slide_331.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 10: 11_LA716-2_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 11: LaCa23_5m_Slide_436.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 12: LaCa15_5m_Slide_501.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 13: 24_LA716-4_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 14: LaCa21_Slide_162.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 15: LaCa18_12h_Slide_408.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 16: 06_152-1_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 17: LaCa20_1h_Slide_44.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 18: 20230520_120251_ARTH_15_zone2_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 19: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 20: LaCa12_12h_Slide_127.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 21: LaCa4_Slide_470.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 22: LaCa3_5x_Slide_275.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 23: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 24: 16_M82-3_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 25: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 26: 386_R2_Oct_slice_4.png\n", + "Original Size: 1434x486\n", + "\n", + "Processing Image 27: LaCa17_5m_Slide_50.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 28: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 29: 542_R2_Oct_slice_4.png\n", + "Original Size: 2250x402\n", + "\n", + "Processing Image 30: 20230520_105635_Col0_2B_zone3_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 31: 20230520_105635_Col0_2B_zone3_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 32: LaCa5_5x_Slide_212.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 33: slice_23.tif\n", + "Original Size: 1568x636\n", + "\n", + "Processing Image 34: LaCa18_Slide_251.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 35: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 36: LaCa1_Bottom_Slide_267.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 37: 377_R4_Oct_slice_401.png\n", + "Original Size: 1500x405\n", + "\n", + "Processing Image 38: LaCa13_1h_Slide 248.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 39: 33_130-5_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 40: 21_130-3_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 41: LaCa13_Slide_243.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 42: LaCa16_1h_Slide_126.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 43: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 44: 542_R2_Sep_slice_2.png\n", + "Original Size: 1902x450\n", + "\n", + "Processing Image 45: 542_R1_Sep_slice_3.png\n", + "Original Size: 2163x457\n", + "\n", + "Processing Image 46: LaCa19_Slide_275.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 47: LaCa5_5x_Slide_419.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 48: LaCa13_1h_Slide 169.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 49: LaCa14_1h_Slide_50.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 50: LaCa3_Slide_105.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 51: 15_144-2_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 52: 260_R1_Oct_slice_6.png\n", + "Original Size: 2418x414\n", + "\n", + "Processing Image 53: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 54: 542_R4_Oct_slice_5.png\n", + "Original Size: 1443x417\n", + "\n", + "Processing Image 55: 377_R4_Oct_slice_148.png\n", + "Original Size: 1500x405\n", + "\n", + "Processing Image 56: 20220324_123957_ARTH-COL-0-3_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 57: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 58: 260_R1_Sep_slice_2.png\n", + "Original Size: 2223x441\n", + "\n", + "Processing Image 59: 26_152-4_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 60: LaCa12_Slide_125.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 61: LaCa22_5m_Slide_55.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 62: LaCa4_Slide_11.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 63: LaCa16_1h_Slide_140.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 64: LaCa11_12h_Slide_391.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 65: LaCa8_Slide_488.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 66: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 67: LaCa8_Slide_24.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 68: 260_R1_Oct_slice_5.png\n", + "Original Size: 2418x414\n", + "\n", + "Processing Image 69: Copy of 20220324_125023_ARTH-COL-0-4_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 70: LaCa1_Top_Slide_116.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 71: LaCa15_5m_Slide_265.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 72: LaCa12_Slide_50.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 73: LaCa11_12h_Slide_151.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 74: 20230520_120251_ARTH_15_zone2_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 75: LaCa16_1h_Slide_284.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 76: 542_R2_Sep_slice_4.png\n", + "Original Size: 1902x450\n", + "\n", + "Processing Image 77: LaCa12_Slide_313.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 78: LaCa20_1h_Slide_144.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 79: 14_130-2_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 80: 411_R3_Sep_slice_4.png\n", + "Original Size: 1566x474\n", + "\n", + "Processing Image 81: LaCa22_5m_Slide_98.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 82: 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 83: LaCa3_Slide_11.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 84: LaCa8_12h_Slide_56.png\n", + "Original Size: 1024x444\n", + "\n", + "Processing Image 85: LaCa15_5m_Slide_293.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 86: 24_LA716-4_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 87: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 88: LaCa1_Bottom_Slide_25.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 89: LaCa2_Slide_283.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 90: LaCa12_12h_Slide_146.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 91: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 92: LaCa8_Slide_297.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 93: LaCa8_12h_Slide_125.png\n", + "Original Size: 1024x444\n", + "\n", + "Processing Image 94: 260_R2_Oct_slice_2.png\n", + "Original Size: 1527x471\n", + "\n", + "Processing Image 95: 542_R1_Sep_slice_1.png\n", + "Original Size: 2163x457\n", + "\n", + "Processing Image 96: LaCa21_Slide_85.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 97: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 98: LaCa3_Slide_303.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 99: LaCa1_Top_Slide_27.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 100: LaCa4_Slide_412.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 101: 11_LA716-2_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 102: LaCa14_1h_Slide_382.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 103: LaCa1_Top_Slide_285.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 104: 30_LA716-5_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 105: LaCa13_1h_Slide 348.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 106: LaCa1_Bottom_Slide_340.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 107: LaCa19_Slide_401.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 108: LaCa5_5x_Slide_120.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 109: 260_R2_Oct_slice_4.png\n", + "Original Size: 1527x471\n", + "\n", + "Processing Image 110: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 111: LaCa23_5m_Slide_144.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 112: slice_19.tif\n", + "Original Size: 1568x636\n", + "\n", + "Processing Image 113: LaCa3_Slide_470.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 114: 12_118-2_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 115: LaCa3_5x_Slide_113.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 116: LaCa17_5m_Slide_110.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 117: 386_R2_Oct_slice_1.png\n", + "Original Size: 1434x486\n", + "\n", + "Processing Image 118: LaCa3_Slide_199.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 119: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 120: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 121: 411_R3_Oct_slice_2.png\n", + "Original Size: 1674x530\n", + "\n", + "Processing Image 122: LaCa22_5m_Slide_398.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 123: LaCa5_1h_Slide_57.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 124: slice_18.tif\n", + "Original Size: 1436x658\n", + "\n", + "Processing Image 125: LaCa2_Slide_354.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 126: 20230520_105635_Col0_2B_zone3_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 127: 260_R1_Oct_slice_1.png\n", + "Original Size: 2418x414\n", + "\n", + "Processing Image 128: 20220324_123957_ARTH-COL-0-3_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 129: LaCa18_Slide_426.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 130: 377_R4_Oct_slice_227.png\n", + "Original Size: 1500x405\n", + "\n", + "Processing Image 131: 411_R3_Sep_slice_3.png\n", + "Original Size: 1566x474\n", + "\n", + "Processing Image 132: LaCa11_12h_Slide_361.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 133: slice_25.tif\n", + "Original Size: 1662x578\n", + "\n", + "Processing Image 134: 12_118-2_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 135: LaCa22_5m_Slide_330.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 136: 411_R3_Oct_slice_4.png\n", + "Original Size: 1674x530\n", + "\n", + "Processing Image 137: LaCa8_Slide_93.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 138: slice_02.tif\n", + "Original Size: 1670x650\n", + "\n", + "Processing Image 139: LaCa14_1h_Slide_60.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 140: 260_R1_Sep_slice_4.png\n", + "Original Size: 2223x441\n", + "\n", + "Processing Image 141: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 142: LaCa12_12h_Slide_201.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 143: LaCa1_Top_Slide_403.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 144: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 145: LaCa8_12h_Slide_323.png\n", + "Original Size: 1024x444\n", + "\n", + "Processing Image 146: LaCa16_1h_Slide_477.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 147: LaCa19_Slide_322.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 148: 20230520_120251_ARTH_15_zone2_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 149: LaCa2_5m_Slide_415.png\n", + "Original Size: 734x620\n", + "\n", + "Processing Image 150: LaCa23_5m_Slide_303.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 151: 386_R2_Oct_slice_5.png\n", + "Original Size: 1434x486\n", + "\n", + "Processing Image 152: slice_29.tif\n", + "Original Size: 1662x578\n", + "\n", + "Processing Image 153: 20_152-3_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 154: LaCa10_12h_Slide_147.png\n", + "Original Size: 1216x380\n", + "\n", + "Processing Image 155: slice_06.tif\n", + "Original Size: 1670x650\n", + "\n", + "Processing Image 156: LaCa18_12h_Slide_127.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 157: 20230520_110755_Col0_4B_zone4_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 158: LaCa4_Slide_105.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 159: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 160: LaCa5_5x_Slide_279.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 161: LaCa13_Slide_428.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 162: 486_R2_Oct_slice_4.png\n", + "Original Size: 1952x400\n", + "\n", + "Processing Image 163: LaCa23_5m_Slide_236.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 164: 38_130-6_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 165: 20230520_110755_Col0_4B_zone4_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 166: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 167: LaCa1_Bottom_Slide_186.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 168: slice_13.tif\n", + "Original Size: 1436x658\n", + "\n", + "Processing Image 169: 411_R3_Oct_slice_6.png\n", + "Original Size: 1674x530\n", + "\n", + "Processing Image 170: 411_R3_Oct_slice_5.png\n", + "Original Size: 1674x530\n", + "\n", + "Processing Image 171: LaCa8_12h_Slide_200.png\n", + "Original Size: 1024x444\n", + "\n", + "Processing Image 172: slice_01.tif\n", + "Original Size: 1670x650\n", + "\n", + "Processing Image 173: LaCa2_5m_Slide_451.png\n", + "Original Size: 734x620\n", + "\n", + "Processing Image 174: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 175: slice_08.tif\n", + "Original Size: 1584x724\n", + "\n", + "Processing Image 176: 411_R3_Sep_slice_5.png\n", + "Original Size: 1566x474\n", + "\n", + "Processing Image 177: slice_26.tif\n", + "Original Size: 1662x578\n", + "\n", + "Processing Image 178: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 179: LaCa2_5m_Slide_139.png\n", + "Original Size: 734x620\n", + "\n", + "Processing Image 180: LaCa12_Slide_399.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 181: LaCa13_Slide_149.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 182: LaCa22_5m_Slide_338.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 183: 411_R3_Sep_slice_2.png\n", + "Original Size: 1566x474\n", + "\n", + "Processing Image 184: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 185: LaCa2_Slide_78.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 186: 20220324_123957_ARTH-COL-0-3_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 187: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 188: LaCa1_Bottom_Slide_407.png\n", + "Original Size: 1750x480\n", + "\n", + "Processing Image 189: 377_R4_Oct_slice_47.png\n", + "Original Size: 1500x405\n", + "\n", + "Processing Image 190: LaCa2_Slide_473.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 191: 34_144-5_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 192: 20230520_105635_Col0_2B_zone3_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 193: LaCa11_12h_Slide_83.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 194: LaCa3_5x_Slide_208.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 195: LaCa20_1h_Slide_9.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 196: slice_22.tif\n", + "Original Size: 1568x636\n", + "\n", + "Processing Image 197: 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 198: LaCa1_Top_Slide_200.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 199: 542_R2_Sep_slice_5.png\n", + "Original Size: 1902x450\n", + "\n", + "Processing Image 200: LaCa8_12h_Slide_275.png\n", + "Original Size: 1024x444\n", + "Skipping non-image file: .ipynb_checkpoints\n", + "\n", + "Processing Image 201: LaCa18_12h_Slide_22.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 202: LaCa8_Slide_363.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 203: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 204: 26_152-4_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 205: 17_LA716-3_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 206: LaCa16_1h_Slide_495.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 207: LaCa20_1h_Slide_209.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 208: LaCa13_Slide_337.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 209: LaCa5_1h_Slide_205.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 210: LaCa5_1h_Slide_435.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 211: 20220324_123957_ARTH-COL-0-3_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 212: LaCa5_1h_Slide_303.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 213: slice_12.tif\n", + "Original Size: 1584x724\n", + "\n", + "Processing Image 214: LaCa5_1h_Slide_116.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 215: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 216: LaCa13_Slide_91.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 217: LaCa12_12h_Slide_474.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 218: 542_R4_Oct_slice_4.png\n", + "Original Size: 1443x417\n", + "\n", + "Processing Image 219: 542_R4_Oct_slice_3.png\n", + "Original Size: 1443x417\n", + "\n", + "Processing Image 220: LaCa4_Slide_199.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 221: 20230520_110755_Col0_4B_zone4_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 222: LaCa19_Slide_67.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 223: 386_R2_Oct_slice_2.png\n", + "Original Size: 1434x486\n", + "\n", + "Processing Image 224: LaCa17_5m_Slide_197.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 225: LaCa16_1h_Slide_238.png\n", + "Original Size: 1169x637\n", + "\n", + "Processing Image 226: 542_R2_Oct_slice_1.png\n", + "Original Size: 2250x402\n", + "\n", + "Processing Image 227: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 228: 14_130-2_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 229: LaCa13_1h_Slide 131.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 230: 22_144-3_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 231: 542_R4_Oct_slice_1.png\n", + "Original Size: 1443x417\n", + "\n", + "Processing Image 232: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 233: 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 234: 260_R1_Oct_slice_3.png\n", + "Original Size: 2418x414\n", + "\n", + "Processing Image 235: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 236: LaCa21_Slide_136.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 237: LaCa3_5x_Slide_426.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 238: 542_R4_Oct_slice_2.png\n", + "Original Size: 1443x417\n", + "\n", + "Processing Image 239: 20230520_105635_Col0_2B_zone3_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 240: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 241: LaCa22_5m_Slide_440.png\n", + "Original Size: 1266x552\n", + "\n", + "Processing Image 242: Copy of 20220324_125023_ARTH-COL-0-4_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 243: 411_R3_Sep_slice_1.png\n", + "Original Size: 1566x474\n", + "\n", + "Processing Image 244: 260_R1_Sep_slice_1.png\n", + "Original Size: 2223x441\n", + "\n", + "Processing Image 245: 542_R2_Sep_slice_3.png\n", + "Original Size: 1902x450\n", + "\n", + "Processing Image 246: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 247: LaCa15_5m_Slide_449.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 248: 06_152-1_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 249: 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 250: 260_R2_Oct_slice_1.png\n", + "Original Size: 1527x471\n", + "\n", + "Processing Image 251: LaCa15_5m_Slide_270.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 252: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 253: 20220324_123957_ARTH-COL-0-3_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 254: 23_M82-4_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 255: 20220324_123957_ARTH-COL-0-3_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 256: 13_152-2_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 257: LaCa17_5m_Slide_334.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 258: 16_M82-3_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 259: 15_144-2_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 260: LaCa3_5x_Slide_51.png\n", + "Original Size: 1316x464\n", + "\n", + "Processing Image 261: Copy of 20220324_125023_ARTH-COL-0-4_nophase_500_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 262: LaCa13_1h_Slide 274.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 263: Copy of 20211031_101320_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area3_nophase_slice2.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 264: 260_R1_Sep_slice_3.png\n", + "Original Size: 2223x441\n", + "\n", + "Processing Image 265: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 266: LaCa11_12h_Slide_440.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 267: slice_20.tif\n", + "Original Size: 1568x636\n", + "\n", + "Processing Image 268: slice_16.tif\n", + "Original Size: 1436x658\n", + "\n", + "Processing Image 269: LaCa3_Slide_412.png\n", + "Original Size: 1300x652\n", + "\n", + "Processing Image 270: LaCa12_Slide_231.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 271: LaCa18_Slide_115.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 272: slice_07.tif\n", + "Original Size: 1584x724\n", + "\n", + "Processing Image 273: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 274: 06_152-1_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 275: LaCa23_5m_Slide_56.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 276: LaCa18_12h_Slide_245.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 277: 20230520_110755_Col0_4B_zone4_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 278: 18_118-3_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 279: 17_LA716-3_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 280: LaCa5_1h_Slide_394.png\n", + "Original Size: 1682x632\n", + "\n", + "Processing Image 281: 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 282: LaCa19_Slide_193.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 283: LaCa5_5x_Slide_52.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 284: LaCa18_Slide_314.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 285: 411_R3_Oct_slice_1.png\n", + "Original Size: 1674x530\n", + "\n", + "Processing Image 286: LaCa17_5m_Slide_243.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 287: 486_R2_Oct_slice_2.png\n", + "Original Size: 1952x400\n", + "\n", + "Processing Image 288: LaCa4_Slide_303.png\n", + "Original Size: 1304x546\n", + "\n", + "Processing Image 289: LaCa10_12h_Slide_60.png\n", + "Original Size: 1216x380\n", + "\n", + "Processing Image 290: 486_R2_Oct_slice_1.png\n", + "Original Size: 1952x400\n", + "\n", + "Processing Image 291: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 292: 23_M82-4_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 293: slice_04.tif\n", + "Original Size: 1670x650\n", + "\n", + "Processing Image 294: 20230520_105635_Col0_2B_zone3_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 295: LaCa15_5m_Slide_472.png\n", + "Original Size: 1138x532\n", + "\n", + "Processing Image 296: Copy of 20211031_100214_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area2_nophase_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 297: 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 298: 20230520_120251_ARTH_15_zone2_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 299: 21_130-3_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 300: LaCa14_1h_Slide_306.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 301: 542_R2_Oct_slice_5.png\n", + "Original Size: 2250x402\n", + "\n", + "Processing Image 302: slice_05.tif\n", + "Original Size: 1670x650\n", + "\n", + "Processing Image 303: LaCa21_5m_Slide_64.png\n", + "Original Size: 1194x446\n", + "\n", + "Processing Image 304: 260_R1_Oct_slice_2.png\n", + "Original Size: 2418x414\n", + "\n", + "Processing Image 305: 22_144-3_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 306: LaCa23_5m_Slide_105.png\n", + "Original Size: 1050x762\n", + "\n", + "Processing Image 307: LaCa5_5x_Slide_323.png\n", + "Original Size: 1038x906\n", + "\n", + "Processing Image 308: slice_17.tif\n", + "Original Size: 1436x658\n", + "\n", + "Processing Image 309: slice_11.tif\n", + "Original Size: 1584x724\n", + "\n", + "Processing Image 310: 10_M82-2_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 311: 10_M82-2_1.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 312: Copy of 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 313: 260_R2_Oct_slice_6.png\n", + "Original Size: 1527x471\n", + "\n", + "Processing Image 314: LaCa2_5m_Slide_365.png\n", + "Original Size: 734x620\n", + "\n", + "Processing Image 315: LaCa17_5m_Slide_420.png\n", + "Original Size: 1214x326\n", + "\n", + "Processing Image 316: 20230520_110755_Col0_4B_zone4_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 317: slice_14.tif\n", + "Original Size: 1436x658\n", + "\n", + "Processing Image 318: 13_152-2_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 319: LaCa13_Slide_001.png\n", + "Original Size: 1176x500\n", + "\n", + "Processing Image 320: 542_R1_Sep_slice_2.png\n", + "Original Size: 2163x457\n", + "\n", + "Processing Image 321: Copy of 20220324_115313_ARTH-COL-0-1_1_nophase_500_slice0.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 322: LaCa12_Slide_465.png\n", + "Original Size: 990x556\n", + "\n", + "Processing Image 323: 542_R2_Oct_slice_3.png\n", + "Original Size: 2250x402\n", + "\n", + "Processing Image 324: LaCa1_Top_Slide_342.png\n", + "Original Size: 977x936\n", + "\n", + "Processing Image 325: LaCa8_12h_Slide_410.png\n", + "Original Size: 1024x444\n", + "\n", + "Processing Image 326: 20230520_120251_ARTH_15_zone2_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 327: 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 328: LaCa20_1h_Slide_132.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 329: 542_R2_Oct_slice_2.png\n", + "Original Size: 2250x402\n", + "\n", + "Processing Image 330: Copy of 20211031_102222_Arabidopsis_thaliana_Col0_anatomy_excisedleaf_indiv4_leaf2_area4_nophase_500_slice4.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 331: LaCa12_12h_Slide_235.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 332: LaCa14_1h_Slide_260.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 333: slice_10.tif\n", + "Original Size: 1584x724\n", + "\n", + "Processing Image 334: 486_R2_Oct_slice_3.png\n", + "Original Size: 1952x400\n", + "\n", + "Processing Image 335: 11_LA716-2_3.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 336: LaCa21_Slide_450.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 337: 37_152-6_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 338: LaCa19_Slide_117.png\n", + "Original Size: 1050x323\n", + "\n", + "Processing Image 339: slice_30.tif\n", + "Original Size: 1662x578\n", + "\n", + "Processing Image 340: 30_LA716-5_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 341: LaCa2_Slide_183.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 342: 542_R2_Sep_slice_1.png\n", + "Original Size: 1902x450\n", + "\n", + "Processing Image 343: LaCa18_Slide_197.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 344: 20230520_110755_Col0_4B_zone4_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 345: LaCa14_1h_Slide_256.png\n", + "Original Size: 1173x583\n", + "\n", + "Processing Image 346: LaCa18_Slide_57.png\n", + "Original Size: 1180x490\n", + "\n", + "Processing Image 347: slice_24.tif\n", + "Original Size: 1568x636\n", + "\n", + "Processing Image 348: LaCa20_1h_Slide_156.png\n", + "Original Size: 1110x610\n", + "\n", + "Processing Image 349: 20220324_121139_ARTH-COL-0-2_1_nophase_500_slice5.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 350: LaCa18_12h_Slide_283.png\n", + "Original Size: 1140x868\n", + "\n", + "Processing Image 351: LaCa11_12h_Slide_213.png\n", + "Original Size: 882x648\n", + "\n", + "Processing Image 352: 20230520_120251_ARTH_15_zone2_nophase_500_slice1.tif\n", + "Original Size: 1503x501\n", + "\n", + "Processing Image 353: LaCa21_Slide_359.png\n", + "Original Size: 970x622\n", + "\n", + "Processing Image 354: LaCa12_12h_Slide_437.png\n", + "Original Size: 1068x750\n", + "\n", + "Processing Image 355: 33_130-5_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 356: LaCa8_Slide_222.png\n", + "Original Size: 780x562\n", + "\n", + "Processing Image 357: 10_M82-2_5.tif\n", + "Original Size: 2560x2560\n", + "\n", + "Processing Image 358: LaCa2_Slide_165.png\n", + "Original Size: 1010x596\n", + "\n", + "Processing Image 359: LaCa13_1h_Slide 102.png\n", + "Original Size: 1329x431\n", + "\n", + "Processing Image 360: 542_R1_Sep_slice_4.png\n", + "Original Size: 2163x457\n", + "\n", + "Processing Image 361: Copy of 20220324_123957_ARTH-COL-0-3_nophase_500_slice3.tif\n", + "Original Size: 1503x501\n", + "\n", + "Creating CSV for 361 processed images...\n", + "Success! Data saved to: /pscratch/sd/d/drippner/walnut leaves/General Model Start Images_unetplusplus efficientnetb4 200 epoch batchsize 2 scale point8 12225 11326 test/material_area_and_perimeter.csv\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
file_namebackground area(pix)background perimeter (pix)epidermis area(pix)epidermis perimeter (pix)mesophyll area(pix)mesophyll perimeter (pix)air_space area(pix)air_space perimeter (pix)vein area(pix)vein perimeter (pix)
0LaCa1_Bottom_Slide_90.png477973.43749999998056.67306756765252071.874999999997928.652909899308114814.0624999999729207.61375473225443712.4999999999918729.55762086333214960.9374999999961199.6130944789015
118_118-3_1.tif4994218.74999999916192.021010144905176937.4999999999712007.614126424209598578.124999999943515.69559044249369087.4999999999434462.237533752355322824.999999999946431.564209736037
2LaCa18_12h_Slide_364.png843045.31249999997129.74787848774310762.4999999999983371.857050389671755164.0624999999919258.19528912661218410.93749999999610100.83982184295219021.8749999999961767.0344955833984
3Copy of 20211031_100214_Arabidopsis_thaliana_C...314931.249999999946684.41517754651772484.374999999996681.326656851996274396.8749999999414473.01563906348458554.68749999998512379.4633316599321854.6874999999995334.7500448671873
4260_R2_Oct_slice_5.png238662.499999999946711.2959492440157454.6874999999856749.669279592241216956.2499999999730887.92598393482763342.18749999998517426.88211771311491770.312499999993626.7224907167642
\n", + "
" + ], + "text/plain": [ + " file_name background area(pix) \\\n", + "0 LaCa1_Bottom_Slide_90.png 477973.4374999999 \n", + "1 18_118-3_1.tif 4994218.749999999 \n", + "2 LaCa18_12h_Slide_364.png 843045.3124999999 \n", + "3 Copy of 20211031_100214_Arabidopsis_thaliana_C... 314931.24999999994 \n", + "4 260_R2_Oct_slice_5.png 238662.49999999994 \n", + "\n", + " background perimeter (pix) epidermis area(pix) epidermis perimeter (pix) \\\n", + "0 8056.673067567652 52071.87499999999 7928.652909899308 \n", + "1 16192.021010144905 176937.49999999997 12007.614126424209 \n", + "2 7129.747878487743 10762.499999999998 3371.8570503896717 \n", + "3 6684.415177546517 72484.37499999999 6681.326656851996 \n", + "4 6711.29594924401 57454.687499999985 6749.669279592241 \n", + "\n", + " mesophyll area(pix) mesophyll perimeter (pix) air_space area(pix) \\\n", + "0 114814.06249999997 29207.613754732254 43712.49999999999 \n", + "1 598578.1249999999 43515.69559044249 369087.49999999994 \n", + "2 55164.06249999999 19258.195289126612 18410.937499999996 \n", + "3 274396.87499999994 14473.015639063484 58554.687499999985 \n", + "4 216956.24999999997 30887.925983934827 63342.187499999985 \n", + "\n", + " air_space perimeter (pix) vein area(pix) vein perimeter (pix) \n", + "0 18729.557620863332 14960.937499999996 1199.6130944789015 \n", + "1 34462.237533752355 322824.99999999994 6431.564209736037 \n", + "2 10100.839821842952 19021.874999999996 1767.0344955833984 \n", + "3 12379.463331659932 1854.6874999999995 334.7500448671873 \n", + "4 17426.882117713114 91770.31249999999 3626.7224907167642 " + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# Code Box 7: Data Extraction (Compatible with Code Box 5 & UNet++/EfficientNet)\n", + "import os\n", + "import pandas as pd\n", + "import sys\n", + "import time\n", + "import numpy as np\n", + "from PIL import Image\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.nn.functional as F\n", + "import torchvision.transforms as T\n", + "from skimage import io, measure\n", + "from skimage.measure import label\n", + "import segmentation_models_pytorch as smp\n", + "\n", + "# --- 1. MODEL INITIALIZATION ---\n", + "# Must match Code Box 4 (Training) and Code Box 5 (Viz) exactly\n", + "model = smp.UnetPlusPlus(\n", + " encoder_name=\"efficientnet-b4\",\n", + " encoder_weights=None, # NERSC / No Internet\n", + " in_channels=3,\n", + " classes=num_materials\n", + " )\n", + "\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "model.to(device)\n", + "\n", + "# --- 2. LOAD WEIGHTS ---\n", + "print(f\"Loading model weights: {current_model_name}{model_number}\")\n", + "path = models_directory + model_group + current_model_name + model_number + '.pth'\n", + "model.load_state_dict(torch.load(path))\n", + "model.eval()\n", + "\n", + "# --- 3. DATA EXTRACTION LOOP ---\n", + "print(f\"Checking Inference Directory: {inference_directory}\")\n", + "\n", + "if not os.path.exists(inference_directory):\n", + " print(\"ERROR: The inference directory does not exist! Check the path.\")\n", + "else:\n", + " filenames = os.listdir(inference_directory)\n", + " print(f\"Found {len(filenames)} files in directory.\")\n", + "\n", + "whole_leaf_tables = []\n", + "file_name_list = []\n", + "value_counts = []\n", + "files_processed_count = 0\n", + "\n", + "for filename in filenames:\n", + " # Fix: Case Insensitive Check\n", + " if not filename.lower().endswith(('.png', '.jpg', '.jpeg', '.tif', '.tiff', '.bmp')):\n", + " print(f\"Skipping non-image file: {filename}\")\n", + " continue\n", + "\n", + " files_processed_count += 1\n", + " print(f\"\\nProcessing Image {files_processed_count}: {filename}\")\n", + "\n", + " # Create output directories\n", + " new_dir_name = output_directory\n", + " if not os.path.exists(new_dir_name):\n", + " os.makedirs(new_dir_name)\n", + "\n", + " for mat in materials:\n", + " new_dir_name_mat = os.path.join(new_dir_name, mat.name)\n", + " if not os.path.exists(new_dir_name_mat):\n", + " os.makedirs(new_dir_name_mat)\n", + "\n", + " try:\n", + " # Load Image\n", + " image_path = os.path.join(inference_directory, filename)\n", + " image = Image.open(image_path)\n", + "\n", + " w, h = image.size\n", + " print(f\"Original Size: {w}x{h}\")\n", + "\n", + " # Resize based on scale\n", + " newW, newH = int(scale * w), int(scale * h)\n", + " image = image.resize((newW, newH))\n", + "\n", + " # Save reference for props (numpy array)\n", + " image_resize = np.array(image)\n", + "\n", + " # Initialize background for combined mask\n", + " if len(image_resize.shape) == 3:\n", + " zeros2 = np.zeros((newH, newW), dtype='ubyte')\n", + " else:\n", + " zeros2 = np.zeros_like(image_resize)\n", + "\n", + " # Preprocess for Model\n", + " tensor_transform = T.Compose([\n", + " T.ToTensor(),\n", + " T.Normalize(mean=mean, std=std)\n", + " ])\n", + "\n", + " # Ensure 3 Channels (RGB)\n", + " image_input = image.convert(\"RGB\")\n", + " image_tensor = tensor_transform(image_input)\n", + " image_tensor = image_tensor.unsqueeze(0).to(device=device, dtype=torch.float32)\n", + "\n", + " # Inference\n", + " with torch.no_grad():\n", + "\n", + " # !!!!!!!! PADDING LOGIC (CRITICAL FOR EFFICIENTNET/UNET++) !!!!!!!!\n", + " # 1. Calculate Padding (Multiple of 32)\n", + " current_h, current_w = image_tensor.shape[2], image_tensor.shape[3]\n", + " target_h = ((current_h - 1) // 32 + 1) * 32\n", + " target_w = ((current_w - 1) // 32 + 1) * 32\n", + "\n", + " pad_bottom = target_h - current_h\n", + " pad_right = target_w - current_w\n", + "\n", + " # 2. Pad Input Image\n", + " if pad_bottom > 0 or pad_right > 0:\n", + " image_tensor_padded = F.pad(image_tensor, (0, pad_right, 0, pad_bottom), mode='constant', value=0)\n", + " else:\n", + " image_tensor_padded = image_tensor\n", + "\n", + " # 3. Run Model\n", + " mask = model(image_tensor_padded)\n", + "\n", + " # 4. Crop Prediction back to Original Size (Undo Padding)\n", + " mask = mask[:, :, :current_h, :current_w]\n", + " # !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n", + "\n", + " mask = nn.Sigmoid()(mask)\n", + "\n", + " # Post Processing\n", + " combined_image = zeros2.copy()\n", + " list_of_mat_tables = []\n", + " mask_np = mask.cpu().detach().numpy()\n", + "\n", + " # Handle filename parsing\n", + " if 'following' in globals():\n", + " try:\n", + " base_name = filename.split(following)[0]\n", + " except:\n", + " base_name = filename.split('.')[0]\n", + " else:\n", + " base_name = filename.split('.')[0]\n", + "\n", + " for i, mat in enumerate(materials):\n", + " # Extract mask\n", + " mat_mask = mask_np[0, i, :, :]\n", + "\n", + " # Threshold\n", + " mat_mask[mat_mask >= mat.confidence_threshold] = mat.output_val\n", + " mat_mask[mat_mask < mat.confidence_threshold] = 0\n", + " mat_mask = np.array(mat_mask, dtype='ubyte')\n", + "\n", + " # Add to combined\n", + " if combined_image.shape == mat_mask.shape:\n", + " combined_image = np.add(combined_image, mat_mask)\n", + " else:\n", + " if len(combined_image.shape) == 3:\n", + " # Broadcast mask to 3 channels\n", + " combined_image[:,:,0] = np.add(combined_image[:,:,0], mat_mask)\n", + " combined_image[:,:,1] = np.add(combined_image[:,:,1], mat_mask)\n", + " combined_image[:,:,2] = np.add(combined_image[:,:,2], mat_mask)\n", + " else:\n", + " combined_image = np.add(combined_image, mat_mask)\n", + "\n", + " # SAVE INDIVIDUAL MASK\n", + " save_path = os.path.join(new_dir_name, mat.name, base_name + '_' + mat.name + \"_mask.png\")\n", + " io.imsave(save_path, mat_mask, check_contrast=False)\n", + "\n", + " # Data Extraction\n", + " label_mat = label(mat_mask, background=0)\n", + "\n", + " # Default empty values\n", + " mat_table_a = 0\n", + " mat_table_p = 0\n", + "\n", + " # If mask is not empty\n", + " if np.sum(mat_mask) > 0:\n", + " mat_table = measure.regionprops_table(label_mat, properties=['area','perimeter'])\n", + " df_props = pd.DataFrame(mat_table)\n", + "\n", + " if not df_props.empty:\n", + " mat_table_a = df_props['area'].sum() / (scale**2)\n", + " mat_table_p = df_props['perimeter'].sum() / scale\n", + "\n", + " list_of_mat_tables.append(mat_table_a)\n", + " list_of_mat_tables.append(mat_table_p)\n", + "\n", + " # SAVE COMBINED MASK\n", + " io.imsave(os.path.join(new_dir_name, base_name + '.png'), combined_image, check_contrast=False)\n", + "\n", + " list_of_mat_tables = np.array(list_of_mat_tables)\n", + " value_counts.append(list_of_mat_tables)\n", + " file_name_list.append([filename])\n", + "\n", + " except Exception as e:\n", + " print(f\"ERROR processing {filename}: {e}\")\n", + " import traceback\n", + " traceback.print_exc()\n", + "\n", + "# --- Generate CSV ---\n", + "if files_processed_count == 0:\n", + " print(\"\\nWARNING: No images were processed. Check your file extensions or directory path.\")\n", + "else:\n", + " print(f\"\\nCreating CSV for {len(file_name_list)} processed images...\")\n", + " counts = np.array(value_counts)\n", + " names = np.array(file_name_list)\n", + "\n", + " if len(counts) > 0 and len(names) > 0:\n", + " whole_leaf = np.concatenate((names, counts), axis=1)\n", + " whole_leaf_table = pd.DataFrame(whole_leaf)\n", + "\n", + " table_columns = [[f\"{mat.name} area(pix)\", f\"{mat.name} perimeter (pix)\"] for mat in materials]\n", + " table_columns = [element for sublist in table_columns for element in sublist]\n", + " table_columns = [\"file_name\"] + table_columns\n", + "\n", + " whole_leaf_table.columns = table_columns\n", + "\n", + " # Save CSV\n", + " csv_path = os.path.join(output_directory, 'material_area_and_perimeter.csv')\n", + " whole_leaf_table.to_csv(csv_path)\n", + " print(f\"Success! Data saved to: {csv_path}\")\n", + " display(whole_leaf_table.head())" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "X49s7N2WXHKM" + }, + "source": [ + "#**Image Segmentation and Data Extraction**" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "id": "mwWQvEJK7f8D" + }, + "source": [ + "![image.png](data:image/png;base64,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)" + ] + } + ], + "metadata": { + "accelerator": "GPU", + "colab": { + "gpuType": "A100", + "machine_shape": "hm", + "provenance": [] + }, + "kernelspec": { + "display_name": "pytorch-2.6.0", + "language": "python", + "name": "pytorch-2.6.0" + }, + "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.12.9" + } + }, + "nbformat": 4, + "nbformat_minor": 0 +} \ No newline at end of file diff --git a/segmentation/leaf_segmentation/Dockerfile b/segmentation/leaf_segmentation/Dockerfile new file mode 100644 index 0000000..b47bbe2 --- /dev/null +++ b/segmentation/leaf_segmentation/Dockerfile @@ -0,0 +1,45 @@ +# Most of this is from: https://gitlab.com/NERSC/nersc-official-images/-/blob/main/nersc/python/3.9-anaconda-2021.11/Dockerfile +FROM docker.io/library/ubuntu:latest +WORKDIR /opt + +RUN \ + apt-get update && apt-get install --yes \ + build-essential \ + gfortran \ + git \ + wget \ + libgl1 libegl1 libxext6 libsm6 libxrender1 && \ + apt-get clean all && rm -rf /var/lib/apt/lists/* + +# miniforge +# Download and install the latest Miniforge (comes with Mamba and conda-forge) +RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -O /tmp/miniforge.sh && \ + bash /tmp/miniforge.sh -b -p /opt/miniconda && \ + rm /tmp/miniforge.sh + +# Update PATH environment variable to include Miniforge +ENV PATH="/opt/miniconda/bin:$PATH" + + +# Install base packages + dependencies for leaf segmentation notebook +# PyTorch stack, scipy/scikit-image/pandas/matplotlib/tqdm/Pillow, psutil +RUN mamba install --yes -c conda-forge -c pytorch -c nvidia \ + python=3.10 \ + jupyterlab scipy scikit-learn scikit-image numpy pandas matplotlib tqdm pillow psutil \ + pytorch torchvision torchaudio pytorch-cuda=12.4 \ + && mamba clean --all -f -y + +# Install leaf-segmentation-specific pip packages +RUN pip install \ + segmentation-models-pytorch \ + albumentations \ + wandb + +# for NERSC jupyterhub environment +RUN pip install batchspawner +ENV NERSC_JUPYTER_IMAGE=YES + +# expected by entrypoint script +RUN mkdir -p /alsuser /alsdata + +WORKDIR /alsuser diff --git a/segmentation/leaf_segmentation/docker-compose.yml b/segmentation/leaf_segmentation/docker-compose.yml new file mode 100644 index 0000000..53902c4 --- /dev/null +++ b/segmentation/leaf_segmentation/docker-compose.yml @@ -0,0 +1,13 @@ +services: + leaf-segmentation: + build: + context: . + dockerfile: Dockerfile + platform: linux/amd64 + container_name: leaf-segmentation + volumes: + - .:/alsuser + ports: + - "8001:8001" + command: jupyter lab --ip=0.0.0.0 --port=8001 --no-browser --allow-root --NotebookApp.token='' + restart: unless-stopped From f91a27a6d550a9eaed406a959cc70e4cfb8f2bb5 Mon Sep 17 00:00:00 2001 From: Pradyumna Elavarthi Date: Tue, 17 Mar 2026 12:39:57 -0700 Subject: [PATCH 2/4] Update leaf segmentation: add inference notebook, streamline Dockerfile, and cleanup --- segmentation/leaf_segmentation/Dockerfile | 80 +++--- .../leaf_segmentation/run_inference.ipynb | 252 ++++++++++++++++++ .../leaf_segmentation/run_inference.py | 192 +++++++++++++ 3 files changed, 481 insertions(+), 43 deletions(-) create mode 100644 segmentation/leaf_segmentation/run_inference.ipynb create mode 100644 segmentation/leaf_segmentation/run_inference.py diff --git a/segmentation/leaf_segmentation/Dockerfile b/segmentation/leaf_segmentation/Dockerfile index b47bbe2..9d3e133 100644 --- a/segmentation/leaf_segmentation/Dockerfile +++ b/segmentation/leaf_segmentation/Dockerfile @@ -1,45 +1,39 @@ -# Most of this is from: https://gitlab.com/NERSC/nersc-official-images/-/blob/main/nersc/python/3.9-anaconda-2021.11/Dockerfile -FROM docker.io/library/ubuntu:latest -WORKDIR /opt +# Use a modern PyTorch image with CUDA support +FROM pytorch/pytorch:2.2.1-cuda12.1-cudnn8-runtime -RUN \ - apt-get update && apt-get install --yes \ - build-essential \ - gfortran \ - git \ - wget \ - libgl1 libegl1 libxext6 libsm6 libxrender1 && \ - apt-get clean all && rm -rf /var/lib/apt/lists/* - -# miniforge -# Download and install the latest Miniforge (comes with Mamba and conda-forge) -RUN wget https://github.com/conda-forge/miniforge/releases/latest/download/Miniforge3-Linux-x86_64.sh -O /tmp/miniforge.sh && \ - bash /tmp/miniforge.sh -b -p /opt/miniconda && \ - rm /tmp/miniforge.sh - -# Update PATH environment variable to include Miniforge -ENV PATH="/opt/miniconda/bin:$PATH" - - -# Install base packages + dependencies for leaf segmentation notebook -# PyTorch stack, scipy/scikit-image/pandas/matplotlib/tqdm/Pillow, psutil -RUN mamba install --yes -c conda-forge -c pytorch -c nvidia \ - python=3.10 \ - jupyterlab scipy scikit-learn scikit-image numpy pandas matplotlib tqdm pillow psutil \ - pytorch torchvision torchaudio pytorch-cuda=12.4 \ - && mamba clean --all -f -y +# Set working directory +WORKDIR /app -# Install leaf-segmentation-specific pip packages -RUN pip install \ - segmentation-models-pytorch \ - albumentations \ - wandb - -# for NERSC jupyterhub environment -RUN pip install batchspawner -ENV NERSC_JUPYTER_IMAGE=YES - -# expected by entrypoint script -RUN mkdir -p /alsuser /alsdata - -WORKDIR /alsuser +# Install system dependencies +RUN apt-get update && apt-get install -y \ + libgl1-mesa-glx \ + libglib2.0-0 \ + git \ + && rm -rf /var/lib/apt/lists/* + +# Install Python dependencies +# Using a fixed version for consistency where possible +RUN pip install --no-cache-dir \ + segmentation-models-pytorch==0.3.3 \ + albumentations==1.4.0 \ + pandas \ + matplotlib \ + tqdm \ + pillow \ + scikit-image \ + scikit-learn + +# Create directories for data and output +RUN mkdir -p /app/sample_images /app/output_masks + +# Copy inference script and weights +# Note: Users should ideally mount their own images and weights +COPY run_inference.py . +# COPY best_model.pth . + +# Set environmental variables +ENV PYTHONUNBUFFERED=1 + +# Default command: run inference +# Expects images in /app/sample_images and weights at /app/best_model.pth +CMD ["python", "run_inference.py"] diff --git a/segmentation/leaf_segmentation/run_inference.ipynb b/segmentation/leaf_segmentation/run_inference.ipynb new file mode 100644 index 0000000..28db88a --- /dev/null +++ b/segmentation/leaf_segmentation/run_inference.ipynb @@ -0,0 +1,252 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Leaf Segmentation Inference Notebook\n", + "\n", + "This notebook provides a user-friendly interface for running leaf segmentation using a trained FPN (MiT-B4) model. It is adapted from `run_inference.py`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image\n", + "from tqdm.notebook import tqdm\n", + "from torch.amp import autocast\n", + "import segmentation_models_pytorch as smp\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "Update these paths to match your local setup." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The folder containing the images you want to segment\n", + "INPUT_IMAGES_DIR = \"./sample_images\" \n", + "\n", + "# Path to the pretrained model weights provided to you\n", + "MODEL_WEIGHTS = \"./best_model.pth\"\n", + "\n", + "# Where you want the segmented masks to drop\n", + "OUTPUT_MASKS_DIR = \"./output_masks\"\n", + "\n", + "# Where you want the fraction/porosity measurements saved\n", + "OUTPUT_CSV_FILE = \"./output_fraction_results.csv\"\n", + "\n", + "NUM_CLASSES = 5\n", + "PATCH_SIZE = 320\n", + "STRIDE = 160\n", + "\n", + "CLASS_NAMES = [\"Background\", \"Epidermis\", \"Vascular_Region\", \"Mesophyll\", \"Air_Space\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(f\"Running inference on: {device}\")\n", + "\n", + "# Ensure output directories exist\n", + "os.makedirs(OUTPUT_MASKS_DIR, exist_ok=True)\n", + "\n", + "print(\"Initializing Model...\")\n", + "model_eval = smp.FPN(\n", + " encoder_name=\"mit_b4\",\n", + " encoder_weights=None,\n", + " in_channels=1,\n", + " classes=NUM_CLASSES,\n", + ")\n", + "\n", + "print(f\"Loading weights from: {MODEL_WEIGHTS}\")\n", + "if os.path.exists(MODEL_WEIGHTS):\n", + " checkpoint = torch.load(MODEL_WEIGHTS, map_location=device)\n", + " model_eval.load_state_dict(checkpoint['model_state_dict'])\n", + " model_eval = model_eval.to(device)\n", + " model_eval.eval()\n", + "else:\n", + " print(f\"Error: Weights not found at {MODEL_WEIGHTS}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference Loop" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "img_files = sorted([f for f in os.listdir(INPUT_IMAGES_DIR)\n", + " if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))])\n", + "print(f\"Found {len(img_files)} images to process.\")\n", + "\n", + "results = []\n", + "\n", + "for fname in tqdm(img_files, desc=\"Inference\"):\n", + " img_path = os.path.join(INPUT_IMAGES_DIR, fname)\n", + " img = np.array(Image.open(img_path))\n", + " \n", + " if img.ndim == 3 and img.shape[2] == 4:\n", + " img = img[:, :, :3]\n", + " if img.ndim == 3:\n", + " img = np.dot(img[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8).copy()\n", + " \n", + " img_t = torch.from_numpy(img.copy()).float().unsqueeze(0).unsqueeze(0)\n", + " \n", + " valid_pixels = img_t[img_t > 0]\n", + " if len(valid_pixels) > 0:\n", + " mean, std = valid_pixels.mean(), valid_pixels.std()\n", + " if std > 1e-5:\n", + " img_t = (img_t - mean) / std\n", + "\n", + " h, w = img.shape\n", + " pad_h = (32 - h % 32) % 32\n", + " pad_w = (32 - w % 32) % 32\n", + " if pad_h > 0 or pad_w > 0:\n", + " img_t = torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), mode='reflect')\n", + "\n", + " img_t = img_t.to(device)\n", + " \n", + " _, _, H_t, W_t = img_t.shape\n", + " pred_prob_accum = torch.zeros((1, NUM_CLASSES, H_t, W_t), device=device, dtype=torch.float32)\n", + " count_accum = torch.zeros((1, 1, H_t, W_t), device=device, dtype=torch.float32)\n", + " \n", + " with torch.no_grad():\n", + " dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32\n", + " with autocast('cuda' if torch.cuda.is_available() else 'cpu', dtype=dtype):\n", + " for py in range(0, H_t, STRIDE):\n", + " for px in range(0, W_t, STRIDE):\n", + " y1 = min(py, H_t - PATCH_SIZE)\n", + " y2 = y1 + PATCH_SIZE\n", + " x1 = min(px, W_t - PATCH_SIZE)\n", + " x2 = x1 + PATCH_SIZE\n", + " \n", + " patch = img_t[:, :, y1:y2, x1:x2]\n", + " p_orig = model_eval(patch).softmax(dim=1)\n", + " \n", + " p_hflip = model_eval(torch.flip(patch, dims=[3]))\n", + " p_hflip = torch.flip(p_hflip.softmax(dim=1), dims=[3])\n", + " \n", + " p_vflip = model_eval(torch.flip(patch, dims=[2]))\n", + " p_vflip = torch.flip(p_vflip.softmax(dim=1), dims=[2])\n", + " \n", + " p_avg = (p_orig + p_hflip + p_vflip) / 3.0\n", + " \n", + " pred_prob_accum[:, :, y1:y2, x1:x2] += p_avg\n", + " count_accum[:, :, y1:y2, x1:x2] += 1.0\n", + " \n", + " pred_prob = pred_prob_accum / count_accum\n", + " pred_cls = torch.argmax(pred_prob, dim=1).squeeze().cpu().numpy()\n", + " \n", + " if pad_h > 0 or pad_w > 0:\n", + " pred_cls = pred_cls[:h, :w]\n", + "\n", + " stem = os.path.splitext(fname)[0]\n", + " out_path = os.path.join(OUTPUT_MASKS_DIR, f\"{stem}_pred.png\")\n", + " Image.fromarray(pred_cls.astype(np.uint8)).save(out_path)\n", + " \n", + " total = pred_cls.size\n", + " row = {'filename': fname}\n", + " for c, name in enumerate(CLASS_NAMES):\n", + " count = int(np.sum(pred_cls == c))\n", + " row[f\"{name}_pixels\"] = count\n", + " row[f\"{name}_fraction\"] = round(count / total, 6)\n", + " \n", + " row['porosity'] = row['Air_Space_fraction']\n", + " results.append(row)\n", + "\n", + "results_df = pd.DataFrame(results)\n", + "results_df.to_csv(OUTPUT_CSV_FILE, index=False)\n", + "print(\"Inference Complete!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if results:\n", + " last_img_name = results[-1]['filename']\n", + " last_mask_name = os.path.splitext(last_img_name)[0] + \"_pred.png\"\n", + " \n", + " img = Image.open(os.path.join(INPUT_IMAGES_DIR, last_img_name))\n", + " mask = Image.open(os.path.join(OUTPUT_MASKS_DIR, last_mask_name))\n", + " \n", + " fig, ax = plt.subplots(1, 2, figsize=(12, 6))\n", + " ax[0].imshow(img, cmap='gray')\n", + " ax[0].set_title(\"Original Image\")\n", + " ax[0].axis('off')\n", + " \n", + " ax[1].imshow(mask, cmap='jet')\n", + " ax[1].set_title(\"Predicted Mask\")\n", + " ax[1].axis('off')\n", + " \n", + " plt.show()\n", + " \n", + " display(results_df.head())" + ] + } + ], + "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.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/segmentation/leaf_segmentation/run_inference.py b/segmentation/leaf_segmentation/run_inference.py new file mode 100644 index 0000000..9a52351 --- /dev/null +++ b/segmentation/leaf_segmentation/run_inference.py @@ -0,0 +1,192 @@ +import os +import torch +import numpy as np +import pandas as pd +from PIL import Image +from tqdm import tqdm +from torch.amp import autocast +import segmentation_models_pytorch as smp + +def run_inference( + img_dir, + output_mask_dir, + output_csv_path, + model_weights_path, + num_classes=5, + patch_size=320, + stride=160 +): + """ + Run inference on a folder of grayscale CT images using the trained FPN (MiT-B4) model. + """ + # 1. Setup Device + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Running inference on: {device}") + + # Ensure output directories exist + os.makedirs(output_mask_dir, exist_ok=True) + + # Class names mapping + CLASS_NAMES = ["Background", "Epidermis", "Vascular_Region", "Mesophyll", "Air_Space"] + + # 2. Re-instantiate the Model Model Structure + print("Initializing Model...") + model_eval = smp.FPN( + encoder_name="mit_b4", + encoder_weights=None, # No need to download internet weights for inference + in_channels=1, # Grayscale inputs + classes=num_classes, + ) + + # 3. Load Trained Weights + print(f"Loading weights from: {model_weights_path}") + checkpoint = torch.load(model_weights_path, map_location=device) + model_eval.load_state_dict(checkpoint['model_state_dict']) + model_eval = model_eval.to(device) + model_eval.eval() + + # 4. Find all Images + img_files = sorted([f for f in os.listdir(img_dir) + if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))]) + + print(f"Found {len(img_files)} images to process.") + + results = [] + + # 5. Run Inference Loop + for fname in tqdm(img_files, desc="Inference"): + img = np.array(Image.open(os.path.join(img_dir, fname))) + + # Handle RGB β†’ grayscale if someone passes RGB images by mistake + if img.ndim == 3 and img.shape[2] == 4: + img = img[:, :, :3] + if img.ndim == 3: + img = np.dot(img[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8).copy() + + img_t = torch.from_numpy(img.copy()).float().unsqueeze(0).unsqueeze(0) + + # Z-score standardization on non-zero tissue pixels (MUST MATCH TRAINING!) + valid_pixels = img_t[img_t > 0] + if len(valid_pixels) > 0: + mean, std = valid_pixels.mean(), valid_pixels.std() + if std > 1e-5: + img_t = (img_t - mean) / std + + # Pad the image to be a multiple of 32 (required by FPN scaling) + h, w = img.shape + pad_h = (32 - h % 32) % 32 + pad_w = (32 - w % 32) % 32 + if pad_h > 0 or pad_w > 0: + img_t = torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), mode='reflect') + + img_t = img_t.to(device) + + # --- SLIDING WINDOW INFERENCE TO PREVENT OOM --- + # Splitting large images into patches and averaging the overlaps + _, _, H_t, W_t = img_t.shape + pred_prob_accum = torch.zeros((1, num_classes, H_t, W_t), device=device, dtype=torch.float32) + count_accum = torch.zeros((1, 1, H_t, W_t), device=device, dtype=torch.float32) + + with torch.no_grad(): + # Use mixed precision (bfloat16) to save memory if on GPU, otherwise standard float + dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32 + with autocast('cuda' if torch.cuda.is_available() else 'cpu', dtype=dtype): + # Iterate through grid + for py in range(0, H_t, stride): + for px in range(0, W_t, stride): + y1 = min(py, H_t - patch_size) + y2 = y1 + patch_size + x1 = min(px, W_t - patch_size) + x2 = x1 + patch_size + + patch = img_t[:, :, y1:y2, x1:x2] + + # 1. Original Prediction + p_orig = model_eval(patch).softmax(dim=1) + + # Test Time Augmentation (TTA) for robustness + # 2. Horizontal Flip + p_hflip = model_eval(torch.flip(patch, dims=[3])) + p_hflip = torch.flip(p_hflip.softmax(dim=1), dims=[3]) + + # 3. Vertical Flip + p_vflip = model_eval(torch.flip(patch, dims=[2])) + p_vflip = torch.flip(p_vflip.softmax(dim=1), dims=[2]) + + # Average the TTA predictions + p_avg = (p_orig + p_hflip + p_vflip) / 3.0 + + # Accumulate probabilities in the main image map + pred_prob_accum[:, :, y1:y2, x1:x2] += p_avg + count_accum[:, :, y1:y2, x1:x2] += 1.0 + + # Average the overlaps where patches met + pred_prob = pred_prob_accum / count_accum + + # Take the argmax to get the final class prediction + pred_cls = torch.argmax(pred_prob, dim=1).squeeze().cpu().numpy() + + # Crop back to exact original image size if padding was added earlier + if pad_h > 0 or pad_w > 0: + pred_cls = pred_cls[:h, :w] + + # 6. Save Output Mask + stem = os.path.splitext(fname)[0] + out_path = os.path.join(output_mask_dir, f"{stem}_pred.png") + Image.fromarray(pred_cls.astype(np.uint8)).save(out_path) + + # 7. Calculate Tissue Fractions + total = pred_cls.size + row = {'filename': fname} + for c, name in enumerate(CLASS_NAMES): + count = int(np.sum(pred_cls == c)) + row[f"{name}_pixels"] = count + row[f"{name}_fraction"] = round(count / total, 6) + + # Optional: Define porosity as Air_Space fraction + row['porosity'] = row['Air_Space_fraction'] + results.append(row) + + # 8. Save Dataset Analytics CSV + results_df = pd.DataFrame(results) + results_df.to_csv(output_csv_path, index=False) + + print(f"\nInference Complete!") + print(f"Masks saved to: {output_mask_dir}") + print(f"Data saved to: {output_csv_path}") + +if __name__ == "__main__": + # ========================================================================= + # USER CONFIGURATION: Update these paths for your local machine! + # ========================================================================= + + # The folder containing the images you want to segment + INPUT_IMAGES_DIR = "./sample_images" + + # Path to the pretrained model weights provided to you + MODEL_WEIGHTS = "./best_model.pth" + + # Where you want the segmented masks to drop + OUTPUT_MASKS_DIR = "./output_masks" + + # Where you want the fraction/porosity measurements saved + OUTPUT_CSV_FILE = "./output_fraction_results.csv" + + # ========================================================================= + + # Check that paths exist before running + if not os.path.exists(INPUT_IMAGES_DIR): + print(f"Please create the directory '{INPUT_IMAGES_DIR}' and put images inside it.") + exit(1) + + if not os.path.exists(MODEL_WEIGHTS): + print(f"Error: Could not find model weights at '{MODEL_WEIGHTS}'.") + exit(1) + + # Run the pipeline + run_inference( + img_dir=INPUT_IMAGES_DIR, + output_mask_dir=OUTPUT_MASKS_DIR, + output_csv_path=OUTPUT_CSV_FILE, + model_weights_path=MODEL_WEIGHTS + ) From 8a3c329add7e5a432a97aeeb08b87fa8caf344e4 Mon Sep 17 00:00:00 2001 From: Pradyumna Elavarthi Date: Wed, 25 Mar 2026 12:47:46 -0700 Subject: [PATCH 3/4] Refactor segmentation models: containerization and native PyTorch --- .../workflows/publish-segmentation-image.yaml | 20 +- segmentation/general_model/Dockerfile | 9 +- segmentation/general_model/vtk_trial.ipynb | 165 ++++---- segmentation/leaf_segmentation/Dockerfile | 27 +- .../Leaf_Segmentation_Model/run_inference.py | 192 ++++++++++ .../leaf_segmentation/docker-compose.yml | 13 - .../leaf_segmentation/leaf_segmentation.ipynb | 355 ++++++++++++++++++ 7 files changed, 650 insertions(+), 131 deletions(-) create mode 100644 segmentation/leaf_segmentation/Leaf_Segmentation_Model/run_inference.py delete mode 100644 segmentation/leaf_segmentation/docker-compose.yml create mode 100644 segmentation/leaf_segmentation/leaf_segmentation.ipynb diff --git a/.github/workflows/publish-segmentation-image.yaml b/.github/workflows/publish-segmentation-image.yaml index f883384..583783a 100644 --- a/.github/workflows/publish-segmentation-image.yaml +++ b/.github/workflows/publish-segmentation-image.yaml @@ -14,10 +14,16 @@ env: jobs: build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write + strategy: + fail-fast: false + matrix: + include: + - name: general + context: ./segmentation/general_model + file: ./segmentation/general_model/Dockerfile + - name: leaf + context: ./segmentation/leaf_segmentation + file: ./segmentation/leaf_segmentation/Dockerfile steps: - name: Checkout repository @@ -36,7 +42,7 @@ jobs: id: meta uses: docker/metadata-action@v5 with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-${{ matrix.name }} tags: | type=sha type=raw,value=latest @@ -44,8 +50,8 @@ jobs: - name: Build and push Docker image uses: docker/build-push-action@v6 with: - context: ./segmentation - file: ./segmentation/Dockerfile + context: ${{ matrix.context }} + file: ${{ matrix.file }} push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} diff --git a/segmentation/general_model/Dockerfile b/segmentation/general_model/Dockerfile index 271db0b..f873034 100644 --- a/segmentation/general_model/Dockerfile +++ b/segmentation/general_model/Dockerfile @@ -30,15 +30,10 @@ RUN mamba install --yes -c conda-forge -c astra-toolbox -c simpleitk -c pytorch opencv matplotlib vtk pytorch torchvision torchaudio pytorch-cuda=12.4 \ && mamba clean --all -f -y -# Install Detectron2 (requires pytorch) -RUN python -m pip install --no-build-isolation 'git+https://github.com/facebookresearch/detectron2.git' +# Install Dependencies +RUN python -m pip install opencv-python astra-toolbox vtk jupyter ipykernel tifffile ipywidgets # # install SYRIS -# COPY syris syris -# # RUN git clone git@github.com:ufo-kit/syris.git -# RUN conda install -c conda-forge -y cmake pybind11 -- the NERSC docs say something about not using cmake -# RUN cd syris && pip install -r requirements.txt && pip install . -# RUN rm -fR syris # for NERSC jupyterhub environment diff --git a/segmentation/general_model/vtk_trial.ipynb b/segmentation/general_model/vtk_trial.ipynb index 41a1809..484b02f 100644 --- a/segmentation/general_model/vtk_trial.ipynb +++ b/segmentation/general_model/vtk_trial.ipynb @@ -25,7 +25,7 @@ ], "source": [ "# =========================\n", - "# πŸ“¦ IMPORTS (Clean Version)\n", + "# \ud83d\udce6 IMPORTS (Clean Version)\n", "# =========================\n", "\n", "# ---- Standard Library ----\n", @@ -39,16 +39,7 @@ "import tifffile as tiff\n", "from tqdm.std import tqdm # plain tqdm (no ipywidgets)\n", "\n", - "# ---- PyTorch & Detectron2 ----\n", - "import torch\n", - "import torch.nn as nn\n", - "import torch.nn.functional as F\n", - "from detectron2 import model_zoo\n", - "from detectron2.checkpoint import DetectionCheckpointer\n", - "from detectron2.modeling import META_ARCH_REGISTRY\n", - "from detectron2.modeling.backbone.fpn import build_resnet_fpn_backbone\n", - "from detectron2.layers import ShapeSpec\n", - "from detectron2.config import get_cfg\n", + "# ---- PyTorch & Torchvision ----\nimport torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torchvision.models.detection.backbone_utils import resnet_fpn_backbone\n", "\n", "# ---- Visualization ----\n", "import matplotlib.pyplot as plt\n", @@ -162,7 +153,7 @@ "\n", "###########################\n", "\n", - "WEIGHTS_PATH = \"/pscratch/sd/e/elavarpa/model_final.pth\" # The path to where the model weights are stored\n", + "WEIGHTS_PATH = \"/pscratch/sd/e/elavarpa/pytorch_model.pth\" # The path to where the model weights are stored\n", "\n", "#inputPath = os.path.join(\".\", inputSubFolderName)\n", "\n", @@ -281,7 +272,7 @@ " return cv2.cvtColor(rgb8, cv2.COLOR_RGB2BGR) if assume_rgb else rgb8\n", " raise ValueError(f\"Unsupported image shape: {a.shape}\")\n", "\n", - "# ---- Subset-based percentile estimation (β‰ˆ30% spread across stack) ----\n", + "# ---- Subset-based percentile estimation (\u224830% spread across stack) ----\n", "def pick_spread_subset(files, frac=0.30, seed=42):\n", " \"\"\"Pick ~frac of files, spread across the stack with light randomness.\"\"\"\n", " n = len(files)\n", @@ -353,25 +344,18 @@ ], "source": [ "# ===== model, cfg, weights =====\n", - "# Simple multi-label FPN head\n", - "ARCH_NAME = \"MultiLabelSemSeg_v3\"\n", - "if ARCH_NAME in META_ARCH_REGISTRY._obj_map:\n", - " del META_ARCH_REGISTRY._obj_map[ARCH_NAME]\n", "\n", - "class_weights = torch.ones(NUM_CLASSES, dtype=torch.float32)\n", - "\n", - "@META_ARCH_REGISTRY.register()\n", "class MultiLabelSemSeg_v3(nn.Module):\n", - " def __init__(self, cfg):\n", + " def __init__(self):\n", " super().__init__()\n", - " self.device = torch.device(cfg.MODEL.DEVICE)\n", - " self.num_classes = cfg.MODEL.SEM_SEG_HEAD.NUM_CLASSES\n", - " self.out_channels = cfg.MODEL.FPN.OUT_CHANNELS\n", - " self.pixel_mean = torch.tensor(cfg.MODEL.PIXEL_MEAN).view(3, 1, 1).to(self.device)\n", - " self.pixel_std = torch.tensor(cfg.MODEL.PIXEL_STD).view(3, 1, 1).to(self.device)\n", - " self.class_weights = class_weights.to(self.device)\n", - "\n", - " self.backbone = build_resnet_fpn_backbone(cfg, ShapeSpec(channels=3))\n", + " self.device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + " self.num_classes = NUM_CLASSES\n", + " self.out_channels = 256\n", + " self.pixel_mean = torch.tensor([103.53, 116.28, 123.675]).view(3, 1, 1).to(self.device)\n", + " self.pixel_std = torch.tensor([1.0, 1.0, 1.0]).view(3, 1, 1).to(self.device)\n", + "\n", + " # Pure PyTorch Backbone\n", + " self.backbone = resnet_fpn_backbone('resnet101', weights=None, trainable_layers=5)\n", " self.head = nn.Conv2d(self.out_channels, self.num_classes, kernel_size=1)\n", " self.size_divisibility = 32\n", " self.to(self.device)\n", @@ -382,45 +366,46 @@ "\n", " @torch.no_grad()\n", " def forward(self, batched_inputs):\n", - " from detectron2.structures import ImageList\n", " imgs = [x[\"image\"].to(self.device) for x in batched_inputs]\n", " orig_sizes = [im.shape[-2:] for im in imgs]\n", " imgs = [self.normalize(im) for im in imgs]\n", - " images = ImageList.from_tensors(imgs, size_divisibility=self.size_divisibility)\n", - "\n", - " feats = self.backbone(images.tensor)\n", - " x = feats[\"p2\"]\n", + " \n", + " # Manual Padding to be divisible by 32 (replacing Detectron2 ImageList)\n", + " padded_imgs = []\n", + " for im in imgs:\n", + " _, H, W = im.shape\n", + " pad_h = (self.size_divisibility - H % self.size_divisibility) % self.size_divisibility\n", + " pad_w = (self.size_divisibility - W % self.size_divisibility) % self.size_divisibility\n", + " padded = F.pad(im, (0, pad_w, 0, pad_h))\n", + " padded_imgs.append(padded)\n", + " \n", + " images = torch.stack(padded_imgs)\n", + "\n", + " feats = self.backbone(images)\n", + " # torchvision FPN outputs an OrderedDict with keys '0', '1', '2', '3', 'pool'\n", + " x = feats[\"0\"]\n", " logits = self.head(x)\n", - " logits = F.interpolate(logits, size=images.tensor.shape[-2:], mode=\"bilinear\", align_corners=False)\n", - "\n", - " out = []\n", + " \n", + " # Interpolate back to padded image size exactly like Detectron2\n", + " logits = F.interpolate(logits, size=images.shape[-2:], mode=\"bilinear\", align_corners=False)\n", " probs = torch.sigmoid(logits)\n", + " \n", + " out = []\n", " for b, (H, W) in enumerate(orig_sizes):\n", + " # Slice back to original exact image size\n", " out.append({\"sem_seg_probs\": probs[b, :, :H, :W].detach().cpu()})\n", " return out\n", "\n", - "# ---- Build cfg and load weights ----\n", - "cfg = get_cfg()\n", - "cfg.MODEL.DEVICE = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n", - "cfg.merge_from_file(model_zoo.get_config_file(\"COCO-PanopticSegmentation/panoptic_fpn_R_101_3x.yaml\"))\n", - "cfg.MODEL.META_ARCHITECTURE = ARCH_NAME\n", - "cfg.MODEL.SEM_SEG_HEAD.NUM_CLASSES = NUM_CLASSES\n", - "cfg.MODEL.FPN.OUT_CHANNELS = 256\n", - "cfg.INPUT.FORMAT = \"BGR\"\n", - "\n", - "print(\"INPUT.FORMAT =\", cfg.INPUT.FORMAT)\n", - "print(\"PIXEL_MEAN =\", cfg.MODEL.PIXEL_MEAN)\n", - "print(\"PIXEL_STD =\", cfg.MODEL.PIXEL_STD)\n", - "\n", - "model = MultiLabelSemSeg_v3(cfg).eval()\n", + "model = MultiLabelSemSeg_v3().eval()\n", "\n", "# Point to your trained weights\n", - "# WEIGHTS_PATH = \"/pscratch/sd/e/elavarpa/model_final.pth\"\n", - "DetectionCheckpointer(model).load(WEIGHTS_PATH)\n", - "print(\"Loaded:\", WEIGHTS_PATH)\n", + "# WEIGHTS_PATH = \"/pscratch/sd/e/elavarpa/pytorch_model.pth\"\n", + "state_dict = torch.load(WEIGHTS_PATH, map_location=model.device)\n", + "model.load_state_dict(state_dict)\n", "\n", - "DEVICE = torch.device(cfg.MODEL.DEVICE)\n", - "print(\"Device:\", DEVICE)\n" + "print(\"Loaded Pure PyTorch Weights:\", WEIGHTS_PATH)\n", + "DEVICE = model.device\n", + "print(\"Device:\", DEVICE)" ] }, { @@ -450,7 +435,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–‰| 964/965 [03:30<00:00, 4.58it/s]" + "100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2589| 964/965 [03:30<00:00, 4.58it/s]" ] }, { @@ -657,8 +642,8 @@ "name": "stdout", "output_type": "stream", "text": [ - "⚠️ Folder already exists. Removing: ./20211222_125057_petiole4_00001.tiff_masks\n", - "βœ… Created new folder: ./20211222_125057_petiole4_00001.tiff_masks\n", + "\u26a0\ufe0f Folder already exists. Removing: ./20211222_125057_petiole4_00001.tiff_masks\n", + "\u2705 Created new folder: ./20211222_125057_petiole4_00001.tiff_masks\n", "Subsampling: all => 2160/2160 slices\n" ] }, @@ -666,7 +651,7 @@ "name": "stderr", "output_type": "stream", "text": [ - "Inferβ†’Volumes [all]: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 180/180 [10:47<00:00, 3.60s/it]\n" + "Infer\u2192Volumes [all]: 100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 180/180 [10:47<00:00, 3.60s/it]\n" ] } ], @@ -676,12 +661,12 @@ "outputSubfolderName = f\"{folder_name}_masks\"\n", "outputPath = os.path.join('.', outputSubfolderName) \n", "if os.path.exists(outputPath):\n", - " print(f\"⚠️ Folder already exists. Removing: {outputPath}\")\n", + " print(f\"\u26a0\ufe0f Folder already exists. Removing: {outputPath}\")\n", " shutil.rmtree(outputPath)\n", "\n", "# Now create a fresh folder\n", "os.makedirs(outputPath, exist_ok=True)\n", - "print(f\"βœ… Created new folder: {outputPath}\")\n", + "print(f\"\u2705 Created new folder: {outputPath}\")\n", "\n", "# ===== Fix a small typo from earlier =====\n", "OUT_ROOT = Path(outputPath) # change\n", @@ -697,7 +682,7 @@ "START_OFFSET = 0 # when using every-N, you can offset which residue to take [0..N-1]\n", "MAX_IMAGES_CAP = None # optional hard cap on how many images to process (after subsampling)\n", "# ================================\n", - "# Inference β†’ Direct volume build\n", + "# Inference \u2192 Direct volume build\n", "# ================================\n", "@torch.no_grad()\n", "def infer_and_build_volumes(\n", @@ -781,7 +766,7 @@ " # ---- iterate over batches; fill volumes[z] ----\n", " z_write = 0\n", " use_amp = torch.cuda.is_available()\n", - " for i in tqdm(range(0, total_sel, batch_size), desc=f\"Inferβ†’Volumes [{subset_tag}]\"):\n", + " for i in tqdm(range(0, total_sel, batch_size), desc=f\"Infer\u2192Volumes [{subset_tag}]\"):\n", " chunk = sel_paths[i:i+batch_size]\n", " imgs, stems, shapes = batch_to_tensors(chunk)\n", "\n", @@ -887,7 +872,7 @@ "output_type": "stream", "text": [ "\u001b[0m\u001b[33m2026-02-02 13:21:27.248 ( 25.227s) [ 7FC26ED9E740]vtkXOpenGLRenderWindow.:1458 WARN| bad X server connection. DISPLAY=\u001b[0m\n", - "100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 4/4 [01:06<00:00, 16.74s/it]" + "100%|\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588\u2588| 4/4 [01:06<00:00, 16.74s/it]" ] }, { @@ -928,7 +913,7 @@ "KEEP_PROB = 0.30\n", "MAX_POINTS = 400_000\n", "POINT_SIZE = 1.5 # a bit bigger since we'll add transparency\n", - "ALPHA = 0.25 # <-- transparency amount (0..1); try 0.25–0.5\n", + "ALPHA = 0.25 # <-- transparency amount (0..1); try 0.25\u20130.5\n", "BG = (0.97,0.97,0.97)\n", "\n", "OUT_DIR = Path(\"./vtk_points_alpha\"); OUT_DIR.mkdir(parents=True, exist_ok=True) #### LOCATION CAN BE CHANGED HERE FOR SAVING VOLUME RENDERING\n", @@ -1002,7 +987,7 @@ " cam.Zoom(1)\n", " #ren.ResetCameraClippingRange()\n", "\n", - " # render twiceβ€”helps some EGL stacks\n", + " # render twice\u2014helps some EGL stacks\n", " rw.Render(); rw.Render()\n", "\n", " w2i = vtk.vtkWindowToImageFilter(); w2i.SetInput(rw); w2i.ReadFrontBufferOff(); w2i.Update()\n", @@ -1214,7 +1199,7 @@ "# # aggressive extra stride to cap memory\n", "# stride = int(round((total / MAX_VOXELS) ** (1/3))) + 1\n", "# arr = arr[::stride, ::stride, ::stride]\n", - "# print(f\"⚠️ Reduced mask to {arr.shape} (stride={stride}) to cap memory.\")\n", + "# print(f\"\u26a0\ufe0f Reduced mask to {arr.shape} (stride={stride}) to cap memory.\")\n", "# return arr\n", "\n", "# # -----------------------------------------------------\n", @@ -1225,11 +1210,11 @@ "\n", "# for cname in TARGET_CLASS_NAMES:\n", "# if cname not in volumes:\n", - "# print(f\"⚠️ Missing {cname}\")\n", + "# print(f\"\u26a0\ufe0f Missing {cname}\")\n", "# continue\n", "# vol = volumes[cname][::Z_STEP, ::YX_STEP, ::YX_STEP]\n", "# if vol.max() == 0:\n", - "# print(f\"⚠️ {cname} empty\")\n", + "# print(f\"\u26a0\ufe0f {cname} empty\")\n", "# continue\n", "\n", "# vol = _ensure_reasonable(vol).astype(np.uint8, copy=False)\n", @@ -1278,7 +1263,7 @@ "# actor.GetProperty().SetSpecular(0.05)\n", "# actor.GetProperty().SetSpecularPower(5.0)\n", "# ren.AddActor(actor)\n", - "# print(f\"βœ“ Added mesh: {cname} (iso at 0.5)\")\n", + "# print(f\"\u2713 Added mesh: {cname} (iso at 0.5)\")\n", "\n", "# # Camera & lights\n", "# ren.ResetCamera()\n", @@ -1294,7 +1279,7 @@ "\n", "# win.Render()\n", "# _save_png(win, FIG_PATH)\n", - "# print(f\"βœ… VTK mesh composite saved β†’ {FIG_PATH}\")\n", + "# print(f\"\u2705 VTK mesh composite saved \u2192 {FIG_PATH}\")\n", "\n", "# # -----------------------------------------------------\n", "# # Mode 2: Volume (GPU), conservative defaults\n", @@ -1345,14 +1330,14 @@ "# vol_actor.SetMapper(mapper)\n", "# vol_actor.SetProperty(prop)\n", "# ren.AddVolume(vol_actor)\n", - "# print(f\"βœ“ Added GPU volume: {cname}\")\n", + "# print(f\"\u2713 Added GPU volume: {cname}\")\n", "\n", "# ren.ResetCamera()\n", "# cam = ren.GetActiveCamera()\n", "# _rotate_camera(cam, ELEV, AZIM, ROT_Y_DEG)\n", "# win.Render()\n", "# _save_png(win, FIG_PATH)\n", - "# print(f\"βœ… VTK GPU volume composite saved β†’ {FIG_PATH}\")\n", + "# print(f\"\u2705 VTK GPU volume composite saved \u2192 {FIG_PATH}\")\n", "\n", "# # -----------------------------------------------------\n", "# # Mode 3: Volume (CPU) fallback\n", @@ -1399,14 +1384,14 @@ "# vol_actor.SetMapper(mapper)\n", "# vol_actor.SetProperty(prop)\n", "# ren.AddVolume(vol_actor)\n", - "# print(f\"βœ“ Added CPU volume: {cname}\")\n", + "# print(f\"\u2713 Added CPU volume: {cname}\")\n", "\n", "# ren.ResetCamera()\n", "# cam = ren.GetActiveCamera()\n", "# _rotate_camera(cam, ELEV, AZIM, ROT_Y_DEG)\n", "# win.Render()\n", "# _save_png(win, FIG_PATH)\n", - "# print(f\"βœ… VTK CPU volume composite saved β†’ {FIG_PATH}\")\n", + "# print(f\"\u2705 VTK CPU volume composite saved \u2192 {FIG_PATH}\")\n", "\n", "# # -----------------------------------------------------\n", "# # Driver\n", @@ -1419,12 +1404,12 @@ "# try:\n", "# render_volume_gpu(volumes)\n", "# except Exception as e:\n", - "# print(f\"⚠️ GPU volume failed: {e}\\nβ†’ Falling back to CPU raycast.\")\n", + "# print(f\"\u26a0\ufe0f GPU volume failed: {e}\\n\u2192 Falling back to CPU raycast.\")\n", "# render_volume_cpu(volumes)\n", "# else:\n", "# render_volume_cpu(volumes)\n", "# except Exception as e:\n", - "# print(\"❌ VTK render failed:\", repr(e))\n" + "# print(\"\u274c VTK render failed:\", repr(e))\n" ] }, { @@ -1467,11 +1452,11 @@ "\n", "# for cname in TARGET_CLASS_NAMES:\n", "# if cname not in volumes:\n", - "# print(f\"⚠️ Missing {cname}\")\n", + "# print(f\"\u26a0\ufe0f Missing {cname}\")\n", "# continue\n", "# mask = volumes[cname][::Z_STEP, ::YX_STEP, ::YX_STEP]\n", "# if mask.max() == 0:\n", - "# print(f\"⚠️ {cname} empty\")\n", + "# print(f\"\u26a0\ufe0f {cname} empty\")\n", "# continue\n", "\n", "# vtk_img = vtk.vtkImageData()\n", @@ -1510,7 +1495,7 @@ "# volume.SetMapper(mapper)\n", "# volume.SetProperty(prop)\n", "# renderer.AddVolume(volume)\n", - "# print(f\"βœ“ Added {cname}\")\n", + "# print(f\"\u2713 Added {cname}\")\n", "\n", "# # =====================================================\n", "# # Offscreen Render (EGL)\n", @@ -1541,7 +1526,7 @@ "# writer.SetInputConnection(w2i.GetOutputPort())\n", "# writer.Write()\n", "\n", - "# print(f\"βœ… GPU composite render saved β†’ {FIG_PATH}\")\n" + "# print(f\"\u2705 GPU composite render saved \u2192 {FIG_PATH}\")\n" ] }, { @@ -1677,7 +1662,7 @@ " actor = actor_points(poly, CLASS_TINTS[cname], size_px=POINT_SIZE)\n", " save_scene([actor], OUT_DIR/f\"{cname.replace(' ','_').lower()}.png\")\n", "\n", - "# ---- 2Γ—2 panel (same camera across subviews)\n", + "# ---- 2\u00d72 panel (same camera across subviews)\n", "# simplest is to save singles (above) and montage externally; or\n", "# reuse your existing multi-viewport scaffolding now that points render correctly.\n", "print(\"Saved to\", OUT_DIR)\n" @@ -1911,7 +1896,7 @@ ], "source": [ "# # ============================================\n", - "# # VTK point clouds β€” equalized axes (Z:Y:X = 1:1:1)\n", + "# # VTK point clouds \u2014 equalized axes (Z:Y:X = 1:1:1)\n", "# # ============================================\n", "# import numpy as np, vtk, scipy.ndimage as ndi\n", "# from vtk.util import numpy_support\n", @@ -1964,10 +1949,10 @@ "# \"\"\"\n", "# Z, Y, X = map(float, shape_zyx)\n", "# if equalize:\n", - "# # map to [0,1] per axis β†’ scale by common M so all axes have same visual scale\n", + "# # map to [0,1] per axis \u2192 scale by common M so all axes have same visual scale\n", "# # choose M = max(Z, Y, X) to keep numeric range reasonable\n", "# M = max(Z, Y, X)\n", - "# sx, sy, sz = (M/X, M/Y, M/Z) # multiply x by M/X etc β†’ all axes span ~M\n", + "# sx, sy, sz = (M/X, M/Y, M/Z) # multiply x by M/X etc \u2192 all axes span ~M\n", "# xyz = np.empty((len(ijk), 3), np.float32)\n", "# xyz[:,0] = ijk[:,2] * sx # x\n", "# xyz[:,1] = ijk[:,1] * sy # y\n", @@ -2064,7 +2049,7 @@ "# Z_STEP = int(globals().get(\"Z_STEP\", 1))\n", "# YX_STEP = int(globals().get(\"YX_STEP\", 1))\n", "\n", - "# # Physical spacing (use what’s defined earlier if present, else 1.0)\n", + "# # Physical spacing (use what\u2019s defined earlier if present, else 1.0)\n", "# VOXEL_SPACING = globals().get(\"VOXEL_SPACING\", (1.0, 1.0, 1.0)) # (dz, dy, dx)\n", "# _voxel_volume = float(VOXEL_SPACING[0] * VOXEL_SPACING[1] * VOXEL_SPACING[2])\n", "\n", @@ -2126,10 +2111,10 @@ "# if sample_vol is not None:\n", "# sample_voxels = int(sample_vol.sum())\n", "# if sample_voxels == 0:\n", - "# print(\"⚠️ Sample mask is empty. Fractions will fall back to grid size.\")\n", + "# print(\"\u26a0\ufe0f Sample mask is empty. Fractions will fall back to grid size.\")\n", "# sample_voxels = None\n", "\n", - "# # ====== 2Γ—2 Rendering Panel (unchanged below, but now reading from `volumes`) ======\n", + "# # ====== 2\u00d72 Rendering Panel (unchanged below, but now reading from `volumes`) ======\n", "# fig = plt.figure(figsize=(12, 10), dpi=120)\n", "# panel_names = [\"Bright\", \"Light Gray\", \"Dark Gray\", \"Porosity\"]\n", "\n", @@ -2246,4 +2231,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/segmentation/leaf_segmentation/Dockerfile b/segmentation/leaf_segmentation/Dockerfile index 9d3e133..f0b3a9f 100644 --- a/segmentation/leaf_segmentation/Dockerfile +++ b/segmentation/leaf_segmentation/Dockerfile @@ -11,8 +11,7 @@ RUN apt-get update && apt-get install -y \ git \ && rm -rf /var/lib/apt/lists/* -# Install Python dependencies -# Using a fixed version for consistency where possible +# Install Python dependencies for segmentation and Jupyter RUN pip install --no-cache-dir \ segmentation-models-pytorch==0.3.3 \ albumentations==1.4.0 \ @@ -21,19 +20,19 @@ RUN pip install --no-cache-dir \ tqdm \ pillow \ scikit-image \ - scikit-learn + scikit-learn \ + ipywidgets \ + jupyterlab -# Create directories for data and output -RUN mkdir -p /app/sample_images /app/output_masks +# Copy the notebook into the container +COPY leaf_segmentation.ipynb . +# COPY run_inference.py . # Keeping this for reference if needed -# Copy inference script and weights -# Note: Users should ideally mount their own images and weights -COPY run_inference.py . -# COPY best_model.pth . - -# Set environmental variables +# Set environment variables ENV PYTHONUNBUFFERED=1 -# Default command: run inference -# Expects images in /app/sample_images and weights at /app/best_model.pth -CMD ["python", "run_inference.py"] +# Expose Jupyter port +EXPOSE 8888 + +# Default command: Start Jupyter Lab +CMD ["jupyter", "lab", "--ip=0.0.0.0", "--no-browser", "--allow-root"] diff --git a/segmentation/leaf_segmentation/Leaf_Segmentation_Model/run_inference.py b/segmentation/leaf_segmentation/Leaf_Segmentation_Model/run_inference.py new file mode 100644 index 0000000..9a52351 --- /dev/null +++ b/segmentation/leaf_segmentation/Leaf_Segmentation_Model/run_inference.py @@ -0,0 +1,192 @@ +import os +import torch +import numpy as np +import pandas as pd +from PIL import Image +from tqdm import tqdm +from torch.amp import autocast +import segmentation_models_pytorch as smp + +def run_inference( + img_dir, + output_mask_dir, + output_csv_path, + model_weights_path, + num_classes=5, + patch_size=320, + stride=160 +): + """ + Run inference on a folder of grayscale CT images using the trained FPN (MiT-B4) model. + """ + # 1. Setup Device + device = torch.device("cuda" if torch.cuda.is_available() else "cpu") + print(f"Running inference on: {device}") + + # Ensure output directories exist + os.makedirs(output_mask_dir, exist_ok=True) + + # Class names mapping + CLASS_NAMES = ["Background", "Epidermis", "Vascular_Region", "Mesophyll", "Air_Space"] + + # 2. Re-instantiate the Model Model Structure + print("Initializing Model...") + model_eval = smp.FPN( + encoder_name="mit_b4", + encoder_weights=None, # No need to download internet weights for inference + in_channels=1, # Grayscale inputs + classes=num_classes, + ) + + # 3. Load Trained Weights + print(f"Loading weights from: {model_weights_path}") + checkpoint = torch.load(model_weights_path, map_location=device) + model_eval.load_state_dict(checkpoint['model_state_dict']) + model_eval = model_eval.to(device) + model_eval.eval() + + # 4. Find all Images + img_files = sorted([f for f in os.listdir(img_dir) + if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))]) + + print(f"Found {len(img_files)} images to process.") + + results = [] + + # 5. Run Inference Loop + for fname in tqdm(img_files, desc="Inference"): + img = np.array(Image.open(os.path.join(img_dir, fname))) + + # Handle RGB β†’ grayscale if someone passes RGB images by mistake + if img.ndim == 3 and img.shape[2] == 4: + img = img[:, :, :3] + if img.ndim == 3: + img = np.dot(img[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8).copy() + + img_t = torch.from_numpy(img.copy()).float().unsqueeze(0).unsqueeze(0) + + # Z-score standardization on non-zero tissue pixels (MUST MATCH TRAINING!) + valid_pixels = img_t[img_t > 0] + if len(valid_pixels) > 0: + mean, std = valid_pixels.mean(), valid_pixels.std() + if std > 1e-5: + img_t = (img_t - mean) / std + + # Pad the image to be a multiple of 32 (required by FPN scaling) + h, w = img.shape + pad_h = (32 - h % 32) % 32 + pad_w = (32 - w % 32) % 32 + if pad_h > 0 or pad_w > 0: + img_t = torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), mode='reflect') + + img_t = img_t.to(device) + + # --- SLIDING WINDOW INFERENCE TO PREVENT OOM --- + # Splitting large images into patches and averaging the overlaps + _, _, H_t, W_t = img_t.shape + pred_prob_accum = torch.zeros((1, num_classes, H_t, W_t), device=device, dtype=torch.float32) + count_accum = torch.zeros((1, 1, H_t, W_t), device=device, dtype=torch.float32) + + with torch.no_grad(): + # Use mixed precision (bfloat16) to save memory if on GPU, otherwise standard float + dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32 + with autocast('cuda' if torch.cuda.is_available() else 'cpu', dtype=dtype): + # Iterate through grid + for py in range(0, H_t, stride): + for px in range(0, W_t, stride): + y1 = min(py, H_t - patch_size) + y2 = y1 + patch_size + x1 = min(px, W_t - patch_size) + x2 = x1 + patch_size + + patch = img_t[:, :, y1:y2, x1:x2] + + # 1. Original Prediction + p_orig = model_eval(patch).softmax(dim=1) + + # Test Time Augmentation (TTA) for robustness + # 2. Horizontal Flip + p_hflip = model_eval(torch.flip(patch, dims=[3])) + p_hflip = torch.flip(p_hflip.softmax(dim=1), dims=[3]) + + # 3. Vertical Flip + p_vflip = model_eval(torch.flip(patch, dims=[2])) + p_vflip = torch.flip(p_vflip.softmax(dim=1), dims=[2]) + + # Average the TTA predictions + p_avg = (p_orig + p_hflip + p_vflip) / 3.0 + + # Accumulate probabilities in the main image map + pred_prob_accum[:, :, y1:y2, x1:x2] += p_avg + count_accum[:, :, y1:y2, x1:x2] += 1.0 + + # Average the overlaps where patches met + pred_prob = pred_prob_accum / count_accum + + # Take the argmax to get the final class prediction + pred_cls = torch.argmax(pred_prob, dim=1).squeeze().cpu().numpy() + + # Crop back to exact original image size if padding was added earlier + if pad_h > 0 or pad_w > 0: + pred_cls = pred_cls[:h, :w] + + # 6. Save Output Mask + stem = os.path.splitext(fname)[0] + out_path = os.path.join(output_mask_dir, f"{stem}_pred.png") + Image.fromarray(pred_cls.astype(np.uint8)).save(out_path) + + # 7. Calculate Tissue Fractions + total = pred_cls.size + row = {'filename': fname} + for c, name in enumerate(CLASS_NAMES): + count = int(np.sum(pred_cls == c)) + row[f"{name}_pixels"] = count + row[f"{name}_fraction"] = round(count / total, 6) + + # Optional: Define porosity as Air_Space fraction + row['porosity'] = row['Air_Space_fraction'] + results.append(row) + + # 8. Save Dataset Analytics CSV + results_df = pd.DataFrame(results) + results_df.to_csv(output_csv_path, index=False) + + print(f"\nInference Complete!") + print(f"Masks saved to: {output_mask_dir}") + print(f"Data saved to: {output_csv_path}") + +if __name__ == "__main__": + # ========================================================================= + # USER CONFIGURATION: Update these paths for your local machine! + # ========================================================================= + + # The folder containing the images you want to segment + INPUT_IMAGES_DIR = "./sample_images" + + # Path to the pretrained model weights provided to you + MODEL_WEIGHTS = "./best_model.pth" + + # Where you want the segmented masks to drop + OUTPUT_MASKS_DIR = "./output_masks" + + # Where you want the fraction/porosity measurements saved + OUTPUT_CSV_FILE = "./output_fraction_results.csv" + + # ========================================================================= + + # Check that paths exist before running + if not os.path.exists(INPUT_IMAGES_DIR): + print(f"Please create the directory '{INPUT_IMAGES_DIR}' and put images inside it.") + exit(1) + + if not os.path.exists(MODEL_WEIGHTS): + print(f"Error: Could not find model weights at '{MODEL_WEIGHTS}'.") + exit(1) + + # Run the pipeline + run_inference( + img_dir=INPUT_IMAGES_DIR, + output_mask_dir=OUTPUT_MASKS_DIR, + output_csv_path=OUTPUT_CSV_FILE, + model_weights_path=MODEL_WEIGHTS + ) diff --git a/segmentation/leaf_segmentation/docker-compose.yml b/segmentation/leaf_segmentation/docker-compose.yml deleted file mode 100644 index 53902c4..0000000 --- a/segmentation/leaf_segmentation/docker-compose.yml +++ /dev/null @@ -1,13 +0,0 @@ -services: - leaf-segmentation: - build: - context: . - dockerfile: Dockerfile - platform: linux/amd64 - container_name: leaf-segmentation - volumes: - - .:/alsuser - ports: - - "8001:8001" - command: jupyter lab --ip=0.0.0.0 --port=8001 --no-browser --allow-root --NotebookApp.token='' - restart: unless-stopped diff --git a/segmentation/leaf_segmentation/leaf_segmentation.ipynb b/segmentation/leaf_segmentation/leaf_segmentation.ipynb new file mode 100644 index 0000000..c12df12 --- /dev/null +++ b/segmentation/leaf_segmentation/leaf_segmentation.ipynb @@ -0,0 +1,355 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Leaf Segmentation Inference Notebook\n", + "\n", + "This notebook provides a user-friendly interface for running leaf segmentation using a trained FPN (MiT-B4) model. It is adapted from `run_inference.py`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "import torch\n", + "import numpy as np\n", + "import pandas as pd\n", + "from PIL import Image\n", + "from tqdm.notebook import tqdm\n", + "from torch.amp import autocast\n", + "import segmentation_models_pytorch as smp\n", + "import matplotlib.pyplot as plt\n", + "import ipywidgets as widgets\n", + "from IPython.display import display" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "#this should be the name of the folder that has reconstructions or the parent folder of the folder with the extracted files \n", + "inputSubFolderName = \"/pscratch/sd/e/elavarpa/reconstructions_new\" \n", + "\n", + "#outputSubfolderName = \"/pscratch/sd/e/elavarpa/moth-masks\" #this can be anything you want, I usually choose the current date\n", + "inputPath = os.path.join(\"/alsdata\", inputSubFolderName)\n", + "\n", + "###########################\n", + "WEIGHTS_PATH = \"/pscratch/sd/e/elavarpa/model_final.pth\" # The path to where the model weights are stored\n", + "#inputPath = os.path.join(\".\", inputSubFolderName)\n", + "\n", + "if os.path.isdir(\"/alsuser/pscratch\"):\n", + " wheretosave = \"pscratch\"\n", + "elif os.path.isdir(\"/alsuser/cscratch\"): \n", + " wheretosave = \"cscratch\"\n", + "else:\n", + " wheretosave = \"notebooks\" \n", + "\n", + "filenamelist = os.listdir(inputPath) if os.path.exists(inputPath) else []\n", + "filenamelist.sort()\n", + "for i in range(len(filenamelist)-1,np.maximum(len(filenamelist)-10000,-1),-1):\n", + " print(f'{i}: {filenamelist[i]}')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if filenamelist:\n", + " folder_name = filenamelist[1] #update this number with the index of the file you want to process from the directory listing generated in the previous cell\n", + " folder_path = os.path.join(inputPath, folder_name)\n", + " print(folder_path)\n", + " print(folder_name + 'oo')\n", + " # outputSubfolderName = f\"{folder_name}_masks\"#this can be anything you want, I usually choose the current date \n", + " # outputPath = os.path.join(\"/alsuser/\", wheretosave, outputSubfolderName) \n", + " # if not os.path.exists(outputPath):\n", + " # os.mkdir(outputPath)\n", + "else:\n", + " print(\"No files found or path does not exist.\")\n", + " folder_path = \"./sample_images\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Widget to let the user crop their data across the whole stack.\n", + "try:\n", + " sample_img_name = [f for f in os.listdir(folder_path) if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))][0]\n", + " sample_img_path = os.path.join(folder_path, sample_img_name)\n", + " sample_img = np.array(Image.open(sample_img_path))\n", + " height, width = sample_img.shape[:2]\n", + " \n", + " crop_y = widgets.IntRangeSlider(\n", + " value=[0, height], min=0, max=height, step=1, \n", + " description='Y (Height):', continuous_update=False)\n", + " \n", + " crop_x = widgets.IntRangeSlider(\n", + " value=[0, width], min=0, max=width, step=1, \n", + " description='X (Width):', continuous_update=False)\n", + " \n", + " def update_crop(y_range, x_range):\n", + " cropped = sample_img[y_range[0]:y_range[1], x_range[0]:x_range[1]]\n", + " plt.figure(figsize=(6,6))\n", + " plt.imshow(cropped, cmap='gray')\n", + " plt.title(f\"Cropped region: shape {cropped.shape}\")\n", + " plt.axis('off')\n", + " plt.show()\n", + " \n", + " out = widgets.interactive_output(update_crop, {'y_range': crop_y, 'x_range': crop_x})\n", + " display(widgets.VBox([crop_y, crop_x, out]))\n", + "except Exception as e:\n", + " print(f\"Could not load a sample image for cropping widget: {e}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Data Type Discussion\n", + "Note on `inference.py` and this notebook: The image data is loaded into `float32` tensors for inference. PyTorch model inference uses `.float()` by default here. **Data from MicroCT is generally collected at float32**, but you can optionally change it to `uint8` before processing if your model was specifically trained on `uint8` scaled data (0-255). " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Configuration\n", + "Update these paths to match your local setup." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# The folder containing the images you want to segment\n", + "INPUT_IMAGES_DIR = folder_path if 'folder_path' in locals() else \"./sample_images\" \n", + "\n", + "# Path to the pretrained model weights provided to you\n", + "MODEL_WEIGHTS = WEIGHTS_PATH if 'WEIGHTS_PATH' in locals() else \"./best_model.pth\"\n", + "\n", + "# Where you want the segmented masks to drop\n", + "OUTPUT_MASKS_DIR = \"./output_masks\"\n", + "\n", + "# Where you want the fraction/porosity measurements saved\n", + "OUTPUT_CSV_FILE = \"./output_fraction_results.csv\"\n", + "\n", + "NUM_CLASSES = 5\n", + "PATCH_SIZE = 320\n", + "STRIDE = 160\n", + "\n", + "CLASS_NAMES = [\"Background\", \"Epidermis\", \"Vascular_Region\", \"Mesophyll\", \"Air_Space\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Model Initialization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", + "print(f\"Running inference on: {device}\")\n", + "\n", + "# Ensure output directories exist\n", + "os.makedirs(OUTPUT_MASKS_DIR, exist_ok=True)\n", + "\n", + "print(\"Initializing Model...\")\n", + "model_eval = smp.FPN(\n", + " encoder_name=\"mit_b4\",\n", + " encoder_weights=None,\n", + " in_channels=1,\n", + " classes=NUM_CLASSES,\n", + ")\n", + "\n", + "print(f\"Loading weights from: {MODEL_WEIGHTS}\")\n", + "if os.path.exists(MODEL_WEIGHTS):\n", + " checkpoint = torch.load(MODEL_WEIGHTS, map_location=device)\n", + " model_eval.load_state_dict(checkpoint['model_state_dict'])\n", + " model_eval = model_eval.to(device)\n", + " model_eval.eval()\n", + "else:\n", + " print(f\"Error: Weights not found at {MODEL_WEIGHTS}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Inference Loop" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "img_files = sorted([f for f in os.listdir(INPUT_IMAGES_DIR)\n", + " if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))])\n", + "print(f\"Found {len(img_files)} images to process.\")\n", + "\n", + "results = []\n", + "\n", + "for fname in tqdm(img_files, desc=\"Inference\"):\n", + " img_path = os.path.join(INPUT_IMAGES_DIR, fname)\n", + " img = np.array(Image.open(img_path))\n", + " \n", + " # Apply crop if variables from widget exist\n", + " if 'crop_y' in locals() and 'crop_x' in locals():\n", + " y_min, y_max = crop_y.value\n", + " x_min, x_max = crop_x.value\n", + " img = img[y_min:y_max, x_min:x_max]\n", + " \n", + " # Optional: if model expects uint8 data, uncomment below to cast\n", + " # img = (img / img.max() * 255).astype(np.uint8) if img.dtype != np.uint8 else img\n", + " \n", + " if img.ndim == 3 and img.shape[2] == 4:\n", + " img = img[:, :, :3]\n", + " if img.ndim == 3:\n", + " img = np.dot(img[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8).copy()\n", + " \n", + " img_t = torch.from_numpy(img.copy()).float().unsqueeze(0).unsqueeze(0)\n", + " \n", + " valid_pixels = img_t[img_t > 0]\n", + " if len(valid_pixels) > 0:\n", + " mean, std = valid_pixels.mean(), valid_pixels.std()\n", + " if std > 1e-5:\n", + " img_t = (img_t - mean) / std\n", + "\n", + " h, w = img.shape\n", + " pad_h = (32 - h % 32) % 32\n", + " pad_w = (32 - w % 32) % 32\n", + " if pad_h > 0 or pad_w > 0:\n", + " img_t = torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), mode='reflect')\n", + "\n", + " img_t = img_t.to(device)\n", + " \n", + " _, _, H_t, W_t = img_t.shape\n", + " pred_prob_accum = torch.zeros((1, NUM_CLASSES, H_t, W_t), device=device, dtype=torch.float32)\n", + " count_accum = torch.zeros((1, 1, H_t, W_t), device=device, dtype=torch.float32)\n", + " \n", + " with torch.no_grad():\n", + " dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32\n", + " with autocast('cuda' if torch.cuda.is_available() else 'cpu', dtype=dtype):\n", + " for py in range(0, H_t, STRIDE):\n", + " for px in range(0, W_t, STRIDE):\n", + " y1 = min(py, H_t - PATCH_SIZE)\n", + " y2 = y1 + PATCH_SIZE\n", + " x1 = min(px, W_t - PATCH_SIZE)\n", + " x2 = x1 + PATCH_SIZE\n", + " \n", + " patch = img_t[:, :, y1:y2, x1:x2]\n", + " p_orig = model_eval(patch).softmax(dim=1)\n", + " \n", + " p_hflip = model_eval(torch.flip(patch, dims=[3]))\n", + " p_hflip = torch.flip(p_hflip.softmax(dim=1), dims=[3])\n", + " \n", + " p_vflip = model_eval(torch.flip(patch, dims=[2]))\n", + " p_vflip = torch.flip(p_vflip.softmax(dim=1), dims=[2])\n", + " \n", + " p_avg = (p_orig + p_hflip + p_vflip) / 3.0\n", + " \n", + " pred_prob_accum[:, :, y1:y2, x1:x2] += p_avg\n", + " count_accum[:, :, y1:y2, x1:x2] += 1.0\n", + " \n", + " pred_prob = pred_prob_accum / count_accum\n", + " pred_cls = torch.argmax(pred_prob, dim=1).squeeze().cpu().numpy()\n", + " \n", + " if pad_h > 0 or pad_w > 0:\n", + " pred_cls = pred_cls[:h, :w]\n", + "\n", + " stem = os.path.splitext(fname)[0]\n", + " out_path = os.path.join(OUTPUT_MASKS_DIR, f\"{stem}_pred.png\")\n", + " Image.fromarray(pred_cls.astype(np.uint8)).save(out_path)\n", + " \n", + " total = pred_cls.size\n", + " row = {'filename': fname}\n", + " for c, name in enumerate(CLASS_NAMES):\n", + " count = int(np.sum(pred_cls == c))\n", + " row[f\"{name}_pixels\"] = count\n", + " row[f\"{name}_fraction\"] = round(count / total, 6)\n", + " \n", + " row['porosity'] = row['Air_Space_fraction']\n", + " results.append(row)\n", + "\n", + "results_df = pd.DataFrame(results)\n", + "results_df.to_csv(OUTPUT_CSV_FILE, index=False)\n", + "print(\"Inference Complete!\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Visualization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if results:\n", + " last_img_name = results[-1]['filename']\n", + " last_mask_name = os.path.splitext(last_img_name)[0] + \"_pred.png\"\n", + " \n", + " img = Image.open(os.path.join(INPUT_IMAGES_DIR, last_img_name))\n", + " mask = Image.open(os.path.join(OUTPUT_MASKS_DIR, last_mask_name))\n", + " \n", + " fig, ax = plt.subplots(1, 2, figsize=(12, 6))\n", + " ax[0].imshow(img, cmap='gray')\n", + " ax[0].set_title(\"Original Image\")\n", + " ax[0].axis('off')\n", + " \n", + " ax[1].imshow(mask, cmap='jet')\n", + " ax[1].set_title(\"Predicted Mask\")\n", + " ax[1].axis('off')\n", + " \n", + " plt.show()\n", + " \n", + " display(results_df.head())" + ] + } + ], + "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.10.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} \ No newline at end of file From 36293548785fe40abe1930ee4070cbbbd7d378f4 Mon Sep 17 00:00:00 2001 From: Pradyumna Elavarthi Date: Wed, 25 Mar 2026 15:17:58 -0700 Subject: [PATCH 4/4] Fix GitHub Actions: add missing permissions and dynamic image naming --- .github/workflows/publish-image.yaml | 45 ---- .../workflows/publish-segmentation-image.yaml | 6 +- .gitignore | 4 +- .../leaf_segmentation/run_inference.ipynb | 252 ------------------ 4 files changed, 8 insertions(+), 299 deletions(-) delete mode 100644 .github/workflows/publish-image.yaml delete mode 100644 segmentation/leaf_segmentation/run_inference.ipynb diff --git a/.github/workflows/publish-image.yaml b/.github/workflows/publish-image.yaml deleted file mode 100644 index 501a390..0000000 --- a/.github/workflows/publish-image.yaml +++ /dev/null @@ -1,45 +0,0 @@ -name: Create and publish image - -on: - push: - branches: [ 'master' ] - tags: [ 'v*' ] - -env: - REGISTRY: ghcr.io - IMAGE_NAME: ${{ github.repository }} - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Log in to the Container registry - uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 - with: - images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} - - - name: Build and push Docker image - uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc - with: - context: . - file: Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} diff --git a/.github/workflows/publish-segmentation-image.yaml b/.github/workflows/publish-segmentation-image.yaml index 583783a..4d52c3c 100644 --- a/.github/workflows/publish-segmentation-image.yaml +++ b/.github/workflows/publish-segmentation-image.yaml @@ -10,10 +10,14 @@ on: env: REGISTRY: ghcr.io - IMAGE_NAME: als-computing/microct-segmentation + IMAGE_NAME: ${{ github.repository }} jobs: build-and-push-image: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write strategy: fail-fast: false matrix: diff --git a/.gitignore b/.gitignore index 1147723..74c4f70 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,6 @@ env/ .env/ .venv/ ENV/ -.ENV/.DS_Store +.ENV/ +.DS_Store +*.zip diff --git a/segmentation/leaf_segmentation/run_inference.ipynb b/segmentation/leaf_segmentation/run_inference.ipynb deleted file mode 100644 index 28db88a..0000000 --- a/segmentation/leaf_segmentation/run_inference.ipynb +++ /dev/null @@ -1,252 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Leaf Segmentation Inference Notebook\n", - "\n", - "This notebook provides a user-friendly interface for running leaf segmentation using a trained FPN (MiT-B4) model. It is adapted from `run_inference.py`." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import os\n", - "import torch\n", - "import numpy as np\n", - "import pandas as pd\n", - "from PIL import Image\n", - "from tqdm.notebook import tqdm\n", - "from torch.amp import autocast\n", - "import segmentation_models_pytorch as smp\n", - "import matplotlib.pyplot as plt" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Configuration\n", - "Update these paths to match your local setup." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# The folder containing the images you want to segment\n", - "INPUT_IMAGES_DIR = \"./sample_images\" \n", - "\n", - "# Path to the pretrained model weights provided to you\n", - "MODEL_WEIGHTS = \"./best_model.pth\"\n", - "\n", - "# Where you want the segmented masks to drop\n", - "OUTPUT_MASKS_DIR = \"./output_masks\"\n", - "\n", - "# Where you want the fraction/porosity measurements saved\n", - "OUTPUT_CSV_FILE = \"./output_fraction_results.csv\"\n", - "\n", - "NUM_CLASSES = 5\n", - "PATCH_SIZE = 320\n", - "STRIDE = 160\n", - "\n", - "CLASS_NAMES = [\"Background\", \"Epidermis\", \"Vascular_Region\", \"Mesophyll\", \"Air_Space\"]" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Model Initialization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\n", - "print(f\"Running inference on: {device}\")\n", - "\n", - "# Ensure output directories exist\n", - "os.makedirs(OUTPUT_MASKS_DIR, exist_ok=True)\n", - "\n", - "print(\"Initializing Model...\")\n", - "model_eval = smp.FPN(\n", - " encoder_name=\"mit_b4\",\n", - " encoder_weights=None,\n", - " in_channels=1,\n", - " classes=NUM_CLASSES,\n", - ")\n", - "\n", - "print(f\"Loading weights from: {MODEL_WEIGHTS}\")\n", - "if os.path.exists(MODEL_WEIGHTS):\n", - " checkpoint = torch.load(MODEL_WEIGHTS, map_location=device)\n", - " model_eval.load_state_dict(checkpoint['model_state_dict'])\n", - " model_eval = model_eval.to(device)\n", - " model_eval.eval()\n", - "else:\n", - " print(f\"Error: Weights not found at {MODEL_WEIGHTS}\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Inference Loop" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "img_files = sorted([f for f in os.listdir(INPUT_IMAGES_DIR)\n", - " if f.lower().endswith(('.png', '.tif', '.tiff', '.jpg', '.jpeg'))])\n", - "print(f\"Found {len(img_files)} images to process.\")\n", - "\n", - "results = []\n", - "\n", - "for fname in tqdm(img_files, desc=\"Inference\"):\n", - " img_path = os.path.join(INPUT_IMAGES_DIR, fname)\n", - " img = np.array(Image.open(img_path))\n", - " \n", - " if img.ndim == 3 and img.shape[2] == 4:\n", - " img = img[:, :, :3]\n", - " if img.ndim == 3:\n", - " img = np.dot(img[..., :3], [0.299, 0.587, 0.114]).astype(np.uint8).copy()\n", - " \n", - " img_t = torch.from_numpy(img.copy()).float().unsqueeze(0).unsqueeze(0)\n", - " \n", - " valid_pixels = img_t[img_t > 0]\n", - " if len(valid_pixels) > 0:\n", - " mean, std = valid_pixels.mean(), valid_pixels.std()\n", - " if std > 1e-5:\n", - " img_t = (img_t - mean) / std\n", - "\n", - " h, w = img.shape\n", - " pad_h = (32 - h % 32) % 32\n", - " pad_w = (32 - w % 32) % 32\n", - " if pad_h > 0 or pad_w > 0:\n", - " img_t = torch.nn.functional.pad(img_t, (0, pad_w, 0, pad_h), mode='reflect')\n", - "\n", - " img_t = img_t.to(device)\n", - " \n", - " _, _, H_t, W_t = img_t.shape\n", - " pred_prob_accum = torch.zeros((1, NUM_CLASSES, H_t, W_t), device=device, dtype=torch.float32)\n", - " count_accum = torch.zeros((1, 1, H_t, W_t), device=device, dtype=torch.float32)\n", - " \n", - " with torch.no_grad():\n", - " dtype = torch.bfloat16 if torch.cuda.is_available() else torch.float32\n", - " with autocast('cuda' if torch.cuda.is_available() else 'cpu', dtype=dtype):\n", - " for py in range(0, H_t, STRIDE):\n", - " for px in range(0, W_t, STRIDE):\n", - " y1 = min(py, H_t - PATCH_SIZE)\n", - " y2 = y1 + PATCH_SIZE\n", - " x1 = min(px, W_t - PATCH_SIZE)\n", - " x2 = x1 + PATCH_SIZE\n", - " \n", - " patch = img_t[:, :, y1:y2, x1:x2]\n", - " p_orig = model_eval(patch).softmax(dim=1)\n", - " \n", - " p_hflip = model_eval(torch.flip(patch, dims=[3]))\n", - " p_hflip = torch.flip(p_hflip.softmax(dim=1), dims=[3])\n", - " \n", - " p_vflip = model_eval(torch.flip(patch, dims=[2]))\n", - " p_vflip = torch.flip(p_vflip.softmax(dim=1), dims=[2])\n", - " \n", - " p_avg = (p_orig + p_hflip + p_vflip) / 3.0\n", - " \n", - " pred_prob_accum[:, :, y1:y2, x1:x2] += p_avg\n", - " count_accum[:, :, y1:y2, x1:x2] += 1.0\n", - " \n", - " pred_prob = pred_prob_accum / count_accum\n", - " pred_cls = torch.argmax(pred_prob, dim=1).squeeze().cpu().numpy()\n", - " \n", - " if pad_h > 0 or pad_w > 0:\n", - " pred_cls = pred_cls[:h, :w]\n", - "\n", - " stem = os.path.splitext(fname)[0]\n", - " out_path = os.path.join(OUTPUT_MASKS_DIR, f\"{stem}_pred.png\")\n", - " Image.fromarray(pred_cls.astype(np.uint8)).save(out_path)\n", - " \n", - " total = pred_cls.size\n", - " row = {'filename': fname}\n", - " for c, name in enumerate(CLASS_NAMES):\n", - " count = int(np.sum(pred_cls == c))\n", - " row[f\"{name}_pixels\"] = count\n", - " row[f\"{name}_fraction\"] = round(count / total, 6)\n", - " \n", - " row['porosity'] = row['Air_Space_fraction']\n", - " results.append(row)\n", - "\n", - "results_df = pd.DataFrame(results)\n", - "results_df.to_csv(OUTPUT_CSV_FILE, index=False)\n", - "print(\"Inference Complete!\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## Visualization" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if results:\n", - " last_img_name = results[-1]['filename']\n", - " last_mask_name = os.path.splitext(last_img_name)[0] + \"_pred.png\"\n", - " \n", - " img = Image.open(os.path.join(INPUT_IMAGES_DIR, last_img_name))\n", - " mask = Image.open(os.path.join(OUTPUT_MASKS_DIR, last_mask_name))\n", - " \n", - " fig, ax = plt.subplots(1, 2, figsize=(12, 6))\n", - " ax[0].imshow(img, cmap='gray')\n", - " ax[0].set_title(\"Original Image\")\n", - " ax[0].axis('off')\n", - " \n", - " ax[1].imshow(mask, cmap='jet')\n", - " ax[1].set_title(\"Predicted Mask\")\n", - " ax[1].axis('off')\n", - " \n", - " plt.show()\n", - " \n", - " display(results_df.head())" - ] - } - ], - "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.10.0" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -}