Skip to content

Commit c30d291

Browse files
committed
fix: correct H&E stain ordering heuristic in ExtractHEStains
The previous heuristic incorrectly assumed hematoxylin has higher red channel values than eosin, when the opposite is typically true. Replace red-channel-only comparison with more robust red-blue ratio comparison to better distinguish hematoxylin from eosin stains based on their spectral properties. - Hematoxylin (nuclear, blue): lower red/blue ratio -> first column - Eosin (cytoplasm, pink): higher red/blue ratio -> second column Update tests to reflect corrected stain ordering. Documented behaviour (first column = H, second = E) now matches the actual output. Signed-off-by: Iyassou Shimels <s.iyassou@gmail.com>
1 parent d4ba52e commit c30d291

File tree

3 files changed

+22
-17
lines changed

3 files changed

+22
-17
lines changed

monai/apps/pathology/transforms/stain/array.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,12 @@ def _deconvolution_extract_stain(self, image: np.ndarray) -> np.ndarray:
8585
v_max = eigvecs[:, 1:3].dot(np.array([(np.cos(max_phi), np.sin(max_phi))], dtype=np.float32).T)
8686

8787
# a heuristic to make the vector corresponding to hematoxylin first and the one corresponding to eosin second
88-
if v_min[0] > v_max[0]:
88+
# Hematoxylin: high blue, lower red (low R/B ratio)
89+
# Eosin: high red, lower blue (high R/B ratio)
90+
ε = np.finfo(np.float32).eps
91+
v_min_rb_ratio = v_min[0, 0] / (v_min[2, 0] + ε)
92+
v_max_rb_ratio = v_max[0, 0] / (v_max[2, 0] + ε)
93+
if v_min_rb_ratio < v_max_rb_ratio:
8994
he = np.array((v_min[:, 0], v_max[:, 0]), dtype=np.float32).T
9095
else:
9196
he = np.array((v_max[:, 0], v_min[:, 0]), dtype=np.float32).T

tests/apps/pathology/transforms/test_pathology_he_stain.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@
4848
# input pixels not uniformly filled, leading to two different stains extracted
4949
EXTRACT_STAINS_TEST_CASE_5 = [
5050
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
51-
np.array([[0.70710677, 0.18696113], [0.0, 0.0], [0.70710677, 0.98236734]]),
51+
np.array([[0.18696113, 0.70710677], [0.0, 0.0], [0.98236734, 0.70710677]]),
5252
]
5353

5454
# input pixels all transparent and below the beta absorbance threshold
@@ -68,7 +68,7 @@
6868
NORMALIZE_STAINS_TEST_CASE_4 = [
6969
{"target_he": np.full((3, 2), 1)},
7070
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
71-
np.array([[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]]]),
71+
np.array([[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]]]),
7272
]
7373

7474

@@ -135,7 +135,7 @@ def test_result_value(self, image, expected_data):
135135
[[0.18696113],[0],[0.98236734]] and
136136
[[0.70710677],[0],[0.70710677]] respectively
137137
- the resulting extracted stain should be
138-
[[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]]
138+
[[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]]
139139
"""
140140
if image is None:
141141
with self.assertRaises(TypeError):
@@ -206,17 +206,17 @@ def test_result_value(self, arguments, image, expected_data):
206206
207207
For test case 4:
208208
- For this non-uniformly filled image, the stain extracted should be
209-
[[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]], as validated for the
209+
[[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]], as validated for the
210210
ExtractHEStains class. Solving the linear least squares problem (since
211211
absorbance matrix = stain matrix * concentration matrix), we obtain the concentration
212-
matrix that should be [[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508],
213-
[5.8022, 0, 0, 0, 0, 0]]
212+
matrix that should be [[5.8022, 0, 0, 0, 0, 0],
213+
[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508]]
214214
- Normalizing the concentration matrix, taking the matrix product of the
215215
target stain matrix and the concentration matrix, using the inverse
216216
Beer-Lambert transform to obtain the RGB image from the absorbance
217217
image, and finally converting to uint8, we get that the stain normalized
218-
image should be [[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]],
219-
[[33, 33, 33], [33, 33, 33]]]
218+
image should be [[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]],
219+
[[85, 85, 85], [85, 85, 85]]]
220220
"""
221221
if image is None:
222222
with self.assertRaises(TypeError):

tests/apps/pathology/transforms/test_pathology_he_stain_dict.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
# input pixels not uniformly filled, leading to two different stains extracted
4343
EXTRACT_STAINS_TEST_CASE_5 = [
4444
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
45-
np.array([[0.70710677, 0.18696113], [0.0, 0.0], [0.70710677, 0.98236734]]),
45+
np.array([[0.18696113, 0.70710677], [0.0, 0.0], [0.98236734, 0.70710677]]),
4646
]
4747

4848
# input pixels all transparent and below the beta absorbance threshold
@@ -62,7 +62,7 @@
6262
NORMALIZE_STAINS_TEST_CASE_4 = [
6363
{"target_he": np.full((3, 2), 1)},
6464
np.array([[[100, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]], [[0, 0, 0], [0, 0, 0]]]),
65-
np.array([[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]]]),
65+
np.array([[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]]]),
6666
]
6767

6868

@@ -129,7 +129,7 @@ def test_result_value(self, image, expected_data):
129129
[[0.18696113],[0],[0.98236734]] and
130130
[[0.70710677],[0],[0.70710677]] respectively
131131
- the resulting extracted stain should be
132-
[[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]]
132+
[[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]]
133133
"""
134134
key = "image"
135135
if image is None:
@@ -200,17 +200,17 @@ def test_result_value(self, arguments, image, expected_data):
200200
201201
For test case 4:
202202
- For this non-uniformly filled image, the stain extracted should be
203-
[[0.70710677,0.18696113],[0,0],[0.70710677,0.98236734]], as validated for the
203+
[[0.18696113,0.70710677],[0,0],[0.98236734,0.70710677]], as validated for the
204204
ExtractHEStains class. Solving the linear least squares problem (since
205205
absorbance matrix = stain matrix * concentration matrix), we obtain the concentration
206-
matrix that should be [[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508],
207-
[5.8022, 0, 0, 0, 0, 0]]
206+
matrix that should be [[5.8022, 0, 0, 0, 0, 0],
207+
[-0.3101, 7.7508, 7.7508, 7.7508, 7.7508, 7.7508]]
208208
- Normalizing the concentration matrix, taking the matrix product of the
209209
target stain matrix and the concentration matrix, using the inverse
210210
Beer-Lambert transform to obtain the RGB image from the absorbance
211211
image, and finally converting to uint8, we get that the stain normalized
212-
image should be [[[87, 87, 87], [33, 33, 33]], [[33, 33, 33], [33, 33, 33]],
213-
[[33, 33, 33], [33, 33, 33]]]
212+
image should be [[[31, 31, 31], [85, 85, 85]], [[85, 85, 85], [85, 85, 85]],
213+
[[85, 85, 85], [85, 85, 85]]]
214214
"""
215215
key = "image"
216216
if image is None:

0 commit comments

Comments
 (0)