celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__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 (151) 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 +304 -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/measure_cells.py +565 -0
  81. celldetective/processes/segment_cells.py +760 -0
  82. celldetective/processes/track_cells.py +435 -0
  83. celldetective/processes/train_segmentation_model.py +694 -0
  84. celldetective/processes/train_signal_model.py +265 -0
  85. celldetective/processes/unified_process.py +292 -0
  86. celldetective/regionprops/_regionprops.py +358 -317
  87. celldetective/relative_measurements.py +987 -710
  88. celldetective/scripts/measure_cells.py +313 -212
  89. celldetective/scripts/measure_relative.py +90 -46
  90. celldetective/scripts/segment_cells.py +165 -104
  91. celldetective/scripts/segment_cells_thresholds.py +96 -68
  92. celldetective/scripts/track_cells.py +198 -149
  93. celldetective/scripts/train_segmentation_model.py +324 -201
  94. celldetective/scripts/train_signal_model.py +87 -45
  95. celldetective/segmentation.py +844 -749
  96. celldetective/signals.py +3514 -2861
  97. celldetective/tracking.py +30 -15
  98. celldetective/utils/__init__.py +0 -0
  99. celldetective/utils/cellpose_utils/__init__.py +133 -0
  100. celldetective/utils/color_mappings.py +42 -0
  101. celldetective/utils/data_cleaning.py +630 -0
  102. celldetective/utils/data_loaders.py +450 -0
  103. celldetective/utils/dataset_helpers.py +207 -0
  104. celldetective/utils/downloaders.py +197 -0
  105. celldetective/utils/event_detection/__init__.py +8 -0
  106. celldetective/utils/experiment.py +1782 -0
  107. celldetective/utils/image_augmenters.py +308 -0
  108. celldetective/utils/image_cleaning.py +74 -0
  109. celldetective/utils/image_loaders.py +926 -0
  110. celldetective/utils/image_transforms.py +335 -0
  111. celldetective/utils/io.py +62 -0
  112. celldetective/utils/mask_cleaning.py +348 -0
  113. celldetective/utils/mask_transforms.py +5 -0
  114. celldetective/utils/masks.py +184 -0
  115. celldetective/utils/maths.py +351 -0
  116. celldetective/utils/model_getters.py +325 -0
  117. celldetective/utils/model_loaders.py +296 -0
  118. celldetective/utils/normalization.py +380 -0
  119. celldetective/utils/parsing.py +465 -0
  120. celldetective/utils/plots/__init__.py +0 -0
  121. celldetective/utils/plots/regression.py +53 -0
  122. celldetective/utils/resources.py +34 -0
  123. celldetective/utils/stardist_utils/__init__.py +104 -0
  124. celldetective/utils/stats.py +90 -0
  125. celldetective/utils/types.py +21 -0
  126. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
  127. celldetective-1.5.0b0.dist-info/RECORD +187 -0
  128. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
  129. tests/gui/test_new_project.py +129 -117
  130. tests/gui/test_project.py +127 -79
  131. tests/test_filters.py +39 -15
  132. tests/test_notebooks.py +8 -0
  133. tests/test_tracking.py +232 -13
  134. tests/test_utils.py +123 -77
  135. celldetective/gui/base_components.py +0 -23
  136. celldetective/gui/layouts.py +0 -1602
  137. celldetective/gui/processes/compute_neighborhood.py +0 -594
  138. celldetective/gui/processes/measure_cells.py +0 -360
  139. celldetective/gui/processes/segment_cells.py +0 -499
  140. celldetective/gui/processes/track_cells.py +0 -303
  141. celldetective/gui/processes/train_segmentation_model.py +0 -270
  142. celldetective/gui/processes/train_signal_model.py +0 -108
  143. celldetective/gui/table_ops/merge_groups.py +0 -118
  144. celldetective/gui/viewers.py +0 -1354
  145. celldetective/io.py +0 -3663
  146. celldetective/utils.py +0 -3108
  147. celldetective-1.4.2.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
@@ -1,1373 +1,983 @@
1
1
  import os
2
2
 
3
- from PyQt5.QtWidgets import QGridLayout, QApplication, QMessageBox, QFrame, QSizePolicy, QLineEdit, QListWidget, QVBoxLayout, QComboBox, \
4
- QPushButton, QLabel, QHBoxLayout, QCheckBox, QFileDialog, QToolButton, QMenu, QStylePainter, QStyleOptionComboBox, QStyle
5
- from PyQt5.QtCore import Qt, QSize, QAbstractTableModel, QEvent, pyqtSignal
6
- from PyQt5.QtGui import QDoubleValidator, QIntValidator, QStandardItemModel, QPalette
7
-
8
- from celldetective.gui import Styles, CelldetectiveWidget
3
+ from PyQt5.QtWidgets import (
4
+ QGridLayout,
5
+ QMessageBox,
6
+ QLineEdit,
7
+ QVBoxLayout,
8
+ QComboBox,
9
+ QPushButton,
10
+ QLabel,
11
+ QHBoxLayout,
12
+ QCheckBox,
13
+ QFileDialog,
14
+ )
15
+ from PyQt5.QtCore import Qt, QSize, QAbstractTableModel
16
+ from PyQt5.QtGui import QDoubleValidator, QIntValidator
17
+
18
+ from celldetective.gui.base.list_widget import ListWidget
19
+ from celldetective.gui.base.styles import Styles
20
+ from celldetective.gui.base.components import CelldetectiveWidget
9
21
  from superqt.fonticon import icon
10
22
  from fonticon_mdi6 import MDI6
11
23
 
12
- from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
13
- from matplotlib.backends.backend_qt5agg import NavigationToolbar2QT
14
- import matplotlib.pyplot as plt
15
-
16
- from celldetective.utils import get_software_location
17
24
 
18
- try:
19
- import celldetective.extra_properties as extra_properties
20
- extra_props = True
21
- except Exception as e:
22
- print(f"The module extra_properties seems corrupted: {e}... Skip...")
23
- extra_props = False
25
+ from celldetective.gui.base.utils import center_window
26
+ from celldetective import get_software_location
24
27
 
25
- from inspect import getmembers, isfunction
26
28
  from celldetective.filters import *
27
29
  from os import sep
28
30
  import json
29
31
 
30
32
 
31
- def generic_message(message, msg_type="warning"):
32
-
33
- print(message)
34
- message_box = QMessageBox()
35
- if msg_type=="warning":
36
- message_box.setIcon(QMessageBox.Warning)
37
- elif msg_type=="info":
38
- message_box.setIcon(QMessageBox.Information)
39
- elif msg_type=="critical":
40
- message_box.setIcon(QMessageBox.Critical)
41
- message_box.setText(message)
42
- message_box.setWindowTitle(msg_type)
43
- message_box.setStandardButtons(QMessageBox.Ok)
44
- _ = message_box.exec()
45
-
46
33
  class PreprocessingLayout(QVBoxLayout, Styles):
47
-
48
- """
49
- A widget that allows user to choose preprocessing filters for an image
50
- """
51
-
52
- def __init__(self, parent_window=None, apply_btn_option=True, *args, **kwargs):
53
- super().__init__(*args, **kwargs)
54
-
55
- self.parent_window = parent_window
56
- self.apply_btn_option = apply_btn_option
57
- self.generate_components()
58
- self.add_to_layout()
59
-
60
- def add_to_layout(self):
61
-
62
- self.setContentsMargins(20, 20, 20, 20)
63
-
64
- button_layout = QHBoxLayout()
65
- button_layout.addWidget(self.preprocess_lbl, 85, alignment=Qt.AlignLeft)
66
- button_layout.addWidget(self.delete_filter_btn, 5)
67
- button_layout.addWidget(self.add_filter_btn, 5)
68
- button_layout.addWidget(self.help_prefilter_btn, 5)
69
- self.addLayout(button_layout, 25)
70
-
71
- self.addWidget(self.list, 70)
72
- if self.apply_btn_option:
73
- self.addWidget(self.apply_btn, 5)
74
-
75
- def generate_components(self):
76
-
77
- self.list = ListWidget(FilterChoice, [])
78
-
79
- self.preprocess_lbl = QLabel('Preprocessing')
80
- self.preprocess_lbl.setStyleSheet("font-weight: bold;")
81
-
82
- self.delete_filter_btn = QPushButton()
83
- self.delete_filter_btn.setStyleSheet(self.button_select_all)
84
- self.delete_filter_btn.setIcon(icon(MDI6.trash_can, color="black"))
85
- self.delete_filter_btn.setToolTip("Remove filter")
86
- self.delete_filter_btn.setIconSize(QSize(20, 20))
87
- self.delete_filter_btn.clicked.connect(self.list.removeSel)
88
-
89
- self.add_filter_btn = QPushButton()
90
- self.add_filter_btn.setStyleSheet(self.button_select_all)
91
- self.add_filter_btn.setIcon(icon(MDI6.filter_plus, color="black"))
92
- self.add_filter_btn.setToolTip("Add filter")
93
- self.add_filter_btn.setIconSize(QSize(20, 20))
94
- self.add_filter_btn.clicked.connect(self.list.addItem)
95
-
96
- self.help_prefilter_btn = QPushButton()
97
- self.help_prefilter_btn.setIcon(icon(MDI6.help_circle,color=self.help_color))
98
- self.help_prefilter_btn.setIconSize(QSize(20, 20))
99
- self.help_prefilter_btn.clicked.connect(self.help_prefilter)
100
- self.help_prefilter_btn.setStyleSheet(self.button_select_all)
101
- self.help_prefilter_btn.setToolTip("Help.")
102
-
103
- if self.apply_btn_option:
104
- self.apply_btn = QPushButton("Apply")
105
- self.apply_btn.setIcon(icon(MDI6.filter_cog_outline, color="white"))
106
- self.apply_btn.setIconSize(QSize(20, 20))
107
- self.apply_btn.setStyleSheet(self.button_style_sheet)
108
- self.apply_btn.clicked.connect(self.parent_window.preprocess_image)
109
-
110
- def help_prefilter(self):
111
-
112
- """
113
- Helper for prefiltering strategy
114
- """
115
-
116
- dict_path = os.sep.join([get_software_location(),'celldetective','gui','help','prefilter-for-segmentation.json'])
117
-
118
- with open(dict_path) as f:
119
- d = json.load(f)
120
-
121
- suggestion = help_generic(d)
122
- if isinstance(suggestion, str):
123
- print(f"{suggestion=}")
124
- msgBox = QMessageBox()
125
- msgBox.setIcon(QMessageBox.Information)
126
- msgBox.setTextFormat(Qt.RichText)
127
- msgBox.setText(f"The suggested technique is to {suggestion}.\nSee a tutorial <a href='https://celldetective.readthedocs.io/en/latest/segment.html'>here</a>.")
128
- msgBox.setWindowTitle("Info")
129
- msgBox.setStandardButtons(QMessageBox.Ok)
130
- returnValue = msgBox.exec()
131
- if returnValue == QMessageBox.Ok:
132
- return None
34
+ """
35
+ A widget that allows user to choose preprocessing filters for an image
36
+ """
37
+
38
+ def __init__(self, parent_window=None, apply_btn_option=True, *args, **kwargs):
39
+ super().__init__(*args, **kwargs)
40
+
41
+ self.parent_window = parent_window
42
+ self.apply_btn_option = apply_btn_option
43
+ self.generate_components()
44
+ self.add_to_layout()
45
+
46
+ def add_to_layout(self):
47
+
48
+ self.setContentsMargins(20, 20, 20, 20)
49
+
50
+ button_layout = QHBoxLayout()
51
+ button_layout.addWidget(self.preprocess_lbl, 85, alignment=Qt.AlignLeft)
52
+ button_layout.addWidget(self.delete_filter_btn, 5)
53
+ button_layout.addWidget(self.add_filter_btn, 5)
54
+ button_layout.addWidget(self.help_prefilter_btn, 5)
55
+ self.addLayout(button_layout, 25)
56
+
57
+ self.addWidget(self.list, 70)
58
+ if self.apply_btn_option:
59
+ self.addWidget(self.apply_btn, 5)
60
+
61
+ def generate_components(self):
62
+
63
+ self.list = ListWidget(FilterChoice, [])
64
+
65
+ self.preprocess_lbl = QLabel("Preprocessing")
66
+ self.preprocess_lbl.setStyleSheet("font-weight: bold;")
67
+
68
+ self.delete_filter_btn = QPushButton()
69
+ self.delete_filter_btn.setStyleSheet(self.button_select_all)
70
+ self.delete_filter_btn.setIcon(icon(MDI6.trash_can, color="black"))
71
+ self.delete_filter_btn.setToolTip("Remove filter")
72
+ self.delete_filter_btn.setIconSize(QSize(20, 20))
73
+ self.delete_filter_btn.clicked.connect(self.list.removeSel)
74
+
75
+ self.add_filter_btn = QPushButton()
76
+ self.add_filter_btn.setStyleSheet(self.button_select_all)
77
+ self.add_filter_btn.setIcon(icon(MDI6.filter_plus, color="black"))
78
+ self.add_filter_btn.setToolTip("Add filter")
79
+ self.add_filter_btn.setIconSize(QSize(20, 20))
80
+ self.add_filter_btn.clicked.connect(self.list.addItem)
81
+
82
+ self.help_prefilter_btn = QPushButton()
83
+ self.help_prefilter_btn.setIcon(icon(MDI6.help_circle, color=self.help_color))
84
+ self.help_prefilter_btn.setIconSize(QSize(20, 20))
85
+ self.help_prefilter_btn.clicked.connect(self.help_prefilter)
86
+ self.help_prefilter_btn.setStyleSheet(self.button_select_all)
87
+ self.help_prefilter_btn.setToolTip("Help.")
88
+
89
+ if self.apply_btn_option:
90
+ self.apply_btn = QPushButton("Apply")
91
+ self.apply_btn.setIcon(icon(MDI6.filter_cog_outline, color="white"))
92
+ self.apply_btn.setIconSize(QSize(20, 20))
93
+ self.apply_btn.setStyleSheet(self.button_style_sheet)
94
+ self.apply_btn.clicked.connect(self.parent_window.preprocess_image)
95
+
96
+ def help_prefilter(self):
97
+ """
98
+ Helper for prefiltering strategy
99
+ """
100
+
101
+ dict_path = os.sep.join(
102
+ [
103
+ get_software_location(),
104
+ "celldetective",
105
+ "gui",
106
+ "help",
107
+ "prefilter-for-segmentation.json",
108
+ ]
109
+ )
110
+
111
+ with open(dict_path) as f:
112
+ d = json.load(f)
113
+
114
+ suggestion = help_generic(d)
115
+ if isinstance(suggestion, str):
116
+ print(f"{suggestion=}")
117
+ msgBox = QMessageBox()
118
+ msgBox.setIcon(QMessageBox.Information)
119
+ msgBox.setTextFormat(Qt.RichText)
120
+ msgBox.setText(
121
+ f"The suggested technique is to {suggestion}.\nSee a tutorial <a href='https://celldetective.readthedocs.io/en/latest/segment.html'>here</a>."
122
+ )
123
+ msgBox.setWindowTitle("Info")
124
+ msgBox.setStandardButtons(QMessageBox.Ok)
125
+ returnValue = msgBox.exec()
126
+ if returnValue == QMessageBox.Ok:
127
+ return None
133
128
 
134
129
 
135
130
  class PreprocessingLayout2(PreprocessingLayout):
136
131
 
137
- def __init__(self, fraction=75, *args, **kwargs):
138
-
139
- self.fraction = fraction
140
- super().__init__(apply_btn_option=False, *args, **kwargs)
141
- self.preprocess_lbl.setText('Preprocessing: ')
142
- self.preprocess_lbl.setStyleSheet("")
143
- self.setContentsMargins(0,0,0,0)
144
-
145
- def add_to_layout(self):
146
-
147
- main_layout = QHBoxLayout()
148
- main_layout.setContentsMargins(0,0,0,0)
149
- main_layout.setSpacing(5)
150
- main_layout.addWidget(self.preprocess_lbl, self.fraction, alignment=Qt.AlignTop)
151
-
152
- list_grid = QGridLayout()
153
- list_grid.addWidget(self.list, 0, 0, 2, 2)
154
- list_grid.addWidget(self.add_filter_btn, 0, 2, 1, 1)
155
- list_grid.addWidget(self.delete_filter_btn, 1, 2, 1, 1)
156
- main_layout.addLayout(list_grid, 100-self.fraction)
157
- self.add_filter_btn.setFixedWidth(35) # Ensure the button width is fixed
158
- self.delete_filter_btn.setFixedWidth(35)
159
- list_grid.setColumnStretch(2, 0)
160
-
161
- self.addLayout(main_layout)
162
-
163
-
164
- class QCheckableComboBox(QComboBox):
165
-
166
- """
167
- adapted from https://stackoverflow.com/questions/22775095/pyqt-how-to-set-combobox-items-be-checkable
168
- """
169
-
170
- activated = pyqtSignal(str)
171
-
172
- def __init__(self, obj='', parent_window=None, *args, **kwargs):
173
-
174
- super().__init__(parent_window, *args, **kwargs)
175
-
176
- self.setTitle('')
177
- self.setModel(QStandardItemModel(self))
178
- self.obj = obj
179
- self.toolButton = QToolButton(parent_window)
180
- self.toolButton.setText('')
181
- self.toolMenu = QMenu(parent_window)
182
- self.toolButton.setMenu(self.toolMenu)
183
- self.toolButton.setPopupMode(QToolButton.InstantPopup)
184
- self.anySelected = False
185
-
186
- self.view().viewport().installEventFilter(self)
187
- self.view().pressed.connect(self.handleItemPressed)
188
-
189
- def clear(self):
190
-
191
- self.unselectAll()
192
- self.toolMenu.clear()
193
- super().clear()
194
-
195
-
196
- def handleItemPressed(self, index):
197
-
198
- idx = index.row()
199
- actions = self.toolMenu.actions()
200
-
201
- item = self.model().itemFromIndex(index)
202
- if item.checkState() == Qt.Checked:
203
- item.setCheckState(Qt.Unchecked)
204
- actions[idx].setChecked(False)
205
- else:
206
- item.setCheckState(Qt.Checked)
207
- actions[idx].setChecked(True)
208
- self.anySelected = True
209
-
210
- options_checked = np.array([a.isChecked() for a in actions])
211
- if len(options_checked[options_checked]) > 1:
212
- self.setTitle(f'Multiple {self.obj+"s"} selected...')
213
- elif len(options_checked[options_checked])==1:
214
- idx_selected = np.where(options_checked)[0][0]
215
- if idx_selected!=idx:
216
- item = self.model().item(idx_selected)
217
- self.setTitle(item.text())
218
- elif len(options_checked[options_checked])==0:
219
- self.setTitle(f"No {self.obj} selected...")
220
- self.anySelected = False
221
-
222
- self.activated.emit(self.title())
223
-
224
- def setCurrentIndex(self, index):
225
-
226
- super().setCurrentIndex(index)
227
-
228
- item = self.model().item(index)
229
- modelIndex = self.model().indexFromItem(item)
230
-
231
- self.handleItemPressed(modelIndex)
232
-
233
- def selectAll(self):
234
-
235
- actions = self.toolMenu.actions()
236
- for i,a in enumerate(actions):
237
- if not a.isChecked():
238
- self.setCurrentIndex(i)
239
- self.anySelected = True
240
-
241
- def unselectAll(self):
242
-
243
- actions = self.toolMenu.actions()
244
- for i,a in enumerate(actions):
245
- if a.isChecked():
246
- self.setCurrentIndex(i)
247
- self.anySelected = False
248
-
249
- def title(self):
250
- return self._title
251
-
252
- def setTitle(self, title):
253
- self._title = title
254
- self.update()
255
- self.repaint()
256
-
257
- def paintEvent(self, event):
258
-
259
- painter = QStylePainter(self)
260
- painter.setPen(self.palette().color(QPalette.Text))
261
- opt = QStyleOptionComboBox()
262
- self.initStyleOption(opt)
263
- opt.currentText = self._title
264
- painter.drawComplexControl(QStyle.CC_ComboBox, opt)
265
- painter.drawControl(QStyle.CE_ComboBoxLabel, opt)
266
-
267
- def addItem(self, item, tooltip=None):
268
-
269
- super().addItem(item)
270
- idx = self.findText(item)
271
- if tooltip is not None:
272
- self.setItemData(idx, tooltip, Qt.ToolTipRole)
273
- item2 = self.model().item(idx, 0)
274
- item2.setCheckState(Qt.Unchecked)
275
- action = self.toolMenu.addAction(item)
276
- action.setCheckable(True)
277
-
278
- def addItems(self, items):
279
-
280
- super().addItems(items)
281
-
282
- for item in items:
283
-
284
- idx = self.findText(item)
285
- item2 = self.model().item(idx, 0)
286
- item2.setCheckState(Qt.Unchecked)
287
- action = self.toolMenu.addAction(item)
288
- action.setCheckable(True)
289
-
290
- def getSelectedIndices(self):
291
-
292
- actions = self.toolMenu.actions()
293
- options_checked = np.array([a.isChecked() for a in actions])
294
- idx_selected = np.where(options_checked)[0]
295
-
296
- return list(idx_selected)
297
-
298
- def currentText(self):
299
- return self.title()
300
-
301
- def isMultipleSelection(self):
302
- return self.currentText().startswith('Multiple')
303
-
304
- def isSingleSelection(self):
305
- return not self.currentText().startswith('Multiple') and not self.title().startswith('No')
306
-
307
- def isAnySelected(self):
308
- return not self.title().startswith('No')
309
-
310
- def eventFilter(self, source, event):
311
- if source is self.view().viewport():
312
- if event.type() == QEvent.MouseButtonRelease:
313
- return True # Prevent the popup from closing
314
- return super().eventFilter(source, event)
132
+ def __init__(self, fraction=75, *args, **kwargs):
315
133
 
316
- class PandasModel(QAbstractTableModel):
317
-
318
- """
319
- from https://stackoverflow.com/questions/31475965/fastest-way-to-populate-qtableview-from-pandas-data-frame
320
- """
134
+ self.fraction = fraction
135
+ super().__init__(apply_btn_option=False, *args, **kwargs)
136
+ self.preprocess_lbl.setText("Preprocessing: ")
137
+ self.preprocess_lbl.setStyleSheet("")
138
+ self.setContentsMargins(0, 0, 0, 0)
321
139
 
322
- def __init__(self, data):
323
- QAbstractTableModel.__init__(self)
324
- self._data = data
325
- self.colors = dict()
140
+ def add_to_layout(self):
326
141
 
327
- def rowCount(self, parent=None):
328
- return self._data.shape[0]
142
+ main_layout = QHBoxLayout()
143
+ main_layout.setContentsMargins(0, 0, 0, 0)
144
+ main_layout.setSpacing(5)
145
+ main_layout.addWidget(self.preprocess_lbl, self.fraction, alignment=Qt.AlignTop)
329
146
 
330
- def columnCount(self, parent=None):
331
- return self._data.shape[1]
147
+ list_grid = QGridLayout()
148
+ list_grid.addWidget(self.list, 0, 0, 2, 2)
149
+ list_grid.addWidget(self.add_filter_btn, 0, 2, 1, 1)
150
+ list_grid.addWidget(self.delete_filter_btn, 1, 2, 1, 1)
151
+ main_layout.addLayout(list_grid, 100 - self.fraction)
152
+ self.add_filter_btn.setFixedWidth(35) # Ensure the button width is fixed
153
+ self.delete_filter_btn.setFixedWidth(35)
154
+ list_grid.setColumnStretch(2, 0)
332
155
 
333
- def data(self, index, role=Qt.DisplayRole):
334
- if index.isValid():
335
- if role == Qt.DisplayRole:
336
- return str(self._data.iloc[index.row(), index.column()])
337
- if role == Qt.BackgroundRole:
338
- color = self.colors.get((index.row(), index.column()))
339
- if color is not None:
340
- return color
341
- return None
156
+ self.addLayout(main_layout)
342
157
 
343
- def headerData(self, rowcol, orientation, role):
344
- if orientation == Qt.Horizontal and role == Qt.DisplayRole:
345
- return self._data.columns[rowcol]
346
- if orientation == Qt.Vertical and role == Qt.DisplayRole:
347
- return self._data.index[rowcol]
348
- return None
349
158
 
350
- def change_color(self, row, column, color):
351
- ix = self.index(row, column)
352
- self.colors[(row, column)] = color
353
- self.dataChanged.emit(ix, ix, (Qt.BackgroundRole,))
159
+ class PandasModel(QAbstractTableModel):
160
+ """
161
+ from https://stackoverflow.com/questions/31475965/fastest-way-to-populate-qtableview-from-pandas-data-frame
162
+ """
163
+
164
+ def __init__(self, data):
165
+ QAbstractTableModel.__init__(self)
166
+ self._data = data
167
+ self.colors = dict()
168
+
169
+ def rowCount(self, parent=None):
170
+ return self._data.shape[0]
171
+
172
+ def columnCount(self, parent=None):
173
+ return self._data.shape[1]
174
+
175
+ def data(self, index, role=Qt.DisplayRole):
176
+ if index.isValid():
177
+ if role == Qt.DisplayRole:
178
+ return str(self._data.iloc[index.row(), index.column()])
179
+ if role == Qt.BackgroundRole:
180
+ color = self.colors.get((index.row(), index.column()))
181
+ if color is not None:
182
+ return color
183
+ return None
184
+
185
+ def headerData(self, rowcol, orientation, role):
186
+ if orientation == Qt.Horizontal and role == Qt.DisplayRole:
187
+ return self._data.columns[rowcol]
188
+ if orientation == Qt.Vertical and role == Qt.DisplayRole:
189
+ return self._data.index[rowcol]
190
+ return None
191
+
192
+ def change_color(self, row, column, color):
193
+ ix = self.index(row, column)
194
+ self.colors[(row, column)] = color
195
+ self.dataChanged.emit(ix, ix, (Qt.BackgroundRole,))
354
196
 
355
197
 
356
198
  class GenericOpColWidget(CelldetectiveWidget):
357
199
 
358
- def __init__(self, parent_window, column=None, title=''):
359
-
360
- super().__init__()
361
-
362
- self.parent_window = parent_window
363
- self.column = column
364
- self.title = title
200
+ def __init__(self, parent_window, column=None, title=""):
365
201
 
366
- self.setWindowTitle(self.title)
367
- # Create the QComboBox and add some items
368
-
369
- self.layout = QVBoxLayout(self)
370
- self.layout.setContentsMargins(30,30,30,30)
202
+ super().__init__()
371
203
 
372
- self.sublayout = QVBoxLayout()
204
+ self.parent_window = parent_window
205
+ self.column = column
206
+ self.title = title
373
207
 
374
- self.measurements_cb = QComboBox()
375
- self.measurements_cb.addItems(list(self.parent_window.data.columns))
376
- if self.column is not None:
377
- idx = self.measurements_cb.findText(self.column)
378
- self.measurements_cb.setCurrentIndex(idx)
208
+ self.setWindowTitle(self.title)
209
+ # Create the QComboBox and add some items
379
210
 
380
- measurement_layout = QHBoxLayout()
381
- measurement_layout.addWidget(QLabel('measurements: '), 25)
382
- measurement_layout.addWidget(self.measurements_cb, 75)
383
- self.sublayout.addLayout(measurement_layout)
211
+ self.layout = QVBoxLayout(self)
212
+ self.layout.setContentsMargins(30, 30, 30, 30)
384
213
 
385
- self.layout.addLayout(self.sublayout)
386
-
387
- self.submit_btn = QPushButton('Compute')
388
- self.submit_btn.setStyleSheet(self.button_style_sheet)
389
- self.submit_btn.clicked.connect(self.launch_operation)
390
- self.layout.addWidget(self.submit_btn, 30)
214
+ self.sublayout = QVBoxLayout()
391
215
 
392
- self.setAttribute(Qt.WA_DeleteOnClose)
393
- center_window(self)
216
+ self.measurements_cb = QComboBox()
217
+ self.measurements_cb.addItems(list(self.parent_window.data.columns))
218
+ if self.column is not None:
219
+ idx = self.measurements_cb.findText(self.column)
220
+ self.measurements_cb.setCurrentIndex(idx)
394
221
 
395
- def launch_operation(self):
222
+ measurement_layout = QHBoxLayout()
223
+ measurement_layout.addWidget(QLabel("measurements: "), 25)
224
+ measurement_layout.addWidget(self.measurements_cb, 75)
225
+ self.sublayout.addLayout(measurement_layout)
396
226
 
397
- self.compute()
398
- self.parent_window.model = PandasModel(self.parent_window.data)
399
- self.parent_window.table_view.setModel(self.parent_window.model)
400
- self.close()
227
+ self.layout.addLayout(self.sublayout)
401
228
 
402
- def compute(self):
403
- pass
229
+ self.submit_btn = QPushButton("Compute")
230
+ self.submit_btn.setStyleSheet(self.button_style_sheet)
231
+ self.submit_btn.clicked.connect(self.launch_operation)
232
+ self.layout.addWidget(self.submit_btn, 30)
404
233
 
234
+ self.setAttribute(Qt.WA_DeleteOnClose)
235
+ center_window(self)
405
236
 
406
- class QuickSliderLayout(QHBoxLayout):
407
-
408
- """
409
- A layout class that combines a QLabel and a QSlider in a horizontal box layout.
410
-
411
- This layout provides a convenient way to include a slider with an optional label and configurable
412
- parameters such as range, precision, and step size. It allows for both integer and decimal step values,
413
- making it versatile for different types of input adjustments.
414
-
415
- Parameters
416
- ----------
417
- label : str, optional
418
- The label to be displayed next to the slider (default is None).
419
- slider : QSlider
420
- The slider widget to be added to the layout.
421
- layout_ratio : tuple of float, optional
422
- Defines the width ratio between the label and the slider in the layout. The first element is the
423
- ratio for the label, and the second is for the slider (default is (0.25, 0.75)).
424
- slider_initial_value : int or float, optional
425
- The initial value to set for the slider (default is 1).
426
- slider_range : tuple of int or float, optional
427
- A tuple specifying the minimum and maximum values for the slider (default is (0, 1)).
428
- slider_tooltip : str, optional
429
- Tooltip text to display when hovering over the slider (default is None).
430
- decimal_option : bool, optional
431
- If True, the slider allows decimal values with a specified precision (default is True).
432
- precision : float, optional
433
- The step size for the slider when `decimal_option` is enabled (default is 1.0E-03).
434
-
435
- Attributes
436
- ----------
437
- qlabel : QLabel
438
- The label widget that displays the provided label text (only if `label` is provided).
439
- slider : QSlider
440
- The slider widget that allows the user to select a value.
441
- """
442
-
443
- def __init__(self, label=None, slider=None, layout_ratio=(0.25,0.75), slider_initial_value=1, slider_range=(0,1), slider_tooltip=None, decimal_option=True, precision=3, *args):
444
- super().__init__(*args)
445
-
446
- if label is not None and isinstance(label,str):
447
- self.qlabel = QLabel(label)
448
- self.addWidget(self.qlabel, int(100*layout_ratio[0]))
449
-
450
- self.slider = slider
451
- self.slider.setOrientation(Qt.Horizontal)
452
- if decimal_option:
453
- self.slider.setSingleStep(1.0*(10**(-precision)))
454
- self.slider.setTickInterval(1.0*(10**(-precision)))
455
- self.slider.setDecimals(precision)
456
- else:
457
- self.slider.setSingleStep(1)
458
- self.slider.setTickInterval(1)
459
-
460
- self.slider.setRange(*slider_range)
461
- self.slider.setValue(slider_initial_value)
462
- if isinstance(slider_tooltip,str):
463
- self.slider.setToolTip(slider_tooltip)
464
-
465
- self.addWidget(self.slider, int(100*layout_ratio[1]))
466
-
467
-
468
- def center_window(window):
469
-
470
- """
471
- Centers the given window in the middle of the screen.
472
-
473
- This function calculates the current screen's geometry and moves the
474
- specified window to the center of the screen. It works by retrieving the
475
- frame geometry of the window, identifying the screen where the cursor is
476
- currently located, and adjusting the window's position to be centrally
477
- aligned on that screen.
478
-
479
- Parameters
480
- ----------
481
- window : QMainWindow or QWidget
482
- The window or widget to be centered on the screen.
483
- """
484
-
485
- frameGm = window.frameGeometry()
486
- screen = QApplication.desktop().screenNumber(QApplication.desktop().cursor().pos())
487
- centerPoint = QApplication.desktop().screenGeometry(screen).center()
488
- frameGm.moveCenter(centerPoint)
489
- window.move(frameGm.topLeft())
490
-
491
- class ExportPlotBtn(QPushButton, Styles):
492
-
493
- """
494
- A custom QPushButton widget for exporting a matplotlib figure.
495
-
496
- This class combines a QPushButton with functionality to export a given matplotlib
497
- figure (`fig`) to an image file. The button includes an icon and a tooltip for easy
498
- user interaction. When clicked, a file dialog is opened allowing the user to specify
499
- the location and file format to save the plot.
500
-
501
- Parameters
502
- ----------
503
- fig : matplotlib.figure.Figure
504
- The matplotlib figure object to be exported.
505
- export_dir : str, optional
506
- The default directory where the file will be saved. If not provided, the current
507
- working directory will be used.
508
- *args : tuple
509
- Additional positional arguments passed to the parent `QPushButton` constructor.
510
- **kwargs : dict
511
- Additional keyword arguments passed to the parent `QPushButton` constructor.
512
-
513
- Attributes
514
- ----------
515
- fig : matplotlib.figure.Figure
516
- The figure that will be saved when the button is clicked.
517
- export_dir : str or None
518
- The default directory where the file dialog will initially point when saving the image.
519
-
520
- Methods
521
- -------
522
- save_plot():
523
- Opens a file dialog to choose the file name and location for saving the figure.
524
- The figure is then saved in the specified format and location.
525
- """
526
-
527
- def __init__(self, fig, export_dir=None, *args, **kwargs):
528
-
529
- super().__init__()
530
-
531
- self.export_dir = export_dir
532
- self.fig = fig
533
-
534
- self.setText('')
535
- self.setIcon(icon(MDI6.content_save,color="black"))
536
- self.setStyleSheet(self.button_select_all)
537
- self.setToolTip('Export figure.')
538
- self.setIconSize(QSize(20, 20))
539
- self.clicked.connect(self.save_plot)
540
-
541
- def save_plot(self):
542
-
543
- """
544
- Opens a file dialog for the user to specify the location and name to save the plot.
545
-
546
- If the user selects a file, the figure is saved with tight layout and 300 DPI resolution.
547
- Supported formats include PNG, JPG, SVG, and XPM.
548
- """
549
-
550
- if self.export_dir is not None:
551
- guess_dir = self.export_dir+sep+'plot.png'
552
- else:
553
- guess_dir = 'plot.png'
554
- fileName, _ = QFileDialog.getSaveFileName(self,
555
- "Save Image", guess_dir, "Images (*.png *.xpm *.jpg *.svg)") #, options=options
556
- if fileName:
557
- self.fig.tight_layout()
558
- self.fig.savefig(fileName, bbox_inches='tight', dpi=300)
559
-
560
-
561
- class QHSeperationLine(QFrame):
562
- '''
563
- a horizontal seperation line\n
564
- '''
565
-
566
- def __init__(self):
567
- super().__init__()
568
- self.setMinimumWidth(1)
569
- self.setFixedHeight(20)
570
- self.setFrameShape(QFrame.HLine)
571
- self.setFrameShadow(QFrame.Sunken)
572
- self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Minimum)
573
-
574
-
575
- class FeatureChoice(CelldetectiveWidget):
576
-
577
- def __init__(self, parent_window):
578
- super().__init__()
579
- self.parent_window = parent_window
580
- self.setWindowTitle("Add feature")
581
- # Create the QComboBox and add some items
582
- self.combo_box = QComboBox(self)
583
-
584
- standard_measurements = ["area",
585
- "area_bbox",
586
- "area_convex",
587
- "area_filled",
588
- "major_axis_length",
589
- "minor_axis_length",
590
- "eccentricity",
591
- "equivalent_diameter_area",
592
- "euler_number",
593
- "extent",
594
- "feret_diameter_max",
595
- "orientation",
596
- "perimeter",
597
- "perimeter_crofton",
598
- "solidity",
599
- "intensity_mean",
600
- "intensity_max",
601
- "intensity_min",
602
- ]
603
-
604
- if extra_props:
605
- members = getmembers(extra_properties, isfunction)
606
- for o in members:
607
- if isfunction(o[1]) and o[1].__module__=="celldetective.extra_properties":
608
- standard_measurements.append(o[0])
609
-
610
- self.combo_box.addItems(standard_measurements)
611
-
612
- self.add_btn = QPushButton("Add")
613
- self.add_btn.setStyleSheet(self.button_style_sheet)
614
- self.add_btn.clicked.connect(self.add_current_feature)
615
-
616
- # Create the layout
617
- layout = QVBoxLayout(self)
618
- layout.addWidget(self.combo_box)
619
- layout.addWidget(self.add_btn)
620
- center_window(self)
621
-
622
-
623
- def add_current_feature(self):
624
- filtername = self.combo_box.currentText()
625
- self.parent_window.list_widget.addItems([filtername])
626
- self.close()
627
-
237
+ def launch_operation(self):
628
238
 
629
- class FilterChoice(CelldetectiveWidget):
630
-
631
- def __init__(self, parent_window):
632
-
633
- super().__init__()
634
- self.parent_window = parent_window
635
- self.setWindowTitle("Add filter")
636
- # Create the QComboBox and add some items
637
- center_window(self)
638
-
639
- self.default_params = {
640
- 'gauss_filter': {'sigma': 2},
641
- 'median_filter': {'size': 4},
642
- 'maximum_filter': {'size': 4},
643
- 'minimum_filter': {'size': 4},
644
- 'percentile_filter': {'percentile': 99, 'size': 4},
645
- 'variance_filter': {'size': 4},
646
- 'std_filter': {'size': 4},
647
- 'laplace_filter': None,
648
- 'abs_filter': None,
649
- 'ln_filter': None,
650
- 'invert_filter': {'value': 65535},
651
- 'subtract_filter': {'value': 1},
652
- 'dog_filter': {'blob_size': 30},
653
- 'log_filter': {'blob_size': 30},
654
- 'tophat_filter': {'size': 4, 'connectivity': 4},
655
- 'otsu_filter': None,
656
- 'multiotsu_filter': {'classes': 3},
657
- 'local_filter': {'block_size': 73, 'method': 'mean', 'offset': 0},
658
- 'niblack_filter': {'window_size': 15, 'k': 0.2},
659
- # 'sauvola_filter': {'window_size': 15, 'k': 0.2}
660
- }
661
-
662
- layout = QVBoxLayout(self)
663
- self.combo_box = QComboBox(self)
664
- self.combo_box.addItems(list(self.default_params.keys()))
665
- self.combo_box.currentTextChanged.connect(self.update_arguments)
666
- layout.addWidget(self.combo_box)
667
-
668
- self.floatValidator = QDoubleValidator()
669
- self.arguments_le = [QLineEdit() for i in range(3)]
670
- for i in range(3):
671
- self.arguments_le[i].setValidator(self.floatValidator)
672
-
673
- self.arguments_labels = [QLabel('') for i in range(3)]
674
- for i in range(2):
675
- hbox = QHBoxLayout()
676
- hbox.addWidget(self.arguments_labels[i], 40)
677
- hbox.addWidget(self.arguments_le[i], 60)
678
- layout.addLayout(hbox)
679
-
680
- self.add_btn = QPushButton("Add")
681
- self.add_btn.setStyleSheet(self.button_style_sheet)
682
- self.add_btn.clicked.connect(self.add_current_feature)
683
- layout.addWidget(self.add_btn)
684
-
685
- self.combo_box.setCurrentIndex(0)
686
- self.update_arguments()
687
- center_window(self)
688
-
689
- def add_current_feature(self):
690
-
691
- filtername = self.combo_box.currentText()
692
- self.parent_window.list_widget.addItems([filtername])
693
-
694
- filter_instructions = [filtername.split('_')[0]]
695
- for a in self.arguments_le:
696
-
697
- arg = a.text().replace(',','.')
698
- arg_num = arg
699
-
700
- if (arg != '') and arg_num.replace('.', '').replace(',', '').isnumeric():
701
- num = float(arg)
702
- if num.is_integer():
703
- num = int(num)
704
- filter_instructions.append(num)
705
- elif arg != '':
706
- filter_instructions.append(arg)
707
-
708
- print(f'You added filter {filter_instructions}.')
709
-
710
- self.parent_window.items.append(filter_instructions)
711
- self.close()
712
-
713
- def update_arguments(self):
714
-
715
- selected_filter = self.combo_box.currentText()
716
- arguments = self.default_params[selected_filter]
717
- if arguments is not None:
718
- args = list(arguments.keys())
719
- for i in range(len(args)):
720
- self.arguments_labels[i].setEnabled(True)
721
- self.arguments_le[i].setEnabled(True)
722
-
723
- self.arguments_labels[i].setText(args[i])
724
- self.arguments_le[i].setText(str(arguments[args[i]]))
725
-
726
- if len(args) < 2:
727
- for i in range(len(args), 2):
728
- self.arguments_labels[i].setEnabled(False)
729
- self.arguments_labels[i].setText('')
730
- self.arguments_le[i].setEnabled(False)
731
- else:
732
- for i in range(2):
733
- self.arguments_labels[i].setEnabled(False)
734
- self.arguments_le[i].setEnabled(False)
735
- self.arguments_labels[i].setText('')
239
+ self.compute()
240
+ self.parent_window.model = PandasModel(self.parent_window.data)
241
+ self.parent_window.table_view.setModel(self.parent_window.model)
242
+ self.close()
736
243
 
244
+ def compute(self):
245
+ pass
737
246
 
738
- class OperationChoice(CelldetectiveWidget):
739
- """
740
- Mini window to select an operation from numpy to apply on the ROI.
741
-
742
- """
743
247
 
744
- def __init__(self, parent_window):
745
- super().__init__()
746
- self.parent_window = parent_window
747
- self.setWindowTitle("Add feature")
748
- # Create the QComboBox and add some items
749
- self.combo_box = QComboBox(self)
750
- center_window(self)
751
-
752
- self.combo_box.addItems(["mean", "median", "average", "std", "var",
753
- "nanmedian", "nanmean", "nanstd", "nanvar"])
754
-
755
- self.add_btn = QPushButton("Add")
756
- self.add_btn.clicked.connect(self.add_current_feature)
757
-
758
- # Create the layout
759
- layout = QVBoxLayout(self)
760
- layout.addWidget(self.combo_box)
761
- layout.addWidget(self.add_btn)
762
-
763
- def add_current_feature(self):
764
- filtername = self.combo_box.currentText()
765
- self.parent_window.list_widget.addItems([filtername])
766
- self.close()
767
-
768
-
769
- class GeometryChoice(CelldetectiveWidget):
248
+ class QuickSliderLayout(QHBoxLayout):
249
+ """
250
+ A layout class that combines a QLabel and a QSlider in a horizontal box layout.
251
+
252
+ This layout provides a convenient way to include a slider with an optional label and configurable
253
+ parameters such as range, precision, and step size. It allows for both integer and decimal step values,
254
+ making it versatile for different types of input adjustments.
255
+
256
+ Parameters
257
+ ----------
258
+ label : str, optional
259
+ The label to be displayed next to the slider (default is None).
260
+ slider : QSlider
261
+ The slider widget to be added to the layout.
262
+ layout_ratio : tuple of float, optional
263
+ Defines the width ratio between the label and the slider in the layout. The first element is the
264
+ ratio for the label, and the second is for the slider (default is (0.25, 0.75)).
265
+ slider_initial_value : int or float, optional
266
+ The initial value to set for the slider (default is 1).
267
+ slider_range : tuple of int or float, optional
268
+ A tuple specifying the minimum and maximum values for the slider (default is (0, 1)).
269
+ slider_tooltip : str, optional
270
+ Tooltip text to display when hovering over the slider (default is None).
271
+ decimal_option : bool, optional
272
+ If True, the slider allows decimal values with a specified precision (default is True).
273
+ precision : float, optional
274
+ The step size for the slider when `decimal_option` is enabled (default is 1.0E-03).
275
+
276
+ Attributes
277
+ ----------
278
+ qlabel : QLabel
279
+ The label widget that displays the provided label text (only if `label` is provided).
280
+ slider : QSlider
281
+ The slider widget that allows the user to select a value.
282
+ """
283
+
284
+ def __init__(
285
+ self,
286
+ label=None,
287
+ slider=None,
288
+ layout_ratio=(0.25, 0.75),
289
+ slider_initial_value=1,
290
+ slider_range=(0, 1),
291
+ slider_tooltip=None,
292
+ decimal_option=True,
293
+ precision=3,
294
+ *args,
295
+ ):
296
+ super().__init__(*args)
297
+
298
+ if label is not None and isinstance(label, str):
299
+ self.qlabel = QLabel(label)
300
+ self.addWidget(self.qlabel, int(100 * layout_ratio[0]))
301
+
302
+ self.slider = slider
303
+ self.slider.setOrientation(Qt.Horizontal)
304
+ if decimal_option:
305
+ self.slider.setSingleStep(1.0 * (10 ** (-precision)))
306
+ self.slider.setTickInterval(1.0 * (10 ** (-precision)))
307
+ self.slider.setDecimals(precision)
308
+ else:
309
+ self.slider.setSingleStep(1)
310
+ self.slider.setTickInterval(1)
311
+
312
+ self.slider.setRange(*slider_range)
313
+ self.slider.setValue(slider_initial_value)
314
+ if isinstance(slider_tooltip, str):
315
+ self.slider.setToolTip(slider_tooltip)
316
+
317
+ self.addWidget(self.slider, int(100 * layout_ratio[1]))
770
318
 
771
- def __init__(self, parent_window):
772
319
 
773
- super().__init__()
774
- self.parent_window = parent_window
775
- self.setWindowTitle("Set distances")
776
- center_window(self)
320
+ class ExportPlotBtn(QPushButton, Styles):
321
+ """
322
+ A custom QPushButton widget for exporting a matplotlib figure.
323
+
324
+ This class combines a QPushButton with functionality to export a given matplotlib
325
+ figure (`fig`) to an image file. The button includes an icon and a tooltip for easy
326
+ user interaction. When clicked, a file dialog is opened allowing the user to specify
327
+ the location and file format to save the plot.
328
+
329
+ Parameters
330
+ ----------
331
+ fig : matplotlib.figure.Figure
332
+ The matplotlib figure object to be exported.
333
+ export_dir : str, optional
334
+ The default directory where the file will be saved. If not provided, the current
335
+ working directory will be used.
336
+ *args : tuple
337
+ Additional positional arguments passed to the parent `QPushButton` constructor.
338
+ **kwargs : dict
339
+ Additional keyword arguments passed to the parent `QPushButton` constructor.
340
+
341
+ Attributes
342
+ ----------
343
+ fig : matplotlib.figure.Figure
344
+ The figure that will be saved when the button is clicked.
345
+ export_dir : str or None
346
+ The default directory where the file dialog will initially point when saving the image.
347
+
348
+ Methods
349
+ -------
350
+ save_plot():
351
+ Opens a file dialog to choose the file name and location for saving the figure.
352
+ The figure is then saved in the specified format and location.
353
+ """
354
+
355
+ def __init__(self, fig, export_dir=None, *args, **kwargs):
356
+
357
+ super().__init__()
358
+
359
+ self.export_dir = export_dir
360
+ self.fig = fig
361
+
362
+ self.setText("")
363
+ self.setIcon(icon(MDI6.content_save, color="black"))
364
+ self.setStyleSheet(self.button_select_all)
365
+ self.setToolTip("Export figure.")
366
+ self.setIconSize(QSize(20, 20))
367
+ self.clicked.connect(self.save_plot)
368
+
369
+ def save_plot(self):
370
+ """
371
+ Opens a file dialog for the user to specify the location and name to save the plot.
372
+
373
+ If the user selects a file, the figure is saved with tight layout and 300 DPI resolution.
374
+ Supported formats include PNG, JPG, SVG, and XPM.
375
+ """
376
+
377
+ if self.export_dir is not None:
378
+ guess_dir = self.export_dir + sep + "plot.png"
379
+ else:
380
+ guess_dir = "plot.png"
381
+ fileName, _ = QFileDialog.getSaveFileName(
382
+ self, "Save Image", guess_dir, "Images (*.png *.xpm *.jpg *.svg)"
383
+ ) # , options=options
384
+ if fileName:
385
+ self.fig.tight_layout()
386
+ self.fig.savefig(fileName, bbox_inches="tight", dpi=300)
777
387
 
778
- # Create the QComboBox and add some items
779
388
 
780
- self.dist_label = QLabel('Distance [px]: ')
781
- self.dist_le = QLineEdit('10')
389
+ class FilterChoice(CelldetectiveWidget):
782
390
 
783
- self.dist_outer_label = QLabel('Max distance [px]')
784
- self.dist_outer_le = QLineEdit('100')
785
- self.outer_to_hide = [self.dist_outer_le, self.dist_outer_label]
391
+ def __init__(self, parent_window):
392
+
393
+ super().__init__()
394
+ self.parent_window = parent_window
395
+ self.setWindowTitle("Add filter")
396
+ # Create the QComboBox and add some items
397
+ center_window(self)
398
+
399
+ self.default_params = {
400
+ "gauss_filter": {"sigma": 2},
401
+ "median_filter": {"size": 4},
402
+ "maximum_filter": {"size": 4},
403
+ "minimum_filter": {"size": 4},
404
+ "percentile_filter": {"percentile": 99, "size": 4},
405
+ "variance_filter": {"size": 4},
406
+ "std_filter": {"size": 4},
407
+ "laplace_filter": None,
408
+ "abs_filter": None,
409
+ "ln_filter": None,
410
+ "invert_filter": {"value": 65535},
411
+ "subtract_filter": {"value": 1},
412
+ "dog_filter": {"blob_size": 30},
413
+ "log_filter": {"blob_size": 30},
414
+ "tophat_filter": {"size": 4, "connectivity": 4},
415
+ "otsu_filter": None,
416
+ "multiotsu_filter": {"classes": 3},
417
+ "local_filter": {"block_size": 73, "method": "mean", "offset": 0},
418
+ "niblack_filter": {"window_size": 15, "k": 0.2},
419
+ # 'sauvola_filter': {'window_size': 15, 'k': 0.2}
420
+ }
421
+
422
+ layout = QVBoxLayout(self)
423
+ self.combo_box = QComboBox(self)
424
+ self.combo_box.addItems(list(self.default_params.keys()))
425
+ self.combo_box.currentTextChanged.connect(self.update_arguments)
426
+ layout.addWidget(self.combo_box)
427
+
428
+ self.floatValidator = QDoubleValidator()
429
+ self.arguments_le = [QLineEdit() for i in range(3)]
430
+ for i in range(3):
431
+ self.arguments_le[i].setValidator(self.floatValidator)
432
+
433
+ self.arguments_labels = [QLabel("") for i in range(3)]
434
+ for i in range(2):
435
+ hbox = QHBoxLayout()
436
+ hbox.addWidget(self.arguments_labels[i], 40)
437
+ hbox.addWidget(self.arguments_le[i], 60)
438
+ layout.addLayout(hbox)
439
+
440
+ self.add_btn = QPushButton("Add")
441
+ self.add_btn.setStyleSheet(self.button_style_sheet)
442
+ self.add_btn.clicked.connect(self.add_current_feature)
443
+ layout.addWidget(self.add_btn)
444
+
445
+ self.combo_box.setCurrentIndex(0)
446
+ self.update_arguments()
447
+ center_window(self)
448
+
449
+ def add_current_feature(self):
450
+
451
+ filtername = self.combo_box.currentText()
452
+ self.parent_window.list_widget.addItems([filtername])
453
+
454
+ filter_instructions = [filtername.split("_")[0]]
455
+ for a in self.arguments_le:
456
+
457
+ arg = a.text().replace(",", ".")
458
+ arg_num = arg
459
+
460
+ if (arg != "") and arg_num.replace(".", "").replace(",", "").isnumeric():
461
+ num = float(arg)
462
+ if num.is_integer():
463
+ num = int(num)
464
+ filter_instructions.append(num)
465
+ elif arg != "":
466
+ filter_instructions.append(arg)
467
+
468
+ print(f"You added filter {filter_instructions}.")
469
+
470
+ self.parent_window.items.append(filter_instructions)
471
+ self.close()
472
+
473
+ def update_arguments(self):
474
+
475
+ selected_filter = self.combo_box.currentText()
476
+ arguments = self.default_params[selected_filter]
477
+ if arguments is not None:
478
+ args = list(arguments.keys())
479
+ for i in range(len(args)):
480
+ self.arguments_labels[i].setEnabled(True)
481
+ self.arguments_le[i].setEnabled(True)
482
+
483
+ self.arguments_labels[i].setText(args[i])
484
+ self.arguments_le[i].setText(str(arguments[args[i]]))
485
+
486
+ if len(args) < 2:
487
+ for i in range(len(args), 2):
488
+ self.arguments_labels[i].setEnabled(False)
489
+ self.arguments_labels[i].setText("")
490
+ self.arguments_le[i].setEnabled(False)
491
+ else:
492
+ for i in range(2):
493
+ self.arguments_labels[i].setEnabled(False)
494
+ self.arguments_le[i].setEnabled(False)
495
+ self.arguments_labels[i].setText("")
786
496
 
787
- self.outer_btn = QCheckBox('outer distance')
788
- self.outer_btn.clicked.connect(self.activate_outer_value)
789
497
 
790
- self.add_btn = QPushButton("Add")
791
- self.add_btn.clicked.connect(self.add_current_feature)
498
+ class OperationChoice(CelldetectiveWidget):
499
+ """
500
+ Mini window to select an operation from numpy to apply on the ROI.
501
+
502
+ """
503
+
504
+ def __init__(self, parent_window):
505
+ super().__init__()
506
+ self.parent_window = parent_window
507
+ self.setWindowTitle("Add feature")
508
+ # Create the QComboBox and add some items
509
+ self.combo_box = QComboBox(self)
510
+ center_window(self)
511
+
512
+ self.combo_box.addItems(
513
+ [
514
+ "mean",
515
+ "median",
516
+ "average",
517
+ "std",
518
+ "var",
519
+ "nanmedian",
520
+ "nanmean",
521
+ "nanstd",
522
+ "nanvar",
523
+ ]
524
+ )
525
+
526
+ self.add_btn = QPushButton("Add")
527
+ self.add_btn.clicked.connect(self.add_current_feature)
528
+
529
+ # Create the layout
530
+ layout = QVBoxLayout(self)
531
+ layout.addWidget(self.combo_box)
532
+ layout.addWidget(self.add_btn)
533
+
534
+ def add_current_feature(self):
535
+ filtername = self.combo_box.currentText()
536
+ self.parent_window.list_widget.addItems([filtername])
537
+ self.close()
538
+
539
+
540
+ class GeometryChoice(CelldetectiveWidget, Styles):
541
+
542
+ def __init__(self, parent_window):
543
+
544
+ super().__init__()
545
+ self.parent_window = parent_window
546
+ self.setWindowTitle("Set distances")
547
+ center_window(self)
548
+
549
+ # Create the QComboBox and add some items
550
+
551
+ self.dist_label = QLabel("Distance [px]: ")
552
+ self.dist_le = QLineEdit("10")
553
+
554
+ self.dist_outer_label = QLabel("Max distance [px]")
555
+ self.dist_outer_le = QLineEdit("100")
556
+ self.outer_to_hide = [self.dist_outer_le, self.dist_outer_label]
557
+
558
+ self.outer_btn = QCheckBox("outer distance")
559
+ self.outer_btn.clicked.connect(self.activate_outer_value)
560
+
561
+ self.add_btn = QPushButton("Add")
562
+ self.add_btn.clicked.connect(self.add_current_feature)
563
+ self.add_btn.setStyleSheet(self.button_style_sheet)
564
+
565
+ # Create the layout
566
+ layout = QVBoxLayout(self)
567
+ dist_layout = QHBoxLayout()
568
+ dist_layout.addWidget(self.dist_label, 30)
569
+ dist_layout.addWidget(self.dist_le, 70)
570
+
571
+ self.dist_outer_layout = QHBoxLayout()
572
+ self.dist_outer_layout.addWidget(self.dist_outer_label, 30)
573
+ self.dist_outer_layout.addWidget(self.dist_outer_le, 70)
574
+
575
+ layout.addLayout(dist_layout)
576
+ layout.addLayout(self.dist_outer_layout)
577
+ layout.addWidget(self.outer_btn)
578
+ layout.addWidget(self.add_btn)
579
+
580
+ for el in self.outer_to_hide:
581
+ el.hide()
582
+
583
+ def activate_outer_value(self):
584
+ if self.outer_btn.isChecked():
585
+ self.dist_label.setText("Min distance [px]: ")
586
+ for el in self.outer_to_hide:
587
+ el.show()
588
+ else:
589
+ self.dist_label.setText("Distance [px]: ")
590
+ for el in self.outer_to_hide:
591
+ el.hide()
592
+
593
+ def add_current_feature(self):
594
+
595
+ value = self.dist_le.text()
596
+ if self.outer_btn.isChecked():
597
+ value2 = self.dist_outer_le.text()
598
+ values = [value + "-" + value2]
599
+ else:
600
+ values = [value]
601
+ self.parent_window.list_widget.addItems(values)
602
+ self.close()
792
603
 
793
- # Create the layout
794
- layout = QVBoxLayout(self)
795
- dist_layout = QHBoxLayout()
796
- dist_layout.addWidget(self.dist_label, 30)
797
- dist_layout.addWidget(self.dist_le, 70)
798
604
 
799
- self.dist_outer_layout = QHBoxLayout()
800
- self.dist_outer_layout.addWidget(self.dist_outer_label, 30)
801
- self.dist_outer_layout.addWidget(self.dist_outer_le, 70)
605
+ class DistanceChoice(CelldetectiveWidget):
802
606
 
803
- layout.addLayout(dist_layout)
804
- layout.addLayout(self.dist_outer_layout)
805
- layout.addWidget(self.outer_btn)
806
- layout.addWidget(self.add_btn)
607
+ def __init__(self, parent_window):
608
+ super().__init__()
609
+ self.parent_window = parent_window
610
+ self.setWindowTitle("Set distances")
611
+ self.floatValidator = QDoubleValidator()
612
+ center_window(self)
807
613
 
808
- for el in self.outer_to_hide:
809
- el.hide()
614
+ # Create the QComboBox and add some items
810
615
 
811
- def activate_outer_value(self):
812
- if self.outer_btn.isChecked():
813
- self.dist_label.setText('Min distance [px]: ')
814
- for el in self.outer_to_hide:
815
- el.show()
816
- else:
817
- self.dist_label.setText('Distance [px]: ')
818
- for el in self.outer_to_hide:
819
- el.hide()
616
+ self.dist_label = QLabel("Distance [px]: ")
617
+ self.dist_le = QLineEdit("10")
618
+ self.dist_le.setValidator(self.floatValidator)
820
619
 
821
- def add_current_feature(self):
620
+ self.add_btn = QPushButton("Add")
621
+ self.add_btn.clicked.connect(self.add_current_feature)
822
622
 
823
- value = self.dist_le.text()
824
- if self.outer_btn.isChecked():
825
- value2 = self.dist_outer_le.text()
826
- values = [value + '-' + value2]
827
- else:
828
- values = [value]
829
- self.parent_window.list_widget.addItems(values)
830
- self.close()
623
+ # Create the layout
624
+ layout = QVBoxLayout(self)
625
+ dist_layout = QHBoxLayout()
626
+ dist_layout.addWidget(self.dist_label, 30)
627
+ dist_layout.addWidget(self.dist_le, 70)
831
628
 
629
+ layout.addLayout(dist_layout)
630
+ layout.addWidget(self.add_btn)
832
631
 
833
- class DistanceChoice(CelldetectiveWidget):
632
+ def add_current_feature(self):
633
+ value = self.dist_le.text().replace(",", ".")
634
+ values = [value]
635
+ self.parent_window.list_widget.addItems(values)
636
+ self.close()
834
637
 
835
- def __init__(self, parent_window):
836
- super().__init__()
837
- self.parent_window = parent_window
838
- self.setWindowTitle("Set distances")
839
- self.floatValidator = QDoubleValidator()
840
- center_window(self)
841
-
842
- # Create the QComboBox and add some items
843
-
844
- self.dist_label = QLabel('Distance [px]: ')
845
- self.dist_le = QLineEdit('10')
846
- self.dist_le.setValidator(self.floatValidator)
847
-
848
- self.add_btn = QPushButton("Add")
849
- self.add_btn.clicked.connect(self.add_current_feature)
850
-
851
- # Create the layout
852
- layout = QVBoxLayout(self)
853
- dist_layout = QHBoxLayout()
854
- dist_layout.addWidget(self.dist_label, 30)
855
- dist_layout.addWidget(self.dist_le, 70)
856
-
857
- layout.addLayout(dist_layout)
858
- layout.addWidget(self.add_btn)
859
-
860
- def add_current_feature(self):
861
- value = self.dist_le.text().replace(',','.')
862
- values = [value]
863
- self.parent_window.list_widget.addItems(values)
864
- self.close()
865
-
866
-
867
- class ListWidget(CelldetectiveWidget):
868
-
869
- """
870
- A customizable widget for displaying and managing a list of items, with the
871
- ability to add and remove items interactively.
872
-
873
- This widget is built around a `QListWidget` and allows for initialization with
874
- a set of features. It also provides options to retrieve the items, add new items
875
- using a custom widget, and remove selected items. The items can be parsed and
876
- returned as a list, with support for various data types and formatted input (e.g.,
877
- ranges specified with a dash).
878
-
879
- Parameters
880
- ----------
881
- choiceWidget : QWidget
882
- A custom widget that is used to add new items to the list.
883
- initial_features : list
884
- A list of initial items to populate the list widget.
885
- dtype : type, optional
886
- The data type to cast the list items to. Default is `str`.
887
-
888
- Attributes
889
- ----------
890
- initial_features : list
891
- The initial set of features or items displayed in the list.
892
- choiceWidget : QWidget
893
- The widget used to prompt the user to add new items.
894
- dtype : type
895
- The data type to convert items into when retrieved from the list.
896
- items : list
897
- A list to store the current items in the list widget.
898
- list_widget : QListWidget
899
- The core Qt widget that displays the list of items.
900
-
901
- Methods
902
- -------
903
- addItem()
904
- Opens a new window to add an item to the list using the custom `choiceWidget`.
905
- getItems()
906
- Retrieves the items from the list widget, parsing ranges (e.g., 'min-max')
907
- into two values, and converts them to the specified `dtype`.
908
- removeSel()
909
- Removes the currently selected item(s) from the list widget and updates the
910
- internal `items` list accordingly.
911
- """
912
-
913
- def __init__(self, choiceWidget, initial_features, dtype=str, *args, **kwargs):
914
-
915
- super().__init__()
916
- self.initial_features = initial_features
917
- self.choiceWidget = choiceWidget
918
- self.dtype = dtype
919
- self.items = []
920
-
921
- self.setFixedHeight(80)
922
-
923
- # Initialize list widget
924
- self.list_widget = QListWidget()
925
- self.list_widget.addItems(initial_features)
926
-
927
- # Set up layout
928
- main_layout = QVBoxLayout()
929
- main_layout.addWidget(self.list_widget)
930
- self.setLayout(main_layout)
931
- center_window(self)
932
-
933
- def addItem(self):
934
-
935
- """
936
- Opens the custom choiceWidget to add a new item to the list.
937
- """
938
-
939
- self.addItemWindow = self.choiceWidget(self)
940
- self.addItemWindow.show()
941
-
942
- def addItemToList(self, item):
943
- self.list_widget.addItems([item])
944
-
945
- def getItems(self):
946
-
947
- """
948
- Retrieves and returns the items from the list widget.
949
-
950
- This method parses any items that contain a range (formatted as 'min-max')
951
- into a list of two values, and casts all items to the specified `dtype`.
952
-
953
- Returns
954
- -------
955
- list
956
- A list of the items in the list widget, with ranges split into two values.
957
- """
958
-
959
- items = []
960
- for x in range(self.list_widget.count()):
961
- if len(self.list_widget.item(x).text().split('-')) == 2:
962
- if self.list_widget.item(x).text()[0] == '-':
963
- items.append(self.dtype(self.list_widget.item(x).text()))
964
- else:
965
- minn, maxx = self.list_widget.item(x).text().split('-')
966
- to_add = [self.dtype(minn), self.dtype(maxx)]
967
- items.append(to_add)
968
- else:
969
- items.append(self.dtype(self.list_widget.item(x).text()))
970
- return items
971
-
972
- def clear(self):
973
- self.items = []
974
- self.list_widget.clear()
975
-
976
- def removeSel(self):
977
-
978
- """
979
- Removes the selected item(s) from the list widget.
980
-
981
- If there are any selected items, they are removed both from the visual list
982
- and the internal `items` list that tracks the current state of the widget.
983
- """
984
-
985
- listItems = self.list_widget.selectedItems()
986
- if not listItems: return
987
- for item in listItems:
988
- idx = self.list_widget.row(item)
989
- self.list_widget.takeItem(idx)
990
- if self.items:
991
- del self.items[idx]
992
-
993
-
994
- class FigureCanvas(CelldetectiveWidget):
995
- """
996
- Generic figure canvas.
997
- """
998
-
999
- def __init__(self, fig, title="", interactive=True):
1000
- super().__init__()
1001
- self.fig = fig
1002
- self.setWindowTitle(title)
1003
- self.canvas = FigureCanvasQTAgg(self.fig)
1004
- self.canvas.setStyleSheet("background-color: transparent;")
1005
- if interactive:
1006
- self.toolbar = NavigationToolbar2QT(self.canvas)
1007
- self.layout = QVBoxLayout(self)
1008
- self.layout.addWidget(self.canvas,90)
1009
- if interactive:
1010
- self.layout.addWidget(self.toolbar)
1011
-
1012
- center_window(self)
1013
- self.setAttribute(Qt.WA_DeleteOnClose)
1014
-
1015
- def resizeEvent(self, event):
1016
-
1017
- super().resizeEvent(event)
1018
- try:
1019
- self.fig.tight_layout()
1020
- except:
1021
- pass
1022
-
1023
- def draw(self):
1024
- self.canvas.draw()
1025
-
1026
- def closeEvent(self, event):
1027
- """ Delete figure on closing window. """
1028
- # self.canvas.ax.cla() # ****
1029
- self.fig.clf() # ****
1030
- plt.close(self.fig)
1031
- super(FigureCanvas, self).closeEvent(event)
1032
638
 
1033
639
  class ThresholdLineEdit(QLineEdit):
1034
-
1035
- """
1036
- A custom QLineEdit widget to manage and validate threshold values.
1037
-
1038
- This class extends QLineEdit to input and manage threshold values (either float or int),
1039
- with optional validation and interaction with connected QPushButtons. The widget can
1040
- validate the input and enable/disable buttons based on whether a valid threshold is set.
1041
-
1042
- Parameters
1043
- ----------
1044
- init_value : float or int, optional
1045
- The initial threshold value to display in the input field (default is 2.0).
1046
- connected_buttons : QPushButton or list of QPushButton, optional
1047
- QPushButton(s) that should be enabled/disabled based on the validity of the threshold
1048
- value (default is None).
1049
- placeholder : str, optional
1050
- Placeholder text to show when no value is entered in the input field
1051
- (default is 'px > thresh are masked').
1052
- value_type : str, optional
1053
- Specifies the type of threshold value, either 'float' or 'int' (default is 'float').
1054
-
1055
- Methods
1056
- -------
1057
- enable_btn():
1058
- Enables or disables connected QPushButtons based on the validity of the threshold value.
1059
- set_threshold(value):
1060
- Sets the input field to the given threshold value.
1061
- get_threshold(show_warning=True):
1062
- Retrieves the current threshold value from the input field, returning it as a float or int.
1063
- If invalid, optionally displays a warning dialog.
1064
-
1065
- Example
1066
- -------
1067
- >>> threshold_input = ThresholdLineEdit(init_value=5, value_type='int')
1068
- >>> print(threshold_input.get_threshold())
1069
- 5
1070
- """
1071
-
1072
-
1073
- def __init__(self, init_value=2.0, connected_buttons=None, placeholder='px > thresh are masked',value_type='float',*args):
1074
- super().__init__(*args)
1075
-
1076
- self.init_value = init_value
1077
- self.value_type = value_type
1078
- self.connected_buttons = connected_buttons
1079
- self.setPlaceholderText(placeholder)
1080
-
1081
- if self.value_type=="float":
1082
- self.setValidator(QDoubleValidator())
1083
- else:
1084
- self.init_value = int(self.init_value)
1085
- self.setValidator(QIntValidator())
1086
-
1087
- if self.connected_buttons is not None:
1088
- self.textChanged.connect(self.enable_btn)
1089
- self.set_threshold(self.init_value)
1090
-
1091
- def enable_btn(self):
1092
-
1093
- """
1094
- Enable or disable connected QPushButtons based on the threshold value.
1095
-
1096
- If the current threshold value is valid, the connected buttons will be enabled.
1097
- If the value is invalid or empty, the buttons will be disabled.
1098
- """
1099
-
1100
- thresh = self.get_threshold(show_warning=False)
1101
- if isinstance(self.connected_buttons, QPushButton):
1102
- cbs = [self.connected_buttons]
1103
- else:
1104
- cbs = self.connected_buttons
1105
-
1106
- if thresh is None:
1107
- for c in cbs:
1108
- c.setEnabled(False)
1109
- else:
1110
- for c in cbs:
1111
- c.setEnabled(True)
1112
-
1113
- def set_threshold(self, value):
1114
-
1115
- """
1116
- Set the input field to the specified threshold value.
1117
-
1118
- Parameters
1119
- ----------
1120
- value : float or int
1121
- The value to set in the input field.
1122
- """
1123
-
1124
- try:
1125
- self.setText(str(value).replace('.',','))
1126
- except:
1127
- print('Please provide a valid threshold value...')
1128
-
1129
- def get_threshold(self, show_warning=True):
1130
-
1131
- """
1132
- Retrieve the current threshold value from the input field.
1133
-
1134
- Converts the value to a float or int based on the `value_type` attribute. If the value
1135
- is invalid and `show_warning` is True, a warning dialog is shown.
1136
-
1137
- Parameters
1138
- ----------
1139
- show_warning : bool, optional
1140
- If True, show a warning dialog if the value is invalid (default is True).
1141
-
1142
- Returns
1143
- -------
1144
- float or int or None
1145
- The threshold value as a float or int, or None if the value is invalid.
1146
- """
1147
-
1148
- try:
1149
- if self.value_type=='float':
1150
- thresh = float(self.text().replace(',','.'))
1151
- else:
1152
- thresh = int(self.text().replace(',','.'))
1153
- except ValueError:
1154
- if show_warning:
1155
- msgBox = QMessageBox()
1156
- msgBox.setWindowTitle('warning')
1157
- msgBox.setIcon(QMessageBox.Critical)
1158
- msgBox.setText("Please set a valid threshold value.")
1159
- msgBox.setWindowTitle("")
1160
- msgBox.setStandardButtons(QMessageBox.Ok)
1161
- returnValue = msgBox.exec()
1162
- thresh = None
1163
-
1164
- return thresh
640
+ """
641
+ A custom QLineEdit widget to manage and validate threshold values.
642
+
643
+ This class extends QLineEdit to input and manage threshold values (either float or int),
644
+ with optional validation and interaction with connected QPushButtons. The widget can
645
+ validate the input and enable/disable buttons based on whether a valid threshold is set.
646
+
647
+ Parameters
648
+ ----------
649
+ init_value : float or int, optional
650
+ The initial threshold value to display in the input field (default is 2.0).
651
+ connected_buttons : QPushButton or list of QPushButton, optional
652
+ QPushButton(s) that should be enabled/disabled based on the validity of the threshold
653
+ value (default is None).
654
+ placeholder : str, optional
655
+ Placeholder text to show when no value is entered in the input field
656
+ (default is 'px > thresh are masked').
657
+ value_type : str, optional
658
+ Specifies the type of threshold value, either 'float' or 'int' (default is 'float').
659
+
660
+ Methods
661
+ -------
662
+ enable_btn():
663
+ Enables or disables connected QPushButtons based on the validity of the threshold value.
664
+ set_threshold(value):
665
+ Sets the input field to the given threshold value.
666
+ get_threshold(show_warning=True):
667
+ Retrieves the current threshold value from the input field, returning it as a float or int.
668
+ If invalid, optionally displays a warning dialog.
669
+
670
+ Example
671
+ -------
672
+ >>> threshold_input = ThresholdLineEdit(init_value=5, value_type='int')
673
+ >>> print(threshold_input.get_threshold())
674
+ 5
675
+ """
676
+
677
+ def __init__(
678
+ self,
679
+ init_value=2.0,
680
+ connected_buttons=None,
681
+ placeholder="px > thresh are masked",
682
+ value_type="float",
683
+ *args,
684
+ ):
685
+ super().__init__(*args)
686
+
687
+ self.init_value = init_value
688
+ self.value_type = value_type
689
+ self.connected_buttons = connected_buttons
690
+ self.setPlaceholderText(placeholder)
691
+
692
+ if self.value_type == "float":
693
+ self.setValidator(QDoubleValidator())
694
+ else:
695
+ self.init_value = int(self.init_value)
696
+ self.setValidator(QIntValidator())
697
+
698
+ if self.connected_buttons is not None:
699
+ self.textChanged.connect(self.enable_btn)
700
+ self.set_threshold(self.init_value)
701
+
702
+ def enable_btn(self):
703
+ """
704
+ Enable or disable connected QPushButtons based on the threshold value.
705
+
706
+ If the current threshold value is valid, the connected buttons will be enabled.
707
+ If the value is invalid or empty, the buttons will be disabled.
708
+ """
709
+
710
+ thresh = self.get_threshold(show_warning=False)
711
+ if isinstance(self.connected_buttons, QPushButton):
712
+ cbs = [self.connected_buttons]
713
+ else:
714
+ cbs = self.connected_buttons
715
+
716
+ if thresh is None:
717
+ for c in cbs:
718
+ c.setEnabled(False)
719
+ else:
720
+ for c in cbs:
721
+ c.setEnabled(True)
722
+
723
+ def set_threshold(self, value):
724
+ """
725
+ Set the input field to the specified threshold value.
726
+
727
+ Parameters
728
+ ----------
729
+ value : float or int
730
+ The value to set in the input field.
731
+ """
732
+
733
+ try:
734
+ self.setText(str(value).replace(".", ","))
735
+ except:
736
+ print("Please provide a valid threshold value...")
737
+
738
+ def get_threshold(self, show_warning=True):
739
+ """
740
+ Retrieve the current threshold value from the input field.
741
+
742
+ Converts the value to a float or int based on the `value_type` attribute. If the value
743
+ is invalid and `show_warning` is True, a warning dialog is shown.
744
+
745
+ Parameters
746
+ ----------
747
+ show_warning : bool, optional
748
+ If True, show a warning dialog if the value is invalid (default is True).
749
+
750
+ Returns
751
+ -------
752
+ float or int or None
753
+ The threshold value as a float or int, or None if the value is invalid.
754
+ """
755
+
756
+ try:
757
+ if self.value_type == "float":
758
+ thresh = float(self.text().replace(",", "."))
759
+ else:
760
+ thresh = int(self.text().replace(",", "."))
761
+ except ValueError:
762
+ if show_warning:
763
+ msgBox = QMessageBox()
764
+ msgBox.setWindowTitle("warning")
765
+ msgBox.setIcon(QMessageBox.Critical)
766
+ msgBox.setText("Please set a valid threshold value.")
767
+ msgBox.setWindowTitle("")
768
+ msgBox.setStandardButtons(QMessageBox.Ok)
769
+ returnValue = msgBox.exec()
770
+ thresh = None
771
+
772
+ return thresh
1165
773
 
1166
774
 
1167
775
  def color_from_status(status, recently_modified=False):
1168
- if not recently_modified:
1169
- if status == 0:
1170
- return 'tab:blue'
1171
- elif status == 1:
1172
- return 'tab:red'
1173
- elif status == 2:
1174
- return 'yellow'
1175
- else:
1176
- return 'k'
1177
- else:
1178
- if status == 0:
1179
- return 'tab:cyan'
1180
- elif status == 1:
1181
- return 'tab:orange'
1182
- elif status == 2:
1183
- return 'tab:olive'
1184
- else:
1185
- return 'k'
776
+ if not recently_modified:
777
+ if status == 0:
778
+ return "tab:blue"
779
+ elif status == 1:
780
+ return "tab:red"
781
+ elif status == 2:
782
+ return "yellow"
783
+ else:
784
+ return "k"
785
+ else:
786
+ if status == 0:
787
+ return "tab:cyan"
788
+ elif status == 1:
789
+ return "tab:orange"
790
+ elif status == 2:
791
+ return "tab:olive"
792
+ else:
793
+ return "k"
1186
794
 
1187
- def color_from_state(state, recently_modified=False):
1188
795
 
1189
- """
1190
- Generate a color map based on unique values in the provided state array.
1191
-
1192
- This function creates a mapping between the unique values found in the `state` array
1193
- and colors from the `tab10` colormap in Matplotlib. A special condition is applied
1194
- to the value `99`, which is assigned the color black ('k').
1195
-
1196
- Parameters
1197
- ----------
1198
- state : array-like
1199
- An array or list of state values to be used for generating the color map.
1200
-
1201
- Returns
1202
- -------
1203
- dict
1204
- A dictionary where the keys are the unique state values from the `state` array,
1205
- and the values are the corresponding colors from Matplotlib's `tab10` colormap.
1206
- The value `99` is mapped to the color black ('k').
1207
-
1208
- Notes
1209
- -----
1210
- - Matplotlib's `tab10` colormap is used for values other than `99`.
1211
- """
1212
-
1213
- unique_values = np.unique(state)
1214
- color_map = {}
1215
- for value in unique_values:
1216
-
1217
- if np.isnan(value):
1218
- value = "nan"
1219
- color_map[value] = 'k'
1220
- elif value==0:
1221
- color_map[value] = 'tab:blue'
1222
- elif value==1:
1223
- color_map[value] = 'tab:red'
1224
- elif value==99:
1225
- color_map[value] = 'k'
1226
- else:
1227
- color_map[value] = plt.cm.tab20(value/20.0)
1228
-
1229
- return color_map
796
+ def color_from_state(state, recently_modified=False):
797
+ """
798
+ Generate a color map based on unique values in the provided state array.
799
+
800
+ This function creates a mapping between the unique values found in the `state` array
801
+ and colors from the `tab10` colormap in Matplotlib. A special condition is applied
802
+ to the value `99`, which is assigned the color black ('k').
803
+
804
+ Parameters
805
+ ----------
806
+ state : array-like
807
+ An array or list of state values to be used for generating the color map.
808
+
809
+ Returns
810
+ -------
811
+ dict
812
+ A dictionary where the keys are the unique state values from the `state` array,
813
+ and the values are the corresponding colors from Matplotlib's `tab10` colormap.
814
+ The value `99` is mapped to the color black ('k').
815
+
816
+ Notes
817
+ -----
818
+ - Matplotlib's `tab10` colormap is used for values other than `99`.
819
+ """
820
+
821
+ unique_values = np.unique(state)
822
+ color_map = {}
823
+ for value in unique_values:
824
+
825
+ if np.isnan(value):
826
+ value = "nan"
827
+ color_map[value] = "k"
828
+ elif value == 0:
829
+ color_map[value] = "tab:blue"
830
+ elif value == 1:
831
+ color_map[value] = "tab:red"
832
+ elif value == 99:
833
+ color_map[value] = "k"
834
+ else:
835
+ import matplotlib.pyplot as plt
836
+
837
+ color_map[value] = plt.cm.tab20(value / 20.0)
838
+
839
+ return color_map
1230
840
 
1231
841
 
1232
842
  def color_from_class(cclass, recently_modified=False):
1233
- if not recently_modified:
1234
- if cclass == 0:
1235
- return 'tab:red'
1236
- elif cclass == 1:
1237
- return 'tab:blue'
1238
- elif cclass == 2:
1239
- return 'yellow'
1240
- else:
1241
- return 'k'
1242
- else:
1243
- if cclass == 0:
1244
- return 'tab:orange'
1245
- elif cclass == 1:
1246
- return 'tab:cyan'
1247
- elif cclass == 2:
1248
- return 'tab:olive'
1249
- else:
1250
- return 'k'
843
+ if not recently_modified:
844
+ if cclass == 0:
845
+ return "tab:red"
846
+ elif cclass == 1:
847
+ return "tab:blue"
848
+ elif cclass == 2:
849
+ return "yellow"
850
+ else:
851
+ return "k"
852
+ else:
853
+ if cclass == 0:
854
+ return "tab:orange"
855
+ elif cclass == 1:
856
+ return "tab:cyan"
857
+ elif cclass == 2:
858
+ return "tab:olive"
859
+ else:
860
+ return "k"
1251
861
 
1252
862
 
1253
863
  class ChannelChoice(CelldetectiveWidget):
1254
864
 
1255
- def __init__(self, parent_window):
1256
- super().__init__()
1257
- self.parent_window = parent_window
1258
- #self.channel_names = channel_names
1259
- self.setWindowTitle("Choose target channel")
1260
- # Create the QComboBox and add some items
1261
- self.combo_box = QComboBox(self)
1262
- center_window(self)
865
+ def __init__(self, parent_window):
866
+ super().__init__()
867
+ self.parent_window = parent_window
868
+ # self.channel_names = channel_names
869
+ self.setWindowTitle("Choose target channel")
870
+ # Create the QComboBox and add some items
871
+ self.combo_box = QComboBox(self)
872
+ center_window(self)
873
+
874
+ channels = parent_window.channel_names
1263
875
 
1264
- channels = parent_window.channel_names
876
+ self.combo_box.addItems(channels)
1265
877
 
1266
- self.combo_box.addItems(channels)
878
+ self.add_btn = QPushButton("Add")
879
+ self.add_btn.clicked.connect(self.add_current_channel)
1267
880
 
1268
- self.add_btn = QPushButton("Add")
1269
- self.add_btn.clicked.connect(self.add_current_channel)
881
+ # Create the layout
882
+ layout = QVBoxLayout(self)
883
+ layout.addWidget(self.combo_box)
884
+ layout.addWidget(self.add_btn)
1270
885
 
1271
- # Create the layout
1272
- layout = QVBoxLayout(self)
1273
- layout.addWidget(self.combo_box)
1274
- layout.addWidget(self.add_btn)
886
+ def add_current_channel(self):
887
+ filtername = self.combo_box.currentText()
888
+ self.parent_window.list_widget.addItems([filtername])
889
+ self.close()
1275
890
 
1276
- def add_current_channel(self):
1277
- filtername = self.combo_box.currentText()
1278
- self.parent_window.list_widget.addItems([filtername])
1279
- self.close()
1280
891
 
1281
892
  def help_generic(tree):
893
+ """
894
+ Interactively traverse a decision tree to provide user guidance based on a nested dictionary structure.
895
+
896
+ This function takes a nested dictionary representing a decision tree and guides the user through
897
+ it step-by-step by displaying messages for user input using the `generic_msg()` function.
898
+ At each step, the user selects a key that corresponds to a further step in the tree, until a
899
+ final suggestion (leaf node) is reached.
900
+
901
+ Parameters
902
+ ----------
903
+ tree : dict
904
+ A dictionary where keys represent options and values represent either further steps (as dictionaries)
905
+ or a final suggestion (leaf nodes).
906
+
907
+ Returns
908
+ -------
909
+ any
910
+ The final suggestion or outcome after traversing the decision tree.
911
+
912
+ Example
913
+ -------
914
+ >>> decision_tree = {
915
+ ... 'Start': {
916
+ ... 'Option 1': {
917
+ ... 'Sub-option 1': 'Final suggestion 1',
918
+ ... 'Sub-option 2': 'Final suggestion 2'
919
+ ... },
920
+ ... 'Option 2': 'Final suggestion 3'
921
+ ... }
922
+ ... }
923
+ >>> result = help_generic(decision_tree)
924
+ # The function prompts the user to choose between "Option 1" or "Option 2",
925
+ # and then proceeds through the tree based on the user's choices.
926
+ """
927
+
928
+ output = generic_msg(list(tree.keys())[0])
929
+ while output is not None:
930
+ tree = tree[list(tree.keys())[0]][output]
931
+ if isinstance(tree, dict):
932
+ output = generic_msg(list(tree.keys())[0])
933
+ else:
934
+ # return the final suggestion
935
+ output = None
936
+ return tree
1282
937
 
1283
- """
1284
- Interactively traverse a decision tree to provide user guidance based on a nested dictionary structure.
1285
-
1286
- This function takes a nested dictionary representing a decision tree and guides the user through
1287
- it step-by-step by displaying messages for user input using the `generic_msg()` function.
1288
- At each step, the user selects a key that corresponds to a further step in the tree, until a
1289
- final suggestion (leaf node) is reached.
1290
-
1291
- Parameters
1292
- ----------
1293
- tree : dict
1294
- A dictionary where keys represent options and values represent either further steps (as dictionaries)
1295
- or a final suggestion (leaf nodes).
1296
-
1297
- Returns
1298
- -------
1299
- any
1300
- The final suggestion or outcome after traversing the decision tree.
1301
-
1302
- Example
1303
- -------
1304
- >>> decision_tree = {
1305
- ... 'Start': {
1306
- ... 'Option 1': {
1307
- ... 'Sub-option 1': 'Final suggestion 1',
1308
- ... 'Sub-option 2': 'Final suggestion 2'
1309
- ... },
1310
- ... 'Option 2': 'Final suggestion 3'
1311
- ... }
1312
- ... }
1313
- >>> result = help_generic(decision_tree)
1314
- # The function prompts the user to choose between "Option 1" or "Option 2",
1315
- # and then proceeds through the tree based on the user's choices.
1316
- """
1317
-
1318
- output = generic_msg(list(tree.keys())[0])
1319
- while output is not None:
1320
- tree = tree[list(tree.keys())[0]][output]
1321
- if isinstance(tree,dict):
1322
- output = generic_msg(list(tree.keys())[0])
1323
- else:
1324
- # return the final suggestion
1325
- output = None
1326
- return tree
1327
938
 
1328
939
  def generic_msg(text):
1329
-
1330
- """
1331
- Display a message box with a question and capture the user's response.
1332
-
1333
- This function creates a message box with a `Yes`, `No`, and `Cancel` option,
1334
- displaying the provided `text` as the question. It returns the user's selection as a string.
1335
-
1336
- Parameters
1337
- ----------
1338
- text : str
1339
- The message or question to display in the message box.
1340
-
1341
- Returns
1342
- -------
1343
- str or None
1344
- The user's response: "yes" if Yes is selected, "no" if No is selected,
1345
- and `None` if Cancel is selected or the dialog is closed.
1346
-
1347
- Example
1348
- -------
1349
- >>> response = generic_msg("Would you like to continue?")
1350
- >>> if response == "yes":
1351
- ... print("User chose Yes")
1352
- ... elif response == "no":
1353
- ... print("User chose No")
1354
- ... else:
1355
- ... print("User cancelled the action")
1356
-
1357
- Notes
1358
- -----
1359
- - The message box displays a window with three options: Yes, No, and Cancel.
1360
- """
1361
-
1362
- msgBox = QMessageBox()
1363
- msgBox.setIcon(QMessageBox.Question)
1364
- msgBox.setText(text)
1365
- msgBox.setWindowTitle("Question")
1366
- msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
1367
- returnValue = msgBox.exec()
1368
- if returnValue == QMessageBox.Yes:
1369
- return "yes"
1370
- elif returnValue == QMessageBox.No:
1371
- return "no"
1372
- else:
1373
- return None
940
+ """
941
+ Display a message box with a question and capture the user's response.
942
+
943
+ This function creates a message box with a `Yes`, `No`, and `Cancel` option,
944
+ displaying the provided `text` as the question. It returns the user's selection as a string.
945
+
946
+ Parameters
947
+ ----------
948
+ text : str
949
+ The message or question to display in the message box.
950
+
951
+ Returns
952
+ -------
953
+ str or None
954
+ The user's response: "yes" if Yes is selected, "no" if No is selected,
955
+ and `None` if Cancel is selected or the dialog is closed.
956
+
957
+ Example
958
+ -------
959
+ >>> response = generic_msg("Would you like to continue?")
960
+ >>> if response == "yes":
961
+ ... print("User chose Yes")
962
+ ... elif response == "no":
963
+ ... print("User chose No")
964
+ ... else:
965
+ ... print("User cancelled the action")
966
+
967
+ Notes
968
+ -----
969
+ - The message box displays a window with three options: Yes, No, and Cancel.
970
+ """
971
+
972
+ msgBox = QMessageBox()
973
+ msgBox.setIcon(QMessageBox.Question)
974
+ msgBox.setText(text)
975
+ msgBox.setWindowTitle("Question")
976
+ msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No | QMessageBox.Cancel)
977
+ returnValue = msgBox.exec()
978
+ if returnValue == QMessageBox.Yes:
979
+ return "yes"
980
+ elif returnValue == QMessageBox.No:
981
+ return "no"
982
+ else:
983
+ return None