|
| 1 | +import tensorflow as tf |
| 2 | +from torchstain.normalizers.he_normalizer import HENormalizer |
| 3 | +from torchstain.utils import cov, percentile, cov_tf, percentile_tf, solveLS |
| 4 | +import numpy as np |
| 5 | +import tensorflow.keras.backend as K |
| 6 | + |
| 7 | + |
| 8 | +""" |
| 9 | +Source code ported from: https://github.com/schaugf/HEnorm_python |
| 10 | +Original implementation: https://github.com/mitkovetta/staining-normalization |
| 11 | +""" |
| 12 | +class TensorFlowMacenkoNormalizer(HENormalizer): |
| 13 | + def __init__(self): |
| 14 | + super().__init__() |
| 15 | + |
| 16 | + self.HERef = tf.constant([[0.5626, 0.2159], |
| 17 | + [0.7201, 0.8012], |
| 18 | + [0.4062, 0.5581]]) |
| 19 | + self.maxCRef = tf.constant([1.9705, 1.0308]) |
| 20 | + |
| 21 | + def __convert_rgb2od(self, I, Io, beta): |
| 22 | + I = tf.transpose(I, [1, 2, 0]) |
| 23 | + |
| 24 | + # calculate optical density |
| 25 | + OD = -tf.math.log((tf.cast(tf.reshape(I, [tf.math.reduce_prod(I.shape[:-1]), I.shape[-1]]), tf.float32) + 1) / Io) |
| 26 | + |
| 27 | + # remove transparent pixels |
| 28 | + ODhat = OD[~tf.math.reduce_any(OD < beta, axis=1)] |
| 29 | + |
| 30 | + return OD, ODhat |
| 31 | + |
| 32 | + def __find_HE(self, ODhat, eigvecs, alpha): |
| 33 | + # project on the plane spanned by the eigenvectors corresponding to the two |
| 34 | + # largest eigenvalues |
| 35 | + That = tf.linalg.matmul(ODhat, eigvecs) |
| 36 | + phi = tf.math.atan2(That[:, 1], That[:, 0]) |
| 37 | + |
| 38 | + minPhi = percentile_tf(phi, alpha) |
| 39 | + maxPhi = percentile_tf(phi, 100 - alpha) |
| 40 | + |
| 41 | + vMin = tf.matmul(eigvecs, tf.expand_dims(tf.stack((tf.math.cos(minPhi), tf.math.sin(minPhi))), axis=-1)) |
| 42 | + vMax = tf.matmul(eigvecs, tf.expand_dims(tf.stack((tf.math.cos(maxPhi), tf.math.sin(maxPhi))), axis=-1)) |
| 43 | + |
| 44 | + # a heuristic to make the vector corresponding to hematoxylin first and the |
| 45 | + # one corresponding to eosin second |
| 46 | + HE = tf.where(vMin[0] > vMax[0], tf.concat((vMin, vMax), axis=1), tf.concat((vMax, vMin), axis=1)) |
| 47 | + |
| 48 | + return HE |
| 49 | + |
| 50 | + def __find_concentration(self, OD, HE): |
| 51 | + # rows correspond to channels (RGB), columns to OD values |
| 52 | + Y = tf.transpose(OD) |
| 53 | + |
| 54 | + # determine concentrations of the individual stains |
| 55 | + return solveLS(HE, Y)[:2] |
| 56 | + |
| 57 | + def __compute_matrices(self, I, Io, alpha, beta): |
| 58 | + OD, ODhat = self.__convert_rgb2od(I, Io=Io, beta=beta) |
| 59 | + |
| 60 | + # compute eigenvectors |
| 61 | + _, eigvecs = tf.linalg.eigh(cov_tf(tf.transpose(ODhat))) |
| 62 | + eigvecs = eigvecs[:, 1:3] |
| 63 | + |
| 64 | + HE = self.__find_HE(ODhat, eigvecs, alpha) |
| 65 | + |
| 66 | + C = self.__find_concentration(OD, HE) |
| 67 | + maxC = tf.stack([percentile_tf(C[0, :], 99), percentile_tf(C[1, :], 99)]) |
| 68 | + |
| 69 | + return HE, C, maxC |
| 70 | + |
| 71 | + def fit(self, I, Io=240, alpha=1, beta=0.15): |
| 72 | + HE, _, maxC = self.__compute_matrices(I, Io, alpha, beta) |
| 73 | + |
| 74 | + self.HERef = HE |
| 75 | + self.maxCRef = maxC |
| 76 | + |
| 77 | + def normalize(self, I, Io=240, alpha=1, beta=0.15, stains=True): |
| 78 | + ''' Normalize staining appearence of H&E stained images |
| 79 | +
|
| 80 | + Example use: |
| 81 | + see test.py |
| 82 | +
|
| 83 | + Input: |
| 84 | + I: RGB input image: tensor of shape [C, H, W] and type uint8 |
| 85 | + Io: (optional) transmitted light intensity |
| 86 | + alpha: percentile |
| 87 | + beta: transparency threshold |
| 88 | + stains: if true, return also H & E components |
| 89 | +
|
| 90 | + Output: |
| 91 | + Inorm: normalized image |
| 92 | + H: hematoxylin image |
| 93 | + E: eosin image |
| 94 | +
|
| 95 | + Reference: |
| 96 | + A method for normalizing histology slides for quantitative analysis. M. |
| 97 | + Macenko et al., ISBI 2009 |
| 98 | + ''' |
| 99 | + c, h, w = I.shape |
| 100 | + |
| 101 | + HE, C, maxC = self.__compute_matrices(I, Io, alpha, beta) |
| 102 | + |
| 103 | + # normalize stain concentrations |
| 104 | + C *= tf.expand_dims((self.maxCRef / maxC), axis=-1) |
| 105 | + |
| 106 | + # recreate the image using reference mixing matrix |
| 107 | + Inorm = Io * tf.math.exp(-tf.linalg.matmul(self.HERef, C)) |
| 108 | + Inorm = tf.clip_by_value(Inorm, 0, 255) |
| 109 | + Inorm = tf.cast(tf.reshape(tf.transpose(Inorm), shape=(h, w, c)), tf.int32) |
| 110 | + |
| 111 | + H, E = None, None |
| 112 | + |
| 113 | + if stains: |
| 114 | + H = tf.math.multiply(Io, tf.math.exp(tf.linalg.matmul(-tf.expand_dims(self.HERef[:, 0], axis=-1), tf.expand_dims(C[0, :], axis=0)))) |
| 115 | + H = tf.clip_by_value(H, 0, 255) |
| 116 | + H = tf.cast(tf.reshape(tf.transpose(H), shape=(h, w, c)), tf.int32) |
| 117 | + |
| 118 | + E = tf.math.multiply(Io, tf.math.exp(tf.linalg.matmul(-tf.expand_dims(self.HERef[:, 1], axis=-1), tf.expand_dims(C[1, :], axis=0)))) |
| 119 | + E = tf.clip_by_value(E, 0, 255) |
| 120 | + E = tf.cast(tf.reshape(tf.transpose(E), shape=(h, w, c)), tf.int32) |
| 121 | + |
| 122 | + return Inorm, H, E |
0 commit comments