fishfeats 1.1.19.post1.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.
fish_feats/Analysis.py ADDED
@@ -0,0 +1,239 @@
1
+ """
2
+ To handle post pipeline analysis
3
+ - Hierarchical clustering: from csv results file and segmented cells, perform and display clustering.
4
+
5
+ """
6
+
7
+ import numpy as np
8
+ import pathlib, os, csv
9
+
10
+ import matplotlib as mpl
11
+ from matplotlib.backends.backend_qt5agg import FigureCanvas
12
+ from matplotlib.figure import Figure
13
+ import matplotlib.pyplot as plt
14
+
15
+ from scipy.cluster.hierarchy import dendrogram
16
+ from scipy.cluster.hierarchy import fcluster
17
+ from scipy.spatial.distance import pdist, squareform
18
+ from scipy.cluster.hierarchy import ward
19
+ from skimage.io import imsave
20
+ from sklearn.preprocessing import scale
21
+
22
+ import napari
23
+ from magicgui import magicgui
24
+ from napari.utils.notifications import show_info
25
+
26
+ import fish_feats.Utils as ut
27
+ import fish_feats.MainImage as mi
28
+
29
+ ## disable scipy cluster warning
30
+ from scipy.cluster.hierarchy import ClusterWarning
31
+ from warnings import simplefilter
32
+ simplefilter("ignore", ClusterWarning)
33
+
34
+ def do_hierarchy():
35
+ mig = mi.MainImage( talkative=True )
36
+ viewer = napari.current_viewer()
37
+ viewer.title = "ZENnapari"
38
+
39
+ filename = ut.dialog_filename()
40
+ if filename is None:
41
+ print("No file selected")
42
+ return
43
+
44
+ mig.open_image( filename=filename )
45
+ ut.update_history(mig.imagedir)
46
+
47
+ for chanel in range(mig.nbchannels):
48
+ cmap = ut.colormapname(chanel)
49
+ img = mig.get_channel(chanel)
50
+ cview = viewer.add_image( img, name="originalChannel"+str(chanel), blending="additive", scale=(mig.scaleZ, mig.scaleXY, mig.scaleXY), colormap=cmap )
51
+ dint = np.max(img)-np.min(img)
52
+ cview.contrast_limits=(np.min(img), np.max(img)-0.75*dint)
53
+ viewer.axes.visible = True
54
+
55
+ return getScales(mig, viewer)
56
+
57
+ def getScales(mig, viewer):
58
+ @magicgui(call_button="Update",
59
+ scaleXY={"widget_type": "LiteralEvalLineEdit"},
60
+ scaleZ={"widget_type": "LiteralEvalLineEdit"},
61
+ )
62
+ def get_scale( scaleXY= mig.scaleXY, scaleZ= mig.scaleZ,
63
+ segmented_cells=pathlib.Path(mig.junction_filename(dim=2,ifexist=True)),
64
+ ):
65
+ mig.scaleXY = scaleXY
66
+ mig.scaleZ = scaleZ
67
+ for chan in range(mig.nbchannels):
68
+ viewer.layers['originalChannel'+str(chan)].scale = [mig.scaleZ, mig.scaleXY, mig.scaleXY]
69
+ viewer.window.remove_dock_widget("all")
70
+ mig.load_segmentation( segmented_cells )
71
+ mig.popFromJunctions()
72
+
73
+ hiera = HierAnalysis()
74
+ hiera.set(mig, viewer)
75
+ hiera.get_data()
76
+
77
+ wid = viewer.window.add_dock_widget(get_scale, name="Scale")
78
+ return wid
79
+
80
+ ############ hierarchical analysis
81
+ class HierAnalysis:
82
+ """ Perform and display hierarchical analysis based on selected features """
83
+
84
+ def __init__(self):
85
+ self.nclusters = 4
86
+ self.wid = None
87
+ self.clustward = None
88
+
89
+ def set(self, mig, viewer):
90
+ self.mig = mig
91
+ self.viewer = viewer
92
+ self.cluster_img = np.zeros(mig.get_image_shape(in2d=True), np.uint8)
93
+ self.featlayer = self.viewer.add_labels(self.cluster_img, name="ClusteredCells", scale=(mig.scaleXY, mig.scaleXY), opacity=1)
94
+
95
+ def get_data(self):
96
+ """ Interface to select the file and the parameters """
97
+ def load_file():
98
+ """ Load the excel/csv file """
99
+ with open(get_columns.load_file.value, 'r') as infile:
100
+ csvreader = csv.DictReader(infile)
101
+ print(csvreader.fieldnames)
102
+ get_columns.use_column.choices = csvreader.fieldnames
103
+
104
+ def load_table():
105
+ """ Load the specific columns """
106
+ keep = get_columns.use_column.value
107
+ self.prepare_data( get_columns.load_file.value, keep )
108
+ self.show_clusters()
109
+
110
+ def update_clusters():
111
+ """ Update all with the new number of clusters chosen """
112
+ self.nclusters = int(get_columns.nb_clusters.value)
113
+ self.show_clusters()
114
+
115
+ def save_cluscells():
116
+ """ Save image of cells colored by cluster """
117
+ ccells = self.featlayer.data
118
+ outname = self.mig.build_filename( endname="_ClusteredCells_nclus_"+str(self.nclusters)+".png" )
119
+ vis = []
120
+ for lay in self.viewer.layers:
121
+ vis.append(lay.visible)
122
+ lay.visible = False
123
+ self.featlayer.visible = True
124
+ screenshot = self.viewer.screenshot()
125
+ for visib, lay in zip(vis, self.viewer.layers):
126
+ lay.visible = visib
127
+
128
+ imsave(outname, screenshot)
129
+ show_info("Saved in "+outname)
130
+
131
+ def save_dendrogram_img():
132
+ """ Save image of dendrogram to file """
133
+ outname = self.mig.build_filename(endname="_ClusterDendrogram_nclus_"+str(self.nclusters)+".png")
134
+ self.fig.savefig(outname)
135
+ show_info("Saved in "+outname)
136
+
137
+
138
+ @magicgui(call_button="Cluster from selected columns",
139
+ use_column = dict(widget_type="Select", choices=[]),
140
+ nb_clusters={"widget_type": "Slider", "min":1, "max": 50},
141
+ save_clustered_cells={"widget_type":"PushButton", "value": False},
142
+ save_dendrogram={"widget_type":"PushButton", "value": False},
143
+ )
144
+ def get_columns(
145
+ #load_file=pathlib.Path(self.mig.rnacount_filename(ifexist=True)),
146
+ load_file=pathlib.Path(self.mig.resdir),
147
+ use_column = [],
148
+ nb_clusters = 4,
149
+ save_clustered_cells=False, save_dendrogram=False,
150
+ ):
151
+ load_table()
152
+
153
+ get_columns.load_file.changed.connect(load_file)
154
+ get_columns.nb_clusters.changed.connect(update_clusters)
155
+ get_columns.save_clustered_cells.clicked.connect(save_cluscells)
156
+ get_columns.save_dendrogram.clicked.connect(save_dendrogram_img)
157
+ self.viewer.window.add_dock_widget( get_columns, name="Load data" )
158
+
159
+
160
+ def get_cluster_colors(self):
161
+ """ To have same color between the label layer and the matplotlib plot """
162
+ return [0] + [mpl.colors.rgb2hex(self.featlayer.get_color(i+1)) for i in range(self.nclusters)]
163
+
164
+ def check_label(self, columns, lab):
165
+ """ Check if a label is in the list that should not """
166
+ if lab in columns:
167
+ ut.show_warning("Warning, "+lab+" is in the selected features list, that's weird")
168
+
169
+ def prepare_data(self, filename, columns):
170
+ """ normalisation of the data """
171
+ res = []
172
+ self.labels = []
173
+ self.check_label(columns, "CellLabel")
174
+ self.check_label(columns, "CellID")
175
+ self.check_label(columns, "NucleusID")
176
+ self.check_label(columns, "NucleusLabel")
177
+ with open(filename, 'r') as infile:
178
+ csvreader = csv.DictReader(infile)
179
+ for row in csvreader:
180
+ cres = []
181
+ clab = int(row["CellLabel"])
182
+ for col in columns:
183
+ cres.append(float(row[col])+1)
184
+ res.append(cres)
185
+ self.labels.append(clab)
186
+ tab = np.array(res)
187
+ nans = np.isnan( res ).any( axis=1 )
188
+ kinds = [ind for ind in range(tab.shape[0]) if not nans[ind] ]
189
+ tab = tab[ kinds ]
190
+ self.labels = [ self.labels[ind] for ind in kinds]
191
+ negs = ( tab<0 ).any( axis=1 )
192
+ kinds = [ind for ind in range(tab.shape[0]) if not negs[ind] ]
193
+ tab = tab[ kinds ]
194
+ self.labels = [ self.labels[ind] for ind in kinds]
195
+ tab = np.log(tab)
196
+ tab = scale(tab)
197
+ print(tab.shape)
198
+
199
+ dist_tab = pdist(tab, metric='euclidean')
200
+ dist_tab = squareform(dist_tab)
201
+ self.clustward = ward(dist_tab)
202
+
203
+
204
+ def show_clusters(self):
205
+ """ Show dendogram and classified cells """
206
+ if self.clustward is None:
207
+ return
208
+ clustered = fcluster(self.clustward, t=self.nclusters, criterion="maxclust")
209
+
210
+ self.mig.set_cells(self.cluster_img, clustered, self.labels)
211
+ self.featlayer.refresh()
212
+ #self.cmap = self.featlayer.colormap.colors
213
+ if self.wid is None:
214
+ self.wid = self.create_plotwidget()
215
+ self.update_plotwidget(clustered)
216
+ if "Dendrogram" not in self.viewer.window._dock_widgets:
217
+ self.viewer.window.add_dock_widget( self.wid, name="Dendrogram" )
218
+
219
+ def create_plotwidget(self):
220
+ mpl_widget = FigureCanvas( Figure(figsize=(6,6) ) )
221
+ self.fig = mpl_widget.figure
222
+ self.ax = mpl_widget.figure.subplots()
223
+ return mpl_widget
224
+
225
+ def update_plotwidget(self, clustered):
226
+ clus_str = [ f"cluster #{l}: n={c}\n" for (l,c) in zip(*np.unique(clustered, return_counts=True)) ]
227
+
228
+ cluster_colors = self.get_cluster_colors()
229
+ cluster_colors_array = [cluster_colors[cl] for cl in clustered]
230
+ link_cols = {}
231
+ for i, i12 in enumerate(self.clustward[:,:2].astype(int)):
232
+ c1, c2 = (link_cols[x] if x > len(self.clustward) else cluster_colors_array[x] for x in i12)
233
+ link_cols[i+1+len(self.clustward)] = c1 if c1 == c2 else 'k'
234
+
235
+ self.ax.cla()
236
+ dend = dendrogram(self.clustward, p=8, truncate_mode='level', no_labels=True, ax=self.ax, link_color_func=lambda x: link_cols[x] )
237
+ self.fig.canvas.draw_idle()
238
+ #plt.show()
239
+
@@ -0,0 +1,230 @@
1
+ ## Associate contours and nuclei by distance
2
+ ## algorithm hongrois: Kuhn-Munkres
3
+ import numpy as np
4
+ from math import sqrt, floor
5
+ from scipy.ndimage.morphology import distance_transform_edt
6
+ from munkres import Munkres
7
+ from skimage.measure import label, regionprops
8
+
9
+ def distance2DCenters( cent0, cent1, scaleXY ):
10
+ if cent0 is None:
11
+ return 0
12
+ return sqrt( (cent0[0]-cent1[0])*(cent0[0]-cent1[0]) + (cent0[1]-cent1[1])*(cent0[1]-cent1[1]) )*scaleXY
13
+
14
+ def associateWindows(wlab, wbal, dlim, scaleXY):
15
+ labels = np.unique(wlab[wlab>0])
16
+ balels = np.unique(wbal[wbal>0])
17
+ nbal = len(balels)
18
+ nlab = len(labels)
19
+ resbal = np.zeros_like(wbal)
20
+ n = max(nbal, nlab)
21
+ matrix = np.zeros((n, n)) ## should be square.
22
+ if n == 0:
23
+ return resbal
24
+
25
+ ## distance_transform is distance to closest background, so inverse
26
+ dist2lab, nearest_coord = distance_transform_edt(wlab==0, return_indices=True)
27
+ dillabels = np.zeros_like(wlab)
28
+ ## check if within reasonnable distance
29
+ dilate_mask = ((dist2lab*scaleXY*scaleXY) <= dlim)
30
+ masked_nearest_label_coords = [ dind[dilate_mask] for dind in nearest_coord]
31
+ nearest_labels = wlab[tuple(masked_nearest_label_coords)]
32
+ dillabels[dilate_mask] = nearest_labels
33
+
34
+ ## look for best fit
35
+ for j, jlab in enumerate(balels):
36
+ nei = dillabels[wbal==jlab]
37
+ for i, ilab in enumerate(labels):
38
+ count = np.sum(nei==ilab)
39
+ if count > 0:
40
+ matrix[i][j] = 1.0/(count+1)
41
+ else:
42
+ matrix[i][j] = dlim+1
43
+
44
+ ## algorithm hongrois: Kuhn-Munkres
45
+ dmat = np.copy(matrix)
46
+ munk = Munkres()
47
+ assoc = munk.compute(matrix)
48
+ matrix = None
49
+ munk = None
50
+ for asso in assoc:
51
+ if asso[0]<nlab and asso[1]<nbal:
52
+ if (dmat[asso[0]][asso[1]] > 0) and (dmat[asso[0]][asso[1]]<dlim):
53
+ ## associate
54
+ resbal[wbal==balels[asso[1]]] = labels[asso[0]]
55
+
56
+ return resbal
57
+
58
+ def associateLabWithLab(lab, bal, dlim, scaleXY):
59
+ """ associate labels of img 2 to labels of img 1 """
60
+ ##### do overlapping windows otherwise calcul distances too slow (to test)
61
+ sizex = 1000
62
+ sizey = 1000
63
+ over = 50
64
+ sizes = lab.shape
65
+
66
+ posy = 0
67
+ resbal = np.zeros(bal.shape, dtype="uint16")
68
+ while posy < sizes[0]:
69
+ posx = 0
70
+ while posx < sizes[1]:
71
+ windowbal = np.copy(bal[posy:(posy+sizey),posx:(posx+sizex)])
72
+ windowlab = lab[posy:(posy+sizey),posx:(posx+sizex)]
73
+ assobal = associateWindows( windowlab, windowbal, dlim, scaleXY )
74
+ bal[posy:(posy+sizey),posx:(posx+sizex)][assobal>0] = 0
75
+ resbal[posy:(posy+sizey),posx:(posx+sizex)][assobal>0] = assobal[assobal>0]
76
+
77
+ posx += (sizex-over)
78
+ posy += (sizey-over)
79
+
80
+ ## what is left in bal image has not been associated, add it to the result image as new label
81
+ maxlab = np.max(lab)
82
+ for l in np.unique(bal):
83
+ if l > 0:
84
+ resbal[bal==l] = maxlab+1
85
+ maxlab = np.max(resbal)
86
+
87
+ return resbal
88
+
89
+ def associateNucleus(labs, dlimit=3, scaleXY=1):
90
+ """ Associate each slice with previous slice """
91
+ for i in range(len(labs)):
92
+ if i > 1:
93
+ rlab = associateLabWithLab( labs[i-1,], labs[i,], dlimit, scaleXY )
94
+ labs[i,] = rlab
95
+ return labs
96
+
97
+ def associateOverlap( labimg, labimgprev, threshold_overlap=0.25):
98
+ nuclei_prop = regionprops( labimg, intensity_image=labimgprev )
99
+ new_label = np.max(labimgprev) + 1
100
+ #taken = []
101
+ for nucprop in nuclei_prop:
102
+ overlap = nucprop.image_intensity
103
+ overlabs, counts = np.unique(overlap, return_counts=True)
104
+ if 0 in overlabs:
105
+ zero = np.where(overlabs==0)
106
+ zero = zero[0]
107
+ overlabs = [over for i, over in enumerate(overlabs) if i != zero]
108
+ counts = [over for i, over in enumerate(counts) if i != zero]
109
+
110
+ done = False
111
+ if len(counts)>0:
112
+ maxlabind = np.argmax( counts )
113
+ #maxlabind = maxlabind[0]
114
+ if counts[maxlabind]/nucprop.area > threshold_overlap:
115
+ ## overlap between the two labels, associate
116
+ labimg[nucprop.bbox[0]:nucprop.bbox[2], nucprop.bbox[1]:nucprop.bbox[3]][nucprop.image] = overlabs[maxlabind]
117
+ done = True
118
+
119
+ if not done:
120
+ # no match found, new label
121
+ labimg[nucprop.bbox[0]:nucprop.bbox[2], nucprop.bbox[1]:nucprop.bbox[3]][nucprop.image] = new_label
122
+ new_label = new_label + 1
123
+ #taken.append(maxlabind)
124
+ return labimg
125
+
126
+
127
+ def associateNucleusOverlap(labs, threshold_overlap):
128
+ """ Associate each slice with previous slice based on IOU """
129
+ for i, lab in enumerate(labs):
130
+ if i >= 1:
131
+ plab = associateOverlap( lab, plab, threshold_overlap )
132
+ else:
133
+ # first slice
134
+ plab = lab
135
+ labs[i,] = plab
136
+ return labs
137
+
138
+
139
+ def associate_objects(pop, wnuc, wcells, dlim, scaleXY, scaleZ):
140
+ ## algorithm hongrois: Kuhn-Munkres
141
+ ncells = len(wcells)
142
+ nnuclei = len(wnuc)
143
+ n = max(ncells, nnuclei)
144
+ #print(str(ncells)+" "+str(nnuclei)+" "+str(n))
145
+
146
+ matrix = np.zeros((n,n)) ## should be square.
147
+ row = 0
148
+ for i, cell in enumerate(wcells):
149
+ for j, nuc in enumerate(wnuc):
150
+ matrix[i][j] = pop.distanceNucleusToCell( nuc, cell, scaleXY, scaleZ ) ## put more weights to XY distance than z
151
+
152
+ munk = Munkres()
153
+ dmat = np.copy(matrix)
154
+ assoc = munk.compute(matrix)
155
+ munk = None
156
+ associated = []
157
+ associatedNuc = []
158
+ #acells = []
159
+ ## assoc contient indices. If indices > nnuclei or ncells, mean non associated
160
+ for asso in assoc:
161
+ if asso[0] < ncells and asso[1] < nnuclei:
162
+ if dmat[asso[0]][asso[1]] < dlim:
163
+ ## associate them
164
+ pop.associateNucleusAndRelabel( nucleus=wnuc[asso[1]], cell=wcells[asso[0]] )
165
+ associated.append(asso[0])
166
+ associatedNuc.append(asso[1])
167
+
168
+ dmat = None
169
+ return associated, associatedNuc
170
+
171
+
172
+ def associate_nucleiToCell(pop, imgsizes, dlim=20, scaleXY=1, scaleZ=1, pbar=None):
173
+ """ Need scaling for non isotropic distances """
174
+ ##### do overlapping windows otherwise association too slow (too big matrix)
175
+ sizex = floor(180/scaleXY)
176
+ sizey = floor(180/scaleXY)
177
+ over = floor(40/scaleXY)
178
+ margin = floor(5/scaleXY)
179
+
180
+ posy = 0
181
+ #fullcells = [] ## cells with associated nuclei
182
+ nuclei = pop.nuclei.values()
183
+ cells = pop.cells.values()
184
+ if pbar is not None:
185
+ pbar.total = imgsizes[0]
186
+ while posy < imgsizes[0]:
187
+ if pbar is not None:
188
+ pbar.update(posy)
189
+ posx = 0
190
+ while posx < imgsizes[1]:
191
+ border = (posy, posx, posy+sizey, posx+sizex)
192
+
193
+ winuclei = []
194
+ leftnuclei = []
195
+ #print("newcnts "+str(len(newcnts)))
196
+ for nuc in nuclei:
197
+ if nuc.insideBorderCenter(border):
198
+ winuclei.append(nuc)
199
+ else:
200
+ leftnuclei.append(nuc)
201
+ nuclei = leftnuclei
202
+
203
+ wincells = []
204
+ border = (posy-margin, posx-margin, posy+sizey+margin, posx+sizex+margin)
205
+ leftcells = []
206
+ for cell in cells:
207
+ if cell.insideBorderCenter(border):
208
+ wincells.append(cell)
209
+ else:
210
+ leftcells.append(cell)
211
+
212
+ if len(winuclei)>0 and len(wincells)>0:
213
+ associated, associatedNuc = associate_objects(pop, winuclei, wincells, dlim, scaleXY, scaleZ)
214
+ #fullcells = fullcells + acells
215
+
216
+ for i in range(len(wincells)):
217
+ if i not in associated:
218
+ leftcells.append(wincells[i])
219
+
220
+ for i in range(len(winuclei)):
221
+ if i not in associatedNuc:
222
+ nuclei.append(winuclei[i])
223
+
224
+ cells = leftcells
225
+
226
+ posx += (sizex-over)
227
+ posy += (sizey-over)
228
+
229
+ print("Unassociated nuclei left "+str(len(nuclei)))
230
+ pop.relabelUnassociatedNuclei(nuclei)