scikit-ops 0.1.0.dev0__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.
Files changed (66) hide show
  1. scikit_ops-0.1.0.dev0.dist-info/METADATA +372 -0
  2. scikit_ops-0.1.0.dev0.dist-info/RECORD +66 -0
  3. scikit_ops-0.1.0.dev0.dist-info/WHEEL +4 -0
  4. scikit_ops-0.1.0.dev0.dist-info/entry_points.txt +2 -0
  5. skop/__init__.py +14 -0
  6. skop/envs/cellpose3/pixi.toml +46 -0
  7. skop/envs/cupy/pixi.toml +35 -0
  8. skop/envs/minimal/pixi.toml +25 -0
  9. skop/envs/pytorch/pixi.toml +88 -0
  10. skop/envs/sdeconv/pixi.toml +85 -0
  11. skop/envs/segment-everything/pixi.toml +96 -0
  12. skop/envs/skimage/pixi.toml +28 -0
  13. skop/envs/stardist-tf/pixi.toml +91 -0
  14. skop/envs/unseg-cv/pixi.toml +27 -0
  15. skop/models.py +145 -0
  16. skop/ops/__init__.py +19 -0
  17. skop/ops/_util.py +115 -0
  18. skop/ops/deconvolve/__init__.py +18 -0
  19. skop/ops/deconvolve/_edges.py +92 -0
  20. skop/ops/deconvolve/_pad.py +117 -0
  21. skop/ops/deconvolve/richardson_lucy.py +100 -0
  22. skop/ops/deconvolve/richardson_lucy_cupy.py +116 -0
  23. skop/ops/detect/__init__.py +28 -0
  24. skop/ops/detect/_result.py +45 -0
  25. skop/ops/detect/fastsam.py +103 -0
  26. skop/ops/detect/jdll_yolo.py +115 -0
  27. skop/ops/detect/object_aware_yolo.py +112 -0
  28. skop/ops/detect/yolo.py +441 -0
  29. skop/ops/edges.py +382 -0
  30. skop/ops/generate.py +77 -0
  31. skop/ops/kernels/__init__.py +39 -0
  32. skop/ops/kernels/_fluorophore.py +42 -0
  33. skop/ops/kernels/_recenter.py +60 -0
  34. skop/ops/kernels/gibson_lanni.py +147 -0
  35. skop/ops/kernels/paraxial.py +128 -0
  36. skop/ops/kernels/psf.py +59 -0
  37. skop/ops/labels.py +144 -0
  38. skop/ops/mask/__init__.py +29 -0
  39. skop/ops/mask/_result.py +40 -0
  40. skop/ops/mask/microsam.py +114 -0
  41. skop/ops/mask/mobilesam.py +176 -0
  42. skop/ops/morphology.py +214 -0
  43. skop/ops/normalize.py +85 -0
  44. skop/ops/segment/__init__.py +24 -0
  45. skop/ops/segment/cellpose3.py +245 -0
  46. skop/ops/segment/cellpose4.py +127 -0
  47. skop/ops/segment/instanseg.py +162 -0
  48. skop/ops/segment/stardist2d.py +192 -0
  49. skop/ops/segment/starfun3d.py +104 -0
  50. skop/ops/segment/unseg/__init__.py +138 -0
  51. skop/ops/segment/unseg/_algorithm.py +1618 -0
  52. skop/ops/smooth.py +452 -0
  53. skop/ops/threshold.py +278 -0
  54. skop/ops/toy.py +114 -0
  55. skop/ops/train/__init__.py +81 -0
  56. skop/ops/train/_util.py +152 -0
  57. skop/ops/train/cellpose3.py +189 -0
  58. skop/ops/train/cellpose4.py +164 -0
  59. skop/ops/train/stardist2d.py +439 -0
  60. skop/ops/workflows/__init__.py +43 -0
  61. skop/ops/workflows/deconvolve/__init__.py +7 -0
  62. skop/ops/workflows/deconvolve/with_psf.py +103 -0
  63. skop/ops/workflows/mask/__init__.py +7 -0
  64. skop/ops/workflows/mask/detect_then_mask.py +97 -0
  65. skop/ops/workflows/segment/__init__.py +7 -0
  66. skop/ops/workflows/segment/connect_2d_in_3d.py +145 -0
@@ -0,0 +1,1618 @@
1
+ """
2
+ UNSEG is an algorithm for UNsupervised SEGmentation of nuclei and cells in
3
+ fluorescent images of tissues containing both nucleus and cell membrane markers.
4
+
5
+ @author: Uttam Lab
6
+ """
7
+
8
+ ##### LIGHTLY FORKED VERSION, FOR USE AS AN OP #####
9
+ # Derived from the Appose fork in ctrueden/unseg-fiji. Changes from that fork:
10
+ # * The trailing run block is gone; ops/unseg/__init__.py drives it instead.
11
+ # * report() forwards to opspec.runtime.progress rather than to a task callback that
12
+ # had to be installed by the caller.
13
+ # * matplotlib is gone, along with the plotting it backed. A worker process
14
+ # has no display; the original already suppressed plots under Appose.
15
+ #
16
+ # The algorithm itself is untouched, and deliberately excluded from ruff so
17
+ # that it stays diffable against upstream.
18
+ #
19
+ # NB: this module's imports are heavy on purpose -- it is imported from inside
20
+ # the op function body, never at op module scope.
21
+
22
+ import time
23
+
24
+ import cv2
25
+ import numpy as np
26
+ from scipy import ndimage
27
+ from skimage.filters import gaussian, rank, sobel, threshold_multiotsu
28
+ from skimage.measure import label, regionprops
29
+ from skimage.morphology import dilation, disk, erosion, thin
30
+ from skimage.segmentation import find_boundaries, mark_boundaries, watershed
31
+ from skimage.util import img_as_ubyte
32
+ from sklearn.cluster import KMeans
33
+
34
+ from opspec.runtime import progress
35
+
36
+
37
+ def report(*args):
38
+ """Relay the algorithm's running commentary to whoever is listening."""
39
+ progress(" ".join(str(arg) for arg in args))
40
+
41
+
42
+ def plot_img(*args, **kwargs):
43
+ """Plotting is not available in a worker process."""
44
+
45
+
46
+ ###############################################################################
47
+ ### AUXILIARY FUNCTIONS
48
+ ###############################################################################
49
+
50
+ def scale_img(img, norm='0:1'):
51
+ img = img.astype('float64')
52
+ i_min = np.min(img)
53
+ i_max = np.max(img)
54
+ if i_max-i_min > 0:
55
+ if norm == '-1:1':
56
+ img = 2*(img-i_min)/(i_max-i_min)-1
57
+ else:
58
+ img = (img-i_min)/(i_max-i_min)
59
+ return img
60
+
61
+ def compute_conv(a):
62
+ h, w = a.shape
63
+ conv = 0
64
+
65
+ c = np.roll(a, (1,0), axis=(1,0))
66
+ c[:,0] = 0
67
+ conv = conv + c
68
+
69
+ c = np.roll(a, (1,-1), axis=(1,0))
70
+ c[:,0] = 0
71
+ c[h-1,:] = 0
72
+ conv = conv + c
73
+
74
+ c = np.roll(a, (0,-1), axis=(1,0))
75
+ c[h-1,:] = 0
76
+ conv = conv + c
77
+
78
+ c = np.roll(a, (-1,-1), axis=(1,0))
79
+ c[:,w-1] = 0
80
+ c[h-1,:] = 0
81
+ conv = conv + c
82
+
83
+ c = np.roll(a, (-1,0), axis=(1,0))
84
+ c[:,w-1] = 0
85
+ conv = conv + c
86
+
87
+ c = np.roll(a, (-1,1), axis=(1,0))
88
+ c[:,w-1] = 0
89
+ c[0,:] = 0
90
+ conv = conv + c
91
+
92
+ c = np.roll(a, (0,1), axis=(1,0))
93
+ c[0,:] = 0
94
+ conv = conv + c
95
+
96
+ c = np.roll(a, (1,1), axis=(1,0))
97
+ c[:,0] = 0
98
+ c[0,:] = 0
99
+ conv = conv + c
100
+
101
+ conv = conv + a
102
+ return conv
103
+
104
+ def gradient_adaptive_smoothing(img, k=1):
105
+ """Gradient Adaptive Smoothing"""
106
+ gas = scale_img(img)
107
+ u = sobel(gas)
108
+ u = np.exp(-(u*u)/(2*k**2))
109
+ gas = u*gas
110
+ gas = compute_conv(gas)
111
+ u = compute_conv(u)
112
+ gas = gas / u
113
+ return gas
114
+
115
+ def affv(img, k=5, t=0.5):
116
+ """Local Mean Suppression Filter"""
117
+ h = img.shape[0]
118
+ w = img.shape[1]
119
+ ker = np.ones((2*k+1,), dtype='int32')
120
+ row = np.ones((1,w), dtype='int32')
121
+ img_avr = np.zeros((h,w), dtype='float64')
122
+ norm = np.zeros((h,w), dtype='int32')
123
+ for i in range(h):
124
+ img_avr[i,:] = np.convolve(img[i,:], ker, mode='same')
125
+ norm[i,:] = np.convolve(row[0,:], ker, mode='same')
126
+ for j in range(w):
127
+ img_avr[:,j] = np.convolve(img_avr[:,j], ker, mode='same')
128
+ norm[:,j] = np.convolve(norm[:,j], ker, mode='same')
129
+ img_avr = np.divide(img_avr, norm)
130
+ norm = img < t*img_avr
131
+ img_avr = img.copy()
132
+ img_avr[norm] = 0
133
+ return img_avr
134
+
135
+ def distribution_function(img, x, f, dn, mt='fg'):
136
+ """Distribution of Probabilities"""
137
+ if mt == 'fg':
138
+ vs = 0
139
+ n = dn+1
140
+ elif mt == 'bg':
141
+ vs = 1
142
+ n = np.max(x)+1
143
+ pb = np.zeros((img.shape[0],img.shape[1]), dtype='float64')
144
+ for i in range(n):
145
+ m = img == i
146
+ inx = np.argwhere(x == i)
147
+ if len(inx) == 1:
148
+ inx = inx[0,0]
149
+ pb[m] = f[inx]
150
+ elif len(inx) == 0:
151
+ inx_m = np.ma.masked_less(x, i, copy=True)
152
+ if np.ma.count_masked(inx_m) == 0:
153
+ inx = np.nan
154
+ val = vs
155
+ else:
156
+ inx = np.ma.argmin(inx_m)
157
+ inx = inx - 1
158
+ val = f[inx]
159
+ pb[m] = val
160
+ return pb
161
+
162
+ def find_contour(cc, num='one'):
163
+ """Outer Contour of Connected Component"""
164
+ contours, _ = cv2.findContours(cc, cv2.RETR_TREE, cv2.CHAIN_APPROX_NONE)
165
+ num_contours = len(contours)
166
+ if num_contours == 1:
167
+ ind = 0
168
+ else:
169
+ nc, ind = 0, 0
170
+ for i in range(num_contours):
171
+ if len(contours[i][:,0,0]) > nc:
172
+ nc, ind = len(contours[i][:,0,0]), i
173
+ max_contour = contours[ind]
174
+ if num == 'one':
175
+ return max_contour
176
+ else:
177
+ contours = list(contours)
178
+ contours.pop(ind)
179
+ return max_contour, contours
180
+
181
+ def remove_selfintersection(cc):
182
+ cc = erosion(cc, disk(2))
183
+ cc = dilation(cc, disk(2))
184
+ cc = ndimage.binary_fill_holes(cc)
185
+ return cc
186
+
187
+ def test_selfintersection(cc, contour='no'):
188
+ cnt = find_contour((cc).astype('uint8'))
189
+ hull = cv2.convexHull(cnt, returnPoints=False)
190
+ try:
191
+ defects = cv2.convexityDefects(cnt, hull)
192
+ test_res = True
193
+ except:
194
+ defects = 0
195
+ test_res = False
196
+ if contour == 'no':
197
+ return test_res, defects
198
+ else:
199
+ cnt = cnt[:,0,:]
200
+ return test_res, defects, cnt
201
+
202
+ def add_layer(cc, b=1):
203
+ h, w = cc.shape
204
+ ccb = np.zeros((h+2*b, w+2*b), dtype='bool')
205
+ ccb[b:h+b, b:w+b] = cc
206
+ return ccb
207
+
208
+ def remove_layer(ccb, b=1):
209
+ h, w = ccb.shape
210
+ cc = np.zeros((h-2*b, w-2*b), dtype='float64')
211
+ cc = ccb[b:h-b, b:w-b]
212
+ return cc
213
+
214
+ def get_component_indices(arr):
215
+ n = np.unique(arr)
216
+ n = list(n)
217
+ if 0 in n:
218
+ n.remove(0)
219
+ return n
220
+
221
+ def dynamic_ws(gbm0, lbm0):
222
+ """Perturbation Analysis of Seed Points for Watershed Segmentation"""
223
+ m_thr = 0
224
+ f_thr = 25
225
+ a_g = np.count_nonzero(gbm0)
226
+ a_l = np.count_nonzero(lbm0)
227
+ if a_g == 0 or a_l == 0:
228
+ return np.zeros((gbm0.shape), dtype='int32'), 100
229
+ da = 100*(a_g-a_l)/a_g
230
+ dt0 = ndimage.distance_transform_edt(add_layer(lbm0))
231
+ dt0 = remove_layer(dt0)
232
+ dt0_avr = np.mean(dt0[dt0>0])
233
+ dt0_avr_f = np.floor(dt0_avr)
234
+ dt0_mask = dt0 > dt0_avr
235
+ prop0 = label(dt0_mask, background=False, connectivity=2)
236
+ prop0 = regionprops(prop0)
237
+ n_sp = 0
238
+ for i in range(len(prop0)):
239
+ if prop0[i].area > m_thr:
240
+ n_sp = n_sp + 1
241
+ sp = np.zeros((n_sp,2), dtype='int64')
242
+ j = 0
243
+ for i in range(len(prop0)):
244
+ if prop0[i].area > m_thr:
245
+ pp0 = prop0[i]
246
+ hh1, ww1, hh2, ww2 = pp0.bbox
247
+ temp0 = np.zeros((hh2-hh1,ww2-ww1), dtype='float64')
248
+ temp0[pp0.image] = dt0[hh1:hh2, ww1:ww2][pp0.image]
249
+ yy, xx = np.unravel_index(np.argmax(temp0, axis=None), temp0.shape)
250
+ yy, xx = yy + hh1, xx + ww1
251
+ sp[j,:] = (yy, xx)
252
+ j = j + 1
253
+ shifts = ((0,0), (-1,0), (1,0), (0,-1), (0,1))
254
+ areas = np.zeros((n_sp, 5), dtype='int64')
255
+ for j in range(5):
256
+ dx = int(shifts[j][0] * dt0_avr_f)
257
+ dy = int(shifts[j][1] * dt0_avr_f)
258
+ markers = np.zeros(dt0.shape, dtype='int32')
259
+ for i in range(sp.shape[0]):
260
+ markers[sp[i,0]+dy, sp[i,1]+dx] = i + 1
261
+ seg0 = watershed(-dt0, markers, mask=gbm0)
262
+ for i in range(n_sp):
263
+ areas[i,j] = np.count_nonzero(seg0 == i+1)
264
+ if j == 0:
265
+ seg_0 = seg0.copy()
266
+ areas_n = areas[:,1:].copy()
267
+ for i in range(areas.shape[0]):
268
+ areas_n[i,:] = areas_n[i,:] - areas[i,0]
269
+ dyn_channel_namesange = np.zeros((areas.shape[0],5), dtype='int32')
270
+ dyn_channel_namesange[:,0] = np.count_nonzero(areas_n > 0, axis=1)
271
+ dyn_channel_namesange[:,1] = np.count_nonzero(areas_n == 0, axis=1)
272
+ dyn_channel_namesange[:,2] = np.count_nonzero(areas_n < 0, axis=1)
273
+ dyn_channel_namesange[:,3] = np.count_nonzero(areas[:,1:] < f_thr , axis=1)
274
+ dyn_channel_namesange[:,4] = dyn_channel_namesange[:,0] + dyn_channel_namesange[:,1] - dyn_channel_namesange[:,2]
275
+ sp_fxd = dyn_channel_namesange[:,1] == 4
276
+ sp_uns = np.logical_and(dyn_channel_namesange[:,3]>0, dyn_channel_namesange[:,4]<=0)
277
+ sp_stb = np.logical_not(np.logical_or(sp_fxd, sp_uns))
278
+ seg_final = np.zeros((gbm0.shape), dtype='int32')
279
+ n_cc = 0
280
+ mask = np.zeros(gbm0.shape, dtype='bool')
281
+ for i in range(sp_fxd.shape[0]):
282
+ if sp_fxd[i]:
283
+ seg_final[seg_0 == i+1] = n_cc+1
284
+ n_cc = n_cc + 1
285
+ else:
286
+ mask[seg_0==i+1] = True
287
+ markers = np.zeros(gbm0.shape, dtype='int32')
288
+ j = n_cc + 1
289
+ for i in np.where(sp_stb == True)[0]:
290
+ markers[sp[i,0], sp[i,1]] = j
291
+ j = j + 1
292
+ prop_mask = label(mask, background=False, connectivity=2)
293
+ prop_mask = regionprops(prop_mask)
294
+ sp_add = []
295
+ for p in prop_mask:
296
+ h1, w1, h2, w2 = p.bbox
297
+ if np.count_nonzero(markers[h1:h2,w1:w2][p.image] != 0) == 0:
298
+ temp0 = np.zeros((h2-h1,w2-w1), dtype='float64')
299
+ temp0[p.image] = dt0[h1:h2, w1:w2][p.image]
300
+ yy, xx = np.unravel_index(np.argmax(temp0, axis=None), temp0.shape)
301
+ yy, xx = yy + h1, xx + w1
302
+ sp_add.append((yy,xx))
303
+ markers[yy, xx] = j
304
+ j = j + 1
305
+ sp_add = np.array(sp_add, dtype='int64')
306
+ sp_add = sp_add.reshape(-1,2)
307
+ seg0 = watershed(-dt0, markers, mask=mask)
308
+ inds = get_component_indices(seg0)
309
+ for i in inds:
310
+ seg_final[seg0 == i] = i
311
+ return seg_final, da
312
+
313
+ def boundary_cuts(seg, mask):
314
+ bnds = find_boundaries(seg, connectivity=1, mode='outer', background=0)
315
+ bnds = np.logical_and(mask, bnds)
316
+ seg0 = seg*np.logical_not(bnds).astype('int32')
317
+ return seg0
318
+
319
+ def add_boundary(cc, b):
320
+ h, w = cc.shape
321
+ ccb = np.zeros((h+2*b, w+2*b), dtype='bool')
322
+ ccb[b:h+b, b:w+b] = cc
323
+ return ccb
324
+
325
+ def remove_boundary(ccb, b):
326
+ h, w = ccb.shape
327
+ cc = np.zeros((h-2*b, w-2*b), dtype='bool')
328
+ cc = ccb[b:h-b, b:w-b]
329
+ return cc
330
+
331
+ def adaptive_morphological_filter(cc, d):
332
+ d = int(np.round(d))
333
+ ccb = add_boundary(cc, d)
334
+ ccb = dilation(ccb, disk(d))
335
+ ccb = erosion(ccb, disk(d))
336
+ ccb = remove_boundary(ccb, d)
337
+ return ccb
338
+
339
+ def morphological_smoothing(cc, d=5):
340
+ cc = add_boundary(cc, b=d+2)
341
+ cc = dilation(cc, disk(d))
342
+ cc = erosion(cc, disk(d))
343
+ cc = erosion(cc, disk(d))
344
+ cc = dilation(cc, disk(d))
345
+ cc = remove_boundary(cc, b=d+2)
346
+ return cc
347
+
348
+ def gradient_smoothing(d, n, l):
349
+ v = np.ones((n-1), dtype='float64')
350
+ m = np.diag(v, k=1) - np.diag(v, k=-1)
351
+ m[0,n-1] = -1
352
+ m[n-1,0] = 1
353
+ d = np.matmul(np.linalg.inv(np.identity(n) + l*np.matmul(m.T, m)), d)
354
+ return d
355
+
356
+ def convexity_test(cc, cp_thr=5):
357
+ test_result, defects, cnt = test_selfintersection(cc, contour='yes')
358
+ if test_result:
359
+ if defects is None:
360
+ cp = np.array([], dtype='float64').reshape(-1,4)
361
+ d_max = 0
362
+ else:
363
+ cp = defects[defects[:,0,3]>cp_thr*256,0,:]
364
+ cp = cp.reshape(-1,4)
365
+ d_max = int(np.round(np.max(defects[:,0,3])/256))
366
+ else:
367
+ cp = np.array([], dtype='float64').reshape(-1,4)
368
+ d_max = np.nan
369
+ return test_result, cp, d_max, cnt
370
+
371
+ def virtual_ws_cuts(img, cc, bbox, mask_local_ternary, cnt, cp, dist_transform='DT', large=False):
372
+ """Watershed with Virtual Cuts"""
373
+ h1, w1, h2, w2 = bbox
374
+ lbm0 = np.logical_and(cc, np.logical_not(mask_local_ternary[h1:h2,w1:w2,0]))
375
+ a_g = np.count_nonzero(cc)
376
+ a_l = np.count_nonzero(lbm0)
377
+ da = 100*(a_g-a_l)/a_g
378
+ if da > 0.5:
379
+ cc_wo_cy5 = np.logical_and(np.logical_not(mask_local_ternary[h1:h2,w1:w2,0]), ndimage.binary_fill_holes(cc))
380
+ else:
381
+ cc_wo_cy5 = np.logical_and(np.logical_not(mask_local_ternary[h1:h2,w1:w2,0]), cc)
382
+ dt0 = ndimage.distance_transform_edt(add_layer(cc_wo_cy5))
383
+ dt0 = remove_layer(dt0)
384
+ if dist_transform == 'GDT':
385
+ dt0 = scale_img(dt0)
386
+ dt0 = dt0 * np.exp(1*(1-img[h1:h2,w1:w2,0]))
387
+
388
+ def vc_seg(ii=0):
389
+ x1, x2 = cnt[cp[ii,0],0], cnt[cp[ii,1],0]
390
+ y1, y2 = cnt[cp[ii,0],1], cnt[cp[ii,1],1]
391
+ x0, y0 = cnt[cp[ii,2],0], cnt[cp[ii,2],1]
392
+ dx = np.abs(x1-x2)
393
+ dy = np.abs(y1-y2)
394
+ if dx < dy:
395
+ x = np.linspace(0, w2-w1-1, w2-w1).astype('int32')
396
+ y = y0 - (x2-x1)*(x-x0)/(y2-y1)
397
+ y = np.round(y)
398
+ y = y.astype('int32')
399
+ t = np.logical_and(y>=0, y<h2-h1)
400
+ else:
401
+ y = np.linspace(0, h2-h1-1, h2-h1).astype('int32')
402
+ x = x0 - (y2-y1)*(y-y0)/(x2-x1)
403
+ x = np.round(x)
404
+ x = x.astype('int32')
405
+ t = np.logical_and(x>=0, x<w2-w1)
406
+ x = x[t]
407
+ y = y[t]
408
+ path = np.ones((h2-h1,w2-w1), dtype='uint8')
409
+ xy = np.array([x, y]).T
410
+ xy = xy.reshape((-1,1,2))
411
+ path = cv2.polylines(path, [xy], False, (0,0,0), 2)
412
+ path = path.astype('bool')
413
+ path = np.logical_and(cc, path)
414
+ cc_cut_reg = label(path, background=False, connectivity=2)
415
+ cc_cut_reg = regionprops(cc_cut_reg)
416
+ if len(cc_cut_reg) < 2:
417
+ seg0 = cc.copy()
418
+ seg0 = seg0.astype('int32')
419
+ else:
420
+ sp_0 = np.zeros((len(cc_cut_reg),3), dtype='float64')
421
+ j = 0
422
+ for p in cc_cut_reg:
423
+ hh1, ww1, hh2, ww2 = p.bbox
424
+ temp0 = np.zeros((hh2-hh1,ww2-ww1), dtype='float64')
425
+ temp0[p.image] = dt0[hh1:hh2, ww1:ww2][p.image]
426
+ yy, xx = np.unravel_index(np.argmax(temp0, axis=None), temp0.shape)
427
+ yy, xx = yy + hh1, xx + ww1
428
+ sp_0[j,:] = (yy, xx, dt0[yy,xx])
429
+ j = j + 1
430
+ if sp_0.shape[0] > 2:
431
+ sorted_index = np.argsort(sp_0[:,2])
432
+ sorted_index = sorted_index[::-1]
433
+ sp_0 = sp_0[sorted_index]
434
+ sp_0 = sp_0[0:2,:]
435
+ markers = np.zeros(dt0.shape, dtype='int32')
436
+ for i in range(sp_0.shape[0]):
437
+ markers[int(sp_0[i,0]), int(sp_0[i,1])] = i + 1
438
+ seg0 = watershed(-dt0, markers, mask=cc)
439
+ return seg0
440
+
441
+ def min_area(seg):
442
+ prop = regionprops(seg)
443
+ n_ws = len(prop)
444
+ a = np.zeros((n_ws,1), dtype='int32')
445
+ for i in range(n_ws):
446
+ a[i,0] = prop[i].filled_area
447
+ a_min = np.min(a)
448
+ if n_ws == 1:
449
+ a_min = 0
450
+ return a_min
451
+
452
+ if large:
453
+ seg0 = vc_seg(ii=0)
454
+ a_min_0 = min_area(seg0)
455
+ seg1 = vc_seg(ii=1)
456
+ a_min_1 = min_area(seg1)
457
+ if a_min_0 >= a_min_1:
458
+ return seg0
459
+ else:
460
+ return seg1
461
+ else:
462
+ seg0 = vc_seg(ii=0)
463
+ return seg0
464
+
465
+ def mask_statistics(mask):
466
+ properties = regionprops(mask)
467
+ a = []
468
+ c = []
469
+ s = []
470
+ for p in properties:
471
+ a.append(p.area)
472
+ c.append(p.solidity)
473
+ s.append(((p.perimeter)**2)/(4*np.pi*p.area))
474
+ a = np.array(a)
475
+ c = np.array(c)
476
+ s = np.array(s)
477
+ mask_status = {'n_total':a.shape[0], 'convexity_avr':np.mean(c), 'shape_avr':np.mean(s),
478
+ 'area_avr':np.mean(a), 'area_std':np.std(a), 'area_min':np.min(a), 'area_max':np.max(a)}
479
+ return mask_status
480
+
481
+ ###############################################################################
482
+ ### PROCESSING FUNCTIONS
483
+ ###############################################################################
484
+ def normalize_img(intensity):
485
+ """Image with Scaled Channels (0 nuclei channel, 1 cell membrane channel)"""
486
+ h = intensity.shape[0]
487
+ w = intensity.shape[1]
488
+ c = intensity.shape[2]
489
+ img = np.zeros((h,w,c), dtype='float64')
490
+ for i in range(c):
491
+ img[:,:,i] = scale_img(intensity[:,:,i])
492
+ return img
493
+
494
+ def intensity2rgb(img, return_rgb=False):
495
+ """Create RGB Image Using Nuclei (0) and Cell (1) channel_namesannels"""
496
+ h = img.shape[0]
497
+ w = img.shape[1]
498
+ img_rgb = np.zeros((h,w,3), dtype='float64')
499
+ img_rgb[:,:,0] = img[:,:,1]
500
+ img_rgb[:,:,2] = img[:,:,0]
501
+ if return_rgb:
502
+ return img_rgb
503
+ else:
504
+ plot_img(img_rgb, tlt='Two-channel Image')
505
+
506
+ def calc_empirical_probabilities(img, sigma=3):
507
+ """Nuclei and Cell Empirical Foreground Probabilities"""
508
+ n_otsu = 3
509
+ dn = 255
510
+ n_channel_names = img.shape[2]
511
+ global_otsu_mask = np.zeros((img.shape), dtype='bool')
512
+ thr = np.zeros((n_channel_names,n_otsu-1), dtype='float64')
513
+ for i in range(n_channel_names):
514
+ img_temp = scale_img(gaussian(img[:,:,i], sigma=sigma))
515
+ thr[i,:] = threshold_multiotsu(img_temp, classes=n_otsu)
516
+ global_otsu_mask[:,:,i] = img_temp > thr[i,0]
517
+ p_em = np.zeros((img.shape), dtype='float64')
518
+ for i in range(n_channel_names):
519
+ img_temp = np.floor(dn*img[:,:,i]).astype('uint8')
520
+ if i == 0:
521
+ f_nuc_fg = img_temp[global_otsu_mask[:,:,i]]
522
+ x_nuc_fg, s = np.unique(f_nuc_fg, return_index=False, return_inverse=False, return_counts=True, axis=None)
523
+ n = f_nuc_fg.shape[0]
524
+ f_nuc_fg = np.cumsum(s)/n
525
+ p_em[:,:,i] = distribution_function(img_temp, x_nuc_fg, f_nuc_fg, dn, mt='fg')
526
+ elif i == 1:
527
+ f_cel_fg = img_temp[global_otsu_mask[:,:,i]]
528
+ x_cel_fg, s = np.unique(f_cel_fg, return_index=False, return_inverse=False, return_counts=True, axis=None)
529
+ n = f_cel_fg.shape[0]
530
+ f_cel_fg = np.cumsum(s)/n
531
+ p_em[:,:,i] = distribution_function(img_temp, x_cel_fg, f_cel_fg, dn, mt='fg')
532
+ return p_em
533
+
534
+ def calc_global_binary_mask(img, nk=[5,10,20,40], t0=0.5):
535
+ """Foreground Masks"""
536
+ n = img.shape[2]
537
+ mask_global = np.ones((img.shape), dtype='bool')
538
+ for i in range(n):
539
+ for k in nk:
540
+ likelihood = affv(img[:,:,i], k=k, t=t0)
541
+ mask_global[:,:,i] = np.logical_and(mask_global[:,:,i], likelihood != 0)
542
+ return mask_global
543
+
544
+ def calc_local_binary_mask(img, r0=5, k0=1, smoothing=True):
545
+ """Local Masks"""
546
+ n = img.shape[2]
547
+ temp_img = np.zeros((img.shape), dtype='uint8')
548
+ for i in range(n):
549
+ if smoothing:
550
+ temp_img[:,:,i] = img_as_ubyte(scale_img(gradient_adaptive_smoothing(img[:,:,i], k=k0)))
551
+ else:
552
+ temp_img[:,:,i] = img_as_ubyte(img[:,:,i])
553
+ local_otsu_thr = np.zeros((img.shape), dtype='uint8')
554
+ for i in range(n):
555
+ local_otsu_thr[:,:,i] = rank.otsu(temp_img[:,:,i], disk(r0)) # for old skimage versions use: selem=disk(r0)
556
+ local_otsu_mask = np.zeros((img.shape), dtype='bool')
557
+ for i in range(n):
558
+ local_otsu_mask[:,:,i] = temp_img[:,:,i] > local_otsu_thr[:,:,i]
559
+ return local_otsu_mask
560
+
561
+ def remove_background(mask_local, mask_global, p_em, pct):
562
+ """Use Empirical Propability Threshold to Remove Background in Foreground and Local Masks (Clarification of Masks)"""
563
+ c = mask_global.shape[2]
564
+ for i in range(c):
565
+ if pct[i] == None:
566
+ thr_prb = threshold_multiotsu(p_em[:,:,i], classes=3)
567
+ thr_prb = thr_prb[0]
568
+ else:
569
+ thr_prb = pct[i]
570
+ temp_mask = p_em[:,:,i] > thr_prb
571
+ report('\tpropability threshold (channel {0}) = {1:.03f}'.format(i, thr_prb))
572
+ mask_global[:,:,i] = np.logical_and(mask_global[:,:,i], temp_mask)
573
+ mask_local[:,:,i] = np.logical_and(mask_local[:,:,i], mask_global[:,:,i])
574
+ return mask_local, mask_global
575
+
576
+ def calc_likelihood(img, mask_local, mask_global):
577
+ """Likelihood Function (not vectorized version)"""
578
+ (h, w, c) = img.shape
579
+ i_min = np.zeros((c,1), dtype='float64')
580
+ for i in range(c):
581
+ i_min[i,0] = np.min(img[mask_local[:,:,i],i])
582
+ report('\tmin intensity (channel {0}) = {1:.4f}'.format(i, i_min[i,0]))
583
+ t_t = np.zeros((c,1), dtype='float64')
584
+ for i in range(c):
585
+ t_t[i,0] = threshold_multiotsu(img[:,:,0], classes=2)[0]
586
+ #t_t[i,0] = threshold_multiotsu(img[:,:,i], classes=2)[0]
587
+ img_likelihood = np.zeros((img.shape[0], img.shape[1]), dtype='float64')
588
+ for i in range(h):
589
+ for j in range(w):
590
+ if img[i,j,0] > i_min[0,0] or img[i,j,1] > i_min[1,0]:
591
+ i2 = t_t[1]*img[i,j,0]
592
+ i1 = t_t[0]*img[i,j,1]
593
+ img_likelihood[i,j] = -(i2-i1)/(i2+i1)
594
+ m1 = img_likelihood < 0
595
+ m2 = np.logical_and(mask_global[:,:,0], m1)
596
+ m = m1 != m2
597
+ m1 = img_likelihood > 0
598
+ m2 = np.logical_and(mask_global[:,:,1], m1)
599
+ img_likelihood[m] = 0
600
+ m = m1 != m2
601
+ img_likelihood[m] = 0
602
+ return img_likelihood
603
+
604
+ def calc_aposteriori_probabilities(p_em, mask_local, likelihood):
605
+ """Local & A Posteriori Probabilities to Compute Bayes Masks"""
606
+ lt = 0
607
+ p_local = np.zeros((p_em.shape), dtype='float64')
608
+ p_apriori = np.zeros((p_em.shape), dtype='float64')
609
+ p_global = np.zeros((p_em.shape), dtype='float64')
610
+ for i in range(p_em.shape[2]):
611
+ p_local[mask_local[:,:,i],i] = p_em[mask_local[:,:,i],i]
612
+ p_apriori[:,:,i] = p_em[:,:,i]
613
+ p_apriori[mask_local[:,:,i],i] = 1
614
+ if i == 0:
615
+ m = likelihood < -lt
616
+ elif i == 1:
617
+ m = likelihood > lt
618
+ p_global[m,i] = p_apriori[m,i] + np.abs(likelihood[m]) - p_apriori[m,i] * np.abs(likelihood[m])
619
+ return p_local, p_global
620
+
621
+ def calc_ternary_mask(img, prob, met='Argmax'):
622
+ """Calculation of Ternary (Bayes-like) Mask Using K-Means or Argmax method"""
623
+ mask_ternary = np.zeros((img.shape[0],img.shape[1],3), dtype='uint8')
624
+ if met == 'Kmeans':
625
+ n_clusters = 3
626
+ x = np.zeros((img.shape[0]*img.shape[1],2), dtype='float64')
627
+ pa = np.sum(prob, axis=2)
628
+ m = pa > 0
629
+ x[m.reshape(-1,),0] = (prob[m,0]/pa[m]).reshape(-1,)
630
+ x[m.reshape(-1,),1] = (prob[m,1]/pa[m]).reshape(-1,)
631
+ kmeans = KMeans(n_clusters=n_clusters, n_init=10).fit(x)
632
+ mask_kmeans = kmeans.labels_
633
+ mask_kmeans = mask_kmeans.reshape(img.shape[0], img.shape[1])
634
+ i_n = np.zeros((n_clusters,), dtype='float64')
635
+ for i in range(n_clusters):
636
+ i_n[i] = np.mean(img[mask_kmeans==i,0])
637
+ i_n = np.argmax(i_n)
638
+ i_c = np.zeros((n_clusters,), dtype='float64')
639
+ for i in range(n_clusters):
640
+ i_c[i] = np.mean(img[mask_kmeans==i,1])
641
+ i_c = np.argmax(i_c)
642
+ mask_ternary[mask_kmeans==i_n,2] = 255
643
+ mask_ternary[mask_kmeans==i_c,0] = 255
644
+ elif met == 'Argmax':
645
+ bg = np.sum(prob, axis=2)
646
+ bg = bg == 0
647
+ nc = np.argmax(prob, axis=2)
648
+ mask_ternary[nc==0,2] = 255
649
+ mask_ternary[nc==1,0] = 255
650
+ mask_ternary[bg,:] = 0
651
+ return mask_ternary
652
+
653
+ def convexity_analysis(mask_ternary, area_thr=20, cp_thr=5):
654
+ """Convert Global Ternary Mask into Initial Nuclei Segmentation, List of Nuclei Clusters and List of Small Objects"""
655
+ props = label(mask_ternary[:,:,2] == 255, background=False, connectivity=2)
656
+ props = regionprops(props)
657
+ nuc_seg = np.zeros((mask_ternary.shape[0],mask_ternary.shape[1]), dtype='int32')
658
+ nuc_seg_features = []
659
+ nuc_clust = []
660
+ nuc_clust_features = [[],[],[]]
661
+ small_obj = []
662
+ clusters_self = []
663
+ mask_info = {'removed':0, 'self-intersection':0, 'rectangle':0, 'convex':0, 'cluster':0}
664
+ nuclei_clusters_info = {'nuclei':{}, 'clusters':{}}
665
+ i = 1
666
+ for p in props:
667
+ h1, w1, h2, w2 = p.bbox
668
+ if p.filled_area < area_thr:
669
+ cc_type = 'removed'
670
+ else:
671
+ cnt = find_contour((p.filled_image).astype('uint8'))
672
+ hull = cv2.convexHull(cnt, returnPoints=False)
673
+ try:
674
+ defects = cv2.convexityDefects(cnt, hull)
675
+ if defects is None:
676
+ cc_type = 'rectangle'
677
+ nuc_seg[h1:h2, w1:w2][p.filled_image] = i
678
+ i = i + 1
679
+ nuc_seg_features.append([p.filled_area])
680
+ else:
681
+ cp = defects[defects[:,0,3]>cp_thr*256,0,:]
682
+ n_cp = cp.shape[0]
683
+ if n_cp == 0:
684
+ cc_type = 'convex'
685
+ nuc_seg[h1:h2, w1:w2][p.filled_image] = i
686
+ i = i + 1
687
+ nuc_seg_features.append([p.filled_area])
688
+ else:
689
+ cc_type = 'cluster'
690
+ nuc_clust.append((p.image, p.bbox))
691
+ nuc_clust_features[0].append(n_cp)
692
+ nuc_clust_features[1].append(cp[:,3]/256)
693
+ nuc_clust_features[2].append(p.area)
694
+ except:
695
+ cc_type = 'self-intersection'
696
+ clusters_self.append((p.filled_image, p.bbox))
697
+ mask_info[cc_type] = mask_info[cc_type] + 1
698
+ while len(clusters_self) > 0:
699
+ cl = clusters_self[0]
700
+ cc = cl[0]
701
+ h1, w1, h2, w2 = cl[1]
702
+ clusters_self.remove(cl)
703
+ cc = remove_selfintersection(cc)
704
+ props = label(cc, background=False, connectivity=2)
705
+ props = regionprops(props)
706
+ if len(props) > 0:
707
+ for p in props:
708
+ hh1, ww1, hh2, ww2 = p.bbox
709
+ test_res, defects = test_selfintersection(p.image)
710
+ if p.filled_area < area_thr:
711
+ cc_type = 'removed'
712
+ else:
713
+ dh1 = h1 + hh1
714
+ dh2 = dh1 - hh1 + hh2
715
+ dw1 = w1 + ww1
716
+ dw2 = dw1 - ww1 + ww2
717
+ if test_res:
718
+ if defects is None:
719
+ cc_type = 'rectangle'
720
+ nuc_seg[dh1:dh2, dw1:dw2][p.filled_image] = i
721
+ i = i + 1
722
+ nuc_seg_features.append([p.filled_area])
723
+ else:
724
+ cp = defects[defects[:,0,3]>cp_thr*256,0,:]
725
+ n_cp = cp.shape[0]
726
+ if n_cp == 0:
727
+ cc_type = 'convex'
728
+ nuc_seg[dh1:dh2, dw1:dw2][p.filled_image] = i
729
+ i = i + 1
730
+ nuc_seg_features.append([p.filled_area])
731
+ else:
732
+ cc_type = 'cluster'
733
+ nuc_clust.append((p.image, (dh1, dw1, dh2, dw2)))
734
+ nuc_clust_features[0].append(n_cp)
735
+ nuc_clust_features[1].append(cp[:,3]/256)
736
+ nuc_clust_features[2].append(p.area)
737
+ else:
738
+ cc_type = 'self-intersection'
739
+ clusters_self.append((p.filled_image, (dh1, dw1, dh2, dw2)))
740
+ mask_info[cc_type] = mask_info[cc_type] + 1
741
+ nuc_seg_features = np.array(nuc_seg_features)
742
+ n_avr = np.mean(nuc_seg_features[:,0])
743
+ n_std = np.std(nuc_seg_features[:,0])
744
+ n_max = np.max(nuc_seg_features[:,0])
745
+ n_min = np.min(nuc_seg_features[:,0])
746
+ n_area_thr = int(np.min([n_max-n_std, n_avr/4, n_min+n_std]))
747
+
748
+ props = regionprops(nuc_seg)
749
+ nuc_seg = np.zeros((mask_ternary.shape[0],mask_ternary.shape[1]), dtype='int32')
750
+ nuc_seg_features = []
751
+ i = 1
752
+ for p in props:
753
+ h1, w1, h2, w2 = p.bbox
754
+ if p.area >= n_area_thr:
755
+ ccf = adaptive_morphological_filter(p.filled_image, cp_thr-1)
756
+ nuc_seg[h1:h2,w1:w2][ccf] = i
757
+ nuc_seg_features.append(p.area)
758
+ i = i + 1
759
+ else:
760
+ ccf = adaptive_morphological_filter(p.filled_image, cp_thr-1)
761
+ small_obj.append((ccf, (h1, w1, h2, w2)))
762
+ mask_info['NS'] = np.max(nuc_seg)
763
+ mask_info['SO'] = len(small_obj)
764
+ nuc_seg_features = np.array(nuc_seg_features)
765
+ a_avr = np.array(nuc_clust_features[2])
766
+ a_avr = np.mean(a_avr)
767
+ a_avr = int(np.floor(a_avr))
768
+ n_cp_avr = np.array(nuc_clust_features[0])
769
+ n_cp_avr = np.mean(n_cp_avr)
770
+ n_cp_avr = int(np.floor(n_cp_avr))
771
+ d_cp_avr = []
772
+ for dd in nuc_clust_features[1]:
773
+ for ddd in list(dd):
774
+ d_cp_avr.append(ddd)
775
+ d_cp_avr = np.array(d_cp_avr)
776
+ d_cp_avr = np.mean(d_cp_avr)
777
+ nuclei_clusters_info['nuclei']['number'] = len(nuc_seg_features)
778
+ nuclei_clusters_info['nuclei']['min'] = np.min(nuc_seg_features)
779
+ nuclei_clusters_info['nuclei']['max'] = np.max(nuc_seg_features)
780
+ nuclei_clusters_info['nuclei']['avr'] = np.mean(nuc_seg_features)
781
+ nuclei_clusters_info['nuclei']['std'] = np.std(nuc_seg_features)
782
+ nuclei_clusters_info['nuclei']['data'] = nuc_seg_features
783
+ nuclei_clusters_info['nuclei']['nuclei area threshold'] = n_area_thr
784
+ nuclei_clusters_info['clusters']['Number'] = len(nuc_clust_features[0])
785
+ nuclei_clusters_info['clusters']['AvrArea'] = a_avr
786
+ nuclei_clusters_info['clusters']['Ncp'] = n_cp_avr
787
+ nuclei_clusters_info['clusters']['Dcp'] = d_cp_avr
788
+ report('\t---------------------------------------------------\n'+
789
+ '\tNon-convexity Threshold = {0:.0f} pxs\n'.format(cp_thr)+
790
+ '\tMin Area Threshold = {0:.0f} pxs\n'.format(area_thr)+
791
+ '\tSmall Object Area Threshold = {0:.0f} pxs\n'.format(n_area_thr)+
792
+ '\t---------------------------------------------------\n'+
793
+ '\tSummary for Nuclei Global Ternary Mask\n'+
794
+ '\t---------------------------------------------------\n'+
795
+ '\t\t- Number of removed components = {0} (Area < {1} pxs)\n'.format(mask_info['removed'], area_thr)+
796
+ '\t\t- Number of components with self-intersections = {0} ({1})\n'.format(mask_info['self-intersection'], len(clusters_self))+
797
+ '\t\t- Number of rectangle components = {0}\n'.format(mask_info['rectangle'])+
798
+ '\t\t- Number of convex components = {0}\n'.format(mask_info['convex'])+
799
+ '\t\t-----------------------------------------------\n'+
800
+ '\t\t- Nuclei Segmentation (NS)\n'+
801
+ '\t\t\tNumber of nuclei = {0}\n'.format(nuclei_clusters_info['nuclei']['number'])+
802
+ '\t\t\tMean nuclei area = {0:.1f} pxs\n'.format(nuclei_clusters_info['nuclei']['avr'])+
803
+ '\t\t\tSTD of nuclei area = {0:.1f} pxs\n'.format(nuclei_clusters_info['nuclei']['std'])+
804
+ '\t\t\tMin nuclei area = {0} pxs\n'.format(nuclei_clusters_info['nuclei']['min'])+
805
+ '\t\t\tMax nuclei area = {0} pxs\n'.format(nuclei_clusters_info['nuclei']['max'])+
806
+ '\t\t-----------------------------------------------\n'+
807
+ '\t\t- Nuclei Clusters (NC)\n'+
808
+ '\t\t\tNumber of clusters = {0}\n'.format(nuclei_clusters_info['clusters']['Number'])+
809
+ '\t\t\tMean cluster area = {0:.1f} pxs\n'.format(nuclei_clusters_info['clusters']['AvrArea'])+
810
+ '\t\t\tMean number of CPs = {0:.1f}\n'.format(nuclei_clusters_info['clusters']['Ncp'])+
811
+ '\t\t\tMean defect distance = {0:.1f} pxs\n'.format(nuclei_clusters_info['clusters']['Dcp'])+
812
+ '\t\t-----------------------------------------------\n'+
813
+ '\t\t- Small Objects (SO)\n'+
814
+ '\t\t\tNumber of objects = {0} (Area < {1} pxs)\n'.format(mask_info['SO'], n_area_thr)+
815
+ '\t---------------------------------------------------')
816
+ return nuc_seg, nuc_clust, small_obj, nuclei_clusters_info
817
+
818
+ def process_clusters(img, mask_local_ternary, clusters, nuc_seg, clust_small, nuclei_clusters_info, cp_thr=5, area_thr=20, cy5_thr=25, dist_tr='DT'):
819
+ """Processing of Nuclei Clusters Using Pertubated Watershed and Watershed with Virtual Cuts"""
820
+ report('\ta. Pertubated Watershed')
821
+ try:
822
+ otsu_3 = threshold_multiotsu(nuclei_clusters_info['nuclei']['data'], classes=3)
823
+ a1 = otsu_3[0]
824
+ except:
825
+ a1 = np.min(nuclei_clusters_info['nuclei']['data'])
826
+ a2 = np.max([2*nuclei_clusters_info['nuclei']['avr'], nuclei_clusters_info['nuclei']['max']])
827
+ a3 = np.min([nuclei_clusters_info['nuclei']['avr']+3*nuclei_clusters_info['nuclei']['std'], 2*nuclei_clusters_info['nuclei']['max']])
828
+ c1 = nuclei_clusters_info['clusters']['Dcp']
829
+ clust_unknown = []
830
+ n_nuc = np.max(nuc_seg)
831
+ while len(clusters) > 0:
832
+ cl = clusters[0]
833
+ cc = cl[0]
834
+ h1, w1, h2, w2 = cl[1]
835
+ clusters.remove(cl)
836
+ gbm0 = np.zeros((h2-h1, w2-w1), dtype='bool')
837
+ gbm0[cc] = True
838
+ lbm0 = np.logical_and(gbm0, np.logical_not(mask_local_ternary[h1:h2,w1:w2,0]))
839
+ seg0, da = dynamic_ws(gbm0, lbm0)
840
+ seg0 = boundary_cuts(seg0, gbm0)
841
+ pp = regionprops(seg0)
842
+ n_ws = len(pp)
843
+ if n_ws == 1:
844
+ p = pp[0]
845
+ hh1, ww1, hh2, ww2 = p.bbox
846
+ test_result, cp, d_max, cnt = convexity_test(p.filled_image, cp_thr=cp_thr)
847
+ n_cp = cp.shape[0]
848
+ if n_cp == 0:
849
+ cp_max = 0
850
+ else:
851
+ cp_max = np.max(cp[:,3])/256
852
+ if test_result:
853
+ if p.filled_area < a1:
854
+ if p.filled_area > area_thr:
855
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
856
+ clust_small.append((ccf, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
857
+ else:
858
+ if n_cp >= 2 and da <= cy5_thr and (cp_max > c1 or p.filled_area > a3):
859
+ clust_unknown.append((p.image, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
860
+ else:
861
+ n_nuc = n_nuc + 1
862
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
863
+ nuc_seg[h1+hh1:h1+hh2, w1+ww1:w1+ww2][ccf] = n_nuc
864
+ #nuc_seg[h1:h2, w1:w2][hh1:hh2, ww1:ww2][ccf] = n_nuc
865
+ else:
866
+ report('\t\tWARNING: Self-intersection in Perturbed Watershed: Nsp = 1')
867
+ cc_wosi = remove_selfintersection(p.image)
868
+ cc_wosi_props = label(cc_wosi, background=False, connectivity=2)
869
+ cc_wosi_props = regionprops(cc_wosi_props)
870
+ if len(cc_wosi_props) > 0:
871
+ for p_wosi in cc_wosi_props:
872
+ hhh1, www1, hhh2, www2 = p_wosi.bbox
873
+ if p_wosi.area > area_thr:
874
+ clusters.append((p_wosi.image, (h1+hh1+hhh1, w1+ww1+www1, h1+hh1+hhh2, w1+ww1+www2)))
875
+ elif n_ws > 1:
876
+ for p in pp:
877
+ hh1, ww1, hh2, ww2 = p.bbox
878
+ test_result, cp, d_max, cnt = convexity_test(p.filled_image, cp_thr=cp_thr)
879
+ n_cp = cp.shape[0]
880
+ if n_cp == 0:
881
+ cp_max = 0
882
+ else:
883
+ cp_max = np.max(cp[:,3])/256
884
+ if test_result:
885
+ if p.filled_area < a1:
886
+ if p.filled_area > area_thr:
887
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
888
+ clust_small.append((ccf, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
889
+ elif p.filled_area < a2:
890
+ if n_cp >= 2 and cp_max > c1:
891
+ clusters.append((p.image, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
892
+ else:
893
+ n_nuc = n_nuc + 1
894
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
895
+ nuc_seg[h1+hh1:h1+hh2, w1+ww1:w1+ww2][ccf] = n_nuc
896
+ else:
897
+ clusters.append((p.image, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
898
+ else:
899
+ report('\t\tWARNING: Self-intersection in Perturbed Watershed: Nsp > 1')
900
+ cc_wosi = remove_selfintersection(p.image)
901
+ cc_wosi_props = label(cc_wosi, background=False, connectivity=2)
902
+ cc_wosi_props = regionprops(cc_wosi_props)
903
+ if len(cc_wosi_props) > 0:
904
+ for p_wosi in cc_wosi_props:
905
+ hhh1, www1, hhh2, www2 = p_wosi.bbox
906
+ if p_wosi.area > area_thr:
907
+ clusters.append((p_wosi.image, (h1+hh1+hhh1, w1+ww1+www1, h1+hh1+hhh2, w1+ww1+www2)))
908
+ else:
909
+ report('\t\tWARNING: in Perturbed Watershed the number of seed points is not positive, Nsp = {0}'.format(n_ws))
910
+ report('\t\t- Nuclei Segmentation: {0}\n'.format(n_nuc)+
911
+ '\t\t- Nuclei Clusters\t: {0}\n'.format(len(clust_unknown))+
912
+ '\t\t- Small Objects\t: {0}'.format(len(clust_small)))
913
+ report('\tb. Virtual Cuts')
914
+ while len(clust_unknown) > 0:
915
+ cl = clust_unknown[0]
916
+ cc = cl[0]
917
+ h1, w1, h2, w2 = cl[1]
918
+ clust_unknown.remove(cl)
919
+ test_result, cp, d_max, cnt = convexity_test(cc, cp_thr=cp_thr)
920
+ n_cp = cp.shape[0]
921
+ a_cc = np.count_nonzero(ndimage.binary_fill_holes(cc))
922
+ if n_cp == 0:
923
+ cp_max = 0
924
+ else:
925
+ cp_max = np.max(cp[:,3])/256
926
+ cp = cp[cp[:,3].argsort()]
927
+ cp = cp[::-1]
928
+ if n_cp > 0:
929
+ cp = cp[cp[:,3].argsort()]
930
+ cp = cp[::-1]
931
+ if test_result:
932
+ lbm0 = np.logical_and(cc, np.logical_not(mask_local_ternary[h1:h2,w1:w2,0]))
933
+ a_g = np.count_nonzero(cc)
934
+ a_l = np.count_nonzero(lbm0)
935
+ da = 100*(a_g-a_l)/a_g
936
+ if n_cp >= 2 and (cp_max > c1 or a_cc > a3 or (da < 0.001 and a_cc > 2*a1)):
937
+ if a_cc > 2*a3:
938
+ vl = True
939
+ else:
940
+ vl = False
941
+ seg0 = virtual_ws_cuts(img, cc, (h1, w1, h2, w2), mask_local_ternary, cnt, cp, dist_transform=dist_tr, large=vl)
942
+ pp = regionprops(seg0)
943
+ n_ws = len(pp)
944
+ if n_ws == 1:
945
+ p = pp[0]
946
+ hh1, ww1, hh2, ww2 = p.bbox
947
+ if p.filled_area < a1:
948
+ if p.filled_area > area_thr:
949
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
950
+ clust_small.append((ccf, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
951
+ else:
952
+ n_nuc = n_nuc + 1
953
+ ccf = adaptive_morphological_filter(p.filled_image, d_max)
954
+ nuc_seg[h1+hh1:h1+hh2, w1+ww1:w1+ww2][ccf] = n_nuc
955
+ elif n_ws == 2:
956
+ a = np.zeros((n_ws,1), dtype='int32')
957
+ for i in range(n_ws):
958
+ a[i,0] = pp[i].filled_area
959
+ a_min = np.min(a)
960
+ if a_min < a1:
961
+ n_nuc = n_nuc + 1
962
+ ccf = ndimage.binary_fill_holes(cc)
963
+ ccf = adaptive_morphological_filter(ccf, d_max)
964
+ nuc_seg[h1:h2, w1:w2][ccf] = n_nuc
965
+ else:
966
+ seg0 = boundary_cuts(seg0, cc)
967
+ pp = regionprops(seg0)
968
+ for p in pp:
969
+ hh1, ww1, hh2, ww2 = p.bbox
970
+ clust_unknown.append((p.image, (h1+hh1, w1+ww1, h1+hh2, w1+ww2)))
971
+ else:
972
+ report('\t\tWARNING: in Virtual Cuts the number of seed points is neither 1 nor 2')
973
+ else:
974
+ if a_cc < a1:
975
+ if a_cc > area_thr:
976
+ ccf = adaptive_morphological_filter(cc, d_max)
977
+ clust_small.append((ccf, (h1, w1, h2, w2)))
978
+ else:
979
+ n_nuc = n_nuc + 1
980
+ ccf = adaptive_morphological_filter(cc, d_max)
981
+ nuc_seg[h1:h2, w1:w2][ccf] = n_nuc
982
+ else:
983
+ report('Self-intersection in Virtual Cuts')
984
+ cc_wosi = remove_selfintersection(p.image)
985
+ cc_wosi_props = label(cc_wosi, background=False, connectivity=2)
986
+ cc_wosi_props = regionprops(cc_wosi_props)
987
+ if len(cc_wosi_props) > 0:
988
+ for p_wosi in cc_wosi_props:
989
+ hhh1, www1, hhh2, www2 = p_wosi.bbox
990
+ if p_wosi.area > area_thr:
991
+ clust_unknown.append((p_wosi.image, (h1+hh1+hhh1, w1+ww1+www1, h1+hh1+hhh2, w1+ww1+www2)))
992
+ report('\t\t- Nuclei Segmentation: {0}\n'.format(n_nuc)+
993
+ '\t\t- Nuclei Clusters\t: {0}\n'.format(len(clust_unknown))+
994
+ '\t\t- Small Objects\t: {0}'.format(len(clust_small)))
995
+ return nuc_seg, clust_small
996
+
997
+ def process_small_objects(clust_small, nuc_seg, nuclei_clusters_info, h, w):
998
+ """Processing of Small Connected Components"""
999
+ report('\tc. Adding of Small Objects')
1000
+
1001
+ def wide_mask(cc, d, h1, w1, h2, w2, h, w):
1002
+ hh1 = np.max([0, h1-d])
1003
+ ww1 = np.max([0, w1-d])
1004
+ hh2 = np.min([h, h2+d])
1005
+ ww2 = np.min([w, w2+d])
1006
+ ccb = np.zeros((hh2-hh1, ww2-ww1), dtype='bool')
1007
+ ccb[h1-hh1:h2-hh1, w1-ww1:w2-ww1][cc] = True
1008
+ ccb = dilation(ccb, disk(d))
1009
+ mask_wide = nuc_seg[hh1:hh2, ww1:ww2][ccb]
1010
+ mw = np.count_nonzero(mask_wide)
1011
+ return mw
1012
+
1013
+ d1 = 3
1014
+ small_area = []
1015
+ for i in range(len(clust_small)):
1016
+ cl = clust_small[i]
1017
+ cc = cl[0]
1018
+ small_area.append(np.count_nonzero(cc))
1019
+ small_area = np.array(small_area, dtype='int32')
1020
+ inds = np.argsort(small_area)
1021
+ inds = inds[::-1]
1022
+ otsu_3 = threshold_multiotsu(small_area, classes=3)
1023
+ a_max = np.max([otsu_3[1], nuclei_clusters_info['nuclei']['min']])
1024
+ a_min = np.max([otsu_3[0], nuclei_clusters_info['nuclei']['min']/2])
1025
+ report('\t\t* Min Area Threshold = {0}'.format(a_min))
1026
+ report('\t\t* Max Area Threshold = {0}'.format(a_max))
1027
+ n_nuc = np.max(nuc_seg)
1028
+ for i in inds:
1029
+ cl = clust_small[i]
1030
+ cc = cl[0]
1031
+ h1, w1, h2, w2 = cl[1]
1032
+ ai = np.count_nonzero(cc)
1033
+ mw1 = wide_mask(cc, d1, h1, w1, h2, w2, h, w)
1034
+ mn = np.count_nonzero(nuc_seg[h1:h2, w1:w2][cc])
1035
+ if mn == 0 and ai >= a_max:
1036
+ n_nuc = n_nuc + 1
1037
+ nuc_seg[h1:h2, w1:w2][cc] = n_nuc
1038
+ elif mw1 == 0 and ai >= a_min:
1039
+ n_nuc = n_nuc + 1
1040
+ nuc_seg[h1:h2, w1:w2][cc] = n_nuc
1041
+ report('\t\t- Nuclei Segmentation: {0}\n'.format(n_nuc)+
1042
+ '\t\t- Nuclei Clusters\t: 0\n'+
1043
+ '\t\t- Small Objects\t: {0}'.format(len(clust_small)))
1044
+ return nuc_seg
1045
+
1046
+ def filter_nuclei_mask(mask):
1047
+ l = 2
1048
+ properties = regionprops(mask)
1049
+ new_mask = np.zeros(mask.shape, dtype='int32')
1050
+ for p in properties:
1051
+ h1, w1, h2, w2 = p.bbox
1052
+ h = h2 - h1
1053
+ w = w2 - w1
1054
+ cc0 = new_mask[h1:h2,w1:w2] == 0
1055
+ y0, x0 = p.centroid
1056
+ cnt = find_contour((p.filled_image).astype('uint8'))
1057
+ cnt = cnt[:,0,:]
1058
+ n_theta = cnt.shape[0]
1059
+ r = np.sqrt((cnt[:,0]-x0)**2 + (cnt[:,1]-y0)**2)
1060
+ theta = np.arctan2(cnt[:,1]-y0, cnt[:,0]-x0)*180/np.pi
1061
+ r_smooth = gradient_smoothing(r, n_theta, l)
1062
+ x_smooth = (np.round(x0+r_smooth*np.cos(theta*np.pi/180))).astype('int32')
1063
+ x_smooth = x_smooth.reshape(-1,1)
1064
+ x_smooth = np.where(x_smooth < 0, 0, x_smooth)
1065
+ x_smooth = np.where(x_smooth >= w, w-1, x_smooth)
1066
+ y_smooth = (np.round(y0+r_smooth*np.sin(theta*np.pi/180))).astype('int32')
1067
+ y_smooth = y_smooth.reshape(-1,1)
1068
+ y_smooth = np.where(y_smooth < 0, 0, y_smooth)
1069
+ y_smooth = np.where(y_smooth >= h, h-1, y_smooth)
1070
+ cc = np.zeros((h,w), dtype='uint8')
1071
+ pts = np.concatenate((x_smooth, y_smooth), axis=1)
1072
+ cc = cv2.fillPoly(cc, pts=[pts], color=(1,1,1))
1073
+ cc = cc.astype('bool')
1074
+ cc = np.logical_and(cc0, cc)
1075
+ prop = label(cc, background=False, connectivity=2)
1076
+ prop = regionprops(prop)
1077
+ if len(prop) == 1:
1078
+ new_mask[h1:h2,w1:w2][cc] = p.label
1079
+ elif len(prop) == 0:
1080
+ new_mask[h1:h2,w1:w2][p.filled_image] = p.label
1081
+ else:
1082
+ new_mask[h1:h2,w1:w2][p.filled_image] = p.label
1083
+ n0 = np.max(new_mask)
1084
+ n1 = list(np.unique(new_mask))
1085
+ if 0 in n1:
1086
+ n1.remove(0)
1087
+ n1 = len(n1)
1088
+ if n0 != n1:
1089
+ report('\tWARNING: Nuclei Segmentation Labeling Is Not Continuous')
1090
+ #new_mask = label(new_mask, background=0, connectivity=2)
1091
+ mask_status = mask_statistics(new_mask)
1092
+ report('\t---------------------------------------------------\n'+
1093
+ '\tSummary for Nuclei Segmentation\n'+
1094
+ '\t---------------------------------------------------\n'+
1095
+ '\t\t- Number of nuclei = {0}\n'.format(mask_status['n_total'])+
1096
+ '\t\t-----------------------------------------------\n'+
1097
+ '\t\t- Mean nuclei area = {0:.1f} pxs\n'.format(mask_status['area_avr'])+
1098
+ '\t\t- STD of nuclei area = {0:.1f} pxs\n'.format(mask_status['area_std'])+
1099
+ '\t\t- Min nuclei area = {0} pxs\n'.format(mask_status['area_min'])+
1100
+ '\t\t- Max nuclei area = {0} pxs\n'.format(mask_status['area_max'])+
1101
+ '\t\t-----------------------------------------------\n'+
1102
+ '\t\t- Mean nuclei convexity = {0:.4f}\n'.format(mask_status['convexity_avr'])+
1103
+ '\t\t- Mean nuclei compactness = {0:.4f}\n'.format(mask_status['shape_avr'])+
1104
+ '\t---------------------------------------------------')
1105
+ return new_mask, mask_status
1106
+
1107
+ ### Initialization of Cell Segmentation
1108
+ def initialize_cell_segmentation(nuc_seg):
1109
+ cell_seg = nuc_seg.copy()
1110
+ nuclei_prop_list = regionprops(cell_seg)
1111
+ nuclei_prop = {}
1112
+ for p in nuclei_prop_list:
1113
+ nuclei_prop[p.label] = p
1114
+ nuclei = list(nuclei_prop.keys())
1115
+ n_nuclei = len(nuclei)
1116
+ return cell_seg, nuclei_prop, nuclei, n_nuclei
1117
+
1118
+ ### Skeletonization
1119
+ def get_skeleton_mod(mask):
1120
+ skeleton = thin(mask)
1121
+ skeleton = skeleton.astype('bool')
1122
+ skeleton = dilation(skeleton, disk(1))
1123
+ return skeleton
1124
+
1125
+ def get_mask_cuts_mod(mask, area_threshold_cell):
1126
+ mask_cells = np.zeros((mask.shape[0], mask.shape[1]), dtype='bool')
1127
+ mm = label(mask, background=False, connectivity=2)
1128
+ mm = regionprops(mm)
1129
+ for m in mm:
1130
+ cc = ndimage.binary_fill_holes(m.image)
1131
+ a = np.count_nonzero(cc)
1132
+ if a >= area_threshold_cell:
1133
+ h1, w1, h2, w2 = m.bbox
1134
+ mask_cells[h1:h2, w1:w2][m.image] = True
1135
+ skeleton = get_skeleton_mod(mask_cells)
1136
+ mask_cells = np.logical_and(mask_cells, np.logical_not(skeleton))
1137
+ skeleton = get_skeleton_mod(mask)
1138
+ return mask_cells, skeleton
1139
+
1140
+ ### Euler Number Filter
1141
+ def euler_number_filter_mod(mask_cell_lab, mask_cells_cuts, nuclei, n_nuclei, small_area=50, area_ratio_thr=0.65):
1142
+ n_cells = n_nuclei
1143
+ mask_cell_cuts_prop = label(mask_cells_cuts, background=False, connectivity=2)
1144
+ mask_cell_cuts_prop = regionprops(mask_cell_cuts_prop)
1145
+ for p in mask_cell_cuts_prop:
1146
+ if p.euler_number != 1:
1147
+ a1 = p.area
1148
+ cc0 = ndimage.binary_fill_holes(p.image)
1149
+ a2 = np.count_nonzero(cc0)
1150
+ if a2 >= small_area and a1/a2 < area_ratio_thr:
1151
+ h1, w1, h2, w2 = p.bbox
1152
+ n_unique, n_counts = np.unique(mask_cell_lab[h1:h2, w1:w2][cc0], return_counts=True)
1153
+ n_inds = np.argsort(n_counts)[::-1]
1154
+ n = 0
1155
+ for ind in n_inds:
1156
+ if ind == 0:
1157
+ continue
1158
+ else:
1159
+ n = n_unique[ind]
1160
+ break
1161
+ cc = np.zeros((h2-h1,w2-w1), dtype='bool')
1162
+ cc[cc0] = True
1163
+ if n == 0:
1164
+ mask_allowed = mask_cell_lab[h1:h2,w1:w2] == 0
1165
+ n_cells = n_cells + 1
1166
+ n0 = n_cells
1167
+ else:
1168
+ mask_allowed = np.logical_or(mask_cell_lab[h1:h2,w1:w2] == 0, mask_cell_lab[h1:h2,w1:w2] == n)
1169
+ n0 = n
1170
+ nuclei.remove(n)
1171
+ cc = morphological_smoothing(cc, d=2)
1172
+ cc = ndimage.binary_fill_holes(cc)
1173
+ cc = np.logical_and(mask_allowed, cc)
1174
+ if n_inds.shape[0] > 2:
1175
+ e8 = label(cc, background=False, connectivity=2)
1176
+ e8 = regionprops(e8)
1177
+ e8 = e8[0]
1178
+ e8 = e8.euler_number
1179
+ if e8 != 1 and n != 0:
1180
+ nuclei.append(n)
1181
+ continue
1182
+ mask_cell_lab[h1:h2,w1:w2][cc==True] = n0
1183
+ return mask_cell_lab, nuclei, n_cells
1184
+
1185
+ ### Isolated Nuclei Filter
1186
+ def extend_cell(cc, cc_bbox=(0,0,10,20), mask_hw=(50,100), d=9):
1187
+ h1, w1, h2, w2 = cc_bbox
1188
+ dh = h2 - h1
1189
+ dw = w2 - w1
1190
+ h, w = mask_hw
1191
+ if h1 >= d:
1192
+ y1 = d
1193
+ else:
1194
+ y1 = h1
1195
+ if w1 >= d:
1196
+ x1 = d
1197
+ else:
1198
+ x1 = w1
1199
+ if h-h2 >= d:
1200
+ y2 = d
1201
+ else:
1202
+ y2 = h-h2
1203
+ if w-w2 >= d:
1204
+ x2 = d
1205
+ else:
1206
+ x2 = w-w2
1207
+ ccb = np.zeros((y1+dh+y2,x1+dw+x2), dtype='bool')
1208
+ ccb[y1:y1+dh, x1:x1+dw] = cc
1209
+ ccb = dilation(ccb, disk(d))
1210
+ ccb_bbox = (h1-y1, w1-x1, h2+y2, w2+x2)
1211
+ return ccb, ccb_bbox
1212
+
1213
+ def isolated_nuclei_filter(mask_cell_lab, mask_cells, nuclei_prop, nuclei, u0=9):
1214
+ nuclei_new = nuclei.copy()
1215
+ for n in nuclei:
1216
+ p = nuclei_prop[n]
1217
+ h1, w1, h2, w2 = p.bbox
1218
+ cc = np.zeros((h2-h1,w2-w1), dtype='bool')
1219
+ cc[p.image] = True
1220
+ cc, ext_bbox = extend_cell(cc, cc_bbox=(h1,w1,h2,w2), mask_hw=(mask_cell_lab.shape[0],mask_cell_lab.shape[1]), d=u0)
1221
+ h1, w1, h2, w2 = ext_bbox
1222
+ n_cc = np.sum(np.logical_and(cc, mask_cells[h1:h2,w1:w2]))
1223
+ if n_cc < 25:
1224
+ mask_allowed = np.logical_or(mask_cell_lab[h1:h2,w1:w2] == n, mask_cell_lab[h1:h2,w1:w2] == 0)
1225
+ cc = np.logical_and(mask_allowed, cc)
1226
+ cc = erosion(cc, disk(3))
1227
+ cc = dilation(cc, disk(3))
1228
+ cc = np.logical_and(mask_allowed, cc)
1229
+ mask_cell_lab[h1:h2,w1:w2][cc==True] = n
1230
+ nuclei_new.remove(n)
1231
+ return mask_cell_lab, nuclei_new
1232
+
1233
+ ### Threshold Filter
1234
+ def regularize_cell_boundary(d, thr=10):
1235
+ def smooth_jump(z, m, n, thr=10):
1236
+ smooth = False
1237
+ z1 = np.roll(z, shift=-1)
1238
+ dz = z1 - z
1239
+ z_abs_max = np.max(np.abs(dz))
1240
+ if z_abs_max > thr:
1241
+ smooth = True
1242
+ i = np.argmax(np.abs(dz))
1243
+ if dz[i] > 0:
1244
+ if i < n-1:
1245
+ j = i + 1
1246
+ else:
1247
+ j = 0
1248
+ m[j] = True
1249
+ z[j] = z[i]
1250
+ else:
1251
+ if i < n-1:
1252
+ j = i + 1
1253
+ else:
1254
+ j = 0
1255
+ m[i] = True
1256
+ z[i] = z[j]
1257
+ return z, m, smooth
1258
+
1259
+ d_cell = d.copy()
1260
+ n = d.shape[0]
1261
+ m = np.zeros((n,), dtype='bool')
1262
+ for i in range(n):
1263
+ d_cell, m, smooth = smooth_jump(d_cell, m, n, thr=thr)
1264
+ if not smooth:
1265
+ break
1266
+ d_ind = (m == True).astype('int32')
1267
+ if np.max(d_ind) == 0:
1268
+ return d_cell, thr
1269
+ else:
1270
+ ind_start = (d_ind - np.roll(d_ind, shift=-1)) == -1
1271
+ ind_end = (d_ind - np.roll(d_ind, shift=1)) == -1
1272
+ ind_start_i = np.argwhere(ind_start)
1273
+ ind_end_i = np.argwhere(ind_end)
1274
+ if ind_start_i[0] >= ind_end_i[0]:
1275
+ ind_end_i = np.roll(ind_end_i, shift=-1)
1276
+ ind_start_i[-1,0] = ind_start_i[-1,0]-d_ind.shape[0]
1277
+ for i in range(ind_start_i.shape[0]):
1278
+ t1 = ind_start_i[i,0]
1279
+ x1 = d[t1]
1280
+ t2 = ind_end_i[i,0]
1281
+ x2 = d[t2]
1282
+ for t in range(t1+1, t2):
1283
+ x = x1 + (t-t1)*(x2-x1)/(t2-t1)
1284
+ d_cell[t] = x
1285
+ return d_cell, thr
1286
+
1287
+ def xiyi_2_xnyn(xi, yi, w, h):
1288
+ bnd = False
1289
+ xn = int(np.round(xi))
1290
+ yn = int(np.round(yi))
1291
+ if xn <= 0:
1292
+ xn = 0
1293
+ bnd = True
1294
+ elif xn >= w-1:
1295
+ xn = w-1
1296
+ bnd = True
1297
+ if yn <= 0:
1298
+ yn = 0
1299
+ bnd = True
1300
+ elif yn >= h-1:
1301
+ yn = h-1
1302
+ bnd = True
1303
+ return xn, yn, bnd
1304
+
1305
+ def threshold_filter(mask_cell_lab, mask_skeleton, nuclei_prop, nuclei):
1306
+ n_theta = 72
1307
+ l = 2
1308
+ d = 5
1309
+ theta = (2*np.pi/n_theta)*np.linspace(0, n_theta-1, n_theta)
1310
+ h, w = mask_cell_lab.shape
1311
+ mask_base = mask_cell_lab.copy()
1312
+ mask_base[mask_skeleton == True] = -1
1313
+ for n in nuclei:
1314
+ x_y_d = np.zeros((n_theta,8), dtype='float64')
1315
+ p = nuclei_prop[n]
1316
+ hn1, wn1, hn2, wn2 = p.bbox
1317
+ y0, x0 = p.centroid
1318
+ for i in range(n_theta):
1319
+ xi = x0
1320
+ yi = y0
1321
+ xm = int(np.round(x0))
1322
+ ym = int(np.round(y0))
1323
+ xn = int(np.round(x0))
1324
+ yn = int(np.round(y0))
1325
+ flag = True
1326
+ if theta[i] == np.pi/2:
1327
+ while mask_base[yn,xn] == n or mask_base[yn,xn] == 0:
1328
+ xi = xi
1329
+ yi = yi + 1
1330
+ xn, yn, bnd = xiyi_2_xnyn(xi, yi, w, h)
1331
+ if bnd:
1332
+ break
1333
+ if mask_base[ym,xm] == n and mask_base[yn,xn] != n and flag:
1334
+ flag = False
1335
+ xm = int(np.round(xi))
1336
+ ym = int(np.round(yi-1))
1337
+ elif theta[i] == 3*np.pi/2:
1338
+ while mask_base[yn,xn] == n or mask_base[yn,xn] == 0:
1339
+ xi = xi
1340
+ yi = yi - 1
1341
+ xn, yn, bnd = xiyi_2_xnyn(xi, yi, w, h)
1342
+ if bnd:
1343
+ break
1344
+ if mask_base[ym,xm] == n and mask_base[yn,xn] != n and flag:
1345
+ flag = False
1346
+ xm = int(np.round(xi))
1347
+ ym = int(np.round(yi+1))
1348
+ else:
1349
+ while mask_base[yn,xn] == n or mask_base[yn,xn] == 0:
1350
+ xi = xi + np.cos(theta[i])
1351
+ yi = y0 + np.tan(theta[i])*(xi-x0)
1352
+ xn, yn, bnd = xiyi_2_xnyn(xi, yi, w, h)
1353
+ if bnd:
1354
+ break
1355
+ if mask_base[yn,xn] != n and flag:
1356
+ flag = False
1357
+ xm = int(np.round(xi-np.cos(theta[i])))
1358
+ ym = int(np.round(y0+np.tan(theta[i])*(xi-np.cos(theta[i])-x0)))
1359
+ if flag:
1360
+ x_y_d[i,0] = xn
1361
+ x_y_d[i,1] = yn
1362
+ else:
1363
+ x_y_d[i,0] = xm
1364
+ x_y_d[i,1] = ym
1365
+ x_y_d[i,2] = xn
1366
+ x_y_d[i,3] = yn
1367
+ if mask_base[yn,xn] == -1:
1368
+ x_y_d[i,4] = xn
1369
+ x_y_d[i,5] = yn
1370
+ else:
1371
+ x_y_d[i,4] = x_y_d[i,0]
1372
+ x_y_d[i,5] = x_y_d[i,1]
1373
+ x_y_d[i,6] = np.sqrt((x_y_d[i,4]-x_y_d[i,0])**2 + (x_y_d[i,5]-x_y_d[i,1])**2)
1374
+ x_y_d[i,7] = np.sqrt((x_y_d[i,4]-x0)**2 + (x_y_d[i,5]-y0)**2)
1375
+ zn = x_y_d[:,7]
1376
+ zn1 = np.roll(zn, shift=-1)
1377
+ dz = zn1 - zn
1378
+ dz_min = np.min(dz)
1379
+ dz_max = np.max(dz)
1380
+ if np.max(np.abs([dz_min, dz_max])) > 0.25*p.equivalent_diameter:
1381
+ d_cell, d_thr = regularize_cell_boundary(x_y_d[:,7], thr=0.5*p.equivalent_diameter)
1382
+ else:
1383
+ d_cell = x_y_d[:,7]
1384
+ d_cell = gradient_smoothing(d_cell, n_theta, l)
1385
+ x_cell = (np.round(x0+d_cell*np.cos(theta))).astype('int64')
1386
+ x_cell = x_cell.reshape(-1,1)
1387
+ x_cell = np.where(x_cell < 0, 0, x_cell)
1388
+ x_cell = np.where(x_cell >= w, w-1, x_cell)
1389
+ y_cell = (np.round(y0+d_cell*np.sin(theta))).astype('int64')
1390
+ y_cell = y_cell.reshape(-1,1)
1391
+ y_cell = np.where(y_cell < 0, 0, y_cell)
1392
+ y_cell = np.where(y_cell >= h, h-1, y_cell)
1393
+ hc1 = np.min(y_cell[:,0])
1394
+ hc2 = np.max(y_cell[:,0]) + 1
1395
+ wc1 = np.min(x_cell[:,0])
1396
+ wc2 = np.max(x_cell[:,0]) + 1
1397
+ h1 = np.min([hn1, hc1])
1398
+ h2 = np.max([hn2, hc2])
1399
+ w1 = np.min([wn1, wc1])
1400
+ w2 = np.max([wn2, wc2])
1401
+ cc = np.zeros((h2-h1,w2-w1), dtype='uint8')
1402
+ cc[p.coords[:,0]-h1, p.coords[:,1]-w1] = 1
1403
+ pts = np.concatenate((x_cell-wc1, y_cell-hc1), axis=1)
1404
+ cc = cv2.fillPoly(cc, pts=[pts], color=(1,1,1))
1405
+ cc = cc.astype('bool')
1406
+ cc = morphological_smoothing(cc, d=d)
1407
+ mask_allowed = np.logical_or(mask_cell_lab[h1:h2,w1:w2] == n, mask_cell_lab[h1:h2,w1:w2] == 0)
1408
+ cc = np.logical_and(mask_allowed, cc)
1409
+ mask_cell_lab[h1:h2,w1:w2][cc==True] = n
1410
+ return mask_cell_lab
1411
+
1412
+ ###############################################################################
1413
+ # SEGMENTATION FUNCTIONS
1414
+ ###############################################################################
1415
+ def nuclei_segmentation(intensity,
1416
+ area_threshold=20,
1417
+ convexity_threshold=4,
1418
+ cell_marker_threshold=25,
1419
+ dist_tr='GDT',
1420
+ sigma0=3,
1421
+ k0=1,
1422
+ r0=5,
1423
+ pct=[0.01, 0.01],
1424
+ nk=[5, 10, 20, 40],
1425
+ t0=0.5,
1426
+ ternary_met='Argmax',
1427
+ visualization=False):
1428
+ global_time = time.time()
1429
+ report('\nNUCLEI SEGMENTATION...\n')
1430
+
1431
+ report(' 0. Preprocessing...')
1432
+ local_time = time.time()
1433
+ channel_names = ('Nuclei','Cell Membranes')
1434
+ img = normalize_img(intensity)
1435
+ if visualization:
1436
+ intensity2rgb(img, return_rgb=False)
1437
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1438
+
1439
+ report(' 1. Computing of A Priori Probabilities...')
1440
+ local_time = time.time()
1441
+ p_em = calc_empirical_probabilities(img, sigma=sigma0)
1442
+ if visualization:
1443
+ for i in range(len(channel_names)):
1444
+ plot_img(p_em[:,:,i], tlt='{0}: A Priori Probability'.format(channel_names[i]))
1445
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1446
+
1447
+ report(' 2. Computing of A Priori Global and Local (Binary) Masks...')
1448
+ local_time = time.time()
1449
+ mask_global_binary = calc_global_binary_mask(img, nk=nk, t0=t0)
1450
+ mask_local_binary = calc_local_binary_mask(img, r0=r0, k0=k0, smoothing=True)
1451
+ mask_local_binary, mask_global_binary = remove_background(mask_local_binary, mask_global_binary, p_em, pct)
1452
+ if visualization:
1453
+ for i in range(len(channel_names)):
1454
+ plot_img(mask_global_binary[:,:,i], tlt='{0}: A Priori Global Mask'.format(channel_names[i]))
1455
+ plot_img(mask_local_binary[:,:,i], tlt='{0}: A Priori Local Mask'.format(channel_names[i]))
1456
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1457
+
1458
+ report(' 3. Computing of Likelihood...')
1459
+ local_time = time.time()
1460
+ likelihood = calc_likelihood(img, mask_local_binary, mask_global_binary)
1461
+ if visualization:
1462
+ plot_img(likelihood, tlt='Likelihood Function', cmp='bwr')
1463
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1464
+
1465
+ report(' 4. Computing of A Posteriori Global and Local (Ternary) Masks...')
1466
+ local_time = time.time()
1467
+ p_local, p_global = calc_aposteriori_probabilities(p_em, mask_local_binary, likelihood)
1468
+ del p_em, mask_global_binary, mask_local_binary, likelihood
1469
+ mask_local_ternary = calc_ternary_mask(img, p_local, met=ternary_met)
1470
+ mask_global_ternary = calc_ternary_mask(img, p_global, met=ternary_met)
1471
+ report('\tclustering method: {0}'.format(ternary_met))
1472
+ if visualization:
1473
+ plot_img(mask_local_ternary, tlt='A Posteriori Local Mask ({0})'.format(ternary_met))
1474
+ plot_img(mask_global_ternary, tlt='A Posteriori Global Mask ({0})'.format(ternary_met))
1475
+ del p_local, p_global
1476
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1477
+
1478
+ report(' 5. Convexity Analysis...\n\tformation of:\n'+
1479
+ '\t -initial Nuclei Segmentation (NS)\n\t -list of Nuclei Clusters (NC)\n\t -list of Small Objects (SO)')
1480
+ local_time = time.time()
1481
+ nuc_seg, nuc_clust, small_obj, nuclei_clusters_info = convexity_analysis(mask_global_ternary, area_thr=area_threshold, cp_thr=convexity_threshold)
1482
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1483
+
1484
+ report(' 6. Processing of Nuclei Clusters and Small Objects...')
1485
+ local_time = time.time()
1486
+ nuc_seg, small_obj = process_clusters(img, mask_local_ternary, nuc_clust, nuc_seg, small_obj, nuclei_clusters_info,
1487
+ cp_thr=convexity_threshold, area_thr=area_threshold, cy5_thr=cell_marker_threshold, dist_tr=dist_tr)
1488
+ nuc_seg = process_small_objects(small_obj, nuc_seg, nuclei_clusters_info, img.shape[0], img.shape[1])
1489
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1490
+
1491
+ report(' 7. Shape Smoothing of Segmented Nuclei...')
1492
+ local_time = time.time()
1493
+ nuc_seg, nuc_seg_status = filter_nuclei_mask(nuc_seg)
1494
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1495
+
1496
+ report('\nNUCLEI SEGMENTATION COMPLETED: processing time = {0:.2f} seconds'.format(time.time()-global_time))
1497
+ return nuc_seg, nuc_seg_status, mask_local_ternary, mask_global_ternary
1498
+
1499
+ def nuclei_cell_segmentation(intensity,
1500
+ area_threshold=20,
1501
+ convexity_threshold=4,
1502
+ cell_marker_threshold=25,
1503
+ dist_tr='GDT',
1504
+ sigma0=3,
1505
+ k0=1,
1506
+ r0=5,
1507
+ pct=[0.01, 0.01],
1508
+ nk=[5, 10, 20, 40],
1509
+ t0=0.5,
1510
+ ternary_met='Argmax',
1511
+ visualization=False,
1512
+ area_ratio_threshold=0.65,
1513
+ dilation_radius=9):
1514
+ """Performes unsupervised segmentation of nuclei and then cells successively.
1515
+
1516
+ Parameters
1517
+ ----------
1518
+ intensity : 3D (height, width, channels) numpy array.
1519
+ Intensities of nuclei marker (channel index = 0) and cell membrane marker (channel index = 1).
1520
+ area_threshold : int (positive).
1521
+ The minimal possible area of a nucleus in pixels.
1522
+ convexity_threshold : int (positive)
1523
+ The largest allowable deviation (in pixels) of a boundary point from its convex hull for a convex component.
1524
+ cell_marker_threshold : int (positive).
1525
+ Defines whether a cell membrane marker is present in a cluster of nuclei.
1526
+ dist_tr : string, one of "DT" or "GDT".
1527
+ Defines geometrical distance transform (DT) or gradient distance transform (GDT) in the Virtual Cuts.
1528
+ sigma0 : float (positive).
1529
+ The standard deviation of the Gaussian filter.
1530
+ k0 : float (positive).
1531
+ The degree of smoothing of the Gradient Adaptive Filter.
1532
+ r0 : int (positive).
1533
+ The disk kernel radius of the local Otsu method.
1534
+ pct : list of two positive floats, [float (positive), float (positive)].
1535
+ The background a priori probability thresholds for every of two channels.
1536
+ nk : list of positive integers, [int (positive), int (positive), ...].
1537
+ The list of kernel sizes in the Local Mean Suppression Filter.
1538
+ t0 : float (positive).
1539
+ The intensity threshold in the Local Mean Suppression Filter.
1540
+ ternary_met : string, one of "Argmax" or "Kmeans".
1541
+ Defines the algorithm to compute a posteriori local and global masks.
1542
+ visualization : bool.
1543
+ Plots a priori probabilities, a posteriori local and global masks,
1544
+ contrast based likelihood function, and nuclei and cell segmentations.
1545
+ area_ratio_threshold : float (positive).
1546
+ The area threshold for a cell without a nucleus.
1547
+ dilation_radius : int (positive).
1548
+ The nucleus mask is morphologically dilated by this amount for cells without cell membrane marker expression.
1549
+
1550
+ Returns
1551
+ -------
1552
+ nuc_seg : 2D (height, width) numpy 'int32' array.
1553
+ Mask of segmented nuclei.
1554
+ cel_seg : 2D (height, width) numpy 'int32' array.
1555
+ Mask of segmented cells.
1556
+ n_nuclei : int.
1557
+ Number of segmented nuclei.
1558
+ n_cells : int.
1559
+ Number of segmented cells."""
1560
+ global_time = time.time()
1561
+ nuc_seg, nuc_seg_status, mask_local_ternary, mask_global_ternary = nuclei_segmentation(intensity,
1562
+ area_threshold=area_threshold,
1563
+ convexity_threshold=convexity_threshold,
1564
+ cell_marker_threshold=cell_marker_threshold,
1565
+ dist_tr=dist_tr,
1566
+ sigma0=sigma0,
1567
+ k0=k0,
1568
+ r0=r0,
1569
+ pct=pct,
1570
+ nk=nk,
1571
+ t0=t0,
1572
+ ternary_met=ternary_met,
1573
+ visualization=visualization)
1574
+ report('\nCELL SEGMENTATION...\n')
1575
+
1576
+ report(' 8. Formation of Initial Cell Segmentation...')
1577
+ local_time = time.time()
1578
+ cel_seg, nuclei_prop, nuclei, n_nuclei = initialize_cell_segmentation(nuc_seg)
1579
+ report('\tinitial number of segmented cells = {0}'.format(n_nuclei))
1580
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1581
+
1582
+ report(' 9. Skeletonization of Cell Membrane Global Ternary Mask...')
1583
+ local_time = time.time()
1584
+ mask_membraines = mask_global_ternary[:,:,0] == 255
1585
+ area_threshold_cell = np.max([0.5*nuc_seg_status['area_avr'], nuc_seg_status['area_avr']-nuc_seg_status['area_std']])
1586
+ report('\tMinimal Area of a Cell without Nucleus = {0:.1f} pxs'.format(area_threshold_cell))
1587
+ mask_membraines_cuts, mask_skeleton = get_mask_cuts_mod(mask_membraines, area_threshold_cell)
1588
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1589
+
1590
+ report('10. Euler Number Filtering...')
1591
+ local_time = time.time()
1592
+ cel_seg, nuclei, n_cells = euler_number_filter_mod(cel_seg, mask_membraines_cuts, nuclei, n_nuclei,
1593
+ small_area=area_threshold_cell, area_ratio_thr=area_ratio_threshold)
1594
+ report('\tfinal number of segmented cells = {0}'.format(n_cells))
1595
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1596
+
1597
+ report('11. Isolated Nuclei Filtering...')
1598
+ local_time = time.time()
1599
+ cel_seg, nuclei = isolated_nuclei_filter(cel_seg, mask_membraines, nuclei_prop, nuclei, u0=dilation_radius)
1600
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1601
+
1602
+ report('12. Threshold Filtering and Smoothing of Cell Segmentation...')
1603
+ local_time = time.time()
1604
+ cel_seg = threshold_filter(cel_seg, mask_skeleton, nuclei_prop, nuclei)
1605
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1606
+
1607
+ if visualization:
1608
+ report('13. Plotting of Nuclei and Cell Segmentations...')
1609
+ local_time = time.time()
1610
+ img = normalize_img(intensity)
1611
+ img = intensity2rgb(img, return_rgb=True)
1612
+ plot_img(mark_boundaries(img, nuc_seg, color=(1,1,1)), tlt='Nuclei Segmentation: number of nuclei = {0}'.format(n_nuclei))
1613
+ plot_img(mark_boundaries(img, cel_seg, color=(0,1,0)), tlt='Cell Segmentation: number of cells = {0}'.format(n_cells))
1614
+ report('\t-processing time = {0:.2f} seconds'.format(time.time()-local_time))
1615
+
1616
+ report('\nCELL SEGMENTATION COMPLETED: total processing time = {0:.2f} seconds'.format(time.time()-global_time))
1617
+ return nuc_seg, cel_seg, n_nuclei, n_cells
1618
+