deepliif 1.1.6__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.
@@ -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')
@@ -118,12 +118,13 @@ class BaseOptions():
118
118
  print(message)
119
119
 
120
120
  # save to the disk
121
- expr_dir = os.path.join(opt.checkpoints_dir, opt.name)
122
- util.mkdirs(expr_dir)
123
- file_name = os.path.join(expr_dir, '{}_opt.txt'.format(opt.phase))
124
- with open(file_name, 'wt') as opt_file:
125
- opt_file.write(message)
126
- opt_file.write('\n')
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."""
@@ -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
- def compute_IHC_scoring(mask_image):
375
- """ Computes the number of cells and the IHC score for the given segmentation mask
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
- Returns:
381
- all_cells_no (integer) -- number of all cells
382
- positive_cells_no (integer) -- number of positive cells
383
- negative_cells_no (integer) -- number of negative cells
384
- IHC_score (integer) -- IHC score (percentage of positive cells to all cells)
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
- label_image_red = skimage.measure.label(mask_image[:, :, 0], background=0)
388
- label_image_blue = skimage.measure.label(mask_image[:, :, 2], background=0)
389
- positive_cells_no = (len(np.unique(label_image_red)) - 1)
390
- negative_cells_no = (len(np.unique(label_image_blue)) - 1)
391
- all_cells_no = positive_cells_no + negative_cells_no
392
- IHC_score = round(positive_cells_no / all_cells_no * 100, 1) if all_cells_no > 0 else 0
393
-
394
- return all_cells_no, positive_cells_no, negative_cells_no, IHC_score
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