celldetective 1.4.2__py3-none-any.whl → 1.5.0b1__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 (152) hide show
  1. celldetective/__init__.py +25 -0
  2. celldetective/__main__.py +62 -43
  3. celldetective/_version.py +1 -1
  4. celldetective/extra_properties.py +477 -399
  5. celldetective/filters.py +192 -97
  6. celldetective/gui/InitWindow.py +541 -411
  7. celldetective/gui/__init__.py +0 -15
  8. celldetective/gui/about.py +44 -39
  9. celldetective/gui/analyze_block.py +120 -84
  10. celldetective/gui/base/__init__.py +0 -0
  11. celldetective/gui/base/channel_norm_generator.py +335 -0
  12. celldetective/gui/base/components.py +249 -0
  13. celldetective/gui/base/feature_choice.py +92 -0
  14. celldetective/gui/base/figure_canvas.py +52 -0
  15. celldetective/gui/base/list_widget.py +133 -0
  16. celldetective/gui/{styles.py → base/styles.py} +92 -36
  17. celldetective/gui/base/utils.py +33 -0
  18. celldetective/gui/base_annotator.py +900 -767
  19. celldetective/gui/classifier_widget.py +6 -22
  20. celldetective/gui/configure_new_exp.py +777 -671
  21. celldetective/gui/control_panel.py +635 -524
  22. celldetective/gui/dynamic_progress.py +449 -0
  23. celldetective/gui/event_annotator.py +2023 -1662
  24. celldetective/gui/generic_signal_plot.py +1292 -944
  25. celldetective/gui/gui_utils.py +899 -1289
  26. celldetective/gui/interactions_block.py +658 -0
  27. celldetective/gui/interactive_timeseries_viewer.py +447 -0
  28. celldetective/gui/json_readers.py +48 -15
  29. celldetective/gui/layouts/__init__.py +5 -0
  30. celldetective/gui/layouts/background_model_free_layout.py +537 -0
  31. celldetective/gui/layouts/channel_offset_layout.py +134 -0
  32. celldetective/gui/layouts/local_correction_layout.py +91 -0
  33. celldetective/gui/layouts/model_fit_layout.py +372 -0
  34. celldetective/gui/layouts/operation_layout.py +68 -0
  35. celldetective/gui/layouts/protocol_designer_layout.py +96 -0
  36. celldetective/gui/pair_event_annotator.py +3130 -2435
  37. celldetective/gui/plot_measurements.py +586 -267
  38. celldetective/gui/plot_signals_ui.py +724 -506
  39. celldetective/gui/preprocessing_block.py +395 -0
  40. celldetective/gui/process_block.py +1678 -1831
  41. celldetective/gui/seg_model_loader.py +580 -473
  42. celldetective/gui/settings/__init__.py +0 -7
  43. celldetective/gui/settings/_cellpose_model_params.py +181 -0
  44. celldetective/gui/settings/_event_detection_model_params.py +95 -0
  45. celldetective/gui/settings/_segmentation_model_params.py +159 -0
  46. celldetective/gui/settings/_settings_base.py +77 -65
  47. celldetective/gui/settings/_settings_event_model_training.py +752 -526
  48. celldetective/gui/settings/_settings_measurements.py +1133 -964
  49. celldetective/gui/settings/_settings_neighborhood.py +574 -488
  50. celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
  51. celldetective/gui/settings/_settings_signal_annotator.py +329 -305
  52. celldetective/gui/settings/_settings_tracking.py +1304 -1094
  53. celldetective/gui/settings/_stardist_model_params.py +98 -0
  54. celldetective/gui/survival_ui.py +422 -312
  55. celldetective/gui/tableUI.py +1665 -1701
  56. celldetective/gui/table_ops/_maths.py +295 -0
  57. celldetective/gui/table_ops/_merge_groups.py +140 -0
  58. celldetective/gui/table_ops/_merge_one_hot.py +95 -0
  59. celldetective/gui/table_ops/_query_table.py +43 -0
  60. celldetective/gui/table_ops/_rename_col.py +44 -0
  61. celldetective/gui/thresholds_gui.py +382 -179
  62. celldetective/gui/viewers/__init__.py +0 -0
  63. celldetective/gui/viewers/base_viewer.py +700 -0
  64. celldetective/gui/viewers/channel_offset_viewer.py +331 -0
  65. celldetective/gui/viewers/contour_viewer.py +394 -0
  66. celldetective/gui/viewers/size_viewer.py +153 -0
  67. celldetective/gui/viewers/spot_detection_viewer.py +341 -0
  68. celldetective/gui/viewers/threshold_viewer.py +309 -0
  69. celldetective/gui/workers.py +403 -126
  70. celldetective/log_manager.py +92 -0
  71. celldetective/measure.py +1895 -1478
  72. celldetective/napari/__init__.py +0 -0
  73. celldetective/napari/utils.py +1025 -0
  74. celldetective/neighborhood.py +1914 -1448
  75. celldetective/preprocessing.py +1620 -1220
  76. celldetective/processes/__init__.py +0 -0
  77. celldetective/processes/background_correction.py +271 -0
  78. celldetective/processes/compute_neighborhood.py +894 -0
  79. celldetective/processes/detect_events.py +246 -0
  80. celldetective/processes/downloader.py +137 -0
  81. celldetective/processes/measure_cells.py +565 -0
  82. celldetective/processes/segment_cells.py +760 -0
  83. celldetective/processes/track_cells.py +435 -0
  84. celldetective/processes/train_segmentation_model.py +694 -0
  85. celldetective/processes/train_signal_model.py +265 -0
  86. celldetective/processes/unified_process.py +292 -0
  87. celldetective/regionprops/_regionprops.py +358 -317
  88. celldetective/relative_measurements.py +987 -710
  89. celldetective/scripts/measure_cells.py +313 -212
  90. celldetective/scripts/measure_relative.py +90 -46
  91. celldetective/scripts/segment_cells.py +165 -104
  92. celldetective/scripts/segment_cells_thresholds.py +96 -68
  93. celldetective/scripts/track_cells.py +198 -149
  94. celldetective/scripts/train_segmentation_model.py +324 -201
  95. celldetective/scripts/train_signal_model.py +87 -45
  96. celldetective/segmentation.py +844 -749
  97. celldetective/signals.py +3514 -2861
  98. celldetective/tracking.py +30 -15
  99. celldetective/utils/__init__.py +0 -0
  100. celldetective/utils/cellpose_utils/__init__.py +133 -0
  101. celldetective/utils/color_mappings.py +42 -0
  102. celldetective/utils/data_cleaning.py +630 -0
  103. celldetective/utils/data_loaders.py +450 -0
  104. celldetective/utils/dataset_helpers.py +207 -0
  105. celldetective/utils/downloaders.py +235 -0
  106. celldetective/utils/event_detection/__init__.py +8 -0
  107. celldetective/utils/experiment.py +1782 -0
  108. celldetective/utils/image_augmenters.py +308 -0
  109. celldetective/utils/image_cleaning.py +74 -0
  110. celldetective/utils/image_loaders.py +926 -0
  111. celldetective/utils/image_transforms.py +335 -0
  112. celldetective/utils/io.py +62 -0
  113. celldetective/utils/mask_cleaning.py +348 -0
  114. celldetective/utils/mask_transforms.py +5 -0
  115. celldetective/utils/masks.py +184 -0
  116. celldetective/utils/maths.py +351 -0
  117. celldetective/utils/model_getters.py +325 -0
  118. celldetective/utils/model_loaders.py +296 -0
  119. celldetective/utils/normalization.py +380 -0
  120. celldetective/utils/parsing.py +465 -0
  121. celldetective/utils/plots/__init__.py +0 -0
  122. celldetective/utils/plots/regression.py +53 -0
  123. celldetective/utils/resources.py +34 -0
  124. celldetective/utils/stardist_utils/__init__.py +104 -0
  125. celldetective/utils/stats.py +90 -0
  126. celldetective/utils/types.py +21 -0
  127. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/METADATA +1 -1
  128. celldetective-1.5.0b1.dist-info/RECORD +187 -0
  129. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/WHEEL +1 -1
  130. tests/gui/test_new_project.py +129 -117
  131. tests/gui/test_project.py +127 -79
  132. tests/test_filters.py +39 -15
  133. tests/test_notebooks.py +8 -0
  134. tests/test_tracking.py +232 -13
  135. tests/test_utils.py +123 -77
  136. celldetective/gui/base_components.py +0 -23
  137. celldetective/gui/layouts.py +0 -1602
  138. celldetective/gui/processes/compute_neighborhood.py +0 -594
  139. celldetective/gui/processes/downloader.py +0 -111
  140. celldetective/gui/processes/measure_cells.py +0 -360
  141. celldetective/gui/processes/segment_cells.py +0 -499
  142. celldetective/gui/processes/track_cells.py +0 -303
  143. celldetective/gui/processes/train_segmentation_model.py +0 -270
  144. celldetective/gui/processes/train_signal_model.py +0 -108
  145. celldetective/gui/table_ops/merge_groups.py +0 -118
  146. celldetective/gui/viewers.py +0 -1354
  147. celldetective/io.py +0 -3663
  148. celldetective/utils.py +0 -3108
  149. celldetective-1.4.2.dist-info/RECORD +0 -123
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
  152. {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/top_level.txt +0 -0
@@ -2,1122 +2,1332 @@
2
2
  Copyright © 2023 Laboratoire Adhesion et Inflammation, Authored by Remy Torro.
3
3
  """
4
4
 
5
-
6
- from PyQt5.QtWidgets import QRadioButton, QButtonGroup, QMessageBox, QComboBox, QFrame, QCheckBox, QFileDialog, QGridLayout, QTextEdit, QLineEdit, QVBoxLayout, QLabel, QHBoxLayout, QPushButton
5
+ from PyQt5.QtWidgets import (
6
+ QRadioButton,
7
+ QButtonGroup,
8
+ QMessageBox,
9
+ QComboBox,
10
+ QFrame,
11
+ QCheckBox,
12
+ QFileDialog,
13
+ QGridLayout,
14
+ QTextEdit,
15
+ QLineEdit,
16
+ QVBoxLayout,
17
+ QLabel,
18
+ QHBoxLayout,
19
+ QPushButton,
20
+ )
7
21
  from PyQt5.QtCore import Qt, QSize
22
+ from matplotlib import pyplot as plt
23
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
8
24
 
9
- from celldetective.gui.gui_utils import FeatureChoice, ListWidget, QHSeperationLine, FigureCanvas, help_generic
25
+ from celldetective.gui.gui_utils import (
26
+ help_generic,
27
+ )
28
+ from celldetective.gui.base.figure_canvas import FigureCanvas
29
+ from celldetective.gui.base.list_widget import ListWidget
30
+ from celldetective.gui.base.feature_choice import FeatureChoice
31
+ from celldetective.gui.base.components import QHSeperationLine
10
32
  from superqt import QLabeledDoubleSlider, QLabeledSlider
11
33
  from superqt.fonticon import icon
12
34
  from fonticon_mdi6 import MDI6
13
- from celldetective.utils import extract_experiment_channels, get_software_location
14
- from celldetective.io import interpret_tracking_configuration, load_frames
35
+ from celldetective import get_software_location
15
36
  from celldetective.measure import compute_haralick_features
37
+ from celldetective.utils.experiment import extract_experiment_channels
38
+ from celldetective.utils.data_loaders import interpret_tracking_configuration
16
39
  import numpy as np
17
40
  import json
18
41
  from shutil import copyfile
19
42
  import os
20
- import matplotlib.pyplot as plt
21
- from mpl_toolkits.axes_grid1 import make_axes_locatable
43
+
22
44
  from glob import glob
23
45
  from celldetective.gui.settings._settings_base import CelldetectiveSettingsPanel
46
+ from celldetective.utils.image_loaders import load_frames
47
+ from celldetective import get_logger
48
+
49
+ logger = get_logger()
24
50
 
25
51
 
26
52
  class SettingsTracking(CelldetectiveSettingsPanel):
27
-
28
- """
29
- UI to set tracking parameters for bTrack.
30
-
31
- """
32
-
33
- def __init__(self, parent_window=None):
34
-
35
- self.parent_window = parent_window
36
- self.mode = self.parent_window.mode
37
- self.exp_dir = self.parent_window.exp_dir
38
-
39
- self.config_name = os.sep.join(["configs", f"btrack_config_{self.mode}.json"])
40
- self.track_instructions_write_path = self.parent_window.exp_dir + os.sep.join(["configs", f"tracking_instructions_{self.mode}.json"])
41
-
42
- self.config_path = self.exp_dir + self.config_name
43
- self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
44
- self.channel_names = np.array(self.channel_names)
45
- self.channels = np.array(self.channels)
46
- self.minimum_height = 300
47
-
48
- super().__init__(title="Configure tracking")
49
-
50
- self._add_to_layout()
51
- self._load_previous_instructions()
52
-
53
- self._widget.setMinimumWidth(500)
54
- self._adjustSize()
55
- self.resize(int(self.width()*1.5), int(self._screen_height * 0.8))
56
-
57
- def _add_to_layout(self):
58
-
59
- tracker_hbox = QHBoxLayout()
60
- tracker_hbox.setContentsMargins(15, 15, 15, 15)
61
- tracker_hbox.addWidget(self.btrack_option, 50, alignment=Qt.AlignCenter)
62
- tracker_hbox.addWidget(self.trackpy_option, 50, alignment=Qt.AlignCenter)
63
-
64
- self._layout.addLayout(tracker_hbox)
65
- self._layout.addWidget(self.config_frame)
66
- self._layout.addWidget(self.features_frame)
67
- self._layout.addWidget(self.config_trackpy_frame)
68
- self._layout.addWidget(self.post_proc_frame)
69
- self._layout.addWidget(self.submit_btn)
70
-
71
-
72
- def _create_widgets(self):
73
-
74
- """
75
- Create the multibox design with collapsable frames.
76
-
77
- """
78
-
79
- super()._create_widgets()
80
-
81
- self.btrack_option = QRadioButton('bTrack')
82
- self.btrack_option.setChecked(True)
83
-
84
- self.trackpy_option = QRadioButton('trackpy')
85
- self.tracker_option_group = QButtonGroup()
86
- self.tracker_option_group.addButton(self.btrack_option)
87
- self.tracker_option_group.addButton(self.trackpy_option)
88
-
89
- self.config_frame = QFrame()
90
- self.config_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
91
- self.populate_config_frame()
92
-
93
- # Second collapsable frame FEATURES
94
- self.features_frame = QFrame()
95
- self.features_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
96
- self.populate_features_frame()
97
-
98
- self.config_trackpy_frame = QFrame()
99
- self.config_trackpy_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
100
- self.populate_config_trackpy_frame()
101
- self.config_trackpy_frame.hide()
102
-
103
- # Third collapsable frame POST-PROCESSING
104
- self.post_proc_frame = QFrame()
105
- self.post_proc_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
106
- self.populate_post_proc_frame()
107
-
108
- self.btrack_option.toggled.connect(self.show_tracking_options)
109
- self.trackpy_option.toggled.connect(self.show_tracking_options)
110
-
111
- def show_tracking_options(self):
112
-
113
- if self.btrack_option.isChecked():
114
- self.config_frame.show()
115
- self.features_frame.show()
116
- self.config_trackpy_frame.hide()
117
- #self._adjustSize()
118
- else:
119
- self.config_frame.hide()
120
- self.features_frame.hide()
121
- self.config_trackpy_frame.show()
122
- #self._adjustSize()
123
-
124
-
125
- def populate_post_proc_frame(self):
126
-
127
- """
128
- Add widgets and layout in the POST-PROCESSING frame.
129
- """
130
-
131
- grid = QGridLayout(self.post_proc_frame)
132
-
133
- self.select_post_proc_btn = QPushButton()
134
- self.select_post_proc_btn.clicked.connect(self.activate_post_proc_options)
135
- self.select_post_proc_btn.setStyleSheet(self.button_select_all)
136
-
137
- self.post_proc_lbl = QLabel("POST-PROCESSING")
138
- self.post_proc_lbl.setStyleSheet("""
139
- font-weight: bold;
140
- padding: 0px;
141
- """)
142
- grid.addWidget(self.post_proc_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
143
-
144
- title_hbox = QHBoxLayout()
145
-
146
- self.collapse_post_proc_btn = QPushButton()
147
- self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down, color="black"))
148
- self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
149
- self.collapse_post_proc_btn.setStyleSheet(self.button_select_all)
150
- #grid.addWidget(self.collapse_post_proc_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
151
-
152
- self.help_post_btn = QPushButton()
153
- self.help_post_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
154
- self.help_post_btn.setIconSize(QSize(20, 20))
155
- self.help_post_btn.clicked.connect(self.help_post)
156
- self.help_post_btn.setStyleSheet(self.button_select_all)
157
- self.help_post_btn.setToolTip("Help.")
158
-
159
- title_hbox.addWidget(self.select_post_proc_btn, 5)
160
- title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
161
- title_hbox.addWidget(self.help_post_btn, 5)
162
- title_hbox.addWidget(self.collapse_post_proc_btn, 5)
163
- grid.addLayout(title_hbox, 0, 0, 1, 4)
164
-
165
- self.generate_post_proc_panel_contents()
166
- grid.addWidget(self.ContentsPostProc, 1, 0, 1, 4, alignment=Qt.AlignTop)
167
- self.collapse_post_proc_btn.clicked.connect(lambda: self.ContentsPostProc.setHidden(not self.ContentsPostProc.isHidden()))
168
- self.collapse_post_proc_btn.clicked.connect(self.collapse_post_advanced)
169
- self.ContentsPostProc.hide()
170
- self.uncheck_post_proc()
171
-
172
- def collapse_post_advanced(self):
173
-
174
- features_open = not self.ContentsFeatures.isHidden()
175
- config_open = not self.ContentsConfig.isHidden()
176
- post_open = not self.ContentsPostProc.isHidden()
177
- is_open = np.array([features_open, config_open, post_open])
178
-
179
- if self.ContentsPostProc.isHidden():
180
- self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down, color="black"))
181
- self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
182
- if len(is_open[is_open])==0:
183
- pass
184
- #self._scroll_area.setMinimumHeight(int(self.minimum_height))
185
- #self._adjustSize()
186
- else:
187
- self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_up, color="black"))
188
- self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
189
-
190
-
191
- def help_post(self):
192
-
193
- """
194
- Helper for track post-processing strategy.
195
- """
196
-
197
- dict_path = os.sep.join([get_software_location(), 'celldetective', 'gui', 'help', 'track-postprocessing.json'])
198
-
199
- with open(dict_path) as f:
200
- d = json.load(f)
201
-
202
- suggestion = help_generic(d)
203
- if isinstance(suggestion, str):
204
- print(f"{suggestion=}")
205
- msgBox = QMessageBox()
206
- msgBox.setIcon(QMessageBox.Information)
207
- msgBox.setTextFormat(Qt.RichText)
208
- msgBox.setText(rf"{suggestion}")
209
- msgBox.setWindowTitle("Info")
210
- msgBox.setStandardButtons(QMessageBox.Ok)
211
- returnValue = msgBox.exec()
212
- if returnValue == QMessageBox.Ok:
213
- return None
214
-
215
- def help_feature(self):
216
-
217
- """
218
- Helper for track post-processing strategy.
219
- """
220
-
221
- dict_path = os.sep.join([get_software_location(), 'celldetective', 'gui', 'help', 'feature-btrack.json'])
222
-
223
- with open(dict_path) as f:
224
- d = json.load(f)
225
-
226
- suggestion = help_generic(d)
227
- if isinstance(suggestion, str):
228
- print(f"{suggestion=}")
229
- msgBox = QMessageBox()
230
- msgBox.setIcon(QMessageBox.Information)
231
- msgBox.setTextFormat(Qt.RichText)
232
- msgBox.setText(rf"{suggestion}")
233
- msgBox.setWindowTitle("Info")
234
- msgBox.setStandardButtons(QMessageBox.Ok)
235
- returnValue = msgBox.exec()
236
- if returnValue == QMessageBox.Ok:
237
- return None
238
-
239
- def populate_features_frame(self):
240
-
241
- """
242
- Add widgets and layout in the FEATURES frame.
243
- """
244
-
245
- grid = QGridLayout(self.features_frame)
246
- title_hbox = QHBoxLayout()
247
-
248
- self.select_features_btn = QPushButton()
249
- self.select_features_btn.clicked.connect(self.activate_feature_options)
250
- self.select_features_btn.setStyleSheet(self.button_select_all)
251
-
252
- self.feature_lbl = QLabel("FEATURES")
253
- self.feature_lbl.setStyleSheet("""
254
- font-weight: bold;
255
- padding: 0px;
256
- """)
257
- grid.addWidget(self.feature_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
258
-
259
- self.collapse_features_btn = QPushButton()
260
- self.collapse_features_btn.setIcon(icon(MDI6.chevron_down,color="black"))
261
- self.collapse_features_btn.setIconSize(QSize(20, 20))
262
- self.collapse_features_btn.setStyleSheet(self.button_select_all)
263
-
264
- self.help_feature_btn = QPushButton()
265
- self.help_feature_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
266
- self.help_feature_btn.setIconSize(QSize(20, 20))
267
- self.help_feature_btn.clicked.connect(self.help_feature)
268
- self.help_feature_btn.setStyleSheet(self.button_select_all)
269
- self.help_feature_btn.setToolTip("Help.")
270
-
271
- title_hbox.addWidget(self.select_features_btn, 5)
272
- title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
273
- title_hbox.addWidget(self.help_feature_btn, 5)
274
- title_hbox.addWidget(self.collapse_features_btn, 5)
275
- grid.addLayout(title_hbox, 0, 0, 1, 4)
276
-
277
- self.generate_feature_panel_contents()
278
- grid.addWidget(self.ContentsFeatures, 1, 0, 1, 4, alignment=Qt.AlignTop)
279
- self.collapse_features_btn.clicked.connect(lambda: self.ContentsFeatures.setHidden(not self.ContentsFeatures.isHidden()))
280
- self.collapse_features_btn.clicked.connect(self.collapse_features_advanced)
281
- #self.ContentsFeatures.hide()
282
- self.check_features()
283
-
284
- def collapse_features_advanced(self):
285
-
286
- """
287
- Switch the chevron icon and adjust the size for the FEATURES frame.
288
- """
289
-
290
- features_open = not self.ContentsFeatures.isHidden()
291
- config_open = not self.ContentsConfig.isHidden()
292
- post_open = not self.ContentsPostProc.isHidden()
293
- is_open = np.array([features_open, config_open, post_open])
294
-
295
- if self.ContentsFeatures.isHidden():
296
- self.collapse_features_btn.setIcon(icon(MDI6.chevron_down, color="black"))
297
- self.collapse_features_btn.setIconSize(QSize(20, 20))
298
- if len(is_open[is_open])==0:
299
- pass
300
- #self._scroll_area.setMinimumHeight(int(self.minimum_height))
301
- #self._adjustSize()
302
- else:
303
- self.collapse_features_btn.setIcon(icon(MDI6.chevron_up, color="black"))
304
- self.collapse_features_btn.setIconSize(QSize(20, 20))
305
-
306
-
307
- def generate_post_proc_panel_contents(self):
308
-
309
- self.ContentsPostProc = QFrame()
310
- layout = QVBoxLayout(self.ContentsPostProc)
311
- layout.setContentsMargins(0,0,0,0)
312
-
313
- post_proc_layout = QHBoxLayout()
314
- self.post_proc_lbl = QLabel("Post processing on the tracks:")
315
- post_proc_layout.addWidget(self.post_proc_lbl, 90)
316
- layout.addLayout(post_proc_layout)
317
-
318
- clean_traj_sublayout = QVBoxLayout()
319
- clean_traj_sublayout.setContentsMargins(15,15,15,15)
320
-
321
- tracklength_layout = QHBoxLayout()
322
- self.min_tracklength_slider = QLabeledSlider()
323
- self.min_tracklength_slider.setSingleStep(1)
324
- self.min_tracklength_slider.setTickInterval(1)
325
- self.min_tracklength_slider.setSingleStep(1)
326
- self.min_tracklength_slider.setOrientation(Qt.Horizontal)
327
- self.min_tracklength_slider.setRange(0,self.parent_window.parent_window.len_movie)
328
- self.min_tracklength_slider.setValue(0)
329
- tracklength_layout.addWidget(QLabel('Min. tracklength: '),40)
330
- tracklength_layout.addWidget(self.min_tracklength_slider, 60)
331
- clean_traj_sublayout.addLayout(tracklength_layout)
332
-
333
- self.remove_not_in_first_checkbox = QCheckBox('Remove tracks that do not start at the beginning')
334
- self.remove_not_in_first_checkbox.setIcon(icon(MDI6.arrow_expand_right,color="k"))
335
- clean_traj_sublayout.addWidget(self.remove_not_in_first_checkbox)
336
-
337
- self.remove_not_in_last_checkbox = QCheckBox('Remove tracks that do not end at the end')
338
- self.remove_not_in_last_checkbox.setIcon(icon(MDI6.arrow_expand_left,color="k"))
339
- clean_traj_sublayout.addWidget(self.remove_not_in_last_checkbox)
340
-
341
- self.interpolate_gaps_checkbox = QCheckBox('Interpolate missed detections within tracks')
342
- self.interpolate_gaps_checkbox.setIcon(icon(MDI6.chart_timeline_variant_shimmer,color="k"))
343
- clean_traj_sublayout.addWidget(self.interpolate_gaps_checkbox)
344
-
345
- self.extrapolate_post_checkbox = QCheckBox('Sustain last position until the end of the movie')
346
- self.extrapolate_post_checkbox.setIcon(icon(MDI6.repeat,color="k"))
347
-
348
- self.extrapolate_pre_checkbox = QCheckBox('Sustain first position from the beginning of the movie')
349
- self.extrapolate_pre_checkbox.setIcon(icon(MDI6.repeat,color="k"))
350
-
351
- clean_traj_sublayout.addWidget(self.extrapolate_post_checkbox)
352
- clean_traj_sublayout.addWidget(self.extrapolate_pre_checkbox)
353
-
354
- # self.interpolate_na_features_checkbox = QCheckBox('Interpolate features of missed detections')
355
- # self.interpolate_na_features_checkbox.setIcon(icon(MDI6.format_color_fill,color="k"))
356
-
357
- # clean_traj_sublayout.addWidget(self.interpolate_na_features_checkbox)
358
- clean_traj_sublayout.addStretch()
359
-
360
- self.post_proc_options_to_disable = [self.post_proc_lbl, self.min_tracklength_slider, self.remove_not_in_first_checkbox,
361
- self.remove_not_in_last_checkbox, self.interpolate_gaps_checkbox, self.extrapolate_post_checkbox,
362
- self.extrapolate_pre_checkbox] #, self.interpolate_na_features_checkbox
363
-
364
- layout.addLayout(clean_traj_sublayout)
365
-
366
- def generate_feature_panel_contents(self):
367
-
368
- self.ContentsFeatures = QFrame()
369
- layout = QVBoxLayout(self.ContentsFeatures)
370
- layout.setContentsMargins(0,0,0,0)
371
-
372
- feature_layout = QHBoxLayout()
373
- feature_layout.setContentsMargins(0,0,0,0)
374
-
375
-
376
- self.feature_lbl = QLabel("Add features:")
377
- self.del_feature_btn = QPushButton("")
378
- self.del_feature_btn.setStyleSheet(self.button_select_all)
379
- self.del_feature_btn.setIcon(icon(MDI6.trash_can,color="black"))
380
- self.del_feature_btn.setToolTip("Remove feature")
381
- self.del_feature_btn.setIconSize(QSize(20, 20))
382
-
383
- self.add_feature_btn = QPushButton("")
384
- self.add_feature_btn.setStyleSheet(self.button_select_all)
385
- self.add_feature_btn.setIcon(icon(MDI6.filter_plus,color="black"))
386
- self.add_feature_btn.setToolTip("Add feature")
387
- self.add_feature_btn.setIconSize(QSize(20, 20))
388
-
389
- self.features_list = ListWidget(FeatureChoice, initial_features=['area','intensity_mean',])
390
-
391
- self.del_feature_btn.clicked.connect(self.features_list.removeSel)
392
- self.add_feature_btn.clicked.connect(self.features_list.addItem)
393
-
394
- feature_layout.addWidget(self.feature_lbl, 90)
395
- feature_layout.addWidget(self.del_feature_btn, 5)
396
- feature_layout.addWidget(self.add_feature_btn, 5)
397
- layout.addLayout(feature_layout)
398
- layout.addWidget(self.features_list)
399
-
400
- self.feat_sep2 = QHSeperationLine()
401
- layout.addWidget(self.feat_sep2)
402
-
403
- self.use_channel_lbl = QLabel('Use channel:')
404
- layout.addWidget(self.use_channel_lbl)
405
- self.mask_channels_cb = [QCheckBox() for i in range(len(self.channels))]
406
- for cb,chn in zip(self.mask_channels_cb,self.channel_names):
407
- cb.setText(chn)
408
- cb.setChecked(True)
409
- layout.addWidget(cb)
410
-
411
- self.feat_sep1 = QHSeperationLine()
412
- layout.addWidget(self.feat_sep1)
413
-
414
- self.activate_haralick_btn = QCheckBox('activate Haralick texture features')
415
- self.activate_haralick_btn.toggled.connect(self.show_haralick_options)
416
- # Haralick features parameters
417
-
418
- self.haralick_channel_choice = QComboBox()
419
- self.haralick_channel_choice.addItems(self.channel_names)
420
- self.haralick_channel_lbl = QLabel('Target channel: ')
421
-
422
- self.haralick_distance_le = QLineEdit("1")
423
- self.haralick_distance_lbl = QLabel('Distance: ')
424
-
425
- self.haralick_n_gray_levels_le = QLineEdit("256")
426
- self.haralick_n_gray_levels_lbl = QLabel('# gray levels: ')
427
-
428
- # Slider to set vmin & vmax
429
- self.haralick_scale_slider = QLabeledDoubleSlider()
430
- self.haralick_scale_slider.setTickInterval(0.05)
431
- self.haralick_scale_slider.setSingleStep(1)
432
- self.haralick_scale_slider.setOrientation(Qt.Horizontal)
433
- self.haralick_scale_slider.setRange(0,1)
434
- self.haralick_scale_slider.setValue(0.5)
435
- self.haralick_scale_lbl = QLabel('Scale: ')
436
-
437
- self.haralick_percentile_min_le = QLineEdit('0.01')
438
- self.haralick_percentile_max_le = QLineEdit('99.9')
439
- self.haralick_normalization_mode_btn = QPushButton()
440
- self.haralick_normalization_mode_btn.setStyleSheet(self.button_select_all)
441
- self.haralick_normalization_mode_btn.setIcon(icon(MDI6.percent_circle,color="black"))
442
- self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
443
- self.haralick_normalization_mode_btn.setToolTip("Switch to absolute normalization values.")
444
- self.percentile_mode = True
445
-
446
- self.haralick_percentile_min_lbl = QLabel('Min percentile: ')
447
- self.haralick_percentile_max_lbl = QLabel('Max percentile: ')
448
-
449
- self.haralick_hist_btn = QPushButton()
450
- self.haralick_hist_btn.clicked.connect(self.control_haralick_intensity_histogram)
451
- self.haralick_hist_btn.setIcon(icon(MDI6.poll,color="k"))
452
- self.haralick_hist_btn.setStyleSheet(self.button_select_all)
453
-
454
- self.haralick_digit_btn = QPushButton()
455
- self.haralick_digit_btn.clicked.connect(self.control_haralick_digitalization)
456
- self.haralick_digit_btn.setIcon(icon(MDI6.image_check,color="k"))
457
- self.haralick_digit_btn.setStyleSheet(self.button_select_all)
458
-
459
- self.haralick_layout = QVBoxLayout()
460
- self.haralick_layout.setContentsMargins(20,20,20,20)
461
-
462
- activate_layout = QHBoxLayout()
463
- activate_layout.addWidget(self.activate_haralick_btn, 80)
464
- activate_layout.addWidget(self.haralick_hist_btn, 10)
465
- activate_layout.addWidget(self.haralick_digit_btn, 10)
466
- self.haralick_layout.addLayout(activate_layout)
467
-
468
- channel_layout = QHBoxLayout()
469
- channel_layout.addWidget(self.haralick_channel_lbl, 40)
470
- channel_layout.addWidget(self.haralick_channel_choice, 60)
471
- self.haralick_layout.addLayout(channel_layout)
472
-
473
- distance_layout = QHBoxLayout()
474
- distance_layout.addWidget(self.haralick_distance_lbl,40)
475
- distance_layout.addWidget(self.haralick_distance_le, 60)
476
- self.haralick_layout.addLayout(distance_layout)
477
-
478
- gl_layout = QHBoxLayout()
479
- gl_layout.addWidget(self.haralick_n_gray_levels_lbl,40)
480
- gl_layout.addWidget(self.haralick_n_gray_levels_le,60)
481
- self.haralick_layout.addLayout(gl_layout)
482
-
483
- slider_layout = QHBoxLayout()
484
- slider_layout.addWidget(self.haralick_scale_lbl,40)
485
- slider_layout.addWidget(self.haralick_scale_slider,60)
486
- self.haralick_layout.addLayout(slider_layout)
487
-
488
- slider_min_percentile_layout = QHBoxLayout()
489
- slider_min_percentile_layout.addWidget(self.haralick_percentile_min_lbl,40)
490
- slider_min_percentile_layout.addWidget(self.haralick_percentile_min_le,55)
491
- slider_min_percentile_layout.addWidget(self.haralick_normalization_mode_btn, 5)
492
- self.haralick_layout.addLayout(slider_min_percentile_layout)
493
-
494
- slider_max_percentile_layout = QHBoxLayout()
495
- slider_max_percentile_layout.addWidget(self.haralick_percentile_max_lbl,40)
496
- slider_max_percentile_layout.addWidget(self.haralick_percentile_max_le,60)
497
- self.haralick_layout.addLayout(slider_max_percentile_layout)
498
-
499
- self.haralick_to_hide = [self.haralick_hist_btn, self.haralick_digit_btn, self.haralick_channel_lbl, self.haralick_channel_choice,
500
- self.haralick_distance_le, self.haralick_distance_lbl, self.haralick_n_gray_levels_le, self.haralick_n_gray_levels_lbl,
501
- self.haralick_scale_lbl, self.haralick_scale_slider, self.haralick_percentile_min_lbl, self.haralick_percentile_min_le,
502
- self.haralick_percentile_max_lbl, self.haralick_percentile_max_le, self.haralick_normalization_mode_btn]
503
- for element in self.haralick_to_hide:
504
- element.hide()
505
-
506
- self.features_to_disable = [self.feature_lbl, self.del_feature_btn, self.add_feature_btn, self.features_list,
507
- self.use_channel_lbl, *self.mask_channels_cb, self.activate_haralick_btn]
508
-
509
- self.haralick_normalization_mode_btn.clicked.connect(self.switch_to_absolute_normalization_mode)
510
- layout.addLayout(self.haralick_layout)
511
-
512
- def switch_to_absolute_normalization_mode(self):
513
- if self.percentile_mode:
514
- self.percentile_mode = False
515
- self.haralick_normalization_mode_btn.setIcon(icon(MDI6.percent_circle_outline,color="black"))
516
- self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
517
- self.haralick_normalization_mode_btn.setToolTip("Switch to percentile normalization values.")
518
- self.haralick_percentile_min_lbl.setText('Min value: ')
519
- self.haralick_percentile_max_lbl.setText('Max value: ')
520
- self.haralick_percentile_min_le.setText('0')
521
- self.haralick_percentile_max_le.setText('10000')
522
-
523
- else:
524
- self.percentile_mode = True
525
- self.haralick_normalization_mode_btn.setIcon(icon(MDI6.percent_circle,color="black"))
526
- self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
527
- self.haralick_normalization_mode_btn.setToolTip("Switch to absolute normalization values.")
528
- self.haralick_percentile_min_lbl.setText('Min percentile: ')
529
- self.haralick_percentile_max_lbl.setText('Max percentile: ')
530
- self.haralick_percentile_min_le.setText('0.01')
531
- self.haralick_percentile_max_le.setText('99.99')
532
-
533
- def populate_config_frame(self):
534
-
535
- grid = QGridLayout(self.config_frame)
536
- panel_title = QLabel(f"CONFIGURATION")
537
- panel_title.setStyleSheet("""
538
- font-weight: bold;
539
- padding: 0px;
540
- """)
53
+ """
54
+ UI to set tracking parameters for bTrack.
541
55
 
542
- grid.addWidget(panel_title, 0, 0, 1, 4, alignment=Qt.AlignCenter)
56
+ """
543
57
 
544
- self.collapse_config_btn = QPushButton()
545
- self.collapse_config_btn.setIcon(icon(MDI6.chevron_down,color="black"))
546
- self.collapse_config_btn.setIconSize(QSize(20, 20))
547
- self.collapse_config_btn.setStyleSheet(self.button_select_all)
548
- grid.addWidget(self.collapse_config_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
58
+ def __init__(self, parent_window=None):
549
59
 
550
- self.generate_config_panel_contents()
551
- grid.addWidget(self.ContentsConfig, 1, 0, 1, 4, alignment=Qt.AlignTop)
552
- self.collapse_config_btn.clicked.connect(lambda: self.ContentsConfig.setHidden(not self.ContentsConfig.isHidden()))
553
- self.collapse_config_btn.clicked.connect(self.collapse_config_advanced)
554
- #self.ContentsConfig.hide()
60
+ self.parent_window = parent_window
61
+ self.mode = self.parent_window.mode
62
+ self.exp_dir = self.parent_window.exp_dir
555
63
 
556
- def populate_config_trackpy_frame(self):
64
+ self.config_name = os.sep.join(["configs", f"btrack_config_{self.mode}.json"])
65
+ self.track_instructions_write_path = self.parent_window.exp_dir + os.sep.join(
66
+ ["configs", f"tracking_instructions_{self.mode}.json"]
67
+ )
557
68
 
558
- grid = QGridLayout(self.config_trackpy_frame)
559
- panel_title = QLabel(f"CONFIGURATION")
560
- panel_title.setStyleSheet("""
561
- font-weight: bold;
562
- padding: 0px;
563
- """)
564
-
565
- grid.addWidget(panel_title, 0, 0, 1, 4, alignment=Qt.AlignCenter)
566
-
567
- self.collapse_config_trackpy_btn = QPushButton()
568
- self.collapse_config_trackpy_btn.setIcon(icon(MDI6.chevron_down,color="black"))
569
- self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
570
- self.collapse_config_trackpy_btn.setStyleSheet(self.button_select_all)
571
- grid.addWidget(self.collapse_config_trackpy_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
572
- self.generate_config_trackpy_panel_contents()
573
- grid.addWidget(self.ContentsConfigTrackpy, 1, 0, 1, 4, alignment=Qt.AlignTop)
574
- self.collapse_config_trackpy_btn.clicked.connect(lambda: self.ContentsConfigTrackpy.setHidden(not self.ContentsConfigTrackpy.isHidden()))
575
- self.collapse_config_trackpy_btn.clicked.connect(self.collapse_config_trackpy_advanced)
576
- #self.ContentsConfig.hide()
577
-
578
-
579
- def collapse_config_advanced(self):
580
-
581
- """
582
- Switch the chevron icon and adjust the size for the CONFIG frame.
583
- """
584
-
585
- features_open = not self.ContentsFeatures.isHidden()
586
- config_open = not self.ContentsConfig.isHidden()
587
- post_open = not self.ContentsPostProc.isHidden()
588
- is_open = np.array([features_open, config_open, post_open])
589
-
590
- if self.ContentsConfig.isHidden():
591
- self.collapse_config_btn.setIcon(icon(MDI6.chevron_down,color="black"))
592
- self.collapse_config_btn.setIconSize(QSize(20, 20))
593
- if len(is_open[is_open])==0:
594
- pass
595
- #self._scroll_area.setMinimumHeight(int(self.minimum_height))
596
- #self._adjustSize()
597
- else:
598
- self.collapse_config_btn.setIcon(icon(MDI6.chevron_up,color="black"))
599
- self.collapse_config_btn.setIconSize(QSize(20, 20))
600
-
601
- def collapse_config_trackpy_advanced(self):
602
-
603
- """
604
- Switch the chevron icon and adjust the size for the CONFIG frame.
605
- """
606
-
607
-
608
- post_open = not self.ContentsPostProc.isHidden()
609
- is_open = np.array([post_open])
610
-
611
- if self.ContentsConfigTrackpy.isHidden():
612
- self.collapse_config_trackpy_btn.setIcon(icon(MDI6.chevron_down,color="black"))
613
- self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
614
- if len(is_open[is_open])==0:
615
- pass
616
- #self._scroll_area.setMinimumHeight(int(self.minimum_height))
617
- #self._adjustSize()
618
- else:
619
- self.collapse_config_trackpy_btn.setIcon(icon(MDI6.chevron_up,color="black"))
620
- self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
621
-
622
-
623
- def generate_config_trackpy_panel_contents(self):
624
-
625
- self.ContentsConfigTrackpy = QFrame()
626
- layout = QVBoxLayout(self.ContentsConfigTrackpy)
627
- layout.setContentsMargins(0,0,0,0)
628
-
629
- sr_layout = QHBoxLayout()
630
- self.search_range_lbl = QLabel("search range [px]: ")
631
- self.search_range_le = QLineEdit('30')
632
- self.search_range_le.setPlaceholderText('search distance in pixels')
633
- self.search_range_le.setValidator(self._floatValidator)
634
- sr_layout.addWidget(self.search_range_lbl, 30)
635
- sr_layout.addWidget(self.search_range_le, 70)
636
- layout.addLayout(sr_layout)
637
-
638
- memory_layout = QHBoxLayout()
639
- self.memory_lbl = QLabel("memory [# frames]: ")
640
- self.memory_slider = QLabeledSlider()
641
- self.memory_slider.setSingleStep(1)
642
- self.memory_slider.setTickInterval(1)
643
- self.memory_slider.setSingleStep(1)
644
- self.memory_slider.setOrientation(Qt.Horizontal)
645
- self.memory_slider.setRange(0,self.parent_window.parent_window.len_movie)
646
- self.memory_slider.setValue(0)
647
- memory_layout.addWidget(self.memory_lbl, 30)
648
- memory_layout.addWidget(self.memory_slider, 70)
649
- layout.addLayout(memory_layout)
650
-
651
-
652
- def generate_config_panel_contents(self):
653
-
654
- self.ContentsConfig = QFrame()
655
- layout = QVBoxLayout(self.ContentsConfig)
656
- layout.setContentsMargins(0,0,0,0)
657
-
658
- btrack_config_layout = QHBoxLayout()
659
- self.config_lbl = QLabel("bTrack configuration: ")
660
- btrack_config_layout.addWidget(self.config_lbl, 90)
661
-
662
- self.upload_btrack_config_btn = QPushButton()
663
- self.upload_btrack_config_btn.setIcon(icon(MDI6.plus,color="black"))
664
- self.upload_btrack_config_btn.setIconSize(QSize(20, 20))
665
- self.upload_btrack_config_btn.setToolTip("Upload a new bTrack configuration.")
666
- self.upload_btrack_config_btn.setStyleSheet(self.button_select_all)
667
- self.upload_btrack_config_btn.clicked.connect(self.upload_btrack_config)
668
- btrack_config_layout.addWidget(self.upload_btrack_config_btn, 5) #4,3,1,1, alignment=Qt.AlignLeft
669
-
670
- self.reset_config_btn = QPushButton()
671
- self.reset_config_btn.setIcon(icon(MDI6.arrow_u_right_top,color="black"))
672
- self.reset_config_btn.setIconSize(QSize(20, 20))
673
- self.reset_config_btn.setToolTip("Reset the configuration to the default bTrack config.")
674
- self.reset_config_btn.setStyleSheet(self.button_select_all)
675
- self.reset_config_btn.clicked.connect(self.reset_btrack_config)
676
- btrack_config_layout.addWidget(self.reset_config_btn, 5) #4,3,1,1, alignment=Qt.AlignLeft
677
-
678
- layout.addLayout(btrack_config_layout)
679
-
680
- self.config_le = QTextEdit()
681
- self.config_le.setMinimumHeight(150)
682
- #self.config_le.setStyleSheet("""
683
- # background: #EEEDEB;
684
- # border: 2px solid black;
685
- # """)
686
- layout.addWidget(self.config_le)
687
- self.load_cell_config()
688
-
689
-
690
- def show_haralick_options(self):
691
-
692
- """
693
- Show the Haralick texture options.
694
- """
695
-
696
- if self.activate_haralick_btn.isChecked():
697
- for element in self.haralick_to_hide:
698
- element.show()
699
- else:
700
- for element in self.haralick_to_hide:
701
- element.hide()
702
-
703
- def upload_btrack_config(self):
704
-
705
- """
706
- Upload a specific bTrack config to the experiment folder for the cell population.
707
- """
708
-
709
- self.file_dialog = QFileDialog()
710
- try:
711
- modelpath = os.sep.join([self._software_path, "celldetective","models","tracking_configs"]) + os.sep
712
- print("Track config path: ", modelpath)
713
- self.filename = self.file_dialog.getOpenFileName(None, "Load config", modelpath, "json files (*.json)")[0]
714
- if self.filename!=self.config_path:
715
- copyfile(self.filename, self.config_path)
716
- self.load_cell_config()
717
- except Exception as e:
718
- print(e)
719
- return None
720
-
721
- def reset_btrack_config(self):
722
-
723
- """
724
- Set the bTrack config to the default bTrack config.
725
- """
726
-
727
- msgBox = QMessageBox()
728
- msgBox.setIcon(QMessageBox.Question)
729
- msgBox.setText("You are about to revert to the default bTrack configuration? Do you want to proceed?")
730
- msgBox.setWindowTitle("Confirm")
731
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
732
- returnValue = msgBox.exec()
733
- if returnValue == QMessageBox.Yes:
734
- config = interpret_tracking_configuration(None)
735
- if config!=self.config_path:
736
- copyfile(config, self.config_path)
737
- self.load_cell_config()
738
- else:
739
- return None
740
-
741
- def activate_feature_options(self):
742
-
743
- """
744
- Tick the features option.
745
- """
746
-
747
- self.switch_feature_option()
748
- if self.features_ticked:
749
- for element in self.features_to_disable:
750
- element.setEnabled(True)
751
- self.select_features_btn.setIcon(icon(MDI6.checkbox_outline,color="black"))
752
- self.select_features_btn.setIconSize(QSize(20, 20))
753
- else:
754
- for element in self.features_to_disable:
755
- element.setEnabled(False)
756
- self.select_features_btn.setIcon(icon(MDI6.checkbox_blank_outline,color="black"))
757
- self.select_features_btn.setIconSize(QSize(20, 20))
758
- self.features_list.list_widget.clearSelection()
759
- self.activate_haralick_btn.setChecked(False)
760
-
761
-
762
- def activate_post_proc_options(self):
763
-
764
- """
765
- Tick the features option.
766
- """
767
-
768
- self.switch_post_proc_option()
769
- if self.post_proc_ticked:
770
- for element in self.post_proc_options_to_disable:
771
- element.setEnabled(True)
772
- self.select_post_proc_btn.setIcon(icon(MDI6.checkbox_outline,color="black"))
773
- self.select_post_proc_btn.setIconSize(QSize(20, 20))
774
- else:
775
- for element in self.post_proc_options_to_disable:
776
- element.setEnabled(False)
777
- self.select_post_proc_btn.setIcon(icon(MDI6.checkbox_blank_outline,color="black"))
778
- self.select_post_proc_btn.setIconSize(QSize(20, 20))
779
-
780
- def switch_feature_option(self):
781
-
782
- """
783
- Switch the feature option.
784
- """
785
-
786
- if self.features_ticked == True:
787
- self.features_ticked = False
788
- else:
789
- self.features_ticked = True
790
-
791
- def switch_post_proc_option(self):
792
-
793
- """
794
- Switch the feature option.
795
- """
796
-
797
- if self.post_proc_ticked == True:
798
- self.post_proc_ticked = False
799
- else:
800
- self.post_proc_ticked = True
801
-
802
- # def adjustScrollArea(self):
803
- #
804
- # """
805
- # Auto-adjust scroll area to fill space
806
- # (from https://stackoverflow.com/questions/66417576/make-qscrollarea-use-all-available-space-of-qmainwindow-height-axis)
807
- # """
808
- #
809
- # step = 5
810
- # while self._scroll_area.verticalScrollBar().isVisible() and self.height() < self.maximumHeight():
811
- # self.resize(self.width(), self.height() + step)
812
-
813
- def load_cell_config(self):
814
-
815
- """
816
- Load the cell configuration and write in the QLineEdit.
817
- """
818
-
819
- file_name = interpret_tracking_configuration(self.config_path)
820
- with open(file_name, 'r') as f:
821
- json_data = json.load(f)
822
- self.config_le.setText(json.dumps(json_data, indent=4))
823
-
824
- def _write_instructions(self):
825
-
826
- """
827
- Write the selected options in a json file for later reading by the software.
828
- """
829
-
830
- print('Writing instructions...')
831
-
832
- if self.btrack_option.isChecked():
833
- btrack_option = True
834
- else:
835
- btrack_option = False
836
-
837
- # Fetch trackpky params
838
- if not btrack_option:
839
- search_range = int(self.search_range_le.text().replace(',','.'))
840
- memory = self.memory_slider.value()
841
- else:
842
- search_range = None
843
- memory = None
844
-
845
- tracking_options = {'btrack_config_path': self.config_path}
846
- tracking_options.update({'btrack_option': btrack_option, 'search_range': search_range, 'memory': memory})
847
-
848
- if not self.features_ticked:
849
- features = None
850
- masked_channels = None
851
- else:
852
- features = self.features_list.getItems()
853
- masked_channels = self.channel_names[np.array([not cb.isChecked() for cb in self.mask_channels_cb])]
854
- if len(masked_channels)==0:
855
- masked_channels = None
856
- else:
857
- masked_channels = list(masked_channels)
858
-
859
- tracking_options.update({'features': features, 'mask_channels': masked_channels})
860
-
861
- self.extract_haralick_options()
862
- tracking_options.update({'haralick_options': self.haralick_options})
863
-
864
- if self.post_proc_ticked:
865
- post_processing_options = {"minimum_tracklength": int(self.min_tracklength_slider.value()),
866
- "remove_not_in_first": self.remove_not_in_first_checkbox.isChecked(),
867
- "remove_not_in_last": self.remove_not_in_last_checkbox.isChecked(),
868
- "interpolate_position_gaps": self.interpolate_gaps_checkbox.isChecked(),
869
- "extrapolate_tracks_pre": self.extrapolate_pre_checkbox.isChecked(),
870
- "extrapolate_tracks_post": self.extrapolate_post_checkbox.isChecked(),
871
- 'interpolate_na': False, #self.interpolate_na_features_checkbox.isChecked()
872
- }
873
- else:
874
-
875
- post_processing_options = None
876
-
877
- tracking_options.update({'post_processing_options': post_processing_options})
878
- file_name = self.track_instructions_write_path
879
- with open(file_name, 'w') as f:
880
- json.dump(tracking_options, f, indent=4)
881
-
882
- # Save the JSON data to the file
883
- file_name = self.config_path
884
- with open(file_name, 'w') as f:
885
- f.write(self.config_le.toPlainText())
886
- print('Done.')
887
- self.close()
888
-
889
- def uncheck_post_proc(self):
890
- self.select_post_proc_btn.setIcon(icon(MDI6.checkbox_blank_outline,color="black"))
891
- self.select_post_proc_btn.setIconSize(QSize(20, 20))
892
- self.post_proc_ticked = False
893
- for element in self.post_proc_options_to_disable:
894
- element.setEnabled(False)
895
-
896
- def check_post_proc(self):
897
- self.select_post_proc_btn.setIcon(icon(MDI6.checkbox_outline,color="black"))
898
- self.select_post_proc_btn.setIconSize(QSize(20, 20))
899
- self.post_proc_ticked = True
900
- for element in self.post_proc_options_to_disable:
901
- element.setEnabled(True)
902
-
903
- def uncheck_features(self):
904
- self.select_features_btn.setIcon(icon(MDI6.checkbox_blank_outline,color="black"))
905
- self.select_features_btn.setIconSize(QSize(20, 20))
906
- self.features_ticked = False
907
- for element in self.features_to_disable:
908
- element.setEnabled(False)
909
-
910
- def check_features(self):
911
- self.select_features_btn.setIcon(icon(MDI6.checkbox_outline,color="black"))
912
- self.select_features_btn.setIconSize(QSize(20, 20))
913
- self.features_ticked = True
914
- for element in self.features_to_disable:
915
- element.setEnabled(True)
916
-
917
- def extract_haralick_options(self):
918
-
919
- if self.activate_haralick_btn.isChecked():
920
- self.haralick_options = {"target_channel": self.haralick_channel_choice.currentIndex(),
921
- "scale_factor": float(self.haralick_scale_slider.value()),
922
- "n_intensity_bins": int(self.haralick_n_gray_levels_le.text()),
923
- "distance" : int(self.haralick_distance_le.text()),
924
- }
925
- if self.percentile_mode:
926
- self.haralick_options.update({"percentiles": (float(self.haralick_percentile_min_le.text()), float(self.haralick_percentile_max_le.text())), "clip_values": None})
927
- else:
928
- self.haralick_options.update({"percentiles": None, "clip_values": (float(self.haralick_percentile_min_le.text()), float(self.haralick_percentile_max_le.text()))})
929
-
930
- else:
931
- self.haralick_options = None
932
-
933
- def _load_previous_instructions(self):
934
-
935
- """
936
- Read the tracking options from a previously written json file.
937
- """
938
-
939
- print('Reading instructions..')
940
- if os.path.exists(self.track_instructions_write_path):
941
- with open(self.track_instructions_write_path, 'r') as f:
942
- tracking_instructions = json.load(f)
943
- print(tracking_instructions)
944
-
945
- # Features
946
- features = tracking_instructions['features']
947
- if (features is not None) and len(features)>0:
948
- self.check_features()
949
- self.ContentsFeatures.show()
950
- self.features_list.list_widget.clear()
951
- self.features_list.list_widget.addItems(features)
952
- else:
953
- self.ContentsFeatures.hide()
954
- self.uncheck_features()
955
-
956
- btrack_option = True
957
- if 'btrack_option' in tracking_instructions:
958
- btrack_option = tracking_instructions['btrack_option']
959
- if btrack_option:
960
- self.btrack_option.click()
961
- else:
962
- self.trackpy_option.click()
963
-
964
- if 'search_range' in tracking_instructions:
965
- search_range = tracking_instructions['search_range']
966
- if search_range is not None:
967
- self.search_range_le.setText(str(search_range).replace('.', ','))
968
- if 'memory' in tracking_instructions:
969
- memory = tracking_instructions['memory']
970
- if memory is not None:
971
- self.memory_slider.setValue(memory)
972
-
973
- # Uncheck channels that are masked
974
- mask_channels = tracking_instructions['mask_channels']
975
- if (mask_channels is not None) and len(mask_channels)>0:
976
- for ch in mask_channels:
977
- for cb in self.mask_channels_cb:
978
- if cb.text()==ch:
979
- cb.setChecked(False)
980
-
981
- haralick_options = tracking_instructions['haralick_options']
982
- if haralick_options is None:
983
- self.activate_haralick_btn.setChecked(False)
984
- self.show_haralick_options()
985
- else:
986
- self.activate_haralick_btn.setChecked(True)
987
- self.show_haralick_options()
988
- if 'target_channel' in haralick_options:
989
- idx = haralick_options['target_channel']
990
- #idx = self.haralick_channel_choice.findText(text_to_find)
991
- self.haralick_channel_choice.setCurrentIndex(idx)
992
- if 'scale_factor' in haralick_options:
993
- self.haralick_scale_slider.setValue(float(haralick_options['scale_factor']))
994
- if ('percentiles' in haralick_options) and (haralick_options['percentiles'] is not None):
995
- perc = list(haralick_options['percentiles'])
996
- self.haralick_percentile_min_le.setText(str(perc[0]))
997
- self.haralick_percentile_max_le.setText(str(perc[1]))
998
- if ('clip_values' in haralick_options) and (haralick_options['clip_values'] is not None):
999
- values = list(haralick_options['clip_values'])
1000
- self.haralick_percentile_min_le.setText(str(values[0]))
1001
- self.haralick_percentile_max_le.setText(str(values[1]))
1002
- self.percentile_mode=True
1003
- self.switch_to_absolute_normalization_mode()
1004
- if 'n_intensity_bins' in haralick_options:
1005
- self.haralick_n_gray_levels_le.setText(str(haralick_options['n_intensity_bins']))
1006
- if 'distance' in haralick_options:
1007
- self.haralick_distance_le.setText(str(haralick_options['distance']))
1008
-
1009
-
1010
-
1011
- # Post processing options
1012
- post_processing_options = tracking_instructions['post_processing_options']
1013
- if post_processing_options is None:
1014
- self.uncheck_post_proc()
1015
- self.ContentsPostProc.hide()
1016
- for element in [self.remove_not_in_last_checkbox, self.remove_not_in_first_checkbox, self.interpolate_gaps_checkbox,
1017
- self.extrapolate_post_checkbox, self.extrapolate_pre_checkbox]: #self.interpolate_na_features_checkbox
1018
- element.setChecked(False)
1019
- self.min_tracklength_slider.setValue(0)
1020
-
1021
- else:
1022
- self.check_post_proc()
1023
- self.ContentsPostProc.show()
1024
- if "minimum_tracklength" in post_processing_options:
1025
- self.min_tracklength_slider.setValue(int(post_processing_options["minimum_tracklength"]))
1026
- if "remove_not_in_first" in post_processing_options:
1027
- self.remove_not_in_first_checkbox.setChecked(post_processing_options["remove_not_in_first"])
1028
- if "remove_not_in_last" in post_processing_options:
1029
- self.remove_not_in_last_checkbox.setChecked(post_processing_options["remove_not_in_last"])
1030
- if "interpolate_position_gaps" in post_processing_options:
1031
- self.interpolate_gaps_checkbox.setChecked(post_processing_options["interpolate_position_gaps"])
1032
- if "extrapolate_tracks_pre" in post_processing_options:
1033
- self.extrapolate_pre_checkbox.setChecked(post_processing_options["extrapolate_tracks_pre"])
1034
- if "extrapolate_tracks_post" in post_processing_options:
1035
- self.extrapolate_post_checkbox.setChecked(post_processing_options["extrapolate_tracks_post"])
1036
- # if "interpolate_na" in post_processing_options:
1037
- # self.interpolate_na_features_checkbox.setChecked(post_processing_options["interpolate_na"])
1038
-
1039
- def locate_image(self):
1040
-
1041
- """
1042
- Load the first frame of the first movie found in the experiment folder as a sample.
1043
- """
1044
-
1045
- movies = glob(self.parent_window.parent_window.exp_dir + os.sep.join(["*", "*", "movie", self.parent_window.parent_window.movie_prefix+"*.tif"]))
1046
- if len(movies)==0:
1047
- msgBox = QMessageBox()
1048
- msgBox.setIcon(QMessageBox.Warning)
1049
- msgBox.setText("No movies are detected in the experiment folder. Cannot load an image to test Haralick.")
1050
- msgBox.setWindowTitle("Warning")
1051
- msgBox.setStandardButtons(QMessageBox.Ok)
1052
- returnValue = msgBox.exec()
1053
- if returnValue == QMessageBox.Yes:
1054
- self.test_frame = None
1055
- return None
1056
- else:
1057
- stack0 = movies[0]
1058
- n_channels = len(self.channels)
1059
- self.test_frame = load_frames(np.arange(n_channels), stack0, scale=None, normalize_input=False)
1060
-
1061
- def control_haralick_digitalization(self):
1062
-
1063
- """
1064
- Load an image for the first experiment movie found.
1065
- Apply the Haralick parameters and check the result of the digitization (normalization + binning of intensities).
1066
-
1067
- """
1068
-
1069
- self.locate_image()
1070
- self.extract_haralick_options()
1071
- if self.test_frame is not None:
1072
- digitized_img = compute_haralick_features(self.test_frame, np.zeros(self.test_frame.shape[:2]),
1073
- channels=self.channel_names, return_digit_image_only=True,
1074
- **self.haralick_options
1075
- )
1076
-
1077
- self.fig, self.ax = plt.subplots()
1078
- divider = make_axes_locatable(self.ax)
1079
- cax = divider.append_axes('right', size='5%', pad=0.05)
1080
-
1081
- self.imshow_digit_window = FigureCanvas(self.fig, title="Haralick: control digitization")
1082
- self.ax.clear()
1083
- im = self.ax.imshow(digitized_img, cmap='gray')
1084
- self.fig.colorbar(im, cax=cax, orientation='vertical')
1085
- self.ax.set_xticks([])
1086
- self.ax.set_yticks([])
1087
- self.fig.set_facecolor('none') # or 'None'
1088
- self.fig.canvas.setStyleSheet("background-color: transparent;")
1089
- self.imshow_digit_window.canvas.draw()
1090
- self.imshow_digit_window.show()
1091
-
1092
- def control_haralick_intensity_histogram(self):
1093
-
1094
- """
1095
- Load an image for the first experiment movie found.
1096
- Apply the Haralick normalization parameters and check the normalized intensity histogram.
1097
-
1098
- """
1099
-
1100
- self.locate_image()
1101
- self.extract_haralick_options()
1102
- if self.test_frame is not None:
1103
- norm_img = compute_haralick_features(self.test_frame, np.zeros(self.test_frame.shape[:2]),
1104
- channels=self.channel_names, return_norm_image_only=True,
1105
- **self.haralick_options
1106
- )
1107
- self.fig, self.ax = plt.subplots(1,1,figsize=(4,3))
1108
- self.hist_window = FigureCanvas(self.fig, title="Haralick: control digitized histogram")
1109
- self.ax.clear()
1110
- flat = norm_img.flatten()
1111
- self.ax.hist(flat[flat==flat], bins=self.haralick_options['n_intensity_bins'])
1112
- self.ax.set_xlabel('gray level value')
1113
- self.ax.set_ylabel('#')
1114
- plt.tight_layout()
1115
- self.fig.set_facecolor('none') # or 'None'
1116
- self.fig.canvas.setStyleSheet("background-color: transparent;")
1117
- self.hist_window.canvas.draw()
1118
- self.hist_window.show()
69
+ self.config_path = self.exp_dir + self.config_name
70
+ self.channel_names, self.channels = extract_experiment_channels(self.exp_dir)
71
+ self.channel_names = np.array(self.channel_names)
72
+ self.channels = np.array(self.channels)
73
+ self.minimum_height = 300
74
+
75
+ super().__init__(title="Configure tracking")
76
+
77
+ self._add_to_layout()
78
+ self._load_previous_instructions()
79
+
80
+ self._widget.setMinimumWidth(500)
81
+ self._adjust_size()
82
+ self.resize(int(self.width() * 1.1), int(self._screen_height * 0.8))
83
+
84
+ def _add_to_layout(self):
85
+
86
+ tracker_hbox = QHBoxLayout()
87
+ tracker_hbox.setContentsMargins(15, 15, 15, 15)
88
+ tracker_hbox.addWidget(self.btrack_option, 50, alignment=Qt.AlignCenter)
89
+ tracker_hbox.addWidget(self.trackpy_option, 50, alignment=Qt.AlignCenter)
90
+
91
+ self._layout.addLayout(tracker_hbox)
92
+ self._layout.addWidget(self.config_frame)
93
+ self._layout.addWidget(self.features_frame)
94
+ self._layout.addWidget(self.config_trackpy_frame)
95
+ self._layout.addWidget(self.post_proc_frame)
96
+ self._layout.addWidget(self.submit_btn)
1119
97
 
98
+ def _create_widgets(self):
99
+ """
100
+ Create the multibox design with collapsable frames.
1120
101
 
102
+ """
1121
103
 
104
+ super()._create_widgets()
1122
105
 
106
+ self.btrack_option = QRadioButton("bTrack")
107
+ self.btrack_option.setChecked(True)
1123
108
 
109
+ self.trackpy_option = QRadioButton("trackpy")
110
+ self.tracker_option_group = QButtonGroup()
111
+ self.tracker_option_group.addButton(self.btrack_option)
112
+ self.tracker_option_group.addButton(self.trackpy_option)
113
+
114
+ self.config_frame = QFrame()
115
+ self.config_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
116
+ self.populate_config_frame()
117
+
118
+ # Second collapsable frame FEATURES
119
+ self.features_frame = QFrame()
120
+ self.features_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
121
+ self.populate_features_frame()
122
+
123
+ self.config_trackpy_frame = QFrame()
124
+ self.config_trackpy_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
125
+ self.populate_config_trackpy_frame()
126
+ self.config_trackpy_frame.hide()
127
+
128
+ # Third collapsable frame POST-PROCESSING
129
+ self.post_proc_frame = QFrame()
130
+ self.post_proc_frame.setFrameStyle(QFrame.StyledPanel | QFrame.Raised)
131
+ self.populate_post_proc_frame()
132
+
133
+ self.btrack_option.toggled.connect(self.show_tracking_options)
134
+ self.trackpy_option.toggled.connect(self.show_tracking_options)
135
+
136
+ def show_tracking_options(self):
137
+
138
+ if self.btrack_option.isChecked():
139
+ self.config_frame.show()
140
+ self.features_frame.show()
141
+ self.config_trackpy_frame.hide()
142
+ # self._adjustSize()
143
+ else:
144
+ self.config_frame.hide()
145
+ self.features_frame.hide()
146
+ self.config_trackpy_frame.show()
147
+ # self._adjustSize()
148
+
149
+ def populate_post_proc_frame(self):
150
+ """
151
+ Add widgets and layout in the POST-PROCESSING frame.
152
+ """
153
+
154
+ grid = QGridLayout(self.post_proc_frame)
155
+
156
+ self.select_post_proc_btn = QPushButton()
157
+ self.select_post_proc_btn.clicked.connect(self.activate_post_proc_options)
158
+ self.select_post_proc_btn.setStyleSheet(self.button_select_all)
159
+
160
+ self.post_proc_lbl = QLabel("POST-PROCESSING")
161
+ self.post_proc_lbl.setStyleSheet(
162
+ """
163
+ font-weight: bold;
164
+ padding: 0px;
165
+ """
166
+ )
167
+ grid.addWidget(self.post_proc_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
168
+
169
+ title_hbox = QHBoxLayout()
170
+
171
+ self.collapse_post_proc_btn = QPushButton()
172
+ self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down, color="black"))
173
+ self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
174
+ self.collapse_post_proc_btn.setStyleSheet(self.button_select_all)
175
+ # grid.addWidget(self.collapse_post_proc_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
176
+
177
+ self.help_post_btn = QPushButton()
178
+ self.help_post_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
179
+ self.help_post_btn.setIconSize(QSize(20, 20))
180
+ self.help_post_btn.clicked.connect(self.help_post)
181
+ self.help_post_btn.setStyleSheet(self.button_select_all)
182
+ self.help_post_btn.setToolTip("Help.")
183
+
184
+ title_hbox.addWidget(self.select_post_proc_btn, 5)
185
+ title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
186
+ title_hbox.addWidget(self.help_post_btn, 5)
187
+ title_hbox.addWidget(self.collapse_post_proc_btn, 5)
188
+ grid.addLayout(title_hbox, 0, 0, 1, 4)
189
+
190
+ self.generate_post_proc_panel_contents()
191
+ grid.addWidget(self.ContentsPostProc, 1, 0, 1, 4, alignment=Qt.AlignTop)
192
+ self.collapse_post_proc_btn.clicked.connect(
193
+ lambda: self.ContentsPostProc.setHidden(
194
+ not self.ContentsPostProc.isHidden()
195
+ )
196
+ )
197
+ self.collapse_post_proc_btn.clicked.connect(self.collapse_post_advanced)
198
+ self.ContentsPostProc.hide()
199
+ self.uncheck_post_proc()
200
+
201
+ def collapse_post_advanced(self):
202
+
203
+ features_open = not self.ContentsFeatures.isHidden()
204
+ config_open = not self.ContentsConfig.isHidden()
205
+ post_open = not self.ContentsPostProc.isHidden()
206
+ is_open = np.array([features_open, config_open, post_open])
207
+
208
+ if self.ContentsPostProc.isHidden():
209
+ self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_down, color="black"))
210
+ self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
211
+ if len(is_open[is_open]) == 0:
212
+ pass
213
+ # self._scroll_area.setMinimumHeight(int(self.minimum_height))
214
+ # self._adjustSize()
215
+ else:
216
+ self.collapse_post_proc_btn.setIcon(icon(MDI6.chevron_up, color="black"))
217
+ self.collapse_post_proc_btn.setIconSize(QSize(20, 20))
218
+
219
+ def help_post(self):
220
+ """
221
+ Helper for track post-processing strategy.
222
+ """
223
+
224
+ dict_path = os.sep.join(
225
+ [
226
+ get_software_location(),
227
+ "celldetective",
228
+ "gui",
229
+ "help",
230
+ "track-postprocessing.json",
231
+ ]
232
+ )
233
+
234
+ with open(dict_path) as f:
235
+ d = json.load(f)
236
+
237
+ suggestion = help_generic(d)
238
+ if isinstance(suggestion, str):
239
+ logger.info(f"{suggestion=}")
240
+ msg_box = QMessageBox()
241
+ msg_box.setIcon(QMessageBox.Information)
242
+ msg_box.setTextFormat(Qt.RichText)
243
+ msg_box.setText(rf"{suggestion}")
244
+ msg_box.setWindowTitle("Info")
245
+ msg_box.setStandardButtons(QMessageBox.Ok)
246
+ return_value = msg_box.exec()
247
+ if return_value == QMessageBox.Ok:
248
+ return None
249
+
250
+ def help_feature(self):
251
+ """
252
+ Helper for track post-processing strategy.
253
+ """
254
+
255
+ dict_path = os.sep.join(
256
+ [
257
+ get_software_location(),
258
+ "celldetective",
259
+ "gui",
260
+ "help",
261
+ "feature-btrack.json",
262
+ ]
263
+ )
264
+
265
+ with open(dict_path) as f:
266
+ d = json.load(f)
267
+
268
+ suggestion = help_generic(d)
269
+ if isinstance(suggestion, str):
270
+ logger.info(f"{suggestion=}")
271
+ msg_box = QMessageBox()
272
+ msg_box.setIcon(QMessageBox.Information)
273
+ msg_box.setTextFormat(Qt.RichText)
274
+ msg_box.setText(rf"{suggestion}")
275
+ msg_box.setWindowTitle("Info")
276
+ msg_box.setStandardButtons(QMessageBox.Ok)
277
+ return_value = msg_box.exec()
278
+ if return_value == QMessageBox.Ok:
279
+ return None
280
+
281
+ def populate_features_frame(self):
282
+ """
283
+ Add widgets and layout in the FEATURES frame.
284
+ """
285
+
286
+ grid = QGridLayout(self.features_frame)
287
+ title_hbox = QHBoxLayout()
288
+
289
+ self.select_features_btn = QPushButton()
290
+ self.select_features_btn.clicked.connect(self.activate_feature_options)
291
+ self.select_features_btn.setStyleSheet(self.button_select_all)
292
+
293
+ self.feature_lbl = QLabel("FEATURES")
294
+ self.feature_lbl.setStyleSheet(
295
+ """
296
+ font-weight: bold;
297
+ padding: 0px;
298
+ """
299
+ )
300
+ grid.addWidget(self.feature_lbl, 0, 0, 1, 4, alignment=Qt.AlignCenter)
301
+
302
+ self.collapse_features_btn = QPushButton()
303
+ self.collapse_features_btn.setIcon(icon(MDI6.chevron_down, color="black"))
304
+ self.collapse_features_btn.setIconSize(QSize(20, 20))
305
+ self.collapse_features_btn.setStyleSheet(self.button_select_all)
306
+
307
+ self.help_feature_btn = QPushButton()
308
+ self.help_feature_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
309
+ self.help_feature_btn.setIconSize(QSize(20, 20))
310
+ self.help_feature_btn.clicked.connect(self.help_feature)
311
+ self.help_feature_btn.setStyleSheet(self.button_select_all)
312
+ self.help_feature_btn.setToolTip("Help.")
313
+
314
+ title_hbox.addWidget(self.select_features_btn, 5)
315
+ title_hbox.addWidget(QLabel(), 85, alignment=Qt.AlignCenter)
316
+ title_hbox.addWidget(self.help_feature_btn, 5)
317
+ title_hbox.addWidget(self.collapse_features_btn, 5)
318
+ grid.addLayout(title_hbox, 0, 0, 1, 4)
319
+
320
+ self.generate_feature_panel_contents()
321
+ grid.addWidget(self.ContentsFeatures, 1, 0, 1, 4, alignment=Qt.AlignTop)
322
+ self.collapse_features_btn.clicked.connect(
323
+ lambda: self.ContentsFeatures.setHidden(
324
+ not self.ContentsFeatures.isHidden()
325
+ )
326
+ )
327
+ self.collapse_features_btn.clicked.connect(self.collapse_features_advanced)
328
+ # self.ContentsFeatures.hide()
329
+ self.check_features()
330
+
331
+ def collapse_features_advanced(self):
332
+ """
333
+ Switch the chevron icon and adjust the size for the FEATURES frame.
334
+ """
335
+
336
+ features_open = not self.ContentsFeatures.isHidden()
337
+ config_open = not self.ContentsConfig.isHidden()
338
+ post_open = not self.ContentsPostProc.isHidden()
339
+ is_open = np.array([features_open, config_open, post_open])
340
+
341
+ if self.ContentsFeatures.isHidden():
342
+ self.collapse_features_btn.setIcon(icon(MDI6.chevron_down, color="black"))
343
+ self.collapse_features_btn.setIconSize(QSize(20, 20))
344
+ if len(is_open[is_open]) == 0:
345
+ pass
346
+ # self._scroll_area.setMinimumHeight(int(self.minimum_height))
347
+ # self._adjustSize()
348
+ else:
349
+ self.collapse_features_btn.setIcon(icon(MDI6.chevron_up, color="black"))
350
+ self.collapse_features_btn.setIconSize(QSize(20, 20))
351
+
352
+ def generate_post_proc_panel_contents(self):
353
+
354
+ self.ContentsPostProc = QFrame()
355
+ layout = QVBoxLayout(self.ContentsPostProc)
356
+ layout.setContentsMargins(0, 0, 0, 0)
357
+
358
+ post_proc_layout = QHBoxLayout()
359
+ self.post_proc_lbl = QLabel("Post processing on the tracks:")
360
+ post_proc_layout.addWidget(self.post_proc_lbl, 90)
361
+ layout.addLayout(post_proc_layout)
362
+
363
+ clean_traj_sublayout = QVBoxLayout()
364
+ clean_traj_sublayout.setContentsMargins(15, 15, 15, 15)
365
+
366
+ tracklength_layout = QHBoxLayout()
367
+ self.min_tracklength_slider = QLabeledSlider()
368
+ self.min_tracklength_slider.setSingleStep(1)
369
+ self.min_tracklength_slider.setTickInterval(1)
370
+ self.min_tracklength_slider.setSingleStep(1)
371
+ self.min_tracklength_slider.setOrientation(Qt.Horizontal)
372
+ self.min_tracklength_slider.setRange(
373
+ 0, self.parent_window.parent_window.len_movie
374
+ )
375
+ self.min_tracklength_slider.setValue(0)
376
+ tracklength_layout.addWidget(QLabel("Min. tracklength: "), 40)
377
+ tracklength_layout.addWidget(self.min_tracklength_slider, 60)
378
+ clean_traj_sublayout.addLayout(tracklength_layout)
379
+
380
+ self.remove_not_in_first_checkbox = QCheckBox(
381
+ "Remove tracks that do not start at the beginning"
382
+ )
383
+ self.remove_not_in_first_checkbox.setIcon(
384
+ icon(MDI6.arrow_expand_right, color="k")
385
+ )
386
+ clean_traj_sublayout.addWidget(self.remove_not_in_first_checkbox)
387
+
388
+ self.remove_not_in_last_checkbox = QCheckBox(
389
+ "Remove tracks that do not end at the end"
390
+ )
391
+ self.remove_not_in_last_checkbox.setIcon(
392
+ icon(MDI6.arrow_expand_left, color="k")
393
+ )
394
+ clean_traj_sublayout.addWidget(self.remove_not_in_last_checkbox)
395
+
396
+ self.interpolate_gaps_checkbox = QCheckBox(
397
+ "Interpolate missed detections within tracks"
398
+ )
399
+ self.interpolate_gaps_checkbox.setIcon(
400
+ icon(MDI6.chart_timeline_variant_shimmer, color="k")
401
+ )
402
+ clean_traj_sublayout.addWidget(self.interpolate_gaps_checkbox)
403
+
404
+ self.extrapolate_post_checkbox = QCheckBox(
405
+ "Sustain last position until the end of the movie"
406
+ )
407
+ self.extrapolate_post_checkbox.setIcon(icon(MDI6.repeat, color="k"))
408
+
409
+ self.extrapolate_pre_checkbox = QCheckBox(
410
+ "Sustain first position from the beginning of the movie"
411
+ )
412
+ self.extrapolate_pre_checkbox.setIcon(icon(MDI6.repeat, color="k"))
413
+
414
+ clean_traj_sublayout.addWidget(self.extrapolate_post_checkbox)
415
+ clean_traj_sublayout.addWidget(self.extrapolate_pre_checkbox)
416
+
417
+ # self.interpolate_na_features_checkbox = QCheckBox('Interpolate features of missed detections')
418
+ # self.interpolate_na_features_checkbox.setIcon(icon(MDI6.format_color_fill,color="k"))
419
+
420
+ # clean_traj_sublayout.addWidget(self.interpolate_na_features_checkbox)
421
+ clean_traj_sublayout.addStretch()
422
+
423
+ self.post_proc_options_to_disable = [
424
+ self.post_proc_lbl,
425
+ self.min_tracklength_slider,
426
+ self.remove_not_in_first_checkbox,
427
+ self.remove_not_in_last_checkbox,
428
+ self.interpolate_gaps_checkbox,
429
+ self.extrapolate_post_checkbox,
430
+ self.extrapolate_pre_checkbox,
431
+ ] # , self.interpolate_na_features_checkbox
432
+
433
+ layout.addLayout(clean_traj_sublayout)
434
+
435
+ def generate_feature_panel_contents(self):
436
+
437
+ self.ContentsFeatures = QFrame()
438
+ layout = QVBoxLayout(self.ContentsFeatures)
439
+ layout.setContentsMargins(0, 0, 0, 0)
440
+
441
+ feature_layout = QHBoxLayout()
442
+ feature_layout.setContentsMargins(0, 0, 0, 0)
443
+
444
+ self.feature_lbl = QLabel("Add features:")
445
+ self.del_feature_btn = QPushButton("")
446
+ self.del_feature_btn.setStyleSheet(self.button_select_all)
447
+ self.del_feature_btn.setIcon(icon(MDI6.trash_can, color="black"))
448
+ self.del_feature_btn.setToolTip("Remove feature")
449
+ self.del_feature_btn.setIconSize(QSize(20, 20))
450
+
451
+ self.add_feature_btn = QPushButton("")
452
+ self.add_feature_btn.setStyleSheet(self.button_select_all)
453
+ self.add_feature_btn.setIcon(icon(MDI6.filter_plus, color="black"))
454
+ self.add_feature_btn.setToolTip("Add feature")
455
+ self.add_feature_btn.setIconSize(QSize(20, 20))
456
+
457
+ self.features_list = ListWidget(
458
+ FeatureChoice,
459
+ initial_features=[
460
+ "area",
461
+ "intensity_mean",
462
+ ],
463
+ )
464
+
465
+ self.del_feature_btn.clicked.connect(self.features_list.removeSel)
466
+ self.add_feature_btn.clicked.connect(self.features_list.addItem)
467
+
468
+ feature_layout.addWidget(self.feature_lbl, 90)
469
+ feature_layout.addWidget(self.del_feature_btn, 5)
470
+ feature_layout.addWidget(self.add_feature_btn, 5)
471
+ layout.addLayout(feature_layout)
472
+ layout.addWidget(self.features_list)
473
+
474
+ self.feat_sep2 = QHSeperationLine()
475
+ layout.addWidget(self.feat_sep2)
476
+
477
+ self.use_channel_lbl = QLabel("Use channel:")
478
+ layout.addWidget(self.use_channel_lbl)
479
+ self.mask_channels_cb = [QCheckBox() for i in range(len(self.channels))]
480
+ for cb, chn in zip(self.mask_channels_cb, self.channel_names):
481
+ cb.setText(chn)
482
+ cb.setChecked(True)
483
+ layout.addWidget(cb)
484
+
485
+ self.feat_sep1 = QHSeperationLine()
486
+ layout.addWidget(self.feat_sep1)
487
+
488
+ self.activate_haralick_btn = QCheckBox("activate Haralick texture features")
489
+ self.activate_haralick_btn.toggled.connect(self.show_haralick_options)
490
+ # Haralick features parameters
491
+
492
+ self.haralick_channel_choice = QComboBox()
493
+ self.haralick_channel_choice.addItems(self.channel_names)
494
+ self.haralick_channel_lbl = QLabel("Target channel: ")
495
+
496
+ self.haralick_distance_le = QLineEdit("1")
497
+ self.haralick_distance_lbl = QLabel("Distance: ")
498
+
499
+ self.haralick_n_gray_levels_le = QLineEdit("256")
500
+ self.haralick_n_gray_levels_lbl = QLabel("# gray levels: ")
501
+
502
+ # Slider to set vmin & vmax
503
+ self.haralick_scale_slider = QLabeledDoubleSlider()
504
+ self.haralick_scale_slider.setTickInterval(0.05)
505
+ self.haralick_scale_slider.setSingleStep(1)
506
+ self.haralick_scale_slider.setOrientation(Qt.Horizontal)
507
+ self.haralick_scale_slider.setRange(0, 1)
508
+ self.haralick_scale_slider.setValue(0.5)
509
+ self.haralick_scale_lbl = QLabel("Scale: ")
510
+
511
+ self.haralick_percentile_min_le = QLineEdit("0.01")
512
+ self.haralick_percentile_max_le = QLineEdit("99.9")
513
+ self.haralick_normalization_mode_btn = QPushButton()
514
+ self.haralick_normalization_mode_btn.setStyleSheet(self.button_select_all)
515
+ self.haralick_normalization_mode_btn.setIcon(
516
+ icon(MDI6.percent_circle, color="black")
517
+ )
518
+ self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
519
+ self.haralick_normalization_mode_btn.setToolTip(
520
+ "Switch to absolute normalization values."
521
+ )
522
+ self.percentile_mode = True
523
+
524
+ self.haralick_percentile_min_lbl = QLabel("Min percentile: ")
525
+ self.haralick_percentile_max_lbl = QLabel("Max percentile: ")
526
+
527
+ self.haralick_hist_btn = QPushButton()
528
+ self.haralick_hist_btn.clicked.connect(
529
+ self.control_haralick_intensity_histogram
530
+ )
531
+ self.haralick_hist_btn.setIcon(icon(MDI6.poll, color="k"))
532
+ self.haralick_hist_btn.setStyleSheet(self.button_select_all)
533
+
534
+ self.haralick_digit_btn = QPushButton()
535
+ self.haralick_digit_btn.clicked.connect(self.control_haralick_digitalization)
536
+ self.haralick_digit_btn.setIcon(icon(MDI6.image_check, color="k"))
537
+ self.haralick_digit_btn.setStyleSheet(self.button_select_all)
538
+
539
+ self.haralick_layout = QVBoxLayout()
540
+ self.haralick_layout.setContentsMargins(20, 20, 20, 20)
541
+
542
+ activate_layout = QHBoxLayout()
543
+ activate_layout.addWidget(self.activate_haralick_btn, 80)
544
+ activate_layout.addWidget(self.haralick_hist_btn, 10)
545
+ activate_layout.addWidget(self.haralick_digit_btn, 10)
546
+ self.haralick_layout.addLayout(activate_layout)
547
+
548
+ channel_layout = QHBoxLayout()
549
+ channel_layout.addWidget(self.haralick_channel_lbl, 40)
550
+ channel_layout.addWidget(self.haralick_channel_choice, 60)
551
+ self.haralick_layout.addLayout(channel_layout)
552
+
553
+ distance_layout = QHBoxLayout()
554
+ distance_layout.addWidget(self.haralick_distance_lbl, 40)
555
+ distance_layout.addWidget(self.haralick_distance_le, 60)
556
+ self.haralick_layout.addLayout(distance_layout)
557
+
558
+ gl_layout = QHBoxLayout()
559
+ gl_layout.addWidget(self.haralick_n_gray_levels_lbl, 40)
560
+ gl_layout.addWidget(self.haralick_n_gray_levels_le, 60)
561
+ self.haralick_layout.addLayout(gl_layout)
562
+
563
+ slider_layout = QHBoxLayout()
564
+ slider_layout.addWidget(self.haralick_scale_lbl, 40)
565
+ slider_layout.addWidget(self.haralick_scale_slider, 60)
566
+ self.haralick_layout.addLayout(slider_layout)
567
+
568
+ slider_min_percentile_layout = QHBoxLayout()
569
+ slider_min_percentile_layout.addWidget(self.haralick_percentile_min_lbl, 40)
570
+ slider_min_percentile_layout.addWidget(self.haralick_percentile_min_le, 55)
571
+ slider_min_percentile_layout.addWidget(self.haralick_normalization_mode_btn, 5)
572
+ self.haralick_layout.addLayout(slider_min_percentile_layout)
573
+
574
+ slider_max_percentile_layout = QHBoxLayout()
575
+ slider_max_percentile_layout.addWidget(self.haralick_percentile_max_lbl, 40)
576
+ slider_max_percentile_layout.addWidget(self.haralick_percentile_max_le, 60)
577
+ self.haralick_layout.addLayout(slider_max_percentile_layout)
578
+
579
+ self.haralick_to_hide = [
580
+ self.haralick_hist_btn,
581
+ self.haralick_digit_btn,
582
+ self.haralick_channel_lbl,
583
+ self.haralick_channel_choice,
584
+ self.haralick_distance_le,
585
+ self.haralick_distance_lbl,
586
+ self.haralick_n_gray_levels_le,
587
+ self.haralick_n_gray_levels_lbl,
588
+ self.haralick_scale_lbl,
589
+ self.haralick_scale_slider,
590
+ self.haralick_percentile_min_lbl,
591
+ self.haralick_percentile_min_le,
592
+ self.haralick_percentile_max_lbl,
593
+ self.haralick_percentile_max_le,
594
+ self.haralick_normalization_mode_btn,
595
+ ]
596
+ for element in self.haralick_to_hide:
597
+ element.hide()
598
+
599
+ self.features_to_disable = [
600
+ self.feature_lbl,
601
+ self.del_feature_btn,
602
+ self.add_feature_btn,
603
+ self.features_list,
604
+ self.use_channel_lbl,
605
+ *self.mask_channels_cb,
606
+ self.activate_haralick_btn,
607
+ ]
608
+
609
+ self.haralick_normalization_mode_btn.clicked.connect(
610
+ self.switch_to_absolute_normalization_mode
611
+ )
612
+ layout.addLayout(self.haralick_layout)
613
+
614
+ def switch_to_absolute_normalization_mode(self):
615
+ if self.percentile_mode:
616
+ self.percentile_mode = False
617
+ self.haralick_normalization_mode_btn.setIcon(
618
+ icon(MDI6.percent_circle_outline, color="black")
619
+ )
620
+ self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
621
+ self.haralick_normalization_mode_btn.setToolTip(
622
+ "Switch to percentile normalization values."
623
+ )
624
+ self.haralick_percentile_min_lbl.setText("Min value: ")
625
+ self.haralick_percentile_max_lbl.setText("Max value: ")
626
+ self.haralick_percentile_min_le.setText("0")
627
+ self.haralick_percentile_max_le.setText("10000")
628
+
629
+ else:
630
+ self.percentile_mode = True
631
+ self.haralick_normalization_mode_btn.setIcon(
632
+ icon(MDI6.percent_circle, color="black")
633
+ )
634
+ self.haralick_normalization_mode_btn.setIconSize(QSize(20, 20))
635
+ self.haralick_normalization_mode_btn.setToolTip(
636
+ "Switch to absolute normalization values."
637
+ )
638
+ self.haralick_percentile_min_lbl.setText("Min percentile: ")
639
+ self.haralick_percentile_max_lbl.setText("Max percentile: ")
640
+ self.haralick_percentile_min_le.setText("0.01")
641
+ self.haralick_percentile_max_le.setText("99.99")
642
+
643
+ def populate_config_frame(self):
644
+
645
+ grid = QGridLayout(self.config_frame)
646
+ panel_title = QLabel(f"CONFIGURATION")
647
+ panel_title.setStyleSheet(
648
+ """
649
+ font-weight: bold;
650
+ padding: 0px;
651
+ """
652
+ )
653
+
654
+ grid.addWidget(panel_title, 0, 0, 1, 4, alignment=Qt.AlignCenter)
655
+
656
+ self.collapse_config_btn = QPushButton()
657
+ self.collapse_config_btn.setIcon(icon(MDI6.chevron_down, color="black"))
658
+ self.collapse_config_btn.setIconSize(QSize(20, 20))
659
+ self.collapse_config_btn.setStyleSheet(self.button_select_all)
660
+ grid.addWidget(self.collapse_config_btn, 0, 0, 1, 4, alignment=Qt.AlignRight)
661
+
662
+ self.generate_config_panel_contents()
663
+ grid.addWidget(self.ContentsConfig, 1, 0, 1, 4, alignment=Qt.AlignTop)
664
+ self.collapse_config_btn.clicked.connect(
665
+ lambda: self.ContentsConfig.setHidden(not self.ContentsConfig.isHidden())
666
+ )
667
+ self.collapse_config_btn.clicked.connect(self.collapse_config_advanced)
668
+ # self.ContentsConfig.hide()
669
+
670
+ def populate_config_trackpy_frame(self):
671
+
672
+ grid = QGridLayout(self.config_trackpy_frame)
673
+ panel_title = QLabel(f"CONFIGURATION")
674
+ panel_title.setStyleSheet(
675
+ """
676
+ font-weight: bold;
677
+ padding: 0px;
678
+ """
679
+ )
680
+
681
+ grid.addWidget(panel_title, 0, 0, 1, 4, alignment=Qt.AlignCenter)
682
+
683
+ self.collapse_config_trackpy_btn = QPushButton()
684
+ self.collapse_config_trackpy_btn.setIcon(icon(MDI6.chevron_down, color="black"))
685
+ self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
686
+ self.collapse_config_trackpy_btn.setStyleSheet(self.button_select_all)
687
+ grid.addWidget(
688
+ self.collapse_config_trackpy_btn, 0, 0, 1, 4, alignment=Qt.AlignRight
689
+ )
690
+ self.generate_config_trackpy_panel_contents()
691
+ grid.addWidget(self.ContentsConfigTrackpy, 1, 0, 1, 4, alignment=Qt.AlignTop)
692
+ self.collapse_config_trackpy_btn.clicked.connect(
693
+ lambda: self.ContentsConfigTrackpy.setHidden(
694
+ not self.ContentsConfigTrackpy.isHidden()
695
+ )
696
+ )
697
+ self.collapse_config_trackpy_btn.clicked.connect(
698
+ self.collapse_config_trackpy_advanced
699
+ )
700
+ # self.ContentsConfig.hide()
701
+
702
+ def collapse_config_advanced(self):
703
+ """
704
+ Switch the chevron icon and adjust the size for the CONFIG frame.
705
+ """
706
+
707
+ features_open = not self.ContentsFeatures.isHidden()
708
+ config_open = not self.ContentsConfig.isHidden()
709
+ post_open = not self.ContentsPostProc.isHidden()
710
+ is_open = np.array([features_open, config_open, post_open])
711
+
712
+ if self.ContentsConfig.isHidden():
713
+ self.collapse_config_btn.setIcon(icon(MDI6.chevron_down, color="black"))
714
+ self.collapse_config_btn.setIconSize(QSize(20, 20))
715
+ if len(is_open[is_open]) == 0:
716
+ pass
717
+ # self._scroll_area.setMinimumHeight(int(self.minimum_height))
718
+ # self._adjustSize()
719
+ else:
720
+ self.collapse_config_btn.setIcon(icon(MDI6.chevron_up, color="black"))
721
+ self.collapse_config_btn.setIconSize(QSize(20, 20))
722
+
723
+ def collapse_config_trackpy_advanced(self):
724
+ """
725
+ Switch the chevron icon and adjust the size for the CONFIG frame.
726
+ """
727
+
728
+ post_open = not self.ContentsPostProc.isHidden()
729
+ is_open = np.array([post_open])
730
+
731
+ if self.ContentsConfigTrackpy.isHidden():
732
+ self.collapse_config_trackpy_btn.setIcon(
733
+ icon(MDI6.chevron_down, color="black")
734
+ )
735
+ self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
736
+ if len(is_open[is_open]) == 0:
737
+ pass
738
+ # self._scroll_area.setMinimumHeight(int(self.minimum_height))
739
+ # self._adjustSize()
740
+ else:
741
+ self.collapse_config_trackpy_btn.setIcon(
742
+ icon(MDI6.chevron_up, color="black")
743
+ )
744
+ self.collapse_config_trackpy_btn.setIconSize(QSize(20, 20))
745
+
746
+ def generate_config_trackpy_panel_contents(self):
747
+
748
+ self.ContentsConfigTrackpy = QFrame()
749
+ layout = QVBoxLayout(self.ContentsConfigTrackpy)
750
+ layout.setContentsMargins(0, 0, 0, 0)
751
+
752
+ sr_layout = QHBoxLayout()
753
+ self.search_range_lbl = QLabel("search range [px]: ")
754
+ self.search_range_le = QLineEdit("30")
755
+ self.search_range_le.setPlaceholderText("search distance in pixels")
756
+ self.search_range_le.setValidator(self._floatValidator)
757
+ sr_layout.addWidget(self.search_range_lbl, 30)
758
+ sr_layout.addWidget(self.search_range_le, 70)
759
+ layout.addLayout(sr_layout)
760
+
761
+ memory_layout = QHBoxLayout()
762
+ self.memory_lbl = QLabel("memory [# frames]: ")
763
+ self.memory_slider = QLabeledSlider()
764
+ self.memory_slider.setSingleStep(1)
765
+ self.memory_slider.setTickInterval(1)
766
+ self.memory_slider.setSingleStep(1)
767
+ self.memory_slider.setOrientation(Qt.Horizontal)
768
+ self.memory_slider.setRange(0, self.parent_window.parent_window.len_movie)
769
+ self.memory_slider.setValue(0)
770
+ memory_layout.addWidget(self.memory_lbl, 30)
771
+ memory_layout.addWidget(self.memory_slider, 70)
772
+ layout.addLayout(memory_layout)
773
+
774
+ def generate_config_panel_contents(self):
775
+
776
+ self.ContentsConfig = QFrame()
777
+ layout = QVBoxLayout(self.ContentsConfig)
778
+ layout.setContentsMargins(0, 0, 0, 0)
779
+
780
+ btrack_config_layout = QHBoxLayout()
781
+ self.config_lbl = QLabel("bTrack configuration: ")
782
+ btrack_config_layout.addWidget(self.config_lbl, 90)
783
+
784
+ self.upload_btrack_config_btn = QPushButton()
785
+ self.upload_btrack_config_btn.setIcon(icon(MDI6.plus, color="black"))
786
+ self.upload_btrack_config_btn.setIconSize(QSize(20, 20))
787
+ self.upload_btrack_config_btn.setToolTip("Upload a new bTrack configuration.")
788
+ self.upload_btrack_config_btn.setStyleSheet(self.button_select_all)
789
+ self.upload_btrack_config_btn.clicked.connect(self.upload_btrack_config)
790
+ btrack_config_layout.addWidget(
791
+ self.upload_btrack_config_btn, 5
792
+ ) # 4,3,1,1, alignment=Qt.AlignLeft
793
+
794
+ self.reset_config_btn = QPushButton()
795
+ self.reset_config_btn.setIcon(icon(MDI6.arrow_u_right_top, color="black"))
796
+ self.reset_config_btn.setIconSize(QSize(20, 20))
797
+ self.reset_config_btn.setToolTip(
798
+ "Reset the configuration to the default bTrack config."
799
+ )
800
+ self.reset_config_btn.setStyleSheet(self.button_select_all)
801
+ self.reset_config_btn.clicked.connect(self.reset_btrack_config)
802
+ btrack_config_layout.addWidget(
803
+ self.reset_config_btn, 5
804
+ ) # 4,3,1,1, alignment=Qt.AlignLeft
805
+
806
+ layout.addLayout(btrack_config_layout)
807
+
808
+ self.config_le = QTextEdit()
809
+ self.config_le.setMinimumHeight(150)
810
+ # self.config_le.setStyleSheet("""
811
+ # background: #EEEDEB;
812
+ # border: 2px solid black;
813
+ # """)
814
+ layout.addWidget(self.config_le)
815
+ self.load_cell_config()
816
+
817
+ def show_haralick_options(self):
818
+ """
819
+ Show the Haralick texture options.
820
+ """
821
+
822
+ if self.activate_haralick_btn.isChecked():
823
+ for element in self.haralick_to_hide:
824
+ element.show()
825
+ else:
826
+ for element in self.haralick_to_hide:
827
+ element.hide()
828
+
829
+ def upload_btrack_config(self):
830
+ """
831
+ Upload a specific bTrack config to the experiment folder for the cell population.
832
+ """
833
+
834
+ self.file_dialog = QFileDialog()
835
+ try:
836
+ modelpath = (
837
+ os.sep.join(
838
+ [self._software_path, "celldetective", "models", "tracking_configs"]
839
+ )
840
+ + os.sep
841
+ )
842
+ print("Track config path: ", modelpath)
843
+ self.filename = self.file_dialog.getOpenFileName(
844
+ None, "Load config", modelpath, "json files (*.json)"
845
+ )[0]
846
+ if self.filename != self.config_path:
847
+ copyfile(self.filename, self.config_path)
848
+ self.load_cell_config()
849
+ except Exception as e:
850
+ print(e)
851
+ return None
852
+
853
+ def reset_btrack_config(self):
854
+ """
855
+ Set the bTrack config to the default bTrack config.
856
+ """
857
+
858
+ msgBox = QMessageBox()
859
+ msgBox.setIcon(QMessageBox.Question)
860
+ msgBox.setText(
861
+ "You are about to revert to the default bTrack configuration? Do you want to proceed?"
862
+ )
863
+ msgBox.setWindowTitle("Confirm")
864
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
865
+ returnValue = msgBox.exec()
866
+ if returnValue == QMessageBox.Yes:
867
+ config = interpret_tracking_configuration(None)
868
+ if config != self.config_path:
869
+ copyfile(config, self.config_path)
870
+ self.load_cell_config()
871
+ else:
872
+ return None
873
+
874
+ def activate_feature_options(self):
875
+ """
876
+ Tick the features option.
877
+ """
878
+
879
+ self.switch_feature_option()
880
+ if self.features_ticked:
881
+ for element in self.features_to_disable:
882
+ element.setEnabled(True)
883
+ self.select_features_btn.setIcon(icon(MDI6.checkbox_outline, color="black"))
884
+ self.select_features_btn.setIconSize(QSize(20, 20))
885
+ else:
886
+ for element in self.features_to_disable:
887
+ element.setEnabled(False)
888
+ self.select_features_btn.setIcon(
889
+ icon(MDI6.checkbox_blank_outline, color="black")
890
+ )
891
+ self.select_features_btn.setIconSize(QSize(20, 20))
892
+ self.features_list.list_widget.clearSelection()
893
+ self.activate_haralick_btn.setChecked(False)
894
+
895
+ def activate_post_proc_options(self):
896
+ """
897
+ Tick the features option.
898
+ """
899
+
900
+ self.switch_post_proc_option()
901
+ if self.post_proc_ticked:
902
+ for element in self.post_proc_options_to_disable:
903
+ element.setEnabled(True)
904
+ self.select_post_proc_btn.setIcon(
905
+ icon(MDI6.checkbox_outline, color="black")
906
+ )
907
+ self.select_post_proc_btn.setIconSize(QSize(20, 20))
908
+ else:
909
+ for element in self.post_proc_options_to_disable:
910
+ element.setEnabled(False)
911
+ self.select_post_proc_btn.setIcon(
912
+ icon(MDI6.checkbox_blank_outline, color="black")
913
+ )
914
+ self.select_post_proc_btn.setIconSize(QSize(20, 20))
915
+
916
+ def switch_feature_option(self):
917
+ """
918
+ Switch the feature option.
919
+ """
920
+
921
+ if self.features_ticked == True:
922
+ self.features_ticked = False
923
+ else:
924
+ self.features_ticked = True
925
+
926
+ def switch_post_proc_option(self):
927
+ """
928
+ Switch the feature option.
929
+ """
930
+
931
+ if self.post_proc_ticked == True:
932
+ self.post_proc_ticked = False
933
+ else:
934
+ self.post_proc_ticked = True
935
+
936
+ # def adjustScrollArea(self):
937
+ #
938
+ # """
939
+ # Auto-adjust scroll area to fill space
940
+ # (from https://stackoverflow.com/questions/66417576/make-qscrollarea-use-all-available-space-of-qmainwindow-height-axis)
941
+ # """
942
+ #
943
+ # step = 5
944
+ # while self._scroll_area.verticalScrollBar().isVisible() and self.height() < self.maximumHeight():
945
+ # self.resize(self.width(), self.height() + step)
946
+
947
+ def load_cell_config(self):
948
+ """
949
+ Load the cell configuration and write in the QLineEdit.
950
+ """
951
+
952
+ file_name = interpret_tracking_configuration(self.config_path)
953
+ with open(file_name, "r") as f:
954
+ json_data = json.load(f)
955
+ self.config_le.setText(json.dumps(json_data, indent=4))
956
+
957
+ def _write_instructions(self):
958
+ """
959
+ Write the selected options in a json file for later reading by the software.
960
+ """
961
+
962
+ print("Writing instructions...")
963
+
964
+ if self.btrack_option.isChecked():
965
+ btrack_option = True
966
+ else:
967
+ btrack_option = False
968
+
969
+ # Fetch trackpky params
970
+ if not btrack_option:
971
+ search_range = int(self.search_range_le.text().replace(",", "."))
972
+ memory = self.memory_slider.value()
973
+ else:
974
+ search_range = None
975
+ memory = None
976
+
977
+ tracking_options = {"btrack_config_path": self.config_path}
978
+ tracking_options.update(
979
+ {
980
+ "btrack_option": btrack_option,
981
+ "search_range": search_range,
982
+ "memory": memory,
983
+ }
984
+ )
985
+
986
+ if not self.features_ticked:
987
+ features = None
988
+ masked_channels = None
989
+ else:
990
+ features = self.features_list.getItems()
991
+ masked_channels = self.channel_names[
992
+ np.array([not cb.isChecked() for cb in self.mask_channels_cb])
993
+ ]
994
+ if len(masked_channels) == 0:
995
+ masked_channels = None
996
+ else:
997
+ masked_channels = list(masked_channels)
998
+
999
+ tracking_options.update(
1000
+ {"features": features, "mask_channels": masked_channels}
1001
+ )
1002
+
1003
+ self.extract_haralick_options()
1004
+ tracking_options.update({"haralick_options": self.haralick_options})
1005
+
1006
+ if self.post_proc_ticked:
1007
+ post_processing_options = {
1008
+ "minimum_tracklength": int(self.min_tracklength_slider.value()),
1009
+ "remove_not_in_first": self.remove_not_in_first_checkbox.isChecked(),
1010
+ "remove_not_in_last": self.remove_not_in_last_checkbox.isChecked(),
1011
+ "interpolate_position_gaps": self.interpolate_gaps_checkbox.isChecked(),
1012
+ "extrapolate_tracks_pre": self.extrapolate_pre_checkbox.isChecked(),
1013
+ "extrapolate_tracks_post": self.extrapolate_post_checkbox.isChecked(),
1014
+ "interpolate_na": False, # self.interpolate_na_features_checkbox.isChecked()
1015
+ }
1016
+ else:
1017
+
1018
+ post_processing_options = None
1019
+
1020
+ tracking_options.update({"post_processing_options": post_processing_options})
1021
+ file_name = self.track_instructions_write_path
1022
+ with open(file_name, "w") as f:
1023
+ json.dump(tracking_options, f, indent=4)
1024
+
1025
+ # Save the JSON data to the file
1026
+ file_name = self.config_path
1027
+ with open(file_name, "w") as f:
1028
+ f.write(self.config_le.toPlainText())
1029
+ print("Done.")
1030
+ self.close()
1031
+
1032
+ def uncheck_post_proc(self):
1033
+ self.select_post_proc_btn.setIcon(
1034
+ icon(MDI6.checkbox_blank_outline, color="black")
1035
+ )
1036
+ self.select_post_proc_btn.setIconSize(QSize(20, 20))
1037
+ self.post_proc_ticked = False
1038
+ for element in self.post_proc_options_to_disable:
1039
+ element.setEnabled(False)
1040
+
1041
+ def check_post_proc(self):
1042
+ self.select_post_proc_btn.setIcon(icon(MDI6.checkbox_outline, color="black"))
1043
+ self.select_post_proc_btn.setIconSize(QSize(20, 20))
1044
+ self.post_proc_ticked = True
1045
+ for element in self.post_proc_options_to_disable:
1046
+ element.setEnabled(True)
1047
+
1048
+ def uncheck_features(self):
1049
+ self.select_features_btn.setIcon(
1050
+ icon(MDI6.checkbox_blank_outline, color="black")
1051
+ )
1052
+ self.select_features_btn.setIconSize(QSize(20, 20))
1053
+ self.features_ticked = False
1054
+ for element in self.features_to_disable:
1055
+ element.setEnabled(False)
1056
+
1057
+ def check_features(self):
1058
+ self.select_features_btn.setIcon(icon(MDI6.checkbox_outline, color="black"))
1059
+ self.select_features_btn.setIconSize(QSize(20, 20))
1060
+ self.features_ticked = True
1061
+ for element in self.features_to_disable:
1062
+ element.setEnabled(True)
1063
+
1064
+ def extract_haralick_options(self):
1065
+
1066
+ if self.activate_haralick_btn.isChecked():
1067
+ self.haralick_options = {
1068
+ "target_channel": self.haralick_channel_choice.currentIndex(),
1069
+ "scale_factor": float(self.haralick_scale_slider.value()),
1070
+ "n_intensity_bins": int(self.haralick_n_gray_levels_le.text()),
1071
+ "distance": int(self.haralick_distance_le.text()),
1072
+ }
1073
+ if self.percentile_mode:
1074
+ self.haralick_options.update(
1075
+ {
1076
+ "percentiles": (
1077
+ float(self.haralick_percentile_min_le.text()),
1078
+ float(self.haralick_percentile_max_le.text()),
1079
+ ),
1080
+ "clip_values": None,
1081
+ }
1082
+ )
1083
+ else:
1084
+ self.haralick_options.update(
1085
+ {
1086
+ "percentiles": None,
1087
+ "clip_values": (
1088
+ float(self.haralick_percentile_min_le.text()),
1089
+ float(self.haralick_percentile_max_le.text()),
1090
+ ),
1091
+ }
1092
+ )
1093
+
1094
+ else:
1095
+ self.haralick_options = None
1096
+
1097
+ def _load_previous_instructions(self):
1098
+ """
1099
+ Read the tracking options from a previously written json file.
1100
+ """
1101
+
1102
+ print("Reading instructions..")
1103
+ if os.path.exists(self.track_instructions_write_path):
1104
+ with open(self.track_instructions_write_path, "r") as f:
1105
+ tracking_instructions = json.load(f)
1106
+ print(tracking_instructions)
1107
+
1108
+ # Features
1109
+ features = tracking_instructions["features"]
1110
+ if (features is not None) and len(features) > 0:
1111
+ self.check_features()
1112
+ self.ContentsFeatures.show()
1113
+ self.features_list.list_widget.clear()
1114
+ self.features_list.list_widget.addItems(features)
1115
+ else:
1116
+ self.ContentsFeatures.hide()
1117
+ self.uncheck_features()
1118
+
1119
+ btrack_option = True
1120
+ if "btrack_option" in tracking_instructions:
1121
+ btrack_option = tracking_instructions["btrack_option"]
1122
+ if btrack_option:
1123
+ self.btrack_option.click()
1124
+ else:
1125
+ self.trackpy_option.click()
1126
+
1127
+ if "search_range" in tracking_instructions:
1128
+ search_range = tracking_instructions["search_range"]
1129
+ if search_range is not None:
1130
+ self.search_range_le.setText(
1131
+ str(search_range).replace(".", ",")
1132
+ )
1133
+ if "memory" in tracking_instructions:
1134
+ memory = tracking_instructions["memory"]
1135
+ if memory is not None:
1136
+ self.memory_slider.setValue(memory)
1137
+
1138
+ # Uncheck channels that are masked
1139
+ mask_channels = tracking_instructions["mask_channels"]
1140
+ if (mask_channels is not None) and len(mask_channels) > 0:
1141
+ for ch in mask_channels:
1142
+ for cb in self.mask_channels_cb:
1143
+ if cb.text() == ch:
1144
+ cb.setChecked(False)
1145
+
1146
+ haralick_options = tracking_instructions["haralick_options"]
1147
+ if haralick_options is None:
1148
+ self.activate_haralick_btn.setChecked(False)
1149
+ self.show_haralick_options()
1150
+ else:
1151
+ self.activate_haralick_btn.setChecked(True)
1152
+ self.show_haralick_options()
1153
+ if "target_channel" in haralick_options:
1154
+ idx = haralick_options["target_channel"]
1155
+ # idx = self.haralick_channel_choice.findText(text_to_find)
1156
+ self.haralick_channel_choice.setCurrentIndex(idx)
1157
+ if "scale_factor" in haralick_options:
1158
+ self.haralick_scale_slider.setValue(
1159
+ float(haralick_options["scale_factor"])
1160
+ )
1161
+ if ("percentiles" in haralick_options) and (
1162
+ haralick_options["percentiles"] is not None
1163
+ ):
1164
+ perc = list(haralick_options["percentiles"])
1165
+ self.haralick_percentile_min_le.setText(str(perc[0]))
1166
+ self.haralick_percentile_max_le.setText(str(perc[1]))
1167
+ if ("clip_values" in haralick_options) and (
1168
+ haralick_options["clip_values"] is not None
1169
+ ):
1170
+ values = list(haralick_options["clip_values"])
1171
+ self.haralick_percentile_min_le.setText(str(values[0]))
1172
+ self.haralick_percentile_max_le.setText(str(values[1]))
1173
+ self.percentile_mode = True
1174
+ self.switch_to_absolute_normalization_mode()
1175
+ if "n_intensity_bins" in haralick_options:
1176
+ self.haralick_n_gray_levels_le.setText(
1177
+ str(haralick_options["n_intensity_bins"])
1178
+ )
1179
+ if "distance" in haralick_options:
1180
+ self.haralick_distance_le.setText(
1181
+ str(haralick_options["distance"])
1182
+ )
1183
+
1184
+ # Post processing options
1185
+ post_processing_options = tracking_instructions[
1186
+ "post_processing_options"
1187
+ ]
1188
+ if post_processing_options is None:
1189
+ self.uncheck_post_proc()
1190
+ self.ContentsPostProc.hide()
1191
+ for element in [
1192
+ self.remove_not_in_last_checkbox,
1193
+ self.remove_not_in_first_checkbox,
1194
+ self.interpolate_gaps_checkbox,
1195
+ self.extrapolate_post_checkbox,
1196
+ self.extrapolate_pre_checkbox,
1197
+ ]: # self.interpolate_na_features_checkbox
1198
+ element.setChecked(False)
1199
+ self.min_tracklength_slider.setValue(0)
1200
+
1201
+ else:
1202
+ self.check_post_proc()
1203
+ self.ContentsPostProc.show()
1204
+ if "minimum_tracklength" in post_processing_options:
1205
+ self.min_tracklength_slider.setValue(
1206
+ int(post_processing_options["minimum_tracklength"])
1207
+ )
1208
+ if "remove_not_in_first" in post_processing_options:
1209
+ self.remove_not_in_first_checkbox.setChecked(
1210
+ post_processing_options["remove_not_in_first"]
1211
+ )
1212
+ if "remove_not_in_last" in post_processing_options:
1213
+ self.remove_not_in_last_checkbox.setChecked(
1214
+ post_processing_options["remove_not_in_last"]
1215
+ )
1216
+ if "interpolate_position_gaps" in post_processing_options:
1217
+ self.interpolate_gaps_checkbox.setChecked(
1218
+ post_processing_options["interpolate_position_gaps"]
1219
+ )
1220
+ if "extrapolate_tracks_pre" in post_processing_options:
1221
+ self.extrapolate_pre_checkbox.setChecked(
1222
+ post_processing_options["extrapolate_tracks_pre"]
1223
+ )
1224
+ if "extrapolate_tracks_post" in post_processing_options:
1225
+ self.extrapolate_post_checkbox.setChecked(
1226
+ post_processing_options["extrapolate_tracks_post"]
1227
+ )
1228
+ # if "interpolate_na" in post_processing_options:
1229
+ # self.interpolate_na_features_checkbox.setChecked(post_processing_options["interpolate_na"])
1230
+
1231
+ def locate_image(self):
1232
+ """
1233
+ Load the first frame of the first movie found in the experiment folder as a sample.
1234
+ """
1235
+
1236
+ movies = glob(
1237
+ self.parent_window.parent_window.exp_dir
1238
+ + os.sep.join(
1239
+ [
1240
+ "*",
1241
+ "*",
1242
+ "movie",
1243
+ self.parent_window.parent_window.movie_prefix + "*.tif",
1244
+ ]
1245
+ )
1246
+ )
1247
+ if len(movies) == 0:
1248
+ msgBox = QMessageBox()
1249
+ msgBox.setIcon(QMessageBox.Warning)
1250
+ msgBox.setText(
1251
+ "No movies are detected in the experiment folder. Cannot load an image to test Haralick."
1252
+ )
1253
+ msgBox.setWindowTitle("Warning")
1254
+ msgBox.setStandardButtons(QMessageBox.Ok)
1255
+ returnValue = msgBox.exec()
1256
+ if returnValue == QMessageBox.Yes:
1257
+ self.test_frame = None
1258
+ return None
1259
+ else:
1260
+ stack0 = movies[0]
1261
+ n_channels = len(self.channels)
1262
+ self.test_frame = load_frames(
1263
+ np.arange(n_channels), stack0, scale=None, normalize_input=False
1264
+ )
1265
+
1266
+ def control_haralick_digitalization(self):
1267
+ """
1268
+ Load an image for the first experiment movie found.
1269
+ Apply the Haralick parameters and check the result of the digitization (normalization + binning of intensities).
1270
+
1271
+ """
1272
+
1273
+ self.locate_image()
1274
+ self.extract_haralick_options()
1275
+ if self.test_frame is not None:
1276
+ digitized_img = compute_haralick_features(
1277
+ self.test_frame,
1278
+ np.zeros(self.test_frame.shape[:2]),
1279
+ channels=self.channel_names,
1280
+ return_digit_image_only=True,
1281
+ **self.haralick_options,
1282
+ )
1283
+
1284
+ self.fig, self.ax = plt.subplots()
1285
+ divider = make_axes_locatable(self.ax)
1286
+ cax = divider.append_axes("right", size="5%", pad=0.05)
1287
+
1288
+ self.imshow_digit_window = FigureCanvas(
1289
+ self.fig, title="Haralick: control digitization"
1290
+ )
1291
+ self.ax.clear()
1292
+ im = self.ax.imshow(digitized_img, cmap="gray")
1293
+ self.fig.colorbar(im, cax=cax, orientation="vertical")
1294
+ self.ax.set_xticks([])
1295
+ self.ax.set_yticks([])
1296
+ self.fig.set_facecolor("none") # or 'None'
1297
+ self.fig.canvas.setStyleSheet("background-color: transparent;")
1298
+ self.imshow_digit_window.canvas.draw()
1299
+ self.imshow_digit_window.show()
1300
+
1301
+ def control_haralick_intensity_histogram(self):
1302
+ """
1303
+ Load an image for the first experiment movie found.
1304
+ Apply the Haralick normalization parameters and check the normalized intensity histogram.
1305
+
1306
+ """
1307
+
1308
+ self.locate_image()
1309
+ self.extract_haralick_options()
1310
+ if self.test_frame is not None:
1311
+ norm_img = compute_haralick_features(
1312
+ self.test_frame,
1313
+ np.zeros(self.test_frame.shape[:2]),
1314
+ channels=self.channel_names,
1315
+ return_norm_image_only=True,
1316
+ **self.haralick_options,
1317
+ )
1318
+ self.fig, self.ax = plt.subplots(1, 1, figsize=(4, 3))
1319
+ self.hist_window = FigureCanvas(
1320
+ self.fig, title="Haralick: control digitized histogram"
1321
+ )
1322
+ self.ax.clear()
1323
+ flat = norm_img.flatten()
1324
+ self.ax.hist(
1325
+ flat[flat == flat], bins=self.haralick_options["n_intensity_bins"]
1326
+ )
1327
+ self.ax.set_xlabel("gray level value")
1328
+ self.ax.set_ylabel("#")
1329
+ plt.tight_layout()
1330
+ self.fig.set_facecolor("none") # or 'None'
1331
+ self.fig.canvas.setStyleSheet("background-color: transparent;")
1332
+ self.hist_window.canvas.draw()
1333
+ self.hist_window.show()