celldetective 1.0.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. celldetective/__init__.py +2 -0
  2. celldetective/__main__.py +432 -0
  3. celldetective/datasets/segmentation_annotations/blank +0 -0
  4. celldetective/datasets/signal_annotations/blank +0 -0
  5. celldetective/events.py +149 -0
  6. celldetective/extra_properties.py +100 -0
  7. celldetective/filters.py +89 -0
  8. celldetective/gui/__init__.py +20 -0
  9. celldetective/gui/about.py +44 -0
  10. celldetective/gui/analyze_block.py +563 -0
  11. celldetective/gui/btrack_options.py +898 -0
  12. celldetective/gui/classifier_widget.py +386 -0
  13. celldetective/gui/configure_new_exp.py +532 -0
  14. celldetective/gui/control_panel.py +438 -0
  15. celldetective/gui/gui_utils.py +495 -0
  16. celldetective/gui/json_readers.py +113 -0
  17. celldetective/gui/measurement_options.py +1425 -0
  18. celldetective/gui/neighborhood_options.py +452 -0
  19. celldetective/gui/plot_signals_ui.py +1042 -0
  20. celldetective/gui/process_block.py +1055 -0
  21. celldetective/gui/retrain_segmentation_model_options.py +706 -0
  22. celldetective/gui/retrain_signal_model_options.py +643 -0
  23. celldetective/gui/seg_model_loader.py +460 -0
  24. celldetective/gui/signal_annotator.py +2388 -0
  25. celldetective/gui/signal_annotator_options.py +340 -0
  26. celldetective/gui/styles.py +217 -0
  27. celldetective/gui/survival_ui.py +903 -0
  28. celldetective/gui/tableUI.py +608 -0
  29. celldetective/gui/thresholds_gui.py +1300 -0
  30. celldetective/icons/logo-large.png +0 -0
  31. celldetective/icons/logo.png +0 -0
  32. celldetective/icons/signals_icon.png +0 -0
  33. celldetective/icons/splash-test.png +0 -0
  34. celldetective/icons/splash.png +0 -0
  35. celldetective/icons/splash0.png +0 -0
  36. celldetective/icons/survival2.png +0 -0
  37. celldetective/icons/vignette_signals2.png +0 -0
  38. celldetective/icons/vignette_signals2.svg +114 -0
  39. celldetective/io.py +2050 -0
  40. celldetective/links/zenodo.json +561 -0
  41. celldetective/measure.py +1258 -0
  42. celldetective/models/segmentation_effectors/blank +0 -0
  43. celldetective/models/segmentation_generic/blank +0 -0
  44. celldetective/models/segmentation_targets/blank +0 -0
  45. celldetective/models/signal_detection/blank +0 -0
  46. celldetective/models/tracking_configs/mcf7.json +68 -0
  47. celldetective/models/tracking_configs/ricm.json +203 -0
  48. celldetective/models/tracking_configs/ricm2.json +203 -0
  49. celldetective/neighborhood.py +717 -0
  50. celldetective/scripts/analyze_signals.py +51 -0
  51. celldetective/scripts/measure_cells.py +275 -0
  52. celldetective/scripts/segment_cells.py +212 -0
  53. celldetective/scripts/segment_cells_thresholds.py +140 -0
  54. celldetective/scripts/track_cells.py +206 -0
  55. celldetective/scripts/train_segmentation_model.py +246 -0
  56. celldetective/scripts/train_signal_model.py +49 -0
  57. celldetective/segmentation.py +712 -0
  58. celldetective/signals.py +2826 -0
  59. celldetective/tracking.py +974 -0
  60. celldetective/utils.py +1681 -0
  61. celldetective-1.0.2.dist-info/LICENSE +674 -0
  62. celldetective-1.0.2.dist-info/METADATA +192 -0
  63. celldetective-1.0.2.dist-info/RECORD +66 -0
  64. celldetective-1.0.2.dist-info/WHEEL +5 -0
  65. celldetective-1.0.2.dist-info/entry_points.txt +2 -0
  66. celldetective-1.0.2.dist-info/top_level.txt +1 -0
@@ -0,0 +1,1300 @@
1
+ import math
2
+
3
+ import skimage
4
+ from PyQt5.QtWidgets import QAction, QMenu, QMainWindow, QMessageBox, QLabel, QWidget, QFileDialog, QHBoxLayout, \
5
+ QGridLayout, QLineEdit, QScrollArea, QVBoxLayout, QComboBox, QPushButton, QApplication, QPushButton
6
+ from PyQt5.QtGui import QDoubleValidator, QIntValidator
7
+ from matplotlib.backends.backend_qt import NavigationToolbar2QT
8
+ from matplotlib.patches import Circle
9
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
10
+ from scipy import ndimage
11
+ from skimage.morphology import disk
12
+
13
+ from celldetective.filters import std_filter, gauss_filter
14
+ from celldetective.gui.gui_utils import center_window, FigureCanvas, ListWidget, FilterChoice, color_from_class
15
+ from celldetective.utils import get_software_location, extract_experiment_channels, rename_intensity_column
16
+ from celldetective.io import auto_load_number_of_frames, load_frames
17
+ from celldetective.segmentation import threshold_image, identify_markers_from_binary, apply_watershed, \
18
+ segment_frame_from_thresholds
19
+ from scipy.ndimage import binary_fill_holes
20
+ from PyQt5.QtCore import Qt, QSize
21
+ from glob import glob
22
+ from superqt.fonticon import icon
23
+ from fonticon_mdi6 import MDI6
24
+ import numpy as np
25
+ import matplotlib.pyplot as plt
26
+ from superqt import QLabeledSlider, QLabeledDoubleRangeSlider, QLabeledDoubleSlider
27
+ from celldetective.segmentation import filter_image
28
+ import pandas as pd
29
+ from skimage.measure import regionprops_table
30
+ import json
31
+ import os
32
+
33
+
34
+ class ThresholdConfigWizard(QMainWindow):
35
+ """
36
+ UI to create a threshold pipeline for segmentation.
37
+
38
+ """
39
+
40
+ def __init__(self, parent=None):
41
+
42
+ super().__init__()
43
+ self.parent = parent
44
+ self.screen_height = self.parent.parent.parent.parent.screen_height
45
+ self.screen_width = self.parent.parent.parent.parent.screen_width
46
+ self.setMinimumWidth(int(0.8 * self.screen_width))
47
+ self.setMinimumHeight(int(0.8 * self.screen_height))
48
+ self.setWindowTitle("Threshold configuration wizard")
49
+ center_window(self)
50
+ self._createActions()
51
+ self._createMenuBar()
52
+
53
+ self.mode = self.parent.mode
54
+ self.pos = self.parent.parent.parent.pos
55
+ self.exp_dir = self.parent.parent.exp_dir
56
+ self.soft_path = get_software_location()
57
+ self.footprint = 30
58
+ self.min_dist = 30
59
+ self.onlyFloat = QDoubleValidator()
60
+ self.onlyInt = QIntValidator()
61
+ self.cell_properties = ['centroid', 'area', 'perimeter', 'eccentricity', 'intensity_mean', 'solidity']
62
+
63
+ if self.mode == "targets":
64
+ self.config_out_name = "threshold_targets.json"
65
+ elif self.mode == "effectors":
66
+ self.config_out_name = "threshold_effectors.json"
67
+
68
+ self.locate_stack()
69
+ if self.img is not None:
70
+ self.threshold_slider = QLabeledDoubleRangeSlider()
71
+ self.initialize_histogram()
72
+ self.show_image()
73
+ self.initalize_props_scatter()
74
+ self.prep_cell_properties()
75
+ self.populate_widget()
76
+ self.setAttribute(Qt.WA_DeleteOnClose)
77
+
78
+ def _createMenuBar(self):
79
+ menuBar = self.menuBar()
80
+ # Creating menus using a QMenu object
81
+ fileMenu = QMenu("&File", self)
82
+ fileMenu.addAction(self.openAction)
83
+ menuBar.addMenu(fileMenu)
84
+
85
+ # Creating menus using a title
86
+ # editMenu = menuBar.addMenu("&Edit")
87
+ # helpMenu = menuBar.addMenu("&Help")
88
+
89
+ def _createActions(self):
90
+ # Creating action using the first constructor
91
+ # self.newAction = QAction(self)
92
+ # self.newAction.setText("&New")
93
+ # Creating actions using the second constructor
94
+ self.openAction = QAction(icon(MDI6.folder), "&Open...", self)
95
+ self.openAction.triggered.connect(self.load_previous_config)
96
+
97
+ def populate_widget(self):
98
+
99
+ """
100
+ Create the multibox design.
101
+
102
+ """
103
+ self.button_widget = QWidget()
104
+ main_layout = QHBoxLayout()
105
+ self.button_widget.setLayout(main_layout)
106
+
107
+ main_layout.setContentsMargins(30, 30, 30, 30)
108
+
109
+ self.scroll_area = QScrollArea()
110
+ self.scroll_container = QWidget()
111
+ self.scroll_area.setWidgetResizable(True)
112
+ self.scroll_area.setWidget(self.scroll_container)
113
+
114
+ self.left_panel = QVBoxLayout(self.scroll_container)
115
+ self.left_panel.setContentsMargins(30, 30, 30, 30)
116
+ self.left_panel.setSpacing(10)
117
+ self.populate_left_panel()
118
+
119
+ # Right panel
120
+ self.right_panel = QVBoxLayout()
121
+ self.populate_right_panel()
122
+
123
+ # threhsold options
124
+ # self.left_panel.addWidget(self.cell_fcanvas)
125
+
126
+ # Animation
127
+ # self.right_panel.addWidget(self.fcanvas)
128
+
129
+ # self.populate_left_panel()
130
+ # grid.addLayout(self.left_side, 0, 0, 1, 1)
131
+
132
+ # self.scroll_area.setAlignment(Qt.AlignCenter)
133
+ # self.scroll_area.setLayout(self.left_panel)
134
+ # self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
135
+ # self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAsNeeded)
136
+ # self.scroll_area.setWidgetResizable(True)
137
+
138
+ main_layout.addWidget(self.scroll_area, 35)
139
+ main_layout.addLayout(self.right_panel, 65)
140
+ self.button_widget.adjustSize()
141
+
142
+ self.setCentralWidget(self.button_widget)
143
+ self.show()
144
+
145
+ QApplication.processEvents()
146
+
147
+ def populate_left_panel(self):
148
+
149
+ self.filters_qlist = ListWidget(self, FilterChoice, [])
150
+
151
+ grid_preprocess = QGridLayout()
152
+ grid_preprocess.setContentsMargins(20, 20, 20, 20)
153
+
154
+ filter_list_option_grid = QHBoxLayout()
155
+ section_preprocess = QLabel("Preprocessing")
156
+ section_preprocess.setStyleSheet("font-weight: bold;")
157
+ filter_list_option_grid.addWidget(section_preprocess, 90, alignment=Qt.AlignLeft)
158
+
159
+ self.delete_filter = QPushButton("")
160
+ self.delete_filter.setStyleSheet(self.parent.parent.parent.parent.button_select_all)
161
+ self.delete_filter.setIcon(icon(MDI6.trash_can, color="black"))
162
+ self.delete_filter.setToolTip("Remove filter")
163
+ self.delete_filter.setIconSize(QSize(20, 20))
164
+ self.delete_filter.clicked.connect(self.filters_qlist.removeSel)
165
+
166
+ self.add_filter = QPushButton("")
167
+ self.add_filter.setStyleSheet(self.parent.parent.parent.parent.button_select_all)
168
+ self.add_filter.setIcon(icon(MDI6.filter_plus, color="black"))
169
+ self.add_filter.setToolTip("Add filter")
170
+ self.add_filter.setIconSize(QSize(20, 20))
171
+ self.add_filter.clicked.connect(self.filters_qlist.addItem)
172
+
173
+ # filter_list_option_grid.addWidget(QLabel(""),90)
174
+ filter_list_option_grid.addWidget(self.delete_filter, 5)
175
+ filter_list_option_grid.addWidget(self.add_filter, 5)
176
+
177
+ grid_preprocess.addLayout(filter_list_option_grid, 0, 0, 1, 3)
178
+ grid_preprocess.addWidget(self.filters_qlist, 1, 0, 1, 3)
179
+
180
+ self.apply_filters_btn = QPushButton("Apply")
181
+ self.apply_filters_btn.setIcon(icon(MDI6.filter_cog_outline, color="white"))
182
+ self.apply_filters_btn.setIconSize(QSize(20, 20))
183
+ self.apply_filters_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
184
+ self.apply_filters_btn.clicked.connect(self.preprocess_image)
185
+ grid_preprocess.addWidget(self.apply_filters_btn, 2, 0, 1, 3)
186
+
187
+ self.left_panel.addLayout(grid_preprocess)
188
+
189
+ ###################
190
+ # THRESHOLD SECTION
191
+ ###################
192
+
193
+ grid_threshold = QGridLayout()
194
+ grid_threshold.setContentsMargins(20, 20, 20, 20)
195
+ idx = 0
196
+
197
+ threshold_title_grid = QHBoxLayout()
198
+ section_threshold = QLabel("Threshold")
199
+ section_threshold.setStyleSheet("font-weight: bold;")
200
+ threshold_title_grid.addWidget(section_threshold, 90, alignment=Qt.AlignCenter)
201
+
202
+ self.ylog_check = QPushButton("")
203
+ self.ylog_check.setIcon(icon(MDI6.math_log, color="black"))
204
+ self.ylog_check.setStyleSheet(self.parent.parent.parent.parent.button_select_all)
205
+ self.ylog_check.clicked.connect(self.switch_to_log)
206
+ threshold_title_grid.addWidget(self.ylog_check, 5)
207
+
208
+ self.equalize_option_btn = QPushButton("")
209
+ self.equalize_option_btn.setIcon(icon(MDI6.equalizer, color="black"))
210
+ self.equalize_option_btn.setIconSize(QSize(20, 20))
211
+ self.equalize_option_btn.setStyleSheet(self.parent.parent.parent.parent.button_select_all)
212
+ self.equalize_option_btn.setToolTip("Enable histogram matching")
213
+ self.equalize_option_btn.clicked.connect(self.activate_histogram_equalizer)
214
+ self.equalize_option = False
215
+ threshold_title_grid.addWidget(self.equalize_option_btn, 5)
216
+
217
+ grid_threshold.addLayout(threshold_title_grid, idx, 0, 1, 2)
218
+
219
+ idx += 1
220
+
221
+ # Slider to set vmin & vmax
222
+ self.threshold_slider.setSingleStep(0.00001)
223
+ self.threshold_slider.setTickInterval(0.00001)
224
+ self.threshold_slider.setOrientation(1)
225
+ self.threshold_slider.setDecimals(3)
226
+ self.threshold_slider.setRange(np.amin(self.img), np.amax(self.img))
227
+ self.threshold_slider.setValue([np.percentile(self.img.flatten(), 90), np.amax(self.img)])
228
+ self.threshold_slider.valueChanged.connect(self.threshold_changed)
229
+
230
+ # self.initialize_histogram()
231
+ grid_threshold.addWidget(self.canvas_hist, idx, 0, 1, 3)
232
+
233
+ idx += 1
234
+
235
+ # self.threshold_contrast_range.valueChanged.connect(self.set_clim_thresh)
236
+
237
+ grid_threshold.addWidget(self.threshold_slider, idx, 1, 1, 1)
238
+ self.canvas_hist.setMinimumHeight(self.screen_height // 6)
239
+ self.left_panel.addLayout(grid_threshold)
240
+
241
+ self.generate_marker_contents()
242
+ self.generate_props_contents()
243
+
244
+ #################
245
+ # FINAL SAVE BTN#
246
+ #################
247
+
248
+ self.save_btn = QPushButton('Save')
249
+ self.save_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
250
+ self.save_btn.clicked.connect(self.write_instructions)
251
+ self.left_panel.addWidget(self.save_btn)
252
+
253
+ self.properties_box_widgets = [self.propscanvas, *self.features_cb,
254
+ self.property_query_le, self.submit_query_btn, self.save_btn]
255
+ for p in self.properties_box_widgets:
256
+ p.setEnabled(False)
257
+
258
+ def generate_marker_contents(self):
259
+
260
+ marker_box = QVBoxLayout()
261
+ marker_box.setContentsMargins(30, 30, 30, 30)
262
+
263
+ marker_lbl = QLabel('Markers')
264
+ marker_lbl.setStyleSheet("font-weight: bold;")
265
+ marker_box.addWidget(marker_lbl, alignment=Qt.AlignCenter)
266
+
267
+ hbox_footprint = QHBoxLayout()
268
+ hbox_footprint.addWidget(QLabel('Footprint: '), 20)
269
+ self.footprint_slider = QLabeledSlider()
270
+ self.footprint_slider.setSingleStep(1)
271
+ self.footprint_slider.setOrientation(1)
272
+ self.footprint_slider.setRange(1, self.binary.shape[0] // 4)
273
+ self.footprint_slider.setValue(self.footprint)
274
+ self.footprint_slider.valueChanged.connect(self.set_footprint)
275
+ hbox_footprint.addWidget(self.footprint_slider, 80)
276
+ marker_box.addLayout(hbox_footprint)
277
+
278
+ hbox_distance = QHBoxLayout()
279
+ hbox_distance.addWidget(QLabel('Min distance: '), 20)
280
+ self.min_dist_slider = QLabeledSlider()
281
+ self.min_dist_slider.setSingleStep(1)
282
+ self.min_dist_slider.setOrientation(1)
283
+ self.min_dist_slider.setRange(0, self.binary.shape[0] // 4)
284
+ self.min_dist_slider.setValue(self.min_dist)
285
+ self.min_dist_slider.valueChanged.connect(self.set_min_dist)
286
+ hbox_distance.addWidget(self.min_dist_slider, 80)
287
+ marker_box.addLayout(hbox_distance)
288
+
289
+ hbox_marker_btns = QHBoxLayout()
290
+
291
+ self.markers_btn = QPushButton("Run")
292
+ self.markers_btn.clicked.connect(self.detect_markers)
293
+ self.markers_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
294
+ hbox_marker_btns.addWidget(self.markers_btn)
295
+
296
+ self.watershed_btn = QPushButton("Watershed")
297
+ self.watershed_btn.setIcon(icon(MDI6.waves_arrow_up, color="white"))
298
+ self.watershed_btn.setIconSize(QSize(20, 20))
299
+ self.watershed_btn.clicked.connect(self.apply_watershed_to_selection)
300
+ self.watershed_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
301
+ self.watershed_btn.setEnabled(False)
302
+ hbox_marker_btns.addWidget(self.watershed_btn)
303
+ marker_box.addLayout(hbox_marker_btns)
304
+
305
+ self.left_panel.addLayout(marker_box)
306
+
307
+ def generate_props_contents(self):
308
+
309
+ properties_box = QVBoxLayout()
310
+ properties_box.setContentsMargins(30, 30, 30, 30)
311
+
312
+ properties_lbl = QLabel('Filter on properties')
313
+ properties_lbl.setStyleSheet('font-weight: bold;')
314
+ properties_box.addWidget(properties_lbl, alignment=Qt.AlignCenter)
315
+
316
+ properties_box.addWidget(self.propscanvas)
317
+
318
+ self.features_cb = [QComboBox() for i in range(2)]
319
+ for i in range(2):
320
+ hbox_feat = QHBoxLayout()
321
+ hbox_feat.addWidget(QLabel(f'feature {i}: '), 20)
322
+ hbox_feat.addWidget(self.features_cb[i], 80)
323
+ properties_box.addLayout(hbox_feat)
324
+
325
+ hbox_classify = QHBoxLayout()
326
+ hbox_classify.addWidget(QLabel('remove: '), 10)
327
+ self.property_query_le = QLineEdit()
328
+ self.property_query_le.setPlaceholderText(
329
+ 'eliminate points using a query such as: area > 100 or eccentricity > 0.95')
330
+ hbox_classify.addWidget(self.property_query_le, 70)
331
+ self.submit_query_btn = QPushButton('Submit...')
332
+ self.submit_query_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
333
+ self.submit_query_btn.clicked.connect(self.apply_property_query)
334
+ hbox_classify.addWidget(self.submit_query_btn, 20)
335
+ properties_box.addLayout(hbox_classify)
336
+
337
+ self.left_panel.addLayout(properties_box)
338
+
339
+ def populate_right_panel(self):
340
+
341
+ self.right_panel.addWidget(self.fcanvas, 70)
342
+
343
+ channel_hbox = QHBoxLayout()
344
+ channel_hbox.setContentsMargins(150, 30, 150, 5)
345
+ self.channels_cb = QComboBox()
346
+ self.channels_cb.addItems(self.channel_names)
347
+ self.channels_cb.currentTextChanged.connect(self.reload_frame)
348
+ channel_hbox.addWidget(QLabel('channel: '), 10)
349
+ channel_hbox.addWidget(self.channels_cb, 90)
350
+ self.right_panel.addLayout(channel_hbox)
351
+
352
+ frame_hbox = QHBoxLayout()
353
+ frame_hbox.setContentsMargins(150, 5, 150, 5)
354
+ self.frame_slider = QLabeledSlider()
355
+ self.frame_slider.setSingleStep(1)
356
+ self.frame_slider.setOrientation(1)
357
+ self.frame_slider.setRange(0, self.len_movie - 1)
358
+ self.frame_slider.setValue(0)
359
+ self.frame_slider.valueChanged.connect(self.reload_frame)
360
+ frame_hbox.addWidget(QLabel('frame: '), 10)
361
+ frame_hbox.addWidget(self.frame_slider, 90)
362
+ self.right_panel.addLayout(frame_hbox)
363
+
364
+ contrast_hbox = QHBoxLayout()
365
+ contrast_hbox.setContentsMargins(150, 5, 150, 5)
366
+ self.contrast_slider = QLabeledDoubleRangeSlider()
367
+ self.contrast_slider.setSingleStep(0.00001)
368
+ self.contrast_slider.setTickInterval(0.00001)
369
+ self.contrast_slider.setOrientation(1)
370
+ self.contrast_slider.setRange(np.amin(self.img), np.amax(self.img))
371
+ self.contrast_slider.setValue([np.percentile(self.img.flatten(), 1), np.percentile(self.img.flatten(), 99.99)])
372
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
373
+ contrast_hbox.addWidget(QLabel('contrast: '))
374
+ contrast_hbox.addWidget(self.contrast_slider, 90)
375
+ self.right_panel.addLayout(contrast_hbox)
376
+
377
+ def locate_stack(self):
378
+
379
+ """
380
+ Locate the target movie.
381
+
382
+ """
383
+
384
+ print("this is the loaded position: ", self.pos)
385
+ if isinstance(self.pos, str):
386
+ movies = glob(self.pos + f"movie/{self.parent.parent.parent.movie_prefix}*.tif")
387
+
388
+ else:
389
+ msgBox = QMessageBox()
390
+ msgBox.setIcon(QMessageBox.Warning)
391
+ msgBox.setText("Please select a unique position before launching the wizard...")
392
+ msgBox.setWindowTitle("Warning")
393
+ msgBox.setStandardButtons(QMessageBox.Ok)
394
+ returnValue = msgBox.exec()
395
+ if returnValue == QMessageBox.Ok:
396
+ self.img = None
397
+ self.close()
398
+ return None
399
+
400
+ if len(movies) == 0:
401
+ msgBox = QMessageBox()
402
+ msgBox.setIcon(QMessageBox.Warning)
403
+ msgBox.setText("No movies are detected in the experiment folder. Cannot load an image to test Haralick.")
404
+ msgBox.setWindowTitle("Warning")
405
+ msgBox.setStandardButtons(QMessageBox.Ok)
406
+ returnValue = msgBox.exec()
407
+ if returnValue == QMessageBox.Yes:
408
+ self.close()
409
+ else:
410
+ self.stack_path = movies[0]
411
+ self.len_movie = self.parent.parent.parent.len_movie
412
+ len_movie_auto = auto_load_number_of_frames(self.stack_path)
413
+ if len_movie_auto is not None:
414
+ self.len_movie = len_movie_auto
415
+ exp_config = self.exp_dir + "config.ini"
416
+ self.channel_names, self.channels = extract_experiment_channels(exp_config)
417
+ self.channel_names = np.array(self.channel_names)
418
+ self.channels = np.array(self.channels)
419
+ self.nbr_channels = len(self.channels)
420
+ self.current_channel = 0
421
+ self.img = load_frames(0, self.stack_path, normalize_input=False)
422
+ print(self.img.shape)
423
+ print(f'{self.stack_path} successfully located.')
424
+
425
+ def show_image(self):
426
+
427
+ """
428
+ Load an image.
429
+
430
+ """
431
+
432
+ self.fig, self.ax = plt.subplots(tight_layout=True)
433
+ self.fcanvas = FigureCanvas(self.fig, interactive=True)
434
+ self.ax.clear()
435
+
436
+ self.im = self.ax.imshow(self.img, cmap='gray')
437
+
438
+ self.binary = threshold_image(self.img, self.threshold_slider.value()[0], self.threshold_slider.value()[1],
439
+ foreground_value=255., fill_holes=False)
440
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
441
+ self.image_thresholded = self.ax.imshow(self.thresholded_image, cmap="viridis", alpha=0.5, interpolation='none')
442
+
443
+ self.ax.set_xticks([])
444
+ self.ax.set_yticks([])
445
+ self.ax.set_aspect('equal')
446
+
447
+ self.fig.set_facecolor('none') # or 'None'
448
+ self.fig.canvas.setStyleSheet("background-color: black;")
449
+ self.scat_markers = self.ax.scatter([], [], color="tab:red")
450
+
451
+ self.fcanvas.canvas.draw()
452
+
453
+ def initalize_props_scatter(self):
454
+
455
+ """
456
+ Define properties scatter.
457
+ """
458
+
459
+ self.fig_props, self.ax_props = plt.subplots(tight_layout=True)
460
+ self.propscanvas = FigureCanvas(self.fig_props, interactive=True)
461
+ self.fig_props.set_facecolor('none')
462
+ self.fig_props.canvas.setStyleSheet("background-color: transparent;")
463
+ self.scat_props = self.ax_props.scatter([], [], color='k', alpha=0.75)
464
+ self.propscanvas.canvas.draw_idle()
465
+ self.propscanvas.canvas.setMinimumHeight(self.screen_height // 5)
466
+
467
+ def initialize_histogram(self):
468
+
469
+ self.fig_hist, self.ax_hist = plt.subplots(tight_layout=True)
470
+ self.canvas_hist = FigureCanvas(self.fig_hist, interactive=False)
471
+ self.fig_hist.set_facecolor('none')
472
+ self.fig_hist.canvas.setStyleSheet("background-color: transparent;")
473
+
474
+ # self.ax_hist.clear()
475
+ # self.ax_hist.cla()
476
+ self.ax_hist.patch.set_facecolor('none')
477
+ self.hist_y, x, _ = self.ax_hist.hist(self.img.flatten(), density=True, bins=300, color="k")
478
+ # self.ax_hist.set_xlim(np.amin(self.img),np.amax(self.img))
479
+ self.ax_hist.set_xlabel('intensity [a.u.]')
480
+ self.ax_hist.spines['top'].set_visible(False)
481
+ self.ax_hist.spines['right'].set_visible(False)
482
+ # self.ax_hist.set_yticks([])
483
+ self.ax_hist.set_xlim(np.amin(self.img), np.amax(self.img))
484
+ self.ax_hist.set_ylim(0, self.hist_y.max())
485
+
486
+ self.threshold_slider.setRange(np.amin(self.img), np.amax(self.img))
487
+ self.threshold_slider.setValue([np.nanpercentile(self.img.flatten(), 90), np.amax(self.img)])
488
+ self.add_hist_threshold()
489
+
490
+ self.canvas_hist.canvas.draw_idle()
491
+ self.canvas_hist.canvas.setMinimumHeight(self.screen_height // 8)
492
+
493
+ def update_histogram(self):
494
+
495
+ """
496
+ Redraw the histogram after an update on the image.
497
+ Move the threshold slider accordingly.
498
+
499
+ """
500
+
501
+ self.ax_hist.clear()
502
+ self.ax_hist.patch.set_facecolor('none')
503
+ self.hist_y, x, _ = self.ax_hist.hist(self.img.flatten(), density=True, bins=300, color="k")
504
+ self.ax_hist.set_xlabel('intensity [a.u.]')
505
+ self.ax_hist.spines['top'].set_visible(False)
506
+ self.ax_hist.spines['right'].set_visible(False)
507
+ # self.ax_hist.set_yticks([])
508
+ self.ax_hist.set_xlim(np.amin(self.img), np.amax(self.img))
509
+ self.ax_hist.set_ylim(0, self.hist_y.max())
510
+ self.add_hist_threshold()
511
+ self.canvas_hist.canvas.draw()
512
+
513
+ self.threshold_slider.setRange(np.amin(self.img), np.amax(self.img))
514
+ self.threshold_slider.setValue([np.nanpercentile(self.img.flatten(), 90), np.amax(self.img)])
515
+ self.threshold_changed(self.threshold_slider.value())
516
+
517
+ def add_hist_threshold(self):
518
+
519
+ ymin, ymax = self.ax_hist.get_ylim()
520
+ self.min_intensity_line, = self.ax_hist.plot(
521
+ [self.threshold_slider.value()[0], self.threshold_slider.value()[0]], [0, ymax], c="tab:purple")
522
+ self.max_intensity_line, = self.ax_hist.plot(
523
+ [self.threshold_slider.value()[1], self.threshold_slider.value()[1]], [0, ymax], c="tab:purple")
524
+
525
+ # self.canvas_hist.canvas.draw_idle()
526
+
527
+ def reload_frame(self):
528
+
529
+ """
530
+ Load the frame from the current channel and time choice. Show imshow, update histogram.
531
+ """
532
+
533
+ self.clear_post_threshold_options()
534
+
535
+ self.current_channel = self.channels_cb.currentIndex()
536
+ t = int(self.frame_slider.value())
537
+ idx = t * self.nbr_channels + self.current_channel
538
+ self.img = load_frames(idx, self.stack_path, normalize_input=False)
539
+ if self.img is not None:
540
+ self.refresh_imshow()
541
+ self.update_histogram()
542
+ # self.redo_histogram()
543
+ else:
544
+ print('Frame could not be loaded...')
545
+
546
+ # def redo_histogram(self):
547
+ # self.ax_hist.clear()
548
+ # self.canvas_hist.canvas.draw()
549
+
550
+ def contrast_slider_action(self):
551
+
552
+ """
553
+ Recontrast the imshow as the contrast slider is moved.
554
+ """
555
+
556
+ self.vmin = self.contrast_slider.value()[0]
557
+ self.vmax = self.contrast_slider.value()[1]
558
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
559
+
560
+ self.fcanvas.canvas.draw_idle()
561
+
562
+ def refresh_imshow(self):
563
+
564
+ """
565
+
566
+ Update the imshow based on the current frame selection.
567
+
568
+ """
569
+
570
+ self.vmin = np.nanpercentile(self.img.flatten(), 1)
571
+ self.vmax = np.nanpercentile(self.img.flatten(), 99.)
572
+
573
+ self.contrast_slider.disconnect()
574
+ self.contrast_slider.setRange(np.amin(self.img), np.amax(self.img))
575
+ self.contrast_slider.setValue([self.vmin, self.vmax])
576
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
577
+
578
+ self.im.set_data(self.img)
579
+ self.im.set_clim(vmin=self.vmin, vmax=self.vmax)
580
+ self.fcanvas.canvas.draw_idle()
581
+
582
+ # self.initialize_histogram()
583
+
584
+ def preprocess_image(self):
585
+
586
+ """
587
+ Reload the frame, apply the filters, update imshow and histogram.
588
+
589
+ """
590
+
591
+ self.reload_frame()
592
+ filters = self.filters_qlist.items
593
+ self.img = filter_image(self.img, filters)
594
+ self.refresh_imshow()
595
+ self.update_histogram()
596
+
597
+ def threshold_changed(self, value):
598
+
599
+ """
600
+ Move the threshold values on histogram, when slider is moved.
601
+ """
602
+
603
+ self.clear_post_threshold_options()
604
+
605
+ self.thresh_min = value[0]
606
+ self.thresh_max = value[1]
607
+ ymin, ymax = self.ax_hist.get_ylim()
608
+ self.min_intensity_line.set_data([self.thresh_min, self.thresh_min], [0, ymax])
609
+ self.max_intensity_line.set_data([self.thresh_max, self.thresh_max], [0, ymax])
610
+ self.canvas_hist.canvas.draw_idle()
611
+ # update imshow threshold
612
+ self.update_threshold()
613
+
614
+ def switch_to_log(self):
615
+
616
+ """
617
+ Switch threshold histogram to log scale. Auto adjust.
618
+ """
619
+
620
+ if self.ax_hist.get_yscale() == 'linear':
621
+ self.ax_hist.set_yscale('log')
622
+ else:
623
+ self.ax_hist.set_yscale('linear')
624
+
625
+ # self.ax_hist.autoscale()
626
+ self.ax_hist.set_ylim(0, self.hist_y.max())
627
+ self.canvas_hist.canvas.draw_idle()
628
+
629
+ def update_threshold(self):
630
+
631
+ """
632
+
633
+ Threshold and binarize the image based on the min/max threshold values
634
+ and display on imshow.
635
+
636
+ """
637
+
638
+ self.binary = threshold_image(self.img, self.threshold_slider.value()[0], self.threshold_slider.value()[1],
639
+ foreground_value=255., fill_holes=False)
640
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
641
+ self.image_thresholded.set_data(self.thresholded_image)
642
+ self.fcanvas.canvas.draw_idle()
643
+
644
+ def set_footprint(self):
645
+ self.footprint = self.footprint_slider.value()
646
+
647
+ # print(f"Setting footprint to {self.footprint}")
648
+
649
+ def set_min_dist(self):
650
+ self.min_dist = self.min_dist_slider.value()
651
+
652
+ # print(f"Setting min distance to {self.min_dist}")
653
+
654
+ def detect_markers(self):
655
+
656
+ self.clear_post_threshold_options()
657
+
658
+ if self.binary.ndim == 3:
659
+ self.binary = np.squeeze(self.binary)
660
+ self.binary = binary_fill_holes(self.binary)
661
+ self.coords, self.edt_map = identify_markers_from_binary(self.binary, self.min_dist,
662
+ footprint_size=self.footprint, footprint=None,
663
+ return_edt=True)
664
+ if len(self.coords) > 0:
665
+ self.scat_markers.set_offsets(self.coords[:, [1, 0]])
666
+ self.scat_markers.set_visible(True)
667
+ self.fcanvas.canvas.draw()
668
+ self.scat_props.set_visible(True)
669
+ self.watershed_btn.setEnabled(True)
670
+ else:
671
+ self.watershed_btn.setEnabled(False)
672
+
673
+ def apply_watershed_to_selection(self):
674
+
675
+ self.labels = apply_watershed(self.binary, self.coords, self.edt_map)
676
+
677
+ self.current_channel = self.channels_cb.currentIndex()
678
+ t = int(self.frame_slider.value())
679
+ idx = t * self.nbr_channels + self.current_channel
680
+ self.img = load_frames(idx, self.stack_path, normalize_input=False)
681
+ self.refresh_imshow()
682
+
683
+ self.image_thresholded.set_cmap('tab20c')
684
+ self.image_thresholded.set_data(np.ma.masked_where(self.labels == 0., self.labels))
685
+ self.image_thresholded.autoscale()
686
+ self.fcanvas.canvas.draw_idle()
687
+
688
+ self.compute_features()
689
+ for p in self.properties_box_widgets:
690
+ p.setEnabled(True)
691
+
692
+ for i in range(2):
693
+ self.features_cb[i].currentTextChanged.connect(self.update_props_scatter)
694
+
695
+ def compute_features(self):
696
+
697
+ # Run regionprops to have properties for filtering
698
+ intensity_image_idx = [self.nbr_channels * self.frame_slider.value()]
699
+ for i in range(self.nbr_channels - 1):
700
+ intensity_image_idx += [intensity_image_idx[-1] + 1]
701
+
702
+ # Load channels at time t
703
+ multichannel = load_frames(intensity_image_idx, self.stack_path, normalize_input=False)
704
+ self.props = pd.DataFrame(
705
+ regionprops_table(self.labels, intensity_image=multichannel, properties=self.cell_properties))
706
+ self.props = rename_intensity_column(self.props, self.channel_names)
707
+ self.props['radial_distance'] = np.sqrt((self.props['centroid-1'] - self.img.shape[0] / 2) ** 2 + (
708
+ self.props['centroid-0'] - self.img.shape[1] / 2) ** 2)
709
+
710
+ for i in range(2):
711
+ self.features_cb[i].clear()
712
+ self.features_cb[i].addItems(list(self.props.columns))
713
+ self.features_cb[i].setCurrentIndex(i)
714
+ self.props["class"] = 1
715
+
716
+ self.update_props_scatter()
717
+
718
+ def update_props_scatter(self):
719
+
720
+ self.scat_props.set_offsets(
721
+ self.props[[self.features_cb[1].currentText(), self.features_cb[0].currentText()]].to_numpy())
722
+ self.scat_props.set_facecolor([color_from_class(c) for c in self.props['class'].to_numpy()])
723
+ self.ax_props.set_xlabel(self.features_cb[1].currentText())
724
+ self.ax_props.set_ylabel(self.features_cb[0].currentText())
725
+
726
+ self.scat_markers.set_offsets(self.props[['centroid-1', 'centroid-0']].to_numpy())
727
+ self.scat_markers.set_color(['k'] * len(self.props))
728
+ self.scat_markers.set_facecolor([color_from_class(c) for c in self.props['class'].to_numpy()])
729
+
730
+ self.ax_props.set_xlim(0.75 * self.props[self.features_cb[1].currentText()].min(),
731
+ 1.05 * self.props[self.features_cb[1].currentText()].max())
732
+ self.ax_props.set_ylim(0.75 * self.props[self.features_cb[0].currentText()].min(),
733
+ 1.05 * self.props[self.features_cb[0].currentText()].max())
734
+ self.propscanvas.canvas.draw_idle()
735
+ self.fcanvas.canvas.draw_idle()
736
+
737
+ def prep_cell_properties(self):
738
+
739
+ self.cell_properties_options = list(np.copy(self.cell_properties))
740
+ self.cell_properties_options.remove("centroid")
741
+ for k in range(self.nbr_channels):
742
+ self.cell_properties_options.append(f'intensity_mean-{k}')
743
+ self.cell_properties_options.remove('intensity_mean')
744
+
745
+ def apply_property_query(self):
746
+ query = self.property_query_le.text()
747
+ self.props['class'] = 1
748
+
749
+ if query == '':
750
+ print('empty query')
751
+ else:
752
+ try:
753
+ self.selection = self.props.query(query).index
754
+ print(self.selection)
755
+ self.props.loc[self.selection, 'class'] = 0
756
+ except Exception as e:
757
+ print(e)
758
+ print(self.props.columns)
759
+ msgBox = QMessageBox()
760
+ msgBox.setIcon(QMessageBox.Warning)
761
+ msgBox.setText(f"The query could not be understood. No filtering was applied. {e}")
762
+ msgBox.setWindowTitle("Warning")
763
+ msgBox.setStandardButtons(QMessageBox.Ok)
764
+ returnValue = msgBox.exec()
765
+ if returnValue == QMessageBox.Ok:
766
+ return None
767
+
768
+ self.update_props_scatter()
769
+
770
+ def clear_post_threshold_options(self):
771
+
772
+ self.watershed_btn.setEnabled(False)
773
+
774
+ for p in self.properties_box_widgets:
775
+ p.setEnabled(False)
776
+
777
+ for i in range(2):
778
+ try:
779
+ self.features_cb[i].disconnect()
780
+ except:
781
+ pass
782
+ self.features_cb[i].clear()
783
+
784
+ self.property_query_le.setText('')
785
+
786
+ self.binary = threshold_image(self.img, self.threshold_slider.value()[0], self.threshold_slider.value()[1],
787
+ foreground_value=255., fill_holes=False)
788
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
789
+
790
+ self.scat_markers.set_color('tab:red')
791
+ self.scat_markers.set_visible(False)
792
+ self.image_thresholded.set_data(self.thresholded_image)
793
+ self.image_thresholded.set_cmap('viridis')
794
+ self.image_thresholded.autoscale()
795
+
796
+ def write_instructions(self):
797
+
798
+ instructions = {
799
+ "target_channel": self.channels_cb.currentText(), # for now index but would be more universal to use name
800
+ "thresholds": self.threshold_slider.value(),
801
+ "filters": self.filters_qlist.items,
802
+ "marker_min_distance": self.min_dist,
803
+ "marker_footprint_size": self.footprint,
804
+ "feature_queries": [self.property_query_le.text()],
805
+ "equalize_reference": [self.equalize_option, self.frame_slider.value()],
806
+ }
807
+
808
+ print('The following instructions will be written: ', instructions)
809
+ self.instruction_file = \
810
+ QFileDialog.getSaveFileName(self, "Save File", self.exp_dir + f'configs/threshold_config_{self.mode}.json',
811
+ '.json')[0]
812
+ if self.instruction_file != '':
813
+ json_object = json.dumps(instructions, indent=4)
814
+ with open(self.instruction_file, "w") as outfile:
815
+ outfile.write(json_object)
816
+ print("Configuration successfully written in ", self.instruction_file)
817
+
818
+ self.parent.filename = self.instruction_file
819
+ self.parent.file_label.setText(self.instruction_file[:16] + '...')
820
+ self.parent.file_label.setToolTip(self.instruction_file)
821
+
822
+ self.close()
823
+ else:
824
+ print('The instruction file could not be written...')
825
+
826
+ def activate_histogram_equalizer(self):
827
+
828
+ if not self.equalize_option:
829
+ self.equalize_option = True
830
+ self.equalize_option_btn.setIcon(icon(MDI6.equalizer, color="#1f77b4"))
831
+ self.equalize_option_btn.setIconSize(QSize(20, 20))
832
+ else:
833
+ self.equalize_option = False
834
+ self.equalize_option_btn.setIcon(icon(MDI6.equalizer, color="black"))
835
+ self.equalize_option_btn.setIconSize(QSize(20, 20))
836
+
837
+ def load_previous_config(self):
838
+ self.previous_instruction_file = \
839
+ QFileDialog.getOpenFileName(self, "Load config",
840
+ self.exp_dir + f'configs/threshold_config_{self.mode}.json',
841
+ "JSON (*.json)")[0]
842
+ with open(self.previous_instruction_file, 'r') as f:
843
+ threshold_instructions = json.load(f)
844
+
845
+ target_channel = threshold_instructions['target_channel']
846
+ index = self.channels_cb.findText(target_channel)
847
+ self.channels_cb.setCurrentIndex(index)
848
+
849
+ filters = threshold_instructions['filters']
850
+ items_to_add = [f[0] + '_filter' for f in filters]
851
+ self.filters_qlist.list_widget.clear()
852
+ self.filters_qlist.list_widget.addItems(items_to_add)
853
+ self.filters_qlist.items = filters
854
+
855
+ self.apply_filters_btn.click()
856
+
857
+ thresholds = threshold_instructions['thresholds']
858
+ self.threshold_slider.setValue(thresholds)
859
+
860
+ marker_footprint_size = threshold_instructions['marker_footprint_size']
861
+ self.footprint_slider.setValue(marker_footprint_size)
862
+
863
+ marker_min_dist = threshold_instructions['marker_min_distance']
864
+ self.min_dist_slider.setValue(marker_min_dist)
865
+
866
+ self.markers_btn.click()
867
+ self.watershed_btn.click()
868
+
869
+ feature_queries = threshold_instructions['feature_queries']
870
+ self.property_query_le.setText(feature_queries[0])
871
+ self.submit_query_btn.click()
872
+
873
+
874
+ class ThresholdNormalisation(ThresholdConfigWizard):
875
+ def __init__(self, min_threshold, current_channel, parent=None):
876
+ QMainWindow.__init__(self)
877
+ self.parent = parent
878
+ self.screen_height = self.parent.parent.parent.parent.screen_height
879
+ self.screen_width = self.parent.parent.parent.parent.screen_width
880
+ self.setMaximumWidth(int(self.screen_width // 3))
881
+ self.setMinimumHeight(int(0.8 * self.screen_height))
882
+ self.setWindowTitle("Normalisation threshold preview")
883
+ center_window(self)
884
+ self.img = None
885
+ self.min_threshold = min_threshold
886
+ self.current_channel = current_channel
887
+ self.mode = self.parent.mode
888
+ self.pos = self.parent.parent.parent.pos
889
+ self.exp_dir = self.parent.parent.exp_dir
890
+ self.soft_path = get_software_location()
891
+ self.auto_close = False
892
+
893
+ self.locate_stack()
894
+ if not self.auto_close:
895
+ if self.img is not None:
896
+ self.test_frame = np.squeeze(self.img)
897
+ self.frame = std_filter(gauss_filter(self.test_frame, 2), 4)
898
+ self.threshold_slider = QLabeledDoubleSlider()
899
+ self.threshold_slider.setOrientation(1)
900
+ self.initialize_histogram()
901
+ self.show_threshold_image()
902
+ self.populate_norm_widget()
903
+ self.threshold_changed(self.threshold_slider.value())
904
+ self.setAttribute(Qt.WA_DeleteOnClose)
905
+ else:
906
+ self.close()
907
+
908
+ def show_threshold_image(self):
909
+ if self.test_frame is not None:
910
+ self.fig, self.ax = plt.subplots()
911
+ self.fcanvas = FigureCanvas(self.fig, title="Normalisation: control threshold", interactive=True)
912
+ self.ax.clear()
913
+ self.im = self.ax.imshow(self.frame, cmap='gray')
914
+ self.binary = threshold_image(self.test_frame, float(self.min_threshold), self.test_frame.max(),
915
+ foreground_value=255., fill_holes=False)
916
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
917
+
918
+ self.image_thresholded = self.ax.imshow(self.thresholded_image, cmap="viridis", alpha=0.5,
919
+ interpolation='none')
920
+ self.fcanvas.setMinimumSize(int(self.screen_height * 0.25), int(self.screen_width * 0.25))
921
+ self.ax.set_xticks([])
922
+ self.ax.set_yticks([])
923
+ self.fig.set_facecolor('none') # or 'None'
924
+ self.fig.canvas.setStyleSheet("background-color: transparent;")
925
+ self.fcanvas.canvas.draw()
926
+
927
+ def populate_norm_widget(self):
928
+ """
929
+ Create the multibox design.
930
+
931
+ """
932
+ self.button_widget = QWidget()
933
+ layout = QVBoxLayout()
934
+ self.button_widget.setLayout(layout)
935
+ self.setCentralWidget(self.button_widget)
936
+ threshold_slider_layout = QHBoxLayout()
937
+ threshold_label = QLabel("Threshold: ")
938
+ # self.threshold_slider = QLabeledDoubleSlider()
939
+ threshold_slider_layout.addWidget(threshold_label)
940
+ threshold_slider_layout.addWidget(self.threshold_slider)
941
+ histogram = self.initialize_histogram()
942
+ self.threshold_slider.valueChanged.connect(self.threshold_changed)
943
+ contrast_slider_layout = QHBoxLayout()
944
+ self.contrast_slider = QLabeledDoubleRangeSlider()
945
+ self.contrast_slider.setSingleStep(0.00001)
946
+ self.contrast_slider.setTickInterval(0.00001)
947
+ self.contrast_slider.setOrientation(1)
948
+ self.contrast_slider.setRange(np.amin(self.frame), np.amax(self.frame))
949
+ self.contrast_slider.setValue(
950
+ [np.percentile(self.frame.flatten(), 1), np.percentile(self.frame.flatten(), 99.99)])
951
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
952
+ contrast_label = QLabel("Contrast: ")
953
+ contrast_slider_layout.addWidget(contrast_label)
954
+ contrast_slider_layout.addWidget(self.contrast_slider)
955
+
956
+ self.submit_threshold_btn = QPushButton('Submit')
957
+ self.submit_threshold_btn.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet_2)
958
+ self.submit_threshold_btn.clicked.connect(self.get_threshold)
959
+ self.ylog_check = QPushButton("")
960
+ self.ylog_check.setIcon(icon(MDI6.math_log, color="black"))
961
+ self.ylog_check.setStyleSheet(self.parent.parent.parent.parent.button_select_all)
962
+ self.ylog_check.clicked.connect(self.switch_to_log)
963
+ self.ylog_check.setMaximumWidth(30)
964
+ log_button = QHBoxLayout()
965
+ log_button.addStretch(1)
966
+ log_button.addWidget(self.ylog_check)
967
+ log_button.addSpacing(25)
968
+ layout.addWidget(self.fcanvas)
969
+ layout.addLayout(log_button)
970
+ layout.addWidget(histogram)
971
+ layout.addLayout(threshold_slider_layout)
972
+ layout.addLayout(contrast_slider_layout)
973
+ layout.addWidget(self.submit_threshold_btn)
974
+
975
+ self.setCentralWidget(self.button_widget)
976
+ self.show()
977
+
978
+ QApplication.processEvents()
979
+
980
+ def initialize_histogram(self):
981
+
982
+ self.fig_hist, self.ax_hist = plt.subplots(tight_layout=True)
983
+ self.canvas_hist = FigureCanvas(self.fig_hist, interactive=False)
984
+ self.fig_hist.set_facecolor('none')
985
+ self.fig_hist.canvas.setStyleSheet("background-color: transparent;")
986
+
987
+ self.ax_hist.patch.set_facecolor('none')
988
+ self.hist_y, x, _ = self.ax_hist.hist(self.frame.flatten(), density=True, bins=300, color="k")
989
+ self.ax_hist.set_xlabel('intensity [a.u.]')
990
+ self.ax_hist.spines['top'].set_visible(False)
991
+ self.ax_hist.spines['right'].set_visible(False)
992
+ self.ax_hist.set_xlim(np.amin(self.frame), np.amax(self.frame))
993
+ self.ax_hist.set_ylim(0, self.hist_y.max())
994
+ self.threshold_slider.setSingleStep(0.001)
995
+ self.threshold_slider.setTickInterval(0.001)
996
+ self.threshold_slider.setRange(np.amin(self.frame), np.amax(self.frame))
997
+ self.threshold_slider.setValue(np.percentile(self.frame,90))
998
+ self.add_hist_threshold()
999
+
1000
+ self.canvas_hist.canvas.draw_idle()
1001
+ self.canvas_hist.canvas.setMinimumHeight(int(self.screen_height // 6))
1002
+ return self.canvas_hist
1003
+
1004
+ def clear_post_threshold_options(self):
1005
+
1006
+ self.binary = threshold_image(self.test_frame, self.threshold_slider.value(), self.test_frame.max(),
1007
+ foreground_value=255., fill_holes=False)
1008
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
1009
+
1010
+ self.image_thresholded.set_data(self.thresholded_image)
1011
+ self.image_thresholded.set_cmap('viridis')
1012
+ self.image_thresholded.autoscale()
1013
+
1014
+ def add_hist_threshold(self):
1015
+
1016
+ ymin, ymax = self.ax_hist.get_ylim()
1017
+ self.min_intensity_line, = self.ax_hist.plot(
1018
+ [self.threshold_slider.value(), self.threshold_slider.value()], [0, ymax], c="tab:purple")
1019
+
1020
+ def threshold_changed(self, value):
1021
+
1022
+ """
1023
+ Move the threshold values on histogram, when slider is moved.
1024
+ """
1025
+
1026
+ self.clear_post_threshold_options()
1027
+
1028
+ self.thresh_min = value
1029
+ ymin, ymax = self.ax_hist.get_ylim()
1030
+ self.min_intensity_line.set_data([self.thresh_min, self.thresh_min], [0, ymax])
1031
+ self.canvas_hist.canvas.draw_idle()
1032
+ self.update_threshold()
1033
+
1034
+ def update_threshold(self):
1035
+
1036
+ """
1037
+
1038
+ Threshold and binarize the image based on the min/max threshold values
1039
+ and display on imshow.
1040
+
1041
+ """
1042
+
1043
+ self.binary = threshold_image(self.frame, self.threshold_slider.value(), self.test_frame.max(),
1044
+ foreground_value=255., fill_holes=False)
1045
+ self.thresholded_image = np.ma.masked_where(self.binary == 0., self.binary)
1046
+ self.image_thresholded.set_data(self.thresholded_image)
1047
+ self.fcanvas.canvas.draw_idle()
1048
+
1049
+ def locate_stack(self):
1050
+
1051
+ """
1052
+ Locate the target movie.
1053
+
1054
+ """
1055
+
1056
+ if isinstance(self.pos, str):
1057
+ movies = glob(self.pos + f"movie/{self.parent.parent.parent.movie_prefix}*.tif")
1058
+ else:
1059
+ msgBox = QMessageBox()
1060
+ msgBox.setIcon(QMessageBox.Warning)
1061
+ msgBox.setText("Please select a unique position before launching the wizard...")
1062
+ msgBox.setWindowTitle("Warning")
1063
+ msgBox.setStandardButtons(QMessageBox.Ok)
1064
+ returnValue = msgBox.exec()
1065
+ if returnValue == QMessageBox.Ok:
1066
+ self.img = None
1067
+ self.auto_close = True
1068
+ return None
1069
+
1070
+ if len(movies) == 0:
1071
+ msgBox = QMessageBox()
1072
+ msgBox.setIcon(QMessageBox.Warning)
1073
+ msgBox.setText(
1074
+ "No movies are detected in the experiment folder. Cannot load an image to check normalisation.")
1075
+ msgBox.setWindowTitle("Warning")
1076
+ msgBox.setStandardButtons(QMessageBox.Ok)
1077
+ returnValue = msgBox.exec()
1078
+ if returnValue == QMessageBox.Yes:
1079
+ self.auto_close = True
1080
+ return None
1081
+ else:
1082
+ self.stack_path = movies[0]
1083
+ self.len_movie = self.parent.parent.parent.len_movie
1084
+ len_movie_auto = auto_load_number_of_frames(self.stack_path)
1085
+ if len_movie_auto is not None:
1086
+ self.len_movie = len_movie_auto
1087
+ exp_config = self.exp_dir + "config.ini"
1088
+ self.channel_names, self.channels = extract_experiment_channels(exp_config)
1089
+ self.channel_names = np.array(self.channel_names)
1090
+ self.channels = np.array(self.channels)
1091
+ self.nbr_channels = len(self.channels)
1092
+ t = 1
1093
+ idx = t * self.nbr_channels + self.current_channel
1094
+ self.img = load_frames(idx, self.stack_path, normalize_input=False)
1095
+ print(self.img.shape)
1096
+ print(f'{self.stack_path} successfully located.')
1097
+
1098
+ def get_threshold(self):
1099
+ self.parent.tab2_txt_threshold.setText(str(self.threshold_slider.value()))
1100
+ self.close()
1101
+
1102
+
1103
+ class ThresholdSpot(ThresholdConfigWizard):
1104
+ def __init__(self, current_channel, img, mask, parent=None):
1105
+ QMainWindow.__init__(self)
1106
+ self.parent = parent
1107
+ self.screen_height = self.parent.parent.parent.parent.screen_height
1108
+ self.screen_width = self.parent.parent.parent.parent.screen_width
1109
+ self.setMinimumHeight(int(0.8 * self.screen_height))
1110
+ self.setWindowTitle("Spot threshold preview")
1111
+ center_window(self)
1112
+ self.img = img
1113
+ self.current_channel = current_channel
1114
+ self.mode = self.parent.mode
1115
+ self.pos = self.parent.parent.parent.pos
1116
+ self.exp_dir = self.parent.parent.exp_dir
1117
+ self.onlyFloat = QDoubleValidator()
1118
+ self.onlyInt = QIntValidator()
1119
+ self.soft_path = get_software_location()
1120
+ self.auto_close = False
1121
+
1122
+ if self.img is not None:
1123
+ print(self.img.shape)
1124
+ #self.test_frame = self.img
1125
+ self.frame = self.img
1126
+ self.test_mask = mask
1127
+ self.populate_all()
1128
+ self.setAttribute(Qt.WA_DeleteOnClose)
1129
+ if self.auto_close:
1130
+ self.close()
1131
+
1132
+ def populate_left_panel(self):
1133
+
1134
+ self.left_layout = QVBoxLayout()
1135
+ diameter_layout=QHBoxLayout()
1136
+ self.diameter_lbl = QLabel('Spot diameter: ')
1137
+ self.diameter_value = QLineEdit()
1138
+ self.diameter_value.setText(self.parent.diameter_value.text())
1139
+ self.diameter_value.setValidator(self.onlyFloat)
1140
+ diameter_layout.addWidget(self.diameter_lbl, alignment=Qt.AlignCenter)
1141
+ diameter_layout.addWidget(self.diameter_value, alignment=Qt.AlignCenter)
1142
+ self.left_layout.addLayout(diameter_layout)
1143
+ threshold_layout=QHBoxLayout()
1144
+ self.threshold_lbl = QLabel('Spot threshold: ')
1145
+ self.threshold_value = QLineEdit()
1146
+ self.threshold_value.setValidator(self.onlyFloat)
1147
+ self.threshold_value.setText(self.parent.threshold_value.text())
1148
+ threshold_layout.addWidget(self.threshold_lbl, alignment=Qt.AlignCenter)
1149
+ threshold_layout.addWidget(self.threshold_value, alignment=Qt.AlignCenter)
1150
+ self.left_layout.addLayout(threshold_layout)
1151
+ self.left_panel.addLayout(self.left_layout)
1152
+
1153
+ def enable_preview(self):
1154
+
1155
+ diam = self.diameter_value.text().replace(',','').replace('.','')
1156
+ thresh = self.threshold_value.text().replace(',','').replace('.','')
1157
+ if diam.isnumeric() and thresh.isnumeric():
1158
+ self.preview_button.setEnabled(True)
1159
+ else:
1160
+ self.preview_button.setEnabled(False)
1161
+
1162
+ def draw_spot_preview(self):
1163
+
1164
+ try:
1165
+ diameter_value = float(self.parent.diameter_value.text().replace(',','.'))
1166
+ except:
1167
+ print('Diameter could not be converted to float... Abort.')
1168
+ self.auto_close = True
1169
+ return None
1170
+
1171
+ try:
1172
+ threshold_value = float(self.parent.threshold_value.text().replace(',','.'))
1173
+ except:
1174
+ print('Threshold could not be converted to float... Abort.')
1175
+ self.auto_close = True
1176
+ return None
1177
+
1178
+ lbl = self.test_mask
1179
+ blobs = self.blob_preview(image=self.img[:, :, self.current_channel], label=lbl, threshold=threshold_value,
1180
+ diameter=diameter_value)
1181
+ mask = np.array([lbl[int(y), int(x)] != 0 for y, x, r in blobs])
1182
+ if np.any(mask):
1183
+ blobs_filtered = blobs[mask]
1184
+ else:
1185
+ blobs_filtered=[]
1186
+
1187
+ self.fig_contour, self.ax_contour = plt.subplots(figsize=(4, 6))
1188
+ self.fcanvas = FigureCanvas(self.fig_contour, title="Blob measurement", interactive=True)
1189
+ self.ax_contour.clear()
1190
+ self.im = self.ax_contour.imshow(self.img[:, :, self.current_channel], cmap='gray')
1191
+ self.circles = [Circle((x, y), r, color='red', fill=False, alpha=0.3) for y, x, r in blobs_filtered]
1192
+ for circle in self.circles:
1193
+ self.ax_contour.add_artist(circle)
1194
+ self.ax_contour.set_xticks([])
1195
+ self.ax_contour.set_yticks([])
1196
+ self.fig_contour.set_facecolor('none') # or 'None'
1197
+ self.fig_contour.canvas.setStyleSheet("background-color: transparent;")
1198
+ self.fcanvas.canvas.draw()
1199
+
1200
+ def populate_all(self):
1201
+ self.button_widget = QWidget()
1202
+ main_layout = QVBoxLayout()
1203
+ self.button_widget.setLayout(main_layout)
1204
+ self.right_panel = QVBoxLayout()
1205
+ self.left_panel = QVBoxLayout()
1206
+ self.left_panel.setContentsMargins(30, 30, 30, 30)
1207
+ self.populate_left_panel()
1208
+ self.draw_spot_preview()
1209
+ self.setCentralWidget(self.button_widget)
1210
+ contrast_slider_layout = QHBoxLayout()
1211
+ self.contrast_slider = QLabeledDoubleRangeSlider()
1212
+ self.contrast_slider.setSingleStep(0.00001)
1213
+ self.contrast_slider.setTickInterval(0.00001)
1214
+ self.contrast_slider.setOrientation(1)
1215
+ self.contrast_slider.setRange(np.amin(self.frame[:, :, self.current_channel]), np.amax(self.frame[:, :, self.current_channel]))
1216
+ self.contrast_slider.setValue(
1217
+ [np.percentile(self.frame[:, :, self.current_channel].flatten(), 1), np.percentile(self.frame[:, :, self.current_channel].flatten(), 99.99)])
1218
+ self.contrast_slider.valueChanged.connect(self.contrast_slider_action)
1219
+ contrast_label = QLabel("Contrast: ")
1220
+ contrast_slider_layout.addWidget(contrast_label)
1221
+ contrast_slider_layout.addWidget(self.contrast_slider)
1222
+ self.preview_button=QPushButton("Preview")
1223
+ self.preview_button.clicked.connect(self.update_spots)
1224
+ self.preview_button.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet_2)
1225
+ self.apply_changes=QPushButton("Apply")
1226
+ self.apply_changes.setStyleSheet(self.parent.parent.parent.parent.button_style_sheet)
1227
+ self.apply_changes.clicked.connect(self.apply)
1228
+
1229
+ self.diameter_value.textChanged.connect(self.enable_preview)
1230
+ self.threshold_value.textChanged.connect(self.enable_preview)
1231
+
1232
+ self.right_panel.addWidget(self.fcanvas.canvas)
1233
+ self.right_panel.addWidget(self.fcanvas.toolbar)
1234
+
1235
+ main_layout.addLayout(self.right_panel)
1236
+ main_layout.addLayout(self.left_panel)
1237
+ main_layout.addLayout(contrast_slider_layout)
1238
+ main_layout.addWidget(self.preview_button)
1239
+ main_layout.addWidget(self.apply_changes)
1240
+ self.show()
1241
+
1242
+ def blob_preview(self, image, label, threshold, diameter):
1243
+ removed_background = image.copy()
1244
+ dilated_image = ndimage.grey_dilation(label, footprint=disk(10))
1245
+ removed_background[np.where(dilated_image == 0)] = 0
1246
+ min_sigma = (1 / (1 + math.sqrt(2))) * diameter
1247
+ max_sigma = math.sqrt(2) * min_sigma
1248
+ blobs = skimage.feature.blob_dog(removed_background, threshold=threshold, min_sigma=min_sigma,
1249
+ max_sigma=max_sigma, overlap=0.75)
1250
+ return blobs
1251
+
1252
+ def update_spots(self):
1253
+
1254
+ try:
1255
+ diameter_value = float(self.diameter_value.text().replace(',','.'))
1256
+ except:
1257
+ print('Diameter could not be converted to float... Abort.')
1258
+ return None
1259
+
1260
+ try:
1261
+ threshold_value = float(self.threshold_value.text().replace(',','.'))
1262
+ except:
1263
+ print('Threshold could not be converted to float... Abort.')
1264
+ return None
1265
+ xlim = self.ax_contour.get_xlim()
1266
+ ylim = self.ax_contour.get_ylim()
1267
+ contrast_levels = self.contrast_slider.value()
1268
+ blobs = self.blob_preview(image=self.frame[:, :, self.current_channel], label=self.test_mask,
1269
+ threshold=threshold_value,
1270
+ diameter=diameter_value)
1271
+ mask = np.array([self.test_mask[int(y), int(x)] != 0 for y, x, r in blobs])
1272
+ if np.any(mask):
1273
+ blobs_filtered = blobs[mask]
1274
+ else:
1275
+ blobs_filtered = []
1276
+ self.ax_contour.clear()
1277
+ self.im = self.ax_contour.imshow(self.frame[:, :, self.current_channel], cmap='gray')
1278
+ self.ax_contour.set_xticks([])
1279
+ self.ax_contour.set_yticks([])
1280
+ self.circles = [Circle((x, y), r, color='red', fill=False, alpha=0.3) for y, x, r in blobs_filtered]
1281
+ for circle in self.circles:
1282
+ self.ax_contour.add_artist(circle)
1283
+ self.ax_contour.set_xlim(xlim)
1284
+ self.ax_contour.set_ylim(ylim)
1285
+
1286
+ self.im.set_data(self.frame[:, :, self.current_channel])
1287
+ self.fig_contour.canvas.draw()
1288
+ self.contrast_slider.setValue(contrast_levels)
1289
+
1290
+
1291
+
1292
+
1293
+ def apply(self):
1294
+ self.parent.threshold_value.setText(self.threshold_value.text())
1295
+ self.parent.diameter_value.setText(self.diameter_value.text())
1296
+ self.close()
1297
+
1298
+
1299
+
1300
+