deepliif 1.1.7__py3-none-any.whl → 1.1.8__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- cli.py +76 -102
- deepliif/data/aligned_dataset.py +33 -7
- deepliif/models/DeepLIIFExt_model.py +297 -0
- deepliif/models/DeepLIIF_model.py +10 -5
- deepliif/models/__init__.py +262 -168
- deepliif/models/base_model.py +54 -8
- deepliif/options/__init__.py +101 -0
- deepliif/options/base_options.py +7 -6
- deepliif/postprocessing.py +285 -246
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/METADATA +17 -8
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/RECORD +15 -14
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/LICENSE.md +0 -0
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/WHEEL +0 -0
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/entry_points.txt +0 -0
- {deepliif-1.1.7.dist-info → deepliif-1.1.8.dist-info}/top_level.txt +0 -0
deepliif/options/__init__.py
CHANGED
|
@@ -1 +1,102 @@
|
|
|
1
1
|
"""This package options includes option modules: training options, test options, and basic options (used in both training and test)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
import os
|
|
5
|
+
from ..util.util import mkdirs
|
|
6
|
+
|
|
7
|
+
def read_model_params(file_addr):
|
|
8
|
+
with open(file_addr) as f:
|
|
9
|
+
lines = f.readlines()
|
|
10
|
+
param_dict = {}
|
|
11
|
+
for line in lines:
|
|
12
|
+
if ':' in line:
|
|
13
|
+
key = line.split(':')[0].strip()
|
|
14
|
+
val = line.split(':')[1].split('[')[0].strip()
|
|
15
|
+
param_dict[key] = val
|
|
16
|
+
print(param_dict)
|
|
17
|
+
return param_dict
|
|
18
|
+
|
|
19
|
+
class Options:
|
|
20
|
+
def __init__(self, d_params=None, path_file=None, mode='train'):
|
|
21
|
+
assert d_params is not None or path_file is not None, "either d_params or path_file should be provided"
|
|
22
|
+
assert d_params is None or path_file is None, "only one source can be provided, either being d_params or path_file"
|
|
23
|
+
assert mode in ['train','test'], 'mode should be one of ["train", "test"]'
|
|
24
|
+
|
|
25
|
+
if path_file:
|
|
26
|
+
d_params = read_model_params(path_file)
|
|
27
|
+
|
|
28
|
+
for k,v in d_params.items():
|
|
29
|
+
try:
|
|
30
|
+
if k not in ['phase']: # e.g., k = 'phase', v = 'train', eval(v) is a function rather than a string
|
|
31
|
+
setattr(self,k,eval(v)) # to parse int/float/tuple etc. from string
|
|
32
|
+
else:
|
|
33
|
+
setattr(self,k,v)
|
|
34
|
+
except:
|
|
35
|
+
setattr(self,k,v)
|
|
36
|
+
|
|
37
|
+
if mode != 'train':
|
|
38
|
+
# to account for old settings where gpu_ids value is an integer, not a tuple
|
|
39
|
+
if isinstance(self.gpu_ids,int):
|
|
40
|
+
self.gpu_ids = (self.gpu_ids,)
|
|
41
|
+
|
|
42
|
+
# to account for old settings before modalities_no was introduced
|
|
43
|
+
if not hasattr(self,'modalities_no') and hasattr(self,'targets_no'):
|
|
44
|
+
self.modalities_no = self.targets_no - 1
|
|
45
|
+
del self.targets_no
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
if mode == 'train':
|
|
50
|
+
self.is_train = True
|
|
51
|
+
self.netG = 'resnet_9blocks'
|
|
52
|
+
self.netD = 'n_layers'
|
|
53
|
+
self.n_layers_D = 4
|
|
54
|
+
self.lambda_L1 = 100
|
|
55
|
+
self.lambda_feat = 100
|
|
56
|
+
else:
|
|
57
|
+
self.phase = 'test'
|
|
58
|
+
self.is_train = False
|
|
59
|
+
self.input_nc = 3
|
|
60
|
+
self.output_nc = 3
|
|
61
|
+
self.ngf = 64
|
|
62
|
+
self.norm = 'batch'
|
|
63
|
+
self.use_dropout = True
|
|
64
|
+
#self.padding_type = 'zero' # some models use reflect etc. which adds additional randomness
|
|
65
|
+
#self.padding = 'zero'
|
|
66
|
+
self.use_dropout = False #if self.no_dropout == 'True' else True
|
|
67
|
+
|
|
68
|
+
# reset checkpoints_dir and name based on the model directory
|
|
69
|
+
# when base model is initialized: self.save_dir = os.path.join(opt.checkpoints_dir, opt.name)
|
|
70
|
+
model_dir = Path(path_file).parent
|
|
71
|
+
self.checkpoints_dir = str(model_dir.parent)
|
|
72
|
+
self.name = str(model_dir.name)
|
|
73
|
+
|
|
74
|
+
self.gpu_ids = [] # gpu_ids is only used by eager mode, set to empty / cpu to be the same as the old settings; non-eager mode will use all gpus
|
|
75
|
+
|
|
76
|
+
def _get_kwargs(self):
|
|
77
|
+
common_attr = ['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']
|
|
78
|
+
l_args = [x for x in dir(self) if x not in common_attr]
|
|
79
|
+
return {k:getattr(self,k) for k in l_args}
|
|
80
|
+
|
|
81
|
+
def print_options(opt):
|
|
82
|
+
"""Print and save options
|
|
83
|
+
|
|
84
|
+
It will print both current options and default values(if different).
|
|
85
|
+
It will save options into a text file / [checkpoints_dir] / opt.txt
|
|
86
|
+
"""
|
|
87
|
+
message = ''
|
|
88
|
+
message += '----------------- Options ---------------\n'
|
|
89
|
+
for k, v in sorted(vars(opt).items()):
|
|
90
|
+
comment = ''
|
|
91
|
+
message += '{:>25}: {:<30}{}\n'.format(str(k), str(v), comment)
|
|
92
|
+
message += '----------------- End -------------------'
|
|
93
|
+
print(message)
|
|
94
|
+
|
|
95
|
+
# save to the disk
|
|
96
|
+
if opt.phase == 'train':
|
|
97
|
+
expr_dir = os.path.join(opt.checkpoints_dir, opt.name)
|
|
98
|
+
mkdirs(expr_dir)
|
|
99
|
+
file_name = os.path.join(expr_dir, '{}_opt.txt'.format(opt.phase))
|
|
100
|
+
with open(file_name, 'wt') as opt_file:
|
|
101
|
+
opt_file.write(message)
|
|
102
|
+
opt_file.write('\n')
|
deepliif/options/base_options.py
CHANGED
|
@@ -118,12 +118,13 @@ class BaseOptions():
|
|
|
118
118
|
print(message)
|
|
119
119
|
|
|
120
120
|
# save to the disk
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
121
|
+
if opt.phase == 'train':
|
|
122
|
+
expr_dir = os.path.join(opt.checkpoints_dir, opt.name)
|
|
123
|
+
util.mkdirs(expr_dir)
|
|
124
|
+
file_name = os.path.join(expr_dir, '{}_opt.txt'.format(opt.phase))
|
|
125
|
+
with open(file_name, 'wt') as opt_file:
|
|
126
|
+
opt_file.write(message)
|
|
127
|
+
opt_file.write('\n')
|
|
127
128
|
|
|
128
129
|
def parse(self):
|
|
129
130
|
"""Parse our options, create checkpoints directory suffix, and set up gpu device."""
|
deepliif/postprocessing.py
CHANGED
|
@@ -59,235 +59,6 @@ def remove_cell_noise(mask1, mask2):
|
|
|
59
59
|
return mask1, mask2
|
|
60
60
|
|
|
61
61
|
|
|
62
|
-
@jit(nopython=True)
|
|
63
|
-
def compute_cell_mapping(new_mapping, image_size, small_object_size=20):
|
|
64
|
-
marked = [[False for _ in range(image_size[1])] for _ in range(image_size[0])]
|
|
65
|
-
for i in range(image_size[0]):
|
|
66
|
-
for j in range(image_size[1]):
|
|
67
|
-
if marked[i][j] is False and (new_mapping[i, j, 0] > 0 or new_mapping[i, j, 2] > 0):
|
|
68
|
-
cluster_red_no, cluster_blue_no = 0, 0
|
|
69
|
-
pixels = [(i, j)]
|
|
70
|
-
cluster = [(i, j)]
|
|
71
|
-
marked[i][j] = True
|
|
72
|
-
while len(pixels) > 0:
|
|
73
|
-
pixel = pixels.pop()
|
|
74
|
-
if new_mapping[pixel[0], pixel[1], 0] > 0:
|
|
75
|
-
cluster_red_no += 1
|
|
76
|
-
if new_mapping[pixel[0], pixel[1], 2] > 0:
|
|
77
|
-
cluster_blue_no += 1
|
|
78
|
-
for neigh_i in range(-1, 2):
|
|
79
|
-
for neigh_j in range(-1, 2):
|
|
80
|
-
neigh_pixel = (pixel[0] + neigh_i, pixel[1] + neigh_j)
|
|
81
|
-
if 0 <= neigh_pixel[0] < image_size[0] and 0 <= neigh_pixel[1] < image_size[1] and \
|
|
82
|
-
marked[neigh_pixel[0]][neigh_pixel[1]] is False and (
|
|
83
|
-
new_mapping[neigh_pixel[0], neigh_pixel[1], 0] > 0 or new_mapping[
|
|
84
|
-
neigh_pixel[0], neigh_pixel[1], 2] > 0):
|
|
85
|
-
cluster.append(neigh_pixel)
|
|
86
|
-
pixels.append(neigh_pixel)
|
|
87
|
-
marked[neigh_pixel[0]][neigh_pixel[1]] = True
|
|
88
|
-
cluster_value = None
|
|
89
|
-
if cluster_red_no < cluster_blue_no:
|
|
90
|
-
cluster_value = (0, 0, 255)
|
|
91
|
-
else:
|
|
92
|
-
cluster_value = (255, 0, 0)
|
|
93
|
-
if len(cluster) < small_object_size:
|
|
94
|
-
cluster_value = (0, 0, 0)
|
|
95
|
-
if cluster_value is not None:
|
|
96
|
-
for node in cluster:
|
|
97
|
-
new_mapping[node[0], node[1]] = cluster_value
|
|
98
|
-
return new_mapping
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
@jit(nopython=True)
|
|
102
|
-
def remove_noises(channel, image_size, small_object_size=20):
|
|
103
|
-
marked = [[False for _ in range(image_size[1])] for _ in range(image_size[0])]
|
|
104
|
-
for i in range(image_size[0]):
|
|
105
|
-
for j in range(image_size[1]):
|
|
106
|
-
if marked[i][j] is False and channel[i, j] > 0:
|
|
107
|
-
pixels = [(i, j)]
|
|
108
|
-
cluster = [(i, j)]
|
|
109
|
-
marked[i][j] = True
|
|
110
|
-
while len(pixels) > 0:
|
|
111
|
-
pixel = pixels.pop()
|
|
112
|
-
for neigh_i in range(-1, 2):
|
|
113
|
-
for neigh_j in range(-1, 2):
|
|
114
|
-
neigh_pixel = (pixel[0] + neigh_i, pixel[1] + neigh_j)
|
|
115
|
-
if 0 <= neigh_pixel[0] < image_size[0] and 0 <= neigh_pixel[1] < image_size[1] and \
|
|
116
|
-
marked[neigh_pixel[0]][neigh_pixel[1]] is False and channel[
|
|
117
|
-
neigh_pixel[0], neigh_pixel[1]] > 0:
|
|
118
|
-
cluster.append(neigh_pixel)
|
|
119
|
-
pixels.append(neigh_pixel)
|
|
120
|
-
marked[neigh_pixel[0]][neigh_pixel[1]] = True
|
|
121
|
-
|
|
122
|
-
cluster_value = None
|
|
123
|
-
if len(cluster) < small_object_size:
|
|
124
|
-
cluster_value = 0
|
|
125
|
-
if cluster_value is not None:
|
|
126
|
-
for node in cluster:
|
|
127
|
-
channel[node[0], node[1]] = cluster_value
|
|
128
|
-
return channel
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
def remove_noises_fill_empty_holes(label_img, size=200):
|
|
132
|
-
inverse_img = 255 - label_img
|
|
133
|
-
inverse_img_removed = remove_noises(inverse_img, inverse_img.shape, small_object_size=size)
|
|
134
|
-
label_img[inverse_img_removed == 0] = 255
|
|
135
|
-
return label_img
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
def apply_original_image_intensity(gray, channel, orig_image_intensity_effect=0.1):
|
|
139
|
-
red_image_value = np.zeros((gray.shape[0], gray.shape[1]))
|
|
140
|
-
red_image_value[channel > 10] = gray[channel > 10] * orig_image_intensity_effect
|
|
141
|
-
red_image_value += channel
|
|
142
|
-
red_image_value[red_image_value > 255] = 255
|
|
143
|
-
return red_image_value.astype(np.uint8)
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
def apply_original_image_intensity2(gray, channel, channel2, orig_image_intensity_effect=0.1):
|
|
147
|
-
red_image_value = np.zeros((gray.shape[0], gray.shape[1]))
|
|
148
|
-
red_image_value[channel > 10] = gray[channel > 10] * orig_image_intensity_effect
|
|
149
|
-
red_image_value[channel2 > 10] = gray[channel2 > 10] * orig_image_intensity_effect
|
|
150
|
-
red_image_value += channel
|
|
151
|
-
red_image_value[red_image_value > 255] = 255
|
|
152
|
-
return red_image_value.astype(np.uint8)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def positive_negative_masks(img, mask, marker_image, marker_effect=0.4, thresh=100, noise_objects_size=20):
|
|
156
|
-
positive_mask = np.zeros((mask.shape[0], mask.shape[1]), dtype=np.uint8)
|
|
157
|
-
negative_mask = np.zeros((mask.shape[0], mask.shape[1]), dtype=np.uint8)
|
|
158
|
-
|
|
159
|
-
red = mask[:, :, 0]
|
|
160
|
-
blue = mask[:, :, 2]
|
|
161
|
-
boundary = mask[:, :, 1]
|
|
162
|
-
|
|
163
|
-
# Adding the original image intensity to increase the probability of low-contrast cells
|
|
164
|
-
# with lower probability in the segmentation mask
|
|
165
|
-
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
|
166
|
-
red = apply_original_image_intensity(gray, red, 0.3)
|
|
167
|
-
blue = apply_original_image_intensity(gray, blue, 0.3)
|
|
168
|
-
|
|
169
|
-
# Adding marker_image annotations to red probability mask
|
|
170
|
-
# to increase the probability of positive cells in the segmentation mask
|
|
171
|
-
# gray = cv2.cvtColor(marker_image, cv2.COLOR_RGB2GRAY)
|
|
172
|
-
# # red = apply_original_image_intensity(gray, red, marker_effect)
|
|
173
|
-
# red = apply_original_image_intensity2(gray, red, blue, marker_effect)
|
|
174
|
-
|
|
175
|
-
# Filtering boundary pixels
|
|
176
|
-
boundary[boundary < 80] = 0
|
|
177
|
-
|
|
178
|
-
positive_mask[red > thresh] = 255
|
|
179
|
-
positive_mask[boundary > 0] = 0
|
|
180
|
-
positive_mask[blue > red] = 0
|
|
181
|
-
|
|
182
|
-
negative_mask[blue > thresh] = 255
|
|
183
|
-
negative_mask[boundary > 0] = 0
|
|
184
|
-
negative_mask[red >= blue] = 0
|
|
185
|
-
|
|
186
|
-
cell_mapping = np.zeros_like(mask)
|
|
187
|
-
cell_mapping[:, :, 0] = positive_mask
|
|
188
|
-
cell_mapping[:, :, 2] = negative_mask
|
|
189
|
-
|
|
190
|
-
compute_cell_mapping(cell_mapping, mask.shape, small_object_size=50)
|
|
191
|
-
cell_mapping[cell_mapping > 0] = 255
|
|
192
|
-
|
|
193
|
-
positive_mask = cell_mapping[:, :, 0]
|
|
194
|
-
negative_mask = cell_mapping[:, :, 2]
|
|
195
|
-
|
|
196
|
-
def inner(img):
|
|
197
|
-
img = remove_small_objects_from_image(img, noise_objects_size)
|
|
198
|
-
img = ndi.binary_fill_holes(img).astype(np.uint8)
|
|
199
|
-
return cv2.morphologyEx(img, cv2.MORPH_DILATE, kernel=np.ones((2, 2)))
|
|
200
|
-
|
|
201
|
-
# return inner(positive_mask), inner(negative_mask)
|
|
202
|
-
return remove_noises_fill_empty_holes(positive_mask, noise_objects_size), remove_noises_fill_empty_holes(
|
|
203
|
-
negative_mask, noise_objects_size)
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
def positive_negative_masks_basic(img, mask, thresh=100, noise_objects_size=50, small_object_size=50):
|
|
207
|
-
positive_mask = np.zeros((mask.shape[0], mask.shape[1]), dtype=np.uint8)
|
|
208
|
-
negative_mask = np.zeros((mask.shape[0], mask.shape[1]), dtype=np.uint8)
|
|
209
|
-
|
|
210
|
-
red = mask[:, :, 0]
|
|
211
|
-
blue = mask[:, :, 2]
|
|
212
|
-
boundary = mask[:, :, 1]
|
|
213
|
-
|
|
214
|
-
gray = cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
|
|
215
|
-
|
|
216
|
-
red = apply_original_image_intensity(gray, red)
|
|
217
|
-
blue = apply_original_image_intensity(gray, blue)
|
|
218
|
-
|
|
219
|
-
boundary[boundary < 80] = 0
|
|
220
|
-
|
|
221
|
-
positive_mask[red > thresh] = 255
|
|
222
|
-
positive_mask[boundary > 0] = 0
|
|
223
|
-
positive_mask[blue > red] = 0
|
|
224
|
-
|
|
225
|
-
negative_mask[blue > thresh] = 255
|
|
226
|
-
negative_mask[boundary > 0] = 0
|
|
227
|
-
negative_mask[red >= blue] = 0
|
|
228
|
-
|
|
229
|
-
cell_mapping = np.zeros_like(mask)
|
|
230
|
-
cell_mapping[:, :, 0] = positive_mask
|
|
231
|
-
cell_mapping[:, :, 2] = negative_mask
|
|
232
|
-
|
|
233
|
-
compute_cell_mapping(cell_mapping, mask.shape, small_object_size)
|
|
234
|
-
cell_mapping[cell_mapping > 0] = 255
|
|
235
|
-
|
|
236
|
-
positive_mask = cell_mapping[:, :, 0]
|
|
237
|
-
negative_mask = cell_mapping[:, :, 2]
|
|
238
|
-
|
|
239
|
-
def inner(img):
|
|
240
|
-
img = remove_small_objects_from_image(img, noise_objects_size)
|
|
241
|
-
img = ndi.binary_fill_holes(img).astype(np.uint8)
|
|
242
|
-
return cv2.morphologyEx(img, cv2.MORPH_DILATE, kernel=np.ones((2, 2)))
|
|
243
|
-
|
|
244
|
-
# return inner(positive_mask), inner(negative_mask)
|
|
245
|
-
return remove_noises_fill_empty_holes(positive_mask, noise_objects_size), remove_noises_fill_empty_holes(
|
|
246
|
-
negative_mask, noise_objects_size)
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
def create_final_segmentation_mask_with_boundaries(mask_image):
|
|
250
|
-
refined_mask = mask_image.copy()
|
|
251
|
-
|
|
252
|
-
edges = feature.canny(refined_mask[:, :, 0], sigma=3).astype(np.uint8)
|
|
253
|
-
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] # a more recent cv2 version has 3 returned values
|
|
254
|
-
cv2.drawContours(refined_mask, contours, -1, (0, 255, 0), 2)
|
|
255
|
-
|
|
256
|
-
edges = feature.canny(refined_mask[:, :, 2], sigma=3).astype(np.uint8)
|
|
257
|
-
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] # a more recent cv2 version has 3 returned values
|
|
258
|
-
cv2.drawContours(refined_mask, contours, -1, (0, 255, 0), 2)
|
|
259
|
-
|
|
260
|
-
return refined_mask
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def overlay_final_segmentation_mask(img, mask_image):
|
|
264
|
-
positive_mask, negative_mask = mask_image[:, :, 0], mask_image[:, :, 2]
|
|
265
|
-
|
|
266
|
-
overlaid_mask = img.copy()
|
|
267
|
-
|
|
268
|
-
edges = feature.canny(positive_mask, sigma=3).astype(np.uint8)
|
|
269
|
-
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] # a more recent cv2 version has 3 returned values
|
|
270
|
-
cv2.drawContours(overlaid_mask, contours, -1, (255, 0, 0), 2)
|
|
271
|
-
|
|
272
|
-
edges = feature.canny(negative_mask, sigma=3).astype(np.uint8)
|
|
273
|
-
contours, _ = cv2.findContours(edges, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)[-2:] # a more recent cv2 version has 3 returned values
|
|
274
|
-
cv2.drawContours(overlaid_mask, contours, -1, (0, 0, 255), 2)
|
|
275
|
-
|
|
276
|
-
return overlaid_mask
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def create_final_segmentation_mask(img, seg_img, marker_image, marker_effect=0.4, thresh=80, noise_objects_size=20):
|
|
280
|
-
positive_mask, negative_mask = positive_negative_masks(img, seg_img, marker_image, marker_effect, thresh,
|
|
281
|
-
noise_objects_size)
|
|
282
|
-
|
|
283
|
-
mask = np.zeros_like(img)
|
|
284
|
-
|
|
285
|
-
mask[positive_mask > 0] = (255, 0, 0)
|
|
286
|
-
mask[negative_mask > 0] = (0, 0, 255)
|
|
287
|
-
|
|
288
|
-
return mask
|
|
289
|
-
|
|
290
|
-
|
|
291
62
|
def create_basic_segmentation_mask(img, seg_img, thresh=80, noise_objects_size=20, small_object_size=50):
|
|
292
63
|
positive_mask, negative_mask = positive_negative_masks_basic(img, seg_img, thresh, noise_objects_size, small_object_size)
|
|
293
64
|
|
|
@@ -371,24 +142,292 @@ def adjust_marker(inferred_tile, orig_tile):
|
|
|
371
142
|
return Image.fromarray(processed_tile)
|
|
372
143
|
|
|
373
144
|
|
|
374
|
-
|
|
375
|
-
|
|
145
|
+
# Values for uint8 masks
|
|
146
|
+
MASK_UNKNOWN = 50
|
|
147
|
+
MASK_POSITIVE = 200
|
|
148
|
+
MASK_NEGATIVE = 150
|
|
149
|
+
MASK_BACKGROUND = 0
|
|
150
|
+
MASK_CELL = 255
|
|
151
|
+
MASK_CELL_POSITIVE = 201
|
|
152
|
+
MASK_CELL_NEGATIVE = 151
|
|
153
|
+
MASK_BOUNDARY_POSITIVE = 202
|
|
154
|
+
MASK_BOUNDARY_NEGATIVE = 152
|
|
376
155
|
|
|
377
|
-
Parameters:
|
|
378
|
-
mask_image (numpy array) -- segmentation mask image of red and blue cells
|
|
379
156
|
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
157
|
+
@jit(nopython=True)
|
|
158
|
+
def in_bounds(array, index):
|
|
159
|
+
return index[0] >= 0 and index[0] < array.shape[0] and index[1] >= 0 and index[1] < array.shape[1]
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def create_posneg_mask(seg, thresh):
|
|
163
|
+
"""Create a mask of positive and negative pixels."""
|
|
164
|
+
|
|
165
|
+
cell = np.logical_and(np.add(seg[:,:,0], seg[:,:,2], dtype=np.uint16) > thresh, seg[:,:,1] <= 80)
|
|
166
|
+
pos = np.logical_and(cell, seg[:,:,0] >= seg[:,:,2])
|
|
167
|
+
neg = np.logical_xor(cell, pos)
|
|
385
168
|
|
|
169
|
+
mask = np.full(seg.shape[0:2], MASK_UNKNOWN, dtype=np.uint8)
|
|
170
|
+
mask[pos] = MASK_POSITIVE
|
|
171
|
+
mask[neg] = MASK_NEGATIVE
|
|
172
|
+
|
|
173
|
+
return mask
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@jit(nopython=True)
|
|
177
|
+
def mark_background(mask):
|
|
178
|
+
"""Mask all background pixels by 4-connected region growing unknown boundary pixels."""
|
|
179
|
+
|
|
180
|
+
seeds = []
|
|
181
|
+
for i in range(mask.shape[0]):
|
|
182
|
+
if mask[i, 0] == MASK_UNKNOWN:
|
|
183
|
+
seeds.append((i, 0))
|
|
184
|
+
if mask[i, mask.shape[1]-1] == MASK_UNKNOWN:
|
|
185
|
+
seeds.append((i, mask.shape[1]-1))
|
|
186
|
+
for j in range(mask.shape[1]):
|
|
187
|
+
if mask[0, j] == MASK_UNKNOWN:
|
|
188
|
+
seeds.append((0, j))
|
|
189
|
+
if mask[mask.shape[0]-1, j] == MASK_UNKNOWN:
|
|
190
|
+
seeds.append((mask.shape[0]-1, j))
|
|
191
|
+
|
|
192
|
+
neighbors = [(-1, 0), (1, 0), (0, -1), (0, 1)]
|
|
193
|
+
|
|
194
|
+
while len(seeds) > 0:
|
|
195
|
+
seed = seeds.pop()
|
|
196
|
+
if mask[seed] == MASK_UNKNOWN:
|
|
197
|
+
mask[seed] = MASK_BACKGROUND
|
|
198
|
+
for n in neighbors:
|
|
199
|
+
idx = (seed[0] + n[0], seed[1] + n[1])
|
|
200
|
+
if in_bounds(mask, idx) and mask[idx] == MASK_UNKNOWN:
|
|
201
|
+
seeds.append(idx)
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@jit(nopython=True)
|
|
205
|
+
def compute_cell_classification(mask, marker, size_thresh, marker_thresh, size_thresh_upper = None):
|
|
206
|
+
"""
|
|
207
|
+
Compute the mapping of the mask to positive and negative cell classification.
|
|
208
|
+
|
|
209
|
+
Parameters
|
|
210
|
+
==========
|
|
211
|
+
mask: 2D uint8 numpy array with pixels labeled as positive, negative, background, or unknown.
|
|
212
|
+
After the function executes, the pixels will be labeled as background or cell/boundary pos/neg.
|
|
213
|
+
marker: 2D uint8 numpy array with the restained marker values
|
|
214
|
+
size_thresh: Lower size threshold in pixels. Only include cells larger than this count.
|
|
215
|
+
size_thresh_upper: Upper size threshold in pixels, or None. Only include cells smaller than this count.
|
|
216
|
+
marker_thresh: Classify cell as positive if any marker value within the cell is above this threshold.
|
|
217
|
+
|
|
218
|
+
Returns
|
|
219
|
+
=======
|
|
220
|
+
Dictionary with the following values:
|
|
221
|
+
num_total (integer) -- total number of cells in the image
|
|
222
|
+
num_pos (integer) -- number of positive cells in the image
|
|
223
|
+
num_neg (integer) -- number of negative calles in the image
|
|
224
|
+
percent_pos (floating point) -- percentage of positive cells to all cells (IHC score)
|
|
386
225
|
"""
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
226
|
+
|
|
227
|
+
neighbors = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
|
|
228
|
+
border_neighbors = [(0, -1), (-1, 0), (1, 0), (0, 1)]
|
|
229
|
+
positive_cell_count, negative_cell_count = 0, 0
|
|
230
|
+
|
|
231
|
+
for y in range(mask.shape[0]):
|
|
232
|
+
for x in range(mask.shape[1]):
|
|
233
|
+
if mask[y, x] == MASK_POSITIVE or mask[y, x] == MASK_NEGATIVE:
|
|
234
|
+
seeds = [(y, x)]
|
|
235
|
+
cell_coords = []
|
|
236
|
+
count = 1
|
|
237
|
+
count_posneg = 1 if mask[y, x] != MASK_UNKNOWN else 0
|
|
238
|
+
count_positive = 1 if mask[y, x] == MASK_POSITIVE else 0
|
|
239
|
+
max_marker = marker[y, x] if marker is not None else 0
|
|
240
|
+
mask[y, x] = MASK_CELL
|
|
241
|
+
cell_coords.append((y, x))
|
|
242
|
+
|
|
243
|
+
while len(seeds) > 0:
|
|
244
|
+
seed = seeds.pop()
|
|
245
|
+
for n in neighbors:
|
|
246
|
+
idx = (seed[0] + n[0], seed[1] + n[1])
|
|
247
|
+
if in_bounds(mask, idx) and (mask[idx] == MASK_POSITIVE or mask[idx] == MASK_NEGATIVE or mask[idx] == MASK_UNKNOWN):
|
|
248
|
+
seeds.append(idx)
|
|
249
|
+
if mask[idx] == MASK_POSITIVE:
|
|
250
|
+
count_positive += 1
|
|
251
|
+
if mask[idx] != MASK_UNKNOWN:
|
|
252
|
+
count_posneg += 1
|
|
253
|
+
if marker is not None and marker[idx] > max_marker:
|
|
254
|
+
max_marker = marker[idx]
|
|
255
|
+
mask[idx] = MASK_CELL
|
|
256
|
+
cell_coords.append(idx)
|
|
257
|
+
count += 1
|
|
258
|
+
|
|
259
|
+
if count > size_thresh and (size_thresh_upper is None or count < size_thresh_upper):
|
|
260
|
+
if (count_positive/count_posneg) >= 0.5 or max_marker > marker_thresh:
|
|
261
|
+
fill_value = MASK_CELL_POSITIVE
|
|
262
|
+
border_value = MASK_BOUNDARY_POSITIVE
|
|
263
|
+
positive_cell_count += 1
|
|
264
|
+
else:
|
|
265
|
+
fill_value = MASK_CELL_NEGATIVE
|
|
266
|
+
border_value = MASK_BOUNDARY_NEGATIVE
|
|
267
|
+
negative_cell_count += 1
|
|
268
|
+
else:
|
|
269
|
+
fill_value = MASK_BACKGROUND
|
|
270
|
+
border_value = MASK_BACKGROUND
|
|
271
|
+
|
|
272
|
+
for coord in cell_coords:
|
|
273
|
+
is_boundary = False
|
|
274
|
+
for n in border_neighbors:
|
|
275
|
+
idx = (coord[0] + n[0], coord[1] + n[1])
|
|
276
|
+
if in_bounds(mask, idx) and mask[idx] == MASK_BACKGROUND:
|
|
277
|
+
is_boundary = True
|
|
278
|
+
break
|
|
279
|
+
if is_boundary:
|
|
280
|
+
mask[coord] = border_value
|
|
281
|
+
else:
|
|
282
|
+
mask[coord] = fill_value
|
|
283
|
+
|
|
284
|
+
counts = {
|
|
285
|
+
'num_total': positive_cell_count + negative_cell_count,
|
|
286
|
+
'num_pos': positive_cell_count,
|
|
287
|
+
'num_neg': negative_cell_count,
|
|
288
|
+
}
|
|
289
|
+
return counts
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
@jit(nopython=True)
|
|
293
|
+
def enlarge_cell_boundaries(mask):
|
|
294
|
+
neighbors = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
|
|
295
|
+
for y in range(mask.shape[0]):
|
|
296
|
+
for x in range(mask.shape[1]):
|
|
297
|
+
if mask[y, x] == MASK_BOUNDARY_POSITIVE or mask[y, x] == MASK_BOUNDARY_NEGATIVE:
|
|
298
|
+
value = MASK_POSITIVE if mask[y, x] == MASK_BOUNDARY_POSITIVE else MASK_NEGATIVE
|
|
299
|
+
for n in neighbors:
|
|
300
|
+
idx = (y + n[0], x + n[1])
|
|
301
|
+
if in_bounds(mask, idx) and mask[idx] != MASK_BOUNDARY_POSITIVE and mask[idx] != MASK_BOUNDARY_NEGATIVE:
|
|
302
|
+
mask[idx] = value
|
|
303
|
+
for y in range(mask.shape[0]):
|
|
304
|
+
for x in range(mask.shape[1]):
|
|
305
|
+
if mask[y, x] == MASK_POSITIVE:
|
|
306
|
+
mask[y, x] = MASK_BOUNDARY_POSITIVE
|
|
307
|
+
elif mask[y, x] == MASK_NEGATIVE:
|
|
308
|
+
mask[y, x] = MASK_BOUNDARY_NEGATIVE
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
@jit(nopython=True)
|
|
312
|
+
def compute_cell_sizes(mask):
|
|
313
|
+
neighbors = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)]
|
|
314
|
+
sizes = []
|
|
315
|
+
|
|
316
|
+
for y in range(mask.shape[0]):
|
|
317
|
+
for x in range(mask.shape[1]):
|
|
318
|
+
if mask[y, x] == MASK_POSITIVE or mask[y, x] == MASK_NEGATIVE:
|
|
319
|
+
seeds = [(y, x)]
|
|
320
|
+
count = 1
|
|
321
|
+
mask[y, x] = MASK_CELL_POSITIVE if mask[y, x] == MASK_POSITIVE else MASK_CELL_NEGATIVE
|
|
322
|
+
|
|
323
|
+
while len(seeds) > 0:
|
|
324
|
+
seed = seeds.pop()
|
|
325
|
+
for n in neighbors:
|
|
326
|
+
idx = (seed[0] + n[0], seed[1] + n[1])
|
|
327
|
+
if in_bounds(mask, idx) and (mask[idx] == MASK_POSITIVE or mask[idx] == MASK_NEGATIVE or mask[idx] == MASK_UNKNOWN):
|
|
328
|
+
seeds.append(idx)
|
|
329
|
+
if mask[idx] == MASK_POSITIVE:
|
|
330
|
+
mask[idx] = MASK_CELL_POSITIVE
|
|
331
|
+
elif mask[idx] == MASK_NEGATIVE:
|
|
332
|
+
mask[idx] = MASK_CELL_NEGATIVE
|
|
333
|
+
else:
|
|
334
|
+
mask[idx] = MASK_CELL
|
|
335
|
+
count += 1
|
|
336
|
+
|
|
337
|
+
sizes.append(count)
|
|
338
|
+
|
|
339
|
+
return sizes
|
|
340
|
+
|
|
341
|
+
|
|
342
|
+
@jit(nopython=True)
|
|
343
|
+
def create_kde(values, count, bandwidth = 1.0):
|
|
344
|
+
gaussian_denom_inv = 1 / math.sqrt(2 * math.pi);
|
|
345
|
+
max_value = max(values) + 1;
|
|
346
|
+
step = max_value / count;
|
|
347
|
+
n = values.shape[0];
|
|
348
|
+
h = bandwidth;
|
|
349
|
+
h_inv = 1 / h;
|
|
350
|
+
kde = np.zeros(count, dtype=np.float32)
|
|
351
|
+
|
|
352
|
+
for i in range(count):
|
|
353
|
+
x = i * step
|
|
354
|
+
total = 0
|
|
355
|
+
for j in range(n):
|
|
356
|
+
val = (x - values[j]) * h_inv;
|
|
357
|
+
total += math.exp(-(val*val/2)) * gaussian_denom_inv; # Gaussian
|
|
358
|
+
kde[i] = total / (n*h);
|
|
359
|
+
|
|
360
|
+
return kde, step
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def calc_default_size_thresh(mask, resolution):
|
|
364
|
+
sizes = compute_cell_sizes(mask)
|
|
365
|
+
mask[mask == MASK_CELL_POSITIVE] = MASK_POSITIVE
|
|
366
|
+
mask[mask == MASK_CELL_NEGATIVE] = MASK_NEGATIVE
|
|
367
|
+
mask[mask == MASK_CELL] = MASK_UNKNOWN
|
|
368
|
+
|
|
369
|
+
if len(sizes) > 0:
|
|
370
|
+
kde, step = create_kde(np.sqrt(sizes), 500)
|
|
371
|
+
idx = 1
|
|
372
|
+
for i in range(1, kde.shape[0]-1):
|
|
373
|
+
if kde[i] < kde[i-1] and kde[i] < kde[i+1]:
|
|
374
|
+
idx = i
|
|
375
|
+
break
|
|
376
|
+
thresh_sqrt = (idx - 1) * step
|
|
377
|
+
|
|
378
|
+
allowed_range_sqrt = (4, 7, 10) # [min, default, max] for default sqrt size thresh at 40x
|
|
379
|
+
if resolution == '20x':
|
|
380
|
+
allowed_range_sqrt = (3, 4, 6)
|
|
381
|
+
elif resolution == '10x':
|
|
382
|
+
allowed_range_sqrt = (2, 2, 3)
|
|
383
|
+
|
|
384
|
+
if thresh_sqrt < allowed_range_sqrt[0]:
|
|
385
|
+
thresh_sqrt = allowed_range_sqrt[0]
|
|
386
|
+
elif thresh_sqrt > allowed_range_sqrt[2]:
|
|
387
|
+
thresh_sqrt = allowed_range_sqrt[1]
|
|
388
|
+
|
|
389
|
+
return round(thresh_sqrt * thresh_sqrt)
|
|
390
|
+
|
|
391
|
+
else:
|
|
392
|
+
return 0
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def calc_default_marker_thresh(marker):
|
|
396
|
+
if marker is not None:
|
|
397
|
+
nonzero = marker[marker != 0]
|
|
398
|
+
marker_range = (round(np.percentile(nonzero, 0.1)), round(np.percentile(nonzero, 99.9))) if nonzero.shape[0] > 0 else (0, 0)
|
|
399
|
+
return round((marker_range[1] - marker_range[0]) * 0.9) + marker_range[0]
|
|
400
|
+
else:
|
|
401
|
+
return 0
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def compute_results(orig, seg, marker, resolution=None, seg_thresh=150, size_thresh='default', marker_thresh='default', size_thresh_upper=None):
|
|
405
|
+
mask = create_posneg_mask(seg, seg_thresh)
|
|
406
|
+
mark_background(mask)
|
|
407
|
+
|
|
408
|
+
if size_thresh == 'default':
|
|
409
|
+
size_thresh = calc_default_size_thresh(mask, resolution)
|
|
410
|
+
if marker_thresh == 'default':
|
|
411
|
+
marker_thresh = calc_default_marker_thresh(marker)
|
|
412
|
+
|
|
413
|
+
counts = compute_cell_classification(mask, marker, size_thresh, marker_thresh, size_thresh_upper=None)
|
|
414
|
+
enlarge_cell_boundaries(mask)
|
|
415
|
+
|
|
416
|
+
scoring = {
|
|
417
|
+
'num_total': counts['num_total'],
|
|
418
|
+
'num_pos': counts['num_pos'],
|
|
419
|
+
'num_neg': counts['num_neg'],
|
|
420
|
+
'percent_pos': round(counts['num_pos'] / counts['num_total'] * 100, 1) if counts['num_pos'] > 0 else 0
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
overlay = np.copy(orig)
|
|
424
|
+
overlay[mask == MASK_BOUNDARY_POSITIVE] = (255, 0, 0)
|
|
425
|
+
overlay[mask == MASK_BOUNDARY_NEGATIVE] = (0, 0, 255)
|
|
426
|
+
|
|
427
|
+
refined = np.zeros_like(seg)
|
|
428
|
+
refined[mask == MASK_CELL_POSITIVE, 0] = 255
|
|
429
|
+
refined[mask == MASK_CELL_NEGATIVE, 2] = 255
|
|
430
|
+
refined[mask == MASK_BOUNDARY_POSITIVE, 1] = 255
|
|
431
|
+
refined[mask == MASK_BOUNDARY_NEGATIVE, 1] = 255
|
|
432
|
+
|
|
433
|
+
return overlay, refined, scoring
|