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.
- scikit_ops-0.1.0.dev0.dist-info/METADATA +372 -0
- scikit_ops-0.1.0.dev0.dist-info/RECORD +66 -0
- scikit_ops-0.1.0.dev0.dist-info/WHEEL +4 -0
- scikit_ops-0.1.0.dev0.dist-info/entry_points.txt +2 -0
- skop/__init__.py +14 -0
- skop/envs/cellpose3/pixi.toml +46 -0
- skop/envs/cupy/pixi.toml +35 -0
- skop/envs/minimal/pixi.toml +25 -0
- skop/envs/pytorch/pixi.toml +88 -0
- skop/envs/sdeconv/pixi.toml +85 -0
- skop/envs/segment-everything/pixi.toml +96 -0
- skop/envs/skimage/pixi.toml +28 -0
- skop/envs/stardist-tf/pixi.toml +91 -0
- skop/envs/unseg-cv/pixi.toml +27 -0
- skop/models.py +145 -0
- skop/ops/__init__.py +19 -0
- skop/ops/_util.py +115 -0
- skop/ops/deconvolve/__init__.py +18 -0
- skop/ops/deconvolve/_edges.py +92 -0
- skop/ops/deconvolve/_pad.py +117 -0
- skop/ops/deconvolve/richardson_lucy.py +100 -0
- skop/ops/deconvolve/richardson_lucy_cupy.py +116 -0
- skop/ops/detect/__init__.py +28 -0
- skop/ops/detect/_result.py +45 -0
- skop/ops/detect/fastsam.py +103 -0
- skop/ops/detect/jdll_yolo.py +115 -0
- skop/ops/detect/object_aware_yolo.py +112 -0
- skop/ops/detect/yolo.py +441 -0
- skop/ops/edges.py +382 -0
- skop/ops/generate.py +77 -0
- skop/ops/kernels/__init__.py +39 -0
- skop/ops/kernels/_fluorophore.py +42 -0
- skop/ops/kernels/_recenter.py +60 -0
- skop/ops/kernels/gibson_lanni.py +147 -0
- skop/ops/kernels/paraxial.py +128 -0
- skop/ops/kernels/psf.py +59 -0
- skop/ops/labels.py +144 -0
- skop/ops/mask/__init__.py +29 -0
- skop/ops/mask/_result.py +40 -0
- skop/ops/mask/microsam.py +114 -0
- skop/ops/mask/mobilesam.py +176 -0
- skop/ops/morphology.py +214 -0
- skop/ops/normalize.py +85 -0
- skop/ops/segment/__init__.py +24 -0
- skop/ops/segment/cellpose3.py +245 -0
- skop/ops/segment/cellpose4.py +127 -0
- skop/ops/segment/instanseg.py +162 -0
- skop/ops/segment/stardist2d.py +192 -0
- skop/ops/segment/starfun3d.py +104 -0
- skop/ops/segment/unseg/__init__.py +138 -0
- skop/ops/segment/unseg/_algorithm.py +1618 -0
- skop/ops/smooth.py +452 -0
- skop/ops/threshold.py +278 -0
- skop/ops/toy.py +114 -0
- skop/ops/train/__init__.py +81 -0
- skop/ops/train/_util.py +152 -0
- skop/ops/train/cellpose3.py +189 -0
- skop/ops/train/cellpose4.py +164 -0
- skop/ops/train/stardist2d.py +439 -0
- skop/ops/workflows/__init__.py +43 -0
- skop/ops/workflows/deconvolve/__init__.py +7 -0
- skop/ops/workflows/deconvolve/with_psf.py +103 -0
- skop/ops/workflows/mask/__init__.py +7 -0
- skop/ops/workflows/mask/detect_then_mask.py +97 -0
- skop/ops/workflows/segment/__init__.py +7 -0
- 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
|
+
|