Skip to content

Commit a821628

Browse files
Merge pull request #62 from EIDOSLAB/development
Development
2 parents 204354f + 5ccc187 commit a821628

File tree

12 files changed

+92
-89
lines changed

12 files changed

+92
-89
lines changed

.github/workflows/build.yaml

+4-4
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ jobs:
99
runs-on: ubuntu-20.04
1010

1111
steps:
12-
- uses: actions/checkout@v3
13-
- uses: actions/setup-python@v3
12+
- uses: actions/checkout@v4
13+
- uses: actions/setup-python@v4
1414
with:
1515
python-version: '3.x'
1616

@@ -20,7 +20,7 @@ jobs:
2020
- name: Build wheels
2121
run: python setup.py sdist bdist_wheel
2222

23-
- uses: actions/upload-artifact@v3
23+
- uses: actions/upload-artifact@v4
2424
with:
2525
path: ./dist/*
2626

@@ -31,7 +31,7 @@ jobs:
3131
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
3232

3333
steps:
34-
- uses: actions/download-artifact@v3
34+
- uses: actions/download-artifact@v4
3535
with:
3636
name: artifact
3737
path: dist

.github/workflows/tests_full.yml

+16-22
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: tests
1+
name: Full Tests
22

33
on:
44
push:
@@ -7,6 +7,7 @@ on:
77
pull_request:
88
branches:
99
- main
10+
workflow_dispatch:
1011

1112
jobs:
1213
build:
@@ -15,9 +16,9 @@ jobs:
1516
if: startsWith(github.ref, 'refs/tags/v') != true
1617

1718
steps:
18-
- uses: actions/checkout@v1
19+
- uses: actions/checkout@v4
1920
- name: Set up Python 3.6
20-
uses: actions/setup-python@v2
21+
uses: actions/setup-python@v4
2122
with:
2223
python-version: 3.6
2324

@@ -28,7 +29,7 @@ jobs:
2829
run: python setup.py bdist_wheel
2930

3031
- name: Upload Python wheel
31-
uses: actions/upload-artifact@v2
32+
uses: actions/upload-artifact@v4
3233
with:
3334
name: Python wheel
3435
path: ${{github.workspace}}/dist/torchstain-*.whl
@@ -39,24 +40,24 @@ jobs:
3940
runs-on: ${{ matrix.os }}
4041
strategy:
4142
matrix:
42-
os: [ windows-2019, ubuntu-20.04, macos-12 ]
43+
os: [ windows-2019, ubuntu-20.04, macos-13 ]
4344
python-version: [ 3.7, 3.8, 3.9 ]
4445
tf-version: [2.7.0, 2.8.0, 2.9.0]
4546

4647
steps:
47-
- uses: actions/checkout@v1
48+
- uses: actions/checkout@v4
4849
- name: Set up Python ${{ matrix.python-version }}
49-
uses: actions/setup-python@v2
50+
uses: actions/setup-python@v4
5051
with:
5152
python-version: ${{ matrix.python-version }}
5253

5354
- name: Download artifact
54-
uses: actions/download-artifact@v3
55+
uses: actions/download-artifact@v4
5556
with:
5657
name: "Python wheel"
5758

5859
- name: Install dependencies
59-
run: pip install tensorflow==${{ matrix.tf-version }} protobuf==3.20.* opencv-python-headless scikit-image pytest
60+
run: pip install tensorflow==${{ matrix.tf-version }} protobuf==3.20.* opencv-python-headless scikit-image pytest "numpy<2"
6061

6162
- name: Install wheel
6263
run: pip install --find-links=${{github.workspace}} torchstain-*
@@ -70,31 +71,24 @@ jobs:
7071
runs-on: ${{ matrix.os }}
7172
strategy:
7273
matrix:
73-
os: [ windows-2019, ubuntu-20.04, macos-12 ]
74-
python-version: [ 3.6, 3.7, 3.8, 3.9 ]
74+
os: [ windows-2019, ubuntu-20.04, macos-13 ]
75+
python-version: [ 3.7, 3.8, 3.9 ]
7576
pytorch-version: [1.8.0, 1.9.0, 1.10.0, 1.11.0, 1.12.0, 1.13.0]
76-
exclude:
77-
- python-version: 3.6
78-
pytorch-version: 1.11.0
79-
- python-version: 3.6
80-
pytorch-version: 1.12.0
81-
- python-version: 3.6
82-
pytorch-version: 1.13.0
8377

8478
steps:
85-
- uses: actions/checkout@v1
79+
- uses: actions/checkout@v4
8680
- name: Set up Python ${{ matrix.python-version }}
87-
uses: actions/setup-python@v2
81+
uses: actions/setup-python@v4
8882
with:
8983
python-version: ${{ matrix.python-version }}
9084

9185
- name: Download artifact
92-
uses: actions/download-artifact@v3
86+
uses: actions/download-artifact@v4
9387
with:
9488
name: "Python wheel"
9589

9690
- name: Install dependencies
97-
run: pip install torch==${{ matrix.pytorch-version }} torchvision opencv-python-headless scikit-image pytest
91+
run: pip install torch==${{ matrix.pytorch-version }} torchvision opencv-python-headless scikit-image pytest "numpy<2"
9892

9993
- name: Install wheel
10094
run: pip install --find-links=${{github.workspace}} torchstain-*

.github/workflows/tests_quick.yml

+11-10
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: tests
1+
name: Quick Tests
22

33
on:
44
push:
@@ -7,14 +7,15 @@ on:
77
pull_request:
88
branches-ignore:
99
- main
10+
workflow_dispatch:
1011

1112
jobs:
1213
build:
1314
runs-on: ubuntu-20.04
1415
steps:
15-
- uses: actions/checkout@v1
16+
- uses: actions/checkout@v4
1617
- name: Set up Python 3.6
17-
uses: actions/setup-python@v2
18+
uses: actions/setup-python@v4
1819
with:
1920
python-version: 3.6
2021

@@ -25,7 +26,7 @@ jobs:
2526
run: python setup.py bdist_wheel
2627

2728
- name: Upload Python wheel
28-
uses: actions/upload-artifact@v2
29+
uses: actions/upload-artifact@v4
2930
with:
3031
name: Python wheel
3132
path: ${{github.workspace}}/dist/torchstain-*.whl
@@ -36,14 +37,14 @@ jobs:
3637
runs-on: ubuntu-20.04
3738

3839
steps:
39-
- uses: actions/checkout@v1
40+
- uses: actions/checkout@v4
4041
- name: Set up Python 3.8
41-
uses: actions/setup-python@v2
42+
uses: actions/setup-python@v4
4243
with:
4344
python-version: 3.8
4445

4546
- name: Download artifact
46-
uses: actions/download-artifact@v3
47+
uses: actions/download-artifact@v4
4748
with:
4849
name: "Python wheel"
4950

@@ -62,14 +63,14 @@ jobs:
6263
runs-on: ubuntu-20.04
6364

6465
steps:
65-
- uses: actions/checkout@v1
66+
- uses: actions/checkout@v4
6667
- name: Set up Python 3.8
67-
uses: actions/setup-python@v2
68+
uses: actions/setup-python@v4
6869
with:
6970
python-version: 3.8
7071

7172
- name: Download artifact
72-
uses: actions/download-artifact@v3
73+
uses: actions/download-artifact@v4
7374
with:
7475
name: "Python wheel"
7576

README.md

+11-5
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,16 @@
55
[![Pip Downloads](https://img.shields.io/pypi/dm/torchstain?label=pip%20downloads&logo=python)](https://pypi.org/project/torchstain/)
66
[![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.7692014.svg)](https://doi.org/10.5281/zenodo.7692014)
77

8-
GPU-accelerated stain normalization tools for histopathological images. Compatible with PyTorch, TensorFlow, and Numpy.
9-
Normalization algorithms currently implemented:
8+
GPU-accelerated stain tools for histopathological images. Compatible with PyTorch, TensorFlow, and Numpy.
109

10+
Normalization algorithms currently implemented:
1111
- Macenko [\[1\]](#reference) (ported from [numpy implementation](https://github.com/schaugf/HEnorm_python))
1212
- Reinhard [\[2\]](#reference)
1313
- Modified Reinhard [\[3\]](#reference)
14+
- Multi-target Macenko [\[4\]](#reference)
15+
16+
Augmentation algorithms currently implemented:
17+
- Macenko-Aug [\[1\]](#reference) (inspired by [StainTools](https://github.com/Peter554/StainTools))
1418

1519
## Installation
1620

@@ -52,10 +56,12 @@ norm, H, E = normalizer.normalize(I=t_to_transform, stains=True)
5256
| Macenko | &check; | &check; | &check; |
5357
| Reinhard | &check; | &check; | &check; |
5458
| Modified Reinhard | &check; | &check; | &check; |
59+
| Multi-target Macenko | &cross; | &check; | &cross; |
60+
| Macenko-Aug | &check; | &check; | &check; |
5561

5662
## Backend comparison
5763

58-
Results with 10 runs per size on a Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz
64+
Runtimes using the Macenko algorithm using different backends. Metrics were calculated from 10 repeated runs for each quadratic image size on an Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz.
5965

6066
| size | numpy avg. time | torch avg. time | tf avg. time |
6167
|--------|-------------------|-------------------|------------------|
@@ -73,15 +79,15 @@ Results with 10 runs per size on a Intel(R) Core(TM) i5-8365U CPU @ 1.60GHz
7379
- [1] Macenko, Marc et al. "A method for normalizing histology slides for quantitative analysis." 2009 IEEE International Symposium on Biomedical Imaging: From Nano to Macro. IEEE, 2009.
7480
- [2] Reinhard, Erik et al. "Color transfer between images." IEEE Computer Graphics and Applications. IEEE, 2001.
7581
- [3] Roy, Santanu et al. "Modified Reinhard Algorithm for Color Normalization of Colorectal Cancer Histopathology Images". 2021 29th European Signal Processing Conference (EUSIPCO), IEEE, 2021.
82+
- [4] Ivanov, Desislav et al. "Multi-target stain normalization for histology slides". arXiv (preprint). 2024.
7683

7784
## Citing
7885

7986
If you find this software useful for your research, please cite it as:
8087

8188
```bibtex
8289
@software{barbano2022torchstain,
83-
author = {Carlo Alberto Barbano and
84-
André Pedersen},
90+
author = {Carlo Alberto Barbano and André Pedersen},
8591
title = {EIDOSLAB/torchstain: v1.2.0-stable},
8692
month = aug,
8793
year = 2022,

apps/example_norm.py

-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import time
77
import os
88

9-
109
size = 1024
1110
dir_path = os.path.dirname(os.path.abspath(__file__))
1211
target = cv2.resize(cv2.cvtColor(cv2.imread(dir_path + "/../data/target.png"), cv2.COLOR_BGR2RGB), (size, size))

compare.py

-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import torch
22
from torchvision import transforms
33
import torchstain
4-
54
import cv2
65
import matplotlib.pyplot as plt
76
import time

torchstain/numpy/normalizers/macenko.py

+14-14
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __init__(self):
1616

1717
def __convert_rgb2od(self, I, Io=240, beta=0.15):
1818
# calculate optical density
19-
OD = -np.log((I.astype(float)+1)/Io)
19+
OD = -np.log((I.astype(float) + 1) / Io)
2020

2121
# remove transparent pixels
2222
ODhat = OD[~np.any(OD < beta, axis=1)]
@@ -26,22 +26,22 @@ def __convert_rgb2od(self, I, Io=240, beta=0.15):
2626
def __find_HE(self, ODhat, eigvecs, alpha):
2727
#project on the plane spanned by the eigenvectors corresponding to the two
2828
# largest eigenvalues
29-
That = ODhat.dot(eigvecs[:,1:3])
29+
That = ODhat.dot(eigvecs[:, 1:3])
3030

31-
phi = np.arctan2(That[:,1],That[:,0])
31+
phi = np.arctan2(That[:, 1], That[:, 0])
3232

3333
minPhi = np.percentile(phi, alpha)
34-
maxPhi = np.percentile(phi, 100-alpha)
34+
maxPhi = np.percentile(phi, 100 - alpha)
3535

36-
vMin = eigvecs[:,1:3].dot(np.array([(np.cos(minPhi), np.sin(minPhi))]).T)
37-
vMax = eigvecs[:,1:3].dot(np.array([(np.cos(maxPhi), np.sin(maxPhi))]).T)
36+
vMin = eigvecs[:, 1:3].dot(np.array([(np.cos(minPhi), np.sin(minPhi))]).T)
37+
vMax = eigvecs[:, 1:3].dot(np.array([(np.cos(maxPhi), np.sin(maxPhi))]).T)
3838

3939
# a heuristic to make the vector corresponding to hematoxylin first and the
4040
# one corresponding to eosin second
4141
if vMin[0] > vMax[0]:
42-
HE = np.array((vMin[:,0], vMax[:,0])).T
42+
HE = np.array((vMin[:, 0], vMax[:, 0])).T
4343
else:
44-
HE = np.array((vMax[:,0], vMin[:,0])).T
44+
HE = np.array((vMax[:, 0], vMin[:, 0])).T
4545

4646
return HE
4747

@@ -55,7 +55,7 @@ def __find_concentration(self, OD, HE):
5555
return C
5656

5757
def __compute_matrices(self, I, Io, alpha, beta):
58-
I = I.reshape((-1,3))
58+
I = I.reshape((-1, 3))
5959

6060
OD, ODhat = self.__convert_rgb2od(I, Io=Io, beta=beta)
6161

@@ -67,7 +67,7 @@ def __compute_matrices(self, I, Io, alpha, beta):
6767
C = self.__find_concentration(OD, HE)
6868

6969
# normalize stain concentrations
70-
maxC = np.array([np.percentile(C[0,:], 99), np.percentile(C[1,:],99)])
70+
maxC = np.array([np.percentile(C[0, :], 99), np.percentile(C[1, :],99)])
7171

7272
return HE, C, maxC
7373

@@ -81,7 +81,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
8181
''' Normalize staining appearence of H&E stained images
8282
8383
Example use:
84-
see test.py
84+
see example.py
8585
8686
Input:
8787
I: RGB input image
@@ -97,7 +97,7 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
9797
Macenko et al., ISBI 2009
9898
'''
9999
h, w, c = I.shape
100-
I = I.reshape((-1,3))
100+
I = I.reshape((-1, 3))
101101

102102
HE, C, maxC = self.__compute_matrices(I, Io, alpha, beta)
103103

@@ -113,11 +113,11 @@ def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True):
113113

114114
if stains:
115115
# unmix hematoxylin and eosin
116-
H = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:,0], axis=1).dot(np.expand_dims(C2[0,:], axis=0))))
116+
H = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:, 0], axis=1).dot(np.expand_dims(C2[0, :], axis=0))))
117117
H[H > 255] = 255
118118
H = np.reshape(H.T, (h, w, c)).astype(np.uint8)
119119

120-
E = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:,1], axis=1).dot(np.expand_dims(C2[1,:], axis=0))))
120+
E = np.multiply(Io, np.exp(np.expand_dims(-self.HERef[:, 1], axis=1).dot(np.expand_dims(C2[1, :], axis=0))))
121121
E[E > 255] = 255
122122
E = np.reshape(E.T, (h, w, c)).astype(np.uint8)
123123

torchstain/numpy/normalizers/reinhard.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,7 @@ def fit(self, target):
2525
lab = rgb2lab(target)
2626

2727
# get summary statistics
28-
# stack_ = np.apply_along_axis(get_mean_std, 1, lab_split(lab))
29-
stack_ = np.apply_along_axis(get_mean_std, axis=1, arr=lab_split(lab))
28+
stack_ = np.array([get_mean_std(x) for x in lab_split(lab)])
3029

3130
self.target_means = stack_[:, 0]
3231
self.target_stds = stack_[:, 1]
@@ -40,8 +39,7 @@ def normalize(self, I):
4039
labs = lab_split(lab)
4140

4241
# get summary statistics from LAB
43-
# stack_ = np.apply_along_axis(get_mean_std, 1, labs)
44-
stack_ = np.apply_along_axis(get_mean_std, axis=1, arr=labs)
42+
stack_ = np.array([get_mean_std(x) for x in labs])
4543

4644
mus = stack_[:, 0]
4745
stds = stack_[:, 1]

0 commit comments

Comments
 (0)