Spaces:
Runtime error
Runtime error
Upload 3 files
Browse files- ctranspath.pth +3 -0
- utils_color_norm.py +270 -0
- utils_preprocessing.py +335 -0
ctranspath.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:7c998680060c8743551a412583fac689db43cec07053b72dfec6dcd810113539
|
| 3 |
+
size 111292151
|
utils_color_norm.py
ADDED
|
@@ -0,0 +1,270 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Stain normalization based on the method of:
|
| 3 |
+
|
| 4 |
+
M. Macenko et al., ‘A method for normalizing histology slides for quantitative analysis’,
|
| 5 |
+
in 2009 IEEE International Symposium on Biomedical Imaging: From Nano to Macro, 2009, pp. 1107–1110.
|
| 6 |
+
|
| 7 |
+
Uses the spams package:
|
| 8 |
+
|
| 9 |
+
http://spams-devel.gforge.inria.fr/index.html
|
| 10 |
+
(https://anaconda.org/conda-forge/python-spams
|
| 11 |
+
https://pypi.org/project/spams/)
|
| 12 |
+
|
| 13 |
+
"""
|
| 14 |
+
|
| 15 |
+
from __future__ import division
|
| 16 |
+
|
| 17 |
+
import numpy as np
|
| 18 |
+
import cv2 as cv
|
| 19 |
+
import spams
|
| 20 |
+
import matplotlib.pyplot as plt
|
| 21 |
+
|
| 22 |
+
##================================================================================================
|
| 23 |
+
|
| 24 |
+
def read_image(path):
|
| 25 |
+
"""
|
| 26 |
+
Read an image to RGB uint8
|
| 27 |
+
:param path:
|
| 28 |
+
:return:
|
| 29 |
+
"""
|
| 30 |
+
im = cv.imread(path)
|
| 31 |
+
im = cv.cvtColor(im, cv.COLOR_BGR2RGB)
|
| 32 |
+
return im
|
| 33 |
+
|
| 34 |
+
def show_colors(C):
|
| 35 |
+
"""
|
| 36 |
+
Shows rows of C as colors (RGB)
|
| 37 |
+
:param C:
|
| 38 |
+
:return:
|
| 39 |
+
"""
|
| 40 |
+
n = C.shape[0]
|
| 41 |
+
for i in range(n):
|
| 42 |
+
if C[i].max() > 1.0:
|
| 43 |
+
plt.plot([0, 1], [n - 1 - i, n - 1 - i], c=C[i] / 255, linewidth=20)
|
| 44 |
+
else:
|
| 45 |
+
plt.plot([0, 1], [n - 1 - i, n - 1 - i], c=C[i], linewidth=20)
|
| 46 |
+
plt.axis('off')
|
| 47 |
+
plt.axis([0, 1, -1, n])
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def show(image, now=True, fig_size=(10, 10)):
|
| 51 |
+
"""
|
| 52 |
+
Show an image (np.array).
|
| 53 |
+
Caution! Rescales image to be in range [0,1].
|
| 54 |
+
:param image:
|
| 55 |
+
:param now:
|
| 56 |
+
:param fig_size:
|
| 57 |
+
:return:
|
| 58 |
+
"""
|
| 59 |
+
image = image.astype(np.float32)
|
| 60 |
+
m, M = image.min(), image.max()
|
| 61 |
+
if fig_size != None:
|
| 62 |
+
plt.rcParams['figure.figsize'] = (fig_size[0], fig_size[1])
|
| 63 |
+
plt.imshow((image - m) / (M - m), cmap='gray')
|
| 64 |
+
plt.axis('off')
|
| 65 |
+
if now == True:
|
| 66 |
+
plt.show()
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def build_stack(tup):
|
| 70 |
+
"""
|
| 71 |
+
Build a stack of images from a tuple of images
|
| 72 |
+
:param tup:
|
| 73 |
+
:return:
|
| 74 |
+
"""
|
| 75 |
+
N = len(tup)
|
| 76 |
+
if len(tup[0].shape) == 3:
|
| 77 |
+
h, w, c = tup[0].shape
|
| 78 |
+
stack = np.zeros((N, h, w, c))
|
| 79 |
+
if len(tup[0].shape) == 2:
|
| 80 |
+
h, w = tup[0].shape
|
| 81 |
+
stack = np.zeros((N, h, w))
|
| 82 |
+
for i in range(N):
|
| 83 |
+
stack[i] = tup[i]
|
| 84 |
+
return stack
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
def patch_grid(ims, width=5, sub_sample=None, rand=False, save_name=None):
|
| 88 |
+
"""
|
| 89 |
+
Display a grid of patches
|
| 90 |
+
:param ims:
|
| 91 |
+
:param width:
|
| 92 |
+
:param sub_sample:
|
| 93 |
+
:param rand:
|
| 94 |
+
:return:
|
| 95 |
+
"""
|
| 96 |
+
N0 = np.shape(ims)[0]
|
| 97 |
+
if sub_sample is None:
|
| 98 |
+
N = N0
|
| 99 |
+
stack = ims
|
| 100 |
+
elif sub_sample != None and rand == False:
|
| 101 |
+
N = sub_sample
|
| 102 |
+
stack = ims[:N]
|
| 103 |
+
elif sub_sample != None and rand == True:
|
| 104 |
+
N = sub_sample
|
| 105 |
+
idx = np.random.choice(range(N), sub_sample, replace=False)
|
| 106 |
+
stack = ims[idx]
|
| 107 |
+
height = np.ceil(float(N) / width).astype(np.uint16)
|
| 108 |
+
plt.rcParams['figure.figsize'] = (18, (18 / width) * height)
|
| 109 |
+
plt.figure()
|
| 110 |
+
for i in range(N):
|
| 111 |
+
plt.subplot(height, width, i + 1)
|
| 112 |
+
im = stack[i]
|
| 113 |
+
show(im, now=False, fig_size=None)
|
| 114 |
+
if save_name != None:
|
| 115 |
+
plt.savefig(save_name)
|
| 116 |
+
plt.show()
|
| 117 |
+
|
| 118 |
+
######################################
|
| 119 |
+
|
| 120 |
+
def standardize_brightness(I):
|
| 121 |
+
"""
|
| 122 |
+
:param I:
|
| 123 |
+
:return:
|
| 124 |
+
"""
|
| 125 |
+
p = np.percentile(I, 95)
|
| 126 |
+
return np.clip(I * 255.0 / p, 0, 255).astype(np.uint8)
|
| 127 |
+
|
| 128 |
+
|
| 129 |
+
def remove_zeros(I):
|
| 130 |
+
"""
|
| 131 |
+
Remove zeros, replace with 1's.
|
| 132 |
+
:param I: uint8 array
|
| 133 |
+
:return:
|
| 134 |
+
"""
|
| 135 |
+
mask = (I == 0)
|
| 136 |
+
I[mask] = 1
|
| 137 |
+
return I
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
def RGB_to_OD(I):
|
| 141 |
+
"""
|
| 142 |
+
Convert from RGB to optical density
|
| 143 |
+
:param I:
|
| 144 |
+
:return:
|
| 145 |
+
"""
|
| 146 |
+
I = remove_zeros(I)
|
| 147 |
+
return -1 * np.log(I / 255)
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
def OD_to_RGB(OD):
|
| 151 |
+
"""
|
| 152 |
+
Convert from optical density to RGB
|
| 153 |
+
:param OD:
|
| 154 |
+
:return:
|
| 155 |
+
"""
|
| 156 |
+
return (255 * np.exp(-1 * OD)).astype(np.uint8)
|
| 157 |
+
|
| 158 |
+
|
| 159 |
+
def normalize_rows(A):
|
| 160 |
+
"""
|
| 161 |
+
Normalize rows of an array
|
| 162 |
+
:param A:
|
| 163 |
+
:return:
|
| 164 |
+
"""
|
| 165 |
+
return A / np.linalg.norm(A, axis=1)[:, None]
|
| 166 |
+
|
| 167 |
+
|
| 168 |
+
def notwhite_mask(I, thresh=0.8):
|
| 169 |
+
"""
|
| 170 |
+
Get a binary mask where true denotes 'not white'
|
| 171 |
+
:param I:
|
| 172 |
+
:param thresh:
|
| 173 |
+
:return:
|
| 174 |
+
"""
|
| 175 |
+
I_LAB = cv.cvtColor(I, cv.COLOR_RGB2LAB)
|
| 176 |
+
L = I_LAB[:, :, 0] / 255.0
|
| 177 |
+
return (L < thresh)
|
| 178 |
+
|
| 179 |
+
|
| 180 |
+
def sign(x):
|
| 181 |
+
"""
|
| 182 |
+
Returns the sign of x
|
| 183 |
+
:param x:
|
| 184 |
+
:return:
|
| 185 |
+
"""
|
| 186 |
+
if x > 0:
|
| 187 |
+
return +1
|
| 188 |
+
elif x < 0:
|
| 189 |
+
return -1
|
| 190 |
+
elif x == 0:
|
| 191 |
+
return 0
|
| 192 |
+
|
| 193 |
+
def get_concentrations(I, stain_matrix, lamda=0.01):
|
| 194 |
+
"""
|
| 195 |
+
Get concentrations, a npix x 2 matrix
|
| 196 |
+
:param I:
|
| 197 |
+
:param stain_matrix: a 2x3 stain matrix
|
| 198 |
+
:return:
|
| 199 |
+
"""
|
| 200 |
+
OD = RGB_to_OD(I).reshape((-1, 3))
|
| 201 |
+
return spams.lasso(OD.T, D=stain_matrix.T, mode=2, lambda1=lamda, pos=True).toarray().T
|
| 202 |
+
|
| 203 |
+
##================================================================================================
|
| 204 |
+
def get_stain_matrix(I, beta=0.15, alpha=1):
|
| 205 |
+
"""
|
| 206 |
+
Get stain matrix (2x3)
|
| 207 |
+
:param I:
|
| 208 |
+
:param beta:
|
| 209 |
+
:param alpha:
|
| 210 |
+
:return:
|
| 211 |
+
"""
|
| 212 |
+
OD = RGB_to_OD(I).reshape((-1, 3))
|
| 213 |
+
OD = OD[(OD > beta).any(axis=1), :]
|
| 214 |
+
_, V = np.linalg.eigh(np.cov(OD, rowvar=False))
|
| 215 |
+
V = V[:, [2, 1]]
|
| 216 |
+
if V[0, 0] < 0:
|
| 217 |
+
V[:, 0] *= -1
|
| 218 |
+
if V[0, 1] < 0:
|
| 219 |
+
V[:, 1] *= -1
|
| 220 |
+
That = np.dot(OD, V)
|
| 221 |
+
phi = np.arctan2(That[:, 1], That[:, 0])
|
| 222 |
+
minPhi = np.percentile(phi, alpha)
|
| 223 |
+
maxPhi = np.percentile(phi, 100 - alpha)
|
| 224 |
+
v1 = np.dot(V, np.array([np.cos(minPhi), np.sin(minPhi)]))
|
| 225 |
+
v2 = np.dot(V, np.array([np.cos(maxPhi), np.sin(maxPhi)]))
|
| 226 |
+
if v1[0] > v2[0]:
|
| 227 |
+
HE = np.array([v1, v2])
|
| 228 |
+
else:
|
| 229 |
+
HE = np.array([v2, v1])
|
| 230 |
+
return normalize_rows(HE)
|
| 231 |
+
|
| 232 |
+
##================================================================================================
|
| 233 |
+
|
| 234 |
+
class macenko_normalizer(object):
|
| 235 |
+
"""
|
| 236 |
+
A stain normalization object
|
| 237 |
+
"""
|
| 238 |
+
|
| 239 |
+
def __init__(self):
|
| 240 |
+
self.stain_matrix_target = np.array(
|
| 241 |
+
[[0.5626, 0.2159], [0.7201, 0.8012], [0.4062, 0.5581]], dtype=np.float32).T
|
| 242 |
+
self.target_concentrations = None
|
| 243 |
+
|
| 244 |
+
def fit(self, target):
|
| 245 |
+
target = standardize_brightness(target)
|
| 246 |
+
self.stain_matrix_target = get_stain_matrix(target)
|
| 247 |
+
self.target_concentrations = get_concentrations(target, self.stain_matrix_target)
|
| 248 |
+
|
| 249 |
+
def target_stains(self):
|
| 250 |
+
return OD_to_RGB(self.stain_matrix_target)
|
| 251 |
+
|
| 252 |
+
def transform(self, I):
|
| 253 |
+
I = standardize_brightness(I)
|
| 254 |
+
stain_matrix_source = get_stain_matrix(I)
|
| 255 |
+
source_concentrations = get_concentrations(I, stain_matrix_source)
|
| 256 |
+
maxC_source = np.percentile(source_concentrations, 99, axis=0).reshape((1, 2))
|
| 257 |
+
maxC_target = np.array([1.9705, 1.0308], dtype=float).reshape((1, 2))
|
| 258 |
+
source_concentrations *= maxC_target / maxC_source
|
| 259 |
+
return (255* np.exp(-1*np.dot(source_concentrations, self.stain_matrix_target).reshape(I.shape))).astype(np.uint8)
|
| 260 |
+
|
| 261 |
+
def hematoxylin(self, I):
|
| 262 |
+
I = standardize_brightness(I)
|
| 263 |
+
h, w, c = I.shape
|
| 264 |
+
stain_matrix_source = get_stain_matrix(I)
|
| 265 |
+
source_concentrations = get_concentrations(I, stain_matrix_source)
|
| 266 |
+
H = source_concentrations[:, 0].reshape(h, w)
|
| 267 |
+
H = np.exp(-1 * H)
|
| 268 |
+
return H
|
| 269 |
+
|
| 270 |
+
|
utils_preprocessing.py
ADDED
|
@@ -0,0 +1,335 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import numpy as np
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import matplotlib.pyplot as plt
|
| 4 |
+
import openslide
|
| 5 |
+
from PIL import Image
|
| 6 |
+
|
| 7 |
+
import cv2
|
| 8 |
+
import torch
|
| 9 |
+
from torch import nn
|
| 10 |
+
import torchvision
|
| 11 |
+
#from torchvision.models import resnet50
|
| 12 |
+
import torchvision.transforms as transforms
|
| 13 |
+
# from transformers import ViTImageProcessor, ViTModel
|
| 14 |
+
# from timm.models.vision_transformer import VisionTransformer
|
| 15 |
+
# import timm
|
| 16 |
+
from ctrans_model import CTransPath
|
| 17 |
+
|
| 18 |
+
import utils_color_norm
|
| 19 |
+
color_norm = utils_color_norm.macenko_normalizer()
|
| 20 |
+
|
| 21 |
+
## check available device
|
| 22 |
+
device = (torch.device('cuda') if torch.cuda.is_available() else torch.device('cpu'))
|
| 23 |
+
print("device:", device)
|
| 24 |
+
##======================================================================================================
|
| 25 |
+
class resnet50_feature_extraction(nn.Module):
|
| 26 |
+
def __init__(self, model_type="load_from_saved_file"):
|
| 27 |
+
super().__init__()
|
| 28 |
+
|
| 29 |
+
if model_type == "load_from_internet":
|
| 30 |
+
self.resnet = resnet50(weights=torchvision.models.ResNet50_Weights.IMAGENET1K_V2)
|
| 31 |
+
elif model_type == "load_from_saved_file":
|
| 32 |
+
self.resnet = resnet50(weights=None)
|
| 33 |
+
else:
|
| 34 |
+
print("cannot find model_type can only be load_from_internet or load_from_saved_file")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def forward(self, x):
|
| 38 |
+
x = self.resnet.conv1(x)
|
| 39 |
+
x = self.resnet.bn1(x)
|
| 40 |
+
x = self.resnet.relu(x)
|
| 41 |
+
x = self.resnet.maxpool(x)
|
| 42 |
+
|
| 43 |
+
x = self.resnet.layer1(x)
|
| 44 |
+
x = self.resnet.layer2(x)
|
| 45 |
+
x = self.resnet.layer3(x)
|
| 46 |
+
x = self.resnet.layer4(x)
|
| 47 |
+
|
| 48 |
+
x = self.resnet.avgpool(x)
|
| 49 |
+
x = torch.flatten(x, 1)
|
| 50 |
+
return x
|
| 51 |
+
|
| 52 |
+
##======================================================================================================
|
| 53 |
+
def evaluate_tile_edge(img_np, edge_mag_thrsh, edge_fraction_thrsh):
|
| 54 |
+
|
| 55 |
+
select = 1 ## initial value
|
| 56 |
+
|
| 57 |
+
#img_np = np.array(img_RGB)
|
| 58 |
+
tile_size = img_np.shape[0]
|
| 59 |
+
|
| 60 |
+
##---------------------------------------
|
| 61 |
+
## 0) exclude if edge_mag > 0.5
|
| 62 |
+
img_gray=cv2.cvtColor(img_np, cv2.COLOR_RGB2GRAY)
|
| 63 |
+
|
| 64 |
+
# Remove noise using a Gaussian filter
|
| 65 |
+
#img_gray = cv2.GaussianBlur(img_gray, (5,5), 0)
|
| 66 |
+
|
| 67 |
+
sobelx = cv2.Sobel(img_gray, cv2.CV_32F, 1, 0)
|
| 68 |
+
sobely = cv2.Sobel(img_gray, cv2.CV_32F, 0, 1)
|
| 69 |
+
|
| 70 |
+
sobelx1 = cv2.convertScaleAbs(sobelx)
|
| 71 |
+
sobely1 = cv2.convertScaleAbs(sobely)
|
| 72 |
+
|
| 73 |
+
mag = cv2.addWeighted(sobelx1, 0.5, sobely1, 0.5, 0)
|
| 74 |
+
|
| 75 |
+
unique, counts = np.unique(mag, return_counts=True)
|
| 76 |
+
|
| 77 |
+
edge_mag = counts[np.argwhere(unique < edge_mag_thrsh)].sum()/(tile_size*tile_size)
|
| 78 |
+
|
| 79 |
+
if edge_mag > edge_fraction_thrsh:
|
| 80 |
+
select = 0
|
| 81 |
+
|
| 82 |
+
return select
|
| 83 |
+
|
| 84 |
+
##======================================================================================================
|
| 85 |
+
def evaluate_tile_color(img_np,black_thrsh,black_pct_thrsh,blue_level_thrsh,red_level_thrsh,
|
| 86 |
+
H_min,H_max,S_min,S_max,V_min,V_max,select):
|
| 87 |
+
|
| 88 |
+
#img_np = np.array(img_RGB)
|
| 89 |
+
|
| 90 |
+
L, A, B = cv2.split(cv2.cvtColor((img_np), cv2.COLOR_RGB2LAB))
|
| 91 |
+
|
| 92 |
+
##---------------------------------------
|
| 93 |
+
## 1) remove if percentage of black spot > 0.01
|
| 94 |
+
black_pct = np.mean(L < black_thrsh)
|
| 95 |
+
if black_pct > black_pct_thrsh:
|
| 96 |
+
select = 0
|
| 97 |
+
return select
|
| 98 |
+
##---------------------------------------
|
| 99 |
+
## 2) remove if too blue (heavy mark), or too red (blood)
|
| 100 |
+
red,green,blue = np.mean(img_np[:,:,0]),np.mean(img_np[:,:,1]),np.mean(img_np[:,:,2])
|
| 101 |
+
blue_level = blue/(red + green)
|
| 102 |
+
blue_level2 = blue*blue_level
|
| 103 |
+
|
| 104 |
+
if blue_level2 > blue_level_thrsh:
|
| 105 |
+
select = 0
|
| 106 |
+
return select
|
| 107 |
+
|
| 108 |
+
##---
|
| 109 |
+
red_level = red/(green + blue)
|
| 110 |
+
red_level2 = red*red_level
|
| 111 |
+
|
| 112 |
+
if red_level2 > red_level_thrsh:
|
| 113 |
+
select = 0
|
| 114 |
+
return select
|
| 115 |
+
|
| 116 |
+
##---------------------------------------
|
| 117 |
+
## 3) remove if tile has the same color suggested (using color detection)
|
| 118 |
+
H,S,V = cv2.split(cv2.cvtColor(img_np, cv2.COLOR_RGB2HSV))
|
| 119 |
+
H,S,V = np.mean(H),np.mean(S),np.mean(V)
|
| 120 |
+
|
| 121 |
+
if (H_min <= H and H <= H_max and S_min <= S and S <= S_max and V_min <= V and V <= V_max):
|
| 122 |
+
select = 0
|
| 123 |
+
return select
|
| 124 |
+
|
| 125 |
+
return select
|
| 126 |
+
|
| 127 |
+
##================================================================================================
|
| 128 |
+
def slide2tiles(path2slide, slide_name, slide_file_name, mag_assumed, mag_selected, tile_size,
|
| 129 |
+
mask_downsampling,edge_mag_thrsh,edge_fraction_thrsh,save_tile_file,
|
| 130 |
+
path2mask,path2coordinates):
|
| 131 |
+
|
| 132 |
+
## open slide
|
| 133 |
+
slide = openslide.OpenSlide(f"{path2slide}{slide_file_name}")
|
| 134 |
+
|
| 135 |
+
## magnification max
|
| 136 |
+
if openslide.PROPERTY_NAME_OBJECTIVE_POWER in slide.properties:
|
| 137 |
+
mag_max = slide.properties[openslide.PROPERTY_NAME_OBJECTIVE_POWER]
|
| 138 |
+
print("mag_max:", mag_max)
|
| 139 |
+
mag_original = mag_max
|
| 140 |
+
else:
|
| 141 |
+
print("[WARNING] mag not found, assuming: {mag_assumed}")
|
| 142 |
+
mag_max = mag_assumed
|
| 143 |
+
mag_original = 0
|
| 144 |
+
|
| 145 |
+
## downsample_level
|
| 146 |
+
downsampling = int(int(mag_max)/mag_selected)
|
| 147 |
+
print(f"downsampling: {downsampling}")
|
| 148 |
+
|
| 149 |
+
|
| 150 |
+
mask_tile_size = int(np.ceil(tile_size/mask_downsampling))
|
| 151 |
+
#print("mask_tile_size:", mask_tile_size)
|
| 152 |
+
|
| 153 |
+
##------------------------------------------------------------------
|
| 154 |
+
## slide partitioning
|
| 155 |
+
## slide size at largest level (level=0)
|
| 156 |
+
px0, py0 = slide.level_dimensions[0]
|
| 157 |
+
tile_size0 = int(tile_size*downsampling)
|
| 158 |
+
print(f"px0: {px0}, py0: {py0}, tile_size0: {tile_size0}")
|
| 159 |
+
|
| 160 |
+
n_rows,n_cols = int(py0/tile_size0), int(px0/tile_size0)
|
| 161 |
+
print(f"n_rows: {n_rows}, n_cols: {n_cols}")
|
| 162 |
+
|
| 163 |
+
n_tiles_total = n_rows*n_cols
|
| 164 |
+
print(f"n_tiles_total: {n_tiles_total}")
|
| 165 |
+
|
| 166 |
+
##-----------------------
|
| 167 |
+
img_mask = np.full((int((n_rows)*mask_tile_size),int((n_cols)*mask_tile_size),3),255).astype(np.uint8)
|
| 168 |
+
mask = np.full((int((n_rows)*mask_tile_size),int((n_cols)*mask_tile_size),3),255).astype(np.uint8)
|
| 169 |
+
|
| 170 |
+
i_tile = 0
|
| 171 |
+
tiles_list = []
|
| 172 |
+
|
| 173 |
+
col_list = []
|
| 174 |
+
row_list = []
|
| 175 |
+
i_tile_list = []
|
| 176 |
+
for row in range(n_rows):
|
| 177 |
+
print(f"row: {row}/{n_rows}")
|
| 178 |
+
for col in range(n_cols):
|
| 179 |
+
|
| 180 |
+
tile = slide.read_region((col*tile_size0, row*tile_size0),\
|
| 181 |
+
level=0, size=[tile_size0, tile_size0]).convert("RGB") ## RGBA image --> RGB
|
| 182 |
+
|
| 183 |
+
if tile.size[0] == tile_size0 and tile.size[1] == tile_size0:
|
| 184 |
+
# downsample to target tile size
|
| 185 |
+
tile = tile.resize((tile_size, tile_size))
|
| 186 |
+
|
| 187 |
+
mask_tile = np.array(tile.resize((mask_tile_size, mask_tile_size)))
|
| 188 |
+
|
| 189 |
+
img_mask[int(row*mask_tile_size):int((row+1)*mask_tile_size),\
|
| 190 |
+
int(col*mask_tile_size):int((col+1)*mask_tile_size),:] = mask_tile
|
| 191 |
+
|
| 192 |
+
tile = np.array(tile)
|
| 193 |
+
#print(tile.shape)
|
| 194 |
+
|
| 195 |
+
## evaluate tile
|
| 196 |
+
select = evaluate_tile_edge(tile, edge_mag_thrsh, edge_fraction_thrsh)
|
| 197 |
+
|
| 198 |
+
if select == 1:
|
| 199 |
+
## 2022.09.08: color normalization:
|
| 200 |
+
tile_norm = Image.fromarray(color_norm.transform(tile))
|
| 201 |
+
|
| 202 |
+
mask_tile_norm = np.array(tile_norm.resize((mask_tile_size, mask_tile_size)))
|
| 203 |
+
|
| 204 |
+
mask[int(row*mask_tile_size):int((row+1)*mask_tile_size),\
|
| 205 |
+
int(col*mask_tile_size):int((col+1)*mask_tile_size),:] = mask_tile_norm
|
| 206 |
+
|
| 207 |
+
#tiles_list.append(np.array(tile_norm).astype(np.uint8))
|
| 208 |
+
tiles_list.append(tile_norm)
|
| 209 |
+
|
| 210 |
+
if save_tile_file:
|
| 211 |
+
tile_name = "tile_" + str(row).zfill(5)+"_" + str(col).zfill(5) + "_" \
|
| 212 |
+
+ str(i_tile).zfill(5) + "_" + str(downsampling).zfill(3)
|
| 213 |
+
|
| 214 |
+
tile_norm.save(f"{tile_folder}/{tile_name}.png")
|
| 215 |
+
|
| 216 |
+
## 2023.05.27: tile information
|
| 217 |
+
col_list.append(col)
|
| 218 |
+
row_list.append(row)
|
| 219 |
+
i_tile_list.append(i_tile)
|
| 220 |
+
|
| 221 |
+
i_tile += 1
|
| 222 |
+
|
| 223 |
+
## 2023.05.27: save tile coordinates:
|
| 224 |
+
downsampling_list = [downsampling]*len(row_list)
|
| 225 |
+
df_coordinates = pd.DataFrame({"row": row_list, "col": col_list, "i_tile": i_tile_list, "downsampling": downsampling})
|
| 226 |
+
df_coordinates.to_csv(f"{path2coordinates}{slide_name}.csv", index_label="tile_idx")
|
| 227 |
+
|
| 228 |
+
##======================================================================================================
|
| 229 |
+
## plot: draw color lines on the mask
|
| 230 |
+
line_color = [0,255,0]
|
| 231 |
+
|
| 232 |
+
n_tiles = len(tiles_list)
|
| 233 |
+
|
| 234 |
+
img_mask[:,::mask_tile_size,:] = line_color
|
| 235 |
+
img_mask[::mask_tile_size,:,:] = line_color
|
| 236 |
+
mask[:,::mask_tile_size,:] = line_color
|
| 237 |
+
mask[::mask_tile_size,:,:] = line_color
|
| 238 |
+
|
| 239 |
+
fig, ax = plt.subplots(1,2,figsize=(30,15))
|
| 240 |
+
ax[0].imshow(img_mask)
|
| 241 |
+
ax[1].imshow(mask)
|
| 242 |
+
|
| 243 |
+
ax[0].set_title(f"{slide_name}, mag_original: {mag_original}, mag_assumed: {mag_assumed}")
|
| 244 |
+
ax[1].set_title(f"n_rows: {n_rows}, n_cols: {n_cols}, n_tiles_total: {n_tiles_total}, n_tiles_selected: {n_tiles}")
|
| 245 |
+
|
| 246 |
+
plt.tight_layout(h_pad=0.4, w_pad=0.5)
|
| 247 |
+
plt.savefig(f"{path2mask}{slide_name}.pdf", format="pdf", dpi=50)
|
| 248 |
+
plt.close()
|
| 249 |
+
|
| 250 |
+
img_mask = 0 ; mask = 0
|
| 251 |
+
|
| 252 |
+
print("completed cleaning")
|
| 253 |
+
|
| 254 |
+
return tiles_list
|
| 255 |
+
|
| 256 |
+
##======================================================================================================
|
| 257 |
+
def tile_transform(tiles_list, data_mean, data_std):
|
| 258 |
+
data_transform = transforms.Compose([transforms.Resize(224),
|
| 259 |
+
transforms.ToTensor(),
|
| 260 |
+
transforms.Normalize(mean=data_mean, std=data_std)])
|
| 261 |
+
|
| 262 |
+
## data transform:
|
| 263 |
+
n_tiles = len(tiles_list)
|
| 264 |
+
print("n_tiles:", n_tiles)
|
| 265 |
+
|
| 266 |
+
tiles = []
|
| 267 |
+
for i in range(n_tiles):
|
| 268 |
+
tiles.append(data_transform(tiles_list[i]).unsqueeze(0))
|
| 269 |
+
tiles = torch.cat(tiles, dim=0)
|
| 270 |
+
print("tiles.shape:", tiles.shape)
|
| 271 |
+
tiles_list = 0
|
| 272 |
+
|
| 273 |
+
return tiles ## [n_tiles,3,224,224]
|
| 274 |
+
|
| 275 |
+
##================================================================================================
|
| 276 |
+
def tiles2features(tiles_list, model_name, batch_size):
|
| 277 |
+
|
| 278 |
+
##----------------------------------------
|
| 279 |
+
## model config
|
| 280 |
+
if model_name == "vit":
|
| 281 |
+
path2model = "../vit-base-patch16-224-in21k"
|
| 282 |
+
model = ViTModel.from_pretrained(path2model)
|
| 283 |
+
model.to(device)
|
| 284 |
+
data_mean=[0.5, 0.5, 0.5] ; data_std = [0.5, 0.5, 0.5]
|
| 285 |
+
|
| 286 |
+
if model_name == "dino":
|
| 287 |
+
path2model = "../dino_vit_small_patch16_ep200.pt"
|
| 288 |
+
model = VisionTransformer(img_size=224, patch_size=16,
|
| 289 |
+
embed_dim=384, num_heads=6, num_classes=0)
|
| 290 |
+
model.to(device)
|
| 291 |
+
model.load_state_dict(torch.load(path2model,map_location=device))
|
| 292 |
+
data_mean=[0.485, 0.456, 0.406] ; data_std = [0.229, 0.224, 0.225]
|
| 293 |
+
|
| 294 |
+
if model_name == "ctrans":
|
| 295 |
+
path2model = "../ctranspath.pth"
|
| 296 |
+
model = CTransPath(num_classes=0)
|
| 297 |
+
model.to(device)
|
| 298 |
+
model.load_state_dict(torch.load(path2model)['model'])
|
| 299 |
+
model = model.cpu()
|
| 300 |
+
data_mean=[0.485, 0.456, 0.406] ; data_std = [0.229, 0.224, 0.225]
|
| 301 |
+
|
| 302 |
+
model.eval()
|
| 303 |
+
|
| 304 |
+
## tile transform
|
| 305 |
+
tiles = tile_transform(tiles_list, data_mean, data_std)
|
| 306 |
+
|
| 307 |
+
## extract features from tiles
|
| 308 |
+
n_tiles = tiles.shape[0]
|
| 309 |
+
features = []
|
| 310 |
+
for idx_start in range(0, n_tiles, batch_size):
|
| 311 |
+
idx_end = idx_start + min(batch_size, n_tiles - idx_start)
|
| 312 |
+
|
| 313 |
+
with torch.no_grad():
|
| 314 |
+
y = model(tiles[idx_start:idx_end])
|
| 315 |
+
|
| 316 |
+
if model_name == "vit":
|
| 317 |
+
y = y.last_hidden_state[:, 0]
|
| 318 |
+
|
| 319 |
+
features.append(y.detach().cpu().numpy())
|
| 320 |
+
|
| 321 |
+
features = np.concatenate(features)
|
| 322 |
+
print("features.shape:", features.shape)
|
| 323 |
+
|
| 324 |
+
return features
|
| 325 |
+
##================================================================================================
|
| 326 |
+
def init_random_seed(random_seed=42):
|
| 327 |
+
# Python RNG
|
| 328 |
+
np.random.seed(random_seed)
|
| 329 |
+
|
| 330 |
+
# Torch RNG
|
| 331 |
+
torch.manual_seed(random_seed)
|
| 332 |
+
torch.cuda.manual_seed(random_seed)
|
| 333 |
+
torch.cuda.manual_seed_all(random_seed)
|
| 334 |
+
torch.backends.cudnn.deterministic = True
|
| 335 |
+
torch.backends.cudnn.benchmark = False
|