Skip to content

Commit

Permalink
Merge pull request #1 from frankkramer-lab/rework_stain_normalization
Browse files Browse the repository at this point in the history
Rework stain normalization
  • Loading branch information
muellerdo authored Jan 22, 2024
2 parents 3d8cb0b + 383082f commit e504bfb
Show file tree
Hide file tree
Showing 14 changed files with 328 additions and 44 deletions.
36 changes: 36 additions & 0 deletions .github/workflows/build-package.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# This workflow will install Python dependencies and run tests with a variety of Python versions
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
# CUSTOM Modifications by Dominik Müller (2022)

name: Python Package - Build

on:
push:
branches: [ main ]
pull_request:
branches: [ main ]

jobs:
requirements:
name: Requirements
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ['3.8', '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
sudo apt -y install libvips-dev
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Test with pytest
run: |
git lfs pull
pytest -v
89 changes: 89 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
#==============================================================================#
# Author: Dominik Müller #
# Copyright: 2024 AG-RAIMIA-Müller, University of Augsburg, #
# University of Augsburg #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
#==============================================================================#
# This workflow will:
# - create a CHANGELOG between latest tag and current tag
# - release the package on GitHub
# - dockerize the package on GitHub (GitHub Container Registry)
# - upload it to PyPI

name: Package Release
on:
push:
tags:
- 'v[0-9]+.[0-9]+'

jobs:
release:
name: Release on GitHub
runs-on: ubuntu-latest

steps:
- name: Checkout Code
uses: actions/checkout@v2

- name: Create CHANGELOG
id: changelog
uses: Requarks/changelog-action@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref_name }}
excludeTypes: other,style

- name: Create Release
uses: actions/create-release@v1
with:
tag_name: ${{ github.ref }}
release_name: aucmedi_${{ github.ref }}
body: ${{ steps.changelog.outputs.changes }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

dockerize:
name: Dockerize on GitHub
runs-on: ubuntu-latest
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Log in to the Container registry
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v4
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}

- name: Build and push Docker image
uses: docker/build-push-action@v3
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
59 changes: 59 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#==============================================================================#
# Author: Dominik Müller #
# Copyright: 2024 AG-RAIMIA-Müller, University of Augsburg, #
# University of Augsburg #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <http://www.gnu.org/licenses/>. #
#==============================================================================#
#-----------------------------------------------------#
# Information & System Base #
#-----------------------------------------------------#
# Base image
FROM tensorflow/tensorflow:latest-gpu

# Meta information
LABEL authors="Dominik Müller"
LABEL contact="dominik.mueller@informatik.uni-augsburg.de"
LABEL repository="https://github.com/frankkramer-lab/DeepGleason"
LABEL license="GNU General Public License v3.0"

#-----------------------------------------------------#
# Setup #
#-----------------------------------------------------#
# Setup system environment variables
ENV DEBIAN_FRONTEND=noninteractive
ENV PIP_ROOT_USER_ACTION=ignore

# Copy git repository into container
ADD . /root/DeepGleason

# Install required software dependencies (cv2)
RUN apt-get update && \
apt-get install -y --no-install-recommends python3-opencv && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*

# Update Python pip
RUN python -m pip install pip --upgrade

# Install AUCMEDI from local git repo
RUN pip install -r /root/DeepGleason/requirements.txt

# Create working directory
VOLUME ["/data"]
WORKDIR /data

#-----------------------------------------------------#
# Startup #
#-----------------------------------------------------#
ENTRYPOINT ["/root/DeepGleason/code/main.py"]
15 changes: 12 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,22 @@ Welcome to DeepGleason, an advanced software solution developed for inferring Gl

![viz](docs/viz.png)

It computes a one of the following 7 classes per 1024 x 1024 tile:
It computes a one of the following 6 classes per 1024 x 1024 tile:
Artifact - Sponge, Artifact Dirt, PIN, precursor or unclear tissue, Regular Tissue, Gleason 3, Gleason 4, Gleason 5

![viz](docs/samples.png)

## Usage

**Example:**
```sh
cd DeepGleason/
python code/main.py --input /sandbox/9f3fae7a6ff12c76fbbf89b9c50c66b6.ome.tiff \
--output /sandbox/ \
--model models/model.ConvNeXtBase.hdf5 \
-p /sandbox/predictions.csv
```

**Input**:
- Whole-slide image of prostata cancer
- DeepGleason deep neural network model
Expand All @@ -20,7 +29,7 @@ Artifact - Sponge, Artifact Dirt, PIN, precursor or unclear tissue, Regular Tiss
- CSV file containing class predictions & confidence for each tile

```sh
usage: scripts/pred_cli.py [-h] [-g GPU] [--cache CACHE] -i INPUT [-o OUTPUT] [--model MODEL] [--generate_overlay] [-p PREDICTION]
usage: code/main.py [-h] [-g GPU] [--cache CACHE] -i INPUT [-o OUTPUT] [--model MODEL] [--generate_overlay] [-p PREDICTION]

DeepGleason: Prediction

Expand Down Expand Up @@ -60,7 +69,7 @@ This program utilizes the PyVIPS library to load and store images and AUCMEDI to
The implemented medical image classification pipeline can be summarized in the following core steps:
- Slide preparation with PyVIPS: 1024x1024 pixel tiles
- Tile preprocessing: Padding, resize, pixel intensity normalization
- Deep Learning model architecture: ResNeXt101
- Deep Learning model architecture: ConvNeXt
- Slide reconstruction to bigTIFF overlay

![workflow](docs/workflow.png)
Expand Down
4 changes: 2 additions & 2 deletions code/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# =============================================================================#
# Author: Dominik Müller, Philip Meyer #
# Copyright: 2023 AG-RAIMIA-Müller, University of Augsburg #
# Copyright: 2024 AG-RAIMIA-Müller, University of Augsburg #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
Expand Down Expand Up @@ -131,7 +131,7 @@


config = {}
COL_NAMES = ["A_S", "A_D", "R", "Q", "G3", "G4", "G5"]
COL_NAMES = ["A_S", "A_D", "R", "G3", "G4", "G5"]

# pyvips.cache_set_max_mem(0) #This may be necessary to cache operations.
# On the other hand this is incredibly useful to accelerate null computations
Expand Down
32 changes: 20 additions & 12 deletions code/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# =============================================================================#
# Author: Dominik Müller, Philip Meyer #
# Copyright: 2023 AG-RAIMIA-Müller, University of Augsburg #
# Copyright: 2024 AG-RAIMIA-Müller, University of Augsburg #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
Expand All @@ -20,35 +20,42 @@
# -----------------------------------------------------#
import pandas as pd
import tensorflow as tf

from PIL import Image
import os
# AUCMEDI libraries
from aucmedi import DataGenerator, NeuralNetwork
from aucmedi.data_processing.subfunctions import Padding
from stain_normalization import StainNormalization

# -----------------------------------------------------#
# AUCMEDI Pipeline #
# -----------------------------------------------------#
COL_NAMES = ["A_S", "A_D", "R", "Q", "G3", "G4", "G5"]
COL_NAMES = ["A_S", "A_D", "R", "G3", "G4", "G5"]

def run_aucmedi(x, architecture, config):
# Load stain normalization
dir_path = os.path.dirname(os.path.realpath(__file__))
path_stain_target = os.path.join(dir_path, "stainnormalize_target.png")
target_image = Image.open(path_stain_target)
# Define Subfunctions
sf_list = [Padding(mode="square")]
# Set activation output to softmax for multi-class classification
activation_output = "softmax"
sf_list = [Padding(mode="square"), StainNormalization(target_image)]

# identify architecture
arch_name = architecture.split(".")[-2]

# Initialize model
model = NeuralNetwork(
config["nclasses"],
channels=3,
architecture="2D.ResNeXt101",
multiprocessing=False,
activation_output=activation_output,
architecture="2D." + arch_name,
workers=16,
multiprocessing=True,
)

# Dump latest model
model.load(architecture)
# Load model
model.model.load_weights(architecture)

# Initialize training and validation Data Generators
# Initialize Data Generator
gen = DataGenerator(
x,
config["path_images"],
Expand All @@ -62,6 +69,7 @@ def run_aucmedi(x, architecture, config):
sample_weights=None,
seed=123,
image_format=config["image_format"],
workers=6,
)

# generate predictions
Expand Down
27 changes: 13 additions & 14 deletions code/proc.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# =============================================================================#
# Author: Dominik Müller, Philip Meyer #
# Copyright: 2023 AG-RAIMIA-Müller, University of Augsburg #
# Copyright: 2024 AG-RAIMIA-Müller, University of Augsburg #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
Expand All @@ -21,7 +21,7 @@
import os
import pyvips

os.environ["VIPS_CONCURRENCY"] = "64"
os.environ["VIPS_CONCURRENCY"] = "0"
import numpy as np
import pandas as pd
import math
Expand All @@ -30,12 +30,14 @@
#------------------------------------------------------#
# Processing Utility Functions #
#------------------------------------------------------#
def eval_cb(image, progress):
print(f"\reval: percent = {progress.percent}", end="\n")

def eval_handler(image, progress):
print('run time so far (secs) = {}'.format(progress.run))
print('estimated time of arrival (secs) = {}'.format(progress.eta))
print('total number of pels to process = {}'.format(progress.tpels))
print('number of pels processed so far = {}'.format(progress.npels))
print('percent complete = {}'.format(progress.percent))

def gen_tiles(patch_path, slide, name, PATCH_SIZE):
os.environ["VIPS_CONCURRENCY"] = "128"
props = {
"compression": "jpeg",
"xres": 4000,
Expand All @@ -53,9 +55,8 @@ def gen_tiles(patch_path, slide, name, PATCH_SIZE):
img = img_r.bandjoin([img_g, img_b])
img = img.copy(interpretation="rgb")

img.set_progress(True)
# img.signal_connect('preeval', preeval_cb)
img.signal_connect("eval", eval_cb)
#img.set_progress(True)
#img.signal_connect("eval", eval_handler)

width = img.width
height = img.height
Expand Down Expand Up @@ -95,14 +96,12 @@ def class_reassemble(max_X, max_Y, slide_name, df_res, PATCH_SIZE):
small_version[x, y] = [0, 0, 0] # White
elif samp == "R":
small_version[x, y] = [0, 1, 0] # Green
elif samp == "Q":
small_version[x, y] = [0, 1, 1] # Cyan
elif samp == "G3":
small_version[x, y] = [1, 1, 0] # Yellow
small_version[x, y] = [1, 1, 0] # Yellow
elif samp == "G4":
small_version[x, y] = [1, 0.5, 0] # Orange
small_version[x, y] = [1, 0.5, 0] # Orange
elif samp == "G5":
small_version[x, y] = [1, 0, 0] # Red
small_version[x, y] = [1, 0, 0] # Red

cntr.update(1)
del cntr
Expand Down
Loading

0 comments on commit e504bfb

Please sign in to comment.