def test_is_uint8_image(): R = np.array([[0., 55., 250.], [1., 3., 7.], [2., 4., 7.]]) G = np.array([[0., 55., 250.], [1., 3., 7.], [2., 4., 7.]]) B = np.array([[0., 55., 250.], [1., 3., 7.], [2., 4., 7.]]) I = np.array([R, G, B]) assert not is_uint8_image(I) I = I.astype(np.uint8) assert is_uint8_image(I) I = I / 255 assert not is_uint8_image(I)
def get_stain_matrix(I, luminosity_threshold=0.8, regularizer=0.1): """ Stain matrix estimation via method of: A. Vahadane et al. 'Structure-Preserving Color Normalization and Sparse Stain Separation for Histological Images' :param I: Image RGB uint8. :param luminosity_threshold: :param regularizer: :return: """ assert is_uint8_image(I), "Image should be RGB uint8." # convert to OD and ignore background tissue_mask = LuminosityThresholdTissueLocator.get_tissue_mask( I, luminosity_threshold=luminosity_threshold).reshape((-1, )) OD = convert_RGB_to_OD(I).reshape((-1, 3)) OD = OD[tissue_mask] # do the dictionary learning dictionary = spams.trainDL(X=OD.T, K=2, lambda1=regularizer, mode=2, modeD=0, posAlpha=True, posD=True, verbose=False).T # order H and E. # H on first row. if dictionary[0, 0] < dictionary[1, 0]: dictionary = dictionary[[1, 0], :] return normalize_matrix_rows(dictionary)
def get_stain_matrix(I, luminosity_threshold=0.8, angular_percentile=99): """ Stain matrix estimation via method of: M. Macenko et al. 'A method for normalizing histology slides for quantitative analysis' :param I: Image RGB uint8. :param luminosity_threshold: :param angular_percentile: :return: """ assert is_uint8_image(I), "Image should be RGB uint8." # Convert to OD and ignore background tissue_mask = LuminosityThresholdTissueLocator.get_tissue_mask( I, luminosity_threshold=luminosity_threshold).reshape((-1, )) OD = convert_RGB_to_OD(I).reshape((-1, 3)) OD = OD[tissue_mask] # Yoni's edit!! if tissue_mask.sum( ) <= 1: # If the tissue mask has 0 or 1 pixel the linalg does not work. We will return an untransformed image return False # Eigenvectors of cov in OD space (orthogonal as cov symmetric) _, V = np.linalg.eigh(np.cov(OD, rowvar=False)) # The two principle eigenvectors V = V[:, [2, 1]] # Make sure vectors are pointing the right way if V[0, 0] < 0: V[:, 0] *= -1 if V[0, 1] < 0: V[:, 1] *= -1 # Project on this basis. That = np.dot(OD, V) # Angular coordinates with repect to the prinicple, orthogonal eigenvectors phi = np.arctan2(That[:, 1], That[:, 0]) # Min and max angles minPhi = np.percentile(phi, 100 - angular_percentile) maxPhi = np.percentile(phi, angular_percentile) # the two principle colors v1 = np.dot(V, np.array([np.cos(minPhi), np.sin(minPhi)])) v2 = np.dot(V, np.array([np.cos(maxPhi), np.sin(maxPhi)])) # Order of H and E. # H first row. if v1[0] > v2[0]: HE = np.array([v1, v2]) else: HE = np.array([v2, v1]) return normalize_matrix_rows(HE)
def lab_split(I): """ Convert from RGB uint8 to LAB and split into channels. :param I: Image RGB uint8. :return: """ assert is_uint8_image(I), "Should be a RGB uint8 image" I = cv.cvtColor(I, cv.COLOR_RGB2LAB) I_float = I.astype(np.float32) I1, I2, I3 = cv.split(I_float) I1 /= 2.55 # should now be in range [0,100] I2 -= 128.0 # should now be in range [-127,127] I3 -= 128.0 # should now be in range [-127,127] return I1, I2, I3
def get_mean_std(self, I): """ Get mean and standard deviation of each channel. :param I: Image RGB uint8. :return: """ assert is_uint8_image(I), "Should be a RGB uint8 image" I1, I2, I3 = self.lab_split(I) m1, sd1 = cv.meanStdDev(I1) m2, sd2 = cv.meanStdDev(I2) m3, sd3 = cv.meanStdDev(I3) means = m1, m2, m3 stds = sd1, sd2, sd3 return means, stds
def standardize(I, percentile=95): """ Transform image I to standard brightness. Modifies the luminosity channel such that a fixed percentile is saturated. :param I: Image uint8 RGB. :param percentile: Percentile for luminosity saturation. At least (100 - percentile)% of pixels should be fully luminous (white). :return: Image uint8 RGB with standardized brightness. """ assert is_uint8_image(I), "Image should be RGB uint8." I_LAB = cv.cvtColor(I, cv.COLOR_RGB2LAB) L_float = I_LAB[:, :, 0].astype(float) p = np.percentile(L_float, percentile) I_LAB[:, :, 0] = np.clip(255 * L_float / p, 0, 255).astype(np.uint8) I = cv.cvtColor(I_LAB, cv.COLOR_LAB2RGB) return I
def get_mean_std(self, I, mask): """ Get mean and standard deviation of each channel. :param I: Image RGB uint8. :return: """ assert is_uint8_image(I), "Should be a RGB uint8 image" I1, I2, I3 = self.lab_split(I) m1 = np.mean(I1[mask]) sd1 = np.std(I1[mask]) m2 = np.mean(I2[mask]) sd2 = np.std(I2[mask]) m3 = np.mean(I3[mask]) sd3 = np.std(I3[mask]) means = m1, m2, m3 stds = sd1, sd2, sd3, return means, stds
def get_tissue_mask(I, luminosity_threshold=0.8): """ Get a binary mask where true denotes pixels with a luminosity less than the specified threshold. Typically we use to identify tissue in the image and exclude the bright white background. :param I: RGB uint 8 image. :param luminosity_threshold: Luminosity threshold. :return: Binary mask. """ assert is_uint8_image(I), "Image should be RGB uint8." I_LAB = cv.cvtColor(I, cv.COLOR_RGB2LAB) L = I_LAB[:, :, 0] / 255.0 # Convert to range [0,1]. mask = L < luminosity_threshold # Check it's not empty if mask.sum() == 0: raise TissueMaskException("Empty tissue mask computed") return mask
def test_is_uint8_image_is_false_for_floats(self): rgb = np.random.uniform(0, 256, [2, 7, 3]) get = is_uint8_image(rgb) self.assertFalse(get)
def test_is_uint8_image_is_false_for_ints_outside_0_255(self): rgb = np.random.randint(0, 300, [5, 2, 3]) get = is_uint8_image(rgb) self.assertFalse(get)
def test_is_uint8_image_is_true_for_ints_in_0_255(self): rgb = np.random.randint(0, 256, [5, 4, 3]).astype(np.uint8) get = is_uint8_image(rgb) self.assertTrue(get)