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