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
@@ -1,140 +1,417 @@
1
1
  from multiprocessing import Queue
2
2
  from PyQt5.QtWidgets import QPushButton, QVBoxLayout, QHBoxLayout, QLabel, QProgressBar
3
3
  from PyQt5.QtCore import QRunnable, QObject, pyqtSignal, QThreadPool, QSize, Qt
4
-
5
- from celldetective.gui.base_components import CelldetectiveDialog
6
- from celldetective.gui.gui_utils import center_window
4
+ from PyQt5.QtGui import QPixmap, QImage
7
5
  import math
6
+ import numpy as np
7
+
8
+ from celldetective.gui.base.components import CelldetectiveDialog
9
+ from celldetective.gui.base.utils import center_window
10
+ from celldetective.log_manager import get_logger
11
+
12
+ logger = get_logger(__name__)
13
+
8
14
 
9
15
  class ProgressWindow(CelldetectiveDialog):
10
16
 
11
- def __init__(self, process=None, parent_window=None, title="", position_info=True, process_args=None):
12
-
13
- super().__init__()
14
- #QDialog.__init__(self)
15
-
16
- self.setWindowTitle(f'{title} Progress')
17
- self.__process = process
18
- self.parent_window = parent_window
19
-
20
- self.position_info = position_info
21
- if self.position_info:
22
- self.pos_name = self.parent_window.pos_name
23
-
24
- #self.__btn_run = QPushButton("Start")
25
- self.__btn_stp = QPushButton("Cancel")
26
- if self.position_info:
27
- self.position_label = QLabel(f'Processing position {self.pos_name}...')
28
- self.__label = QLabel("Idle")
29
- self.time_left_lbl = QLabel('')
30
-
31
- self.progress_bar = QProgressBar()
32
- self.progress_bar.setValue(0)
33
-
34
- self.__runner = Runner(process=self.__process, progress_bar=self.progress_bar, process_args=process_args, remaining_time_label=self.time_left_lbl)
35
- self.pool = QThreadPool.globalInstance()
36
- #print("Multithreading with maximum %d threads" % self.pool.maxThreadCount())
37
-
38
- #self.__btn_run.clicked.connect(self.__run_net)
39
- self.__btn_stp.clicked.connect(self.__stp_net)
40
- self.__runner.signals.finished.connect(self.__on_finished)
41
- self.__runner.signals.finished.connect(self.__on_error)
42
- self.__runner.signals.update.connect(self.progress_bar.setValue)
43
- self.__runner.signals.update_time.connect(self.time_left_lbl.setText)
44
-
45
- self.__btn_stp.setDisabled(True)
46
-
47
- self.layout = QVBoxLayout()
48
- if self.position_info:
49
- self.layout.addWidget(self.position_label)
50
- self.layout.addWidget(self.time_left_lbl)
51
- self.layout.addWidget(self.progress_bar)
52
- self.btn_layout = QHBoxLayout()
53
- self.btn_layout.addWidget(self.__btn_stp)
54
- self.btn_layout.addWidget(self.__label)
55
- self.layout.addLayout(self.btn_layout)
56
-
57
- self.setLayout(self.layout)
58
- self.setFixedSize(QSize(250, 130))
59
- self.__run_net()
60
- self.setModal(True)
61
- center_window(self)
62
-
63
- def closeEvent(self, evnt):
64
- # if self._want_to_close:
65
- # super(MyDialog, self).closeEvent(evnt)
66
- # else:
67
- evnt.ignore()
68
- self.setWindowState(Qt.WindowMinimized)
69
-
70
- def __run_net(self):
71
- #self.__btn_run.setDisabled(True)
72
- self.__btn_stp.setEnabled(True)
73
- self.__label.setText("Running...")
74
- self.pool.start(self.__runner)
75
-
76
- def __stp_net(self):
77
- self.__runner.close()
78
- print('\n Job cancelled... Abort.')
79
- self.reject()
80
-
81
- def __on_finished(self):
82
- self.__btn_stp.setDisabled(True)
83
- self.__label.setText("\nFinished!")
84
- self.__runner.close()
85
- self.accept()
86
-
87
- def __on_error(self):
88
- self.__btn_stp.setDisabled(True)
89
- self.__label.setText("\nError")
90
- self.__runner.close()
91
- self.accept()
17
+ def __init__(
18
+ self,
19
+ process=None,
20
+ parent_window=None,
21
+ title="",
22
+ position_info=True,
23
+ process_args=None,
24
+ ):
25
+
26
+ super().__init__()
27
+ # QDialog.__init__(self)
28
+
29
+ self.setWindowTitle(f"{title}")
30
+ self.__process = process
31
+ self.parent_window = parent_window
32
+
33
+ self.position_info = position_info
34
+ if self.position_info:
35
+ self.pos_name = getattr(self.parent_window, "pos_name", "Batch")
36
+
37
+ # self.__btn_run = QPushButton("Start")
38
+ self.__btn_stp = QPushButton("Cancel")
39
+ if self.position_info:
40
+ self.position_label = QLabel(f"Processing position {self.pos_name}...")
41
+ self.__label = QLabel("Idle")
42
+ self.time_left_lbl = QLabel("")
43
+
44
+ self.well_time_lbl = QLabel("Well progress:")
45
+ self.well_progress_bar = QProgressBar()
46
+ self.well_progress_bar.setValue(0)
47
+ self.well_progress_bar.setFormat("Total (Wells): %p%")
48
+
49
+ self.pos_time_lbl = QLabel("Position progress:")
50
+ self.pos_progress_bar = QProgressBar()
51
+ self.pos_progress_bar.setValue(0)
52
+ self.pos_progress_bar.setFormat("Current Well (Positions): %p%")
53
+
54
+ self.frame_time_lbl = QLabel("Frame progress:")
55
+ self.frame_progress_bar = QProgressBar()
56
+ self.frame_progress_bar.setValue(0)
57
+ self.frame_progress_bar.setFormat("Current Position (Frames): %p%")
58
+
59
+ self.__runner = Runner(
60
+ process=self.__process,
61
+ process_args=process_args,
62
+ )
63
+ logger.info("Runner initialized...")
64
+ self.pool = QThreadPool.globalInstance()
65
+
66
+ self.__btn_stp.clicked.connect(self.__stp_net)
67
+ self.__runner.signals.finished.connect(self.__on_finished)
68
+ self.__runner.signals.error.connect(self.__on_error)
69
+
70
+ self.__runner.signals.update_well.connect(self.well_progress_bar.setValue)
71
+ self.__runner.signals.update_well_time.connect(self.well_time_lbl.setText)
72
+
73
+ self.__runner.signals.update_pos.connect(self.pos_progress_bar.setValue)
74
+ self.__runner.signals.update_pos_time.connect(self.pos_time_lbl.setText)
75
+
76
+ self.__runner.signals.update_frame.connect(self.frame_progress_bar.setValue)
77
+ self.__runner.signals.update_frame_time.connect(self.frame_time_lbl.setText)
78
+ self.__runner.signals.update_status.connect(self.__label.setText)
79
+ self.__runner.signals.update_image.connect(self.update_image)
80
+
81
+ self.image_label = QLabel()
82
+ self.image_label.setFixedSize(250, 250)
83
+ self.image_label.setAlignment(Qt.AlignCenter)
84
+ # self.image_label.setScaledContents(True)
85
+ self.image_label.hide()
86
+
87
+ self.__btn_stp.setDisabled(True)
88
+
89
+ self.progress_layout = QVBoxLayout()
90
+ if self.position_info:
91
+ self.progress_layout.addWidget(self.position_label)
92
+
93
+ self.progress_layout.addWidget(self.well_time_lbl)
94
+ self.progress_layout.addWidget(self.well_progress_bar)
95
+
96
+ self.progress_layout.addWidget(self.pos_time_lbl)
97
+ self.progress_layout.addWidget(self.pos_progress_bar)
98
+
99
+ self.progress_layout.addWidget(self.frame_time_lbl)
100
+ self.progress_layout.addWidget(self.frame_progress_bar)
101
+
102
+ self.btn_layout = QHBoxLayout()
103
+ self.btn_layout.addWidget(self.__btn_stp)
104
+ self.btn_layout.addWidget(self.__label)
105
+
106
+ # Left Column Layout (Bars + Buttons)
107
+ self.left_layout = QVBoxLayout()
108
+ self.left_layout.addLayout(self.progress_layout)
109
+ self.left_layout.addLayout(self.btn_layout)
110
+
111
+ # Main Root Layout (Left Column + Image)
112
+ self.root_layout = QHBoxLayout()
113
+ self.root_layout.addLayout(self.left_layout)
114
+ self.root_layout.addWidget(self.image_label)
115
+
116
+ self.setLayout(self.root_layout)
117
+ self.setFixedSize(QSize(400, 220))
118
+ self.show()
119
+ self.raise_()
120
+ self.activateWindow()
121
+ logger.info("ProgressWindow initialized and shown.")
122
+ self.__run_net()
123
+ self.setModal(True)
124
+ # center_window(self)
125
+
126
+ def closeEvent(self, evnt):
127
+ evnt.ignore()
128
+ self.setWindowState(Qt.WindowMinimized)
129
+
130
+ def __run_net(self):
131
+ # self.__btn_run.setDisabled(True)
132
+ self.__btn_stp.setEnabled(True)
133
+ self.__label.setText("Running...")
134
+ self.pool.start(self.__runner)
135
+
136
+ def __stp_net(self):
137
+ self.__runner.close()
138
+ logger.info("\n Job cancelled... Abort.")
139
+ self.reject()
140
+
141
+ def __on_finished(self):
142
+ self.__btn_stp.setDisabled(True)
143
+ self.__label.setText("\nFinished!")
144
+ self.__runner.close()
145
+ self.accept()
146
+
147
+ def __on_error(self, message="Error"):
148
+ self.__btn_stp.setDisabled(True)
149
+ self.__label.setText("\nError")
150
+ self.__runner.close()
151
+
152
+ # Show error in a message box to ensure it's seen
153
+ from PyQt5.QtWidgets import QMessageBox
154
+
155
+ msg = QMessageBox()
156
+ msg.setIcon(QMessageBox.Critical)
157
+ msg.setText("Process failed")
158
+ msg.setInformativeText(str(message))
159
+ msg.setWindowTitle("Error")
160
+ msg.exec_()
161
+
162
+ self.reject()
163
+
164
+ def update_image(self, img_data):
165
+ try:
166
+ if img_data is None:
167
+ return
168
+
169
+ if self.image_label.isHidden():
170
+ self.image_label.show()
171
+ # Expand window width and height to accommodate image
172
+ self.setFixedSize(QSize(750, 320))
173
+ # Normalize for display
174
+ img = img_data.astype(float)
175
+ img = np.nan_to_num(img)
176
+
177
+ min_val = np.min(img)
178
+ max_val = np.max(img)
179
+
180
+ if max_val > min_val:
181
+ img = (img - min_val) / (max_val - min_val) * 255
182
+ else:
183
+ img = np.zeros_like(img)
184
+
185
+ img = img.astype(np.uint8)
186
+ img = np.require(
187
+ img, np.uint8, "C"
188
+ ) # Maintain strict C-contiguity for Qt stability
189
+
190
+ height, width = img.shape[:2]
191
+
192
+ # Grayscale or RGB
193
+ if img.ndim == 3:
194
+ # RGB
195
+ bytes_per_line = 3 * width
196
+ q_img = QImage(
197
+ img.data, width, height, bytes_per_line, QImage.Format_RGB888
198
+ )
199
+ else:
200
+ # Grayscale
201
+ bytes_per_line = width
202
+ q_img = QImage(
203
+ img.data, width, height, bytes_per_line, QImage.Format_Grayscale8
204
+ )
205
+
206
+ # Use .copy() ensures deep copy of data into QPixmap so we don't depend on volatile memory
207
+ pixmap = QPixmap.fromImage(q_img.copy())
208
+ # Scale with Aspect Ratio preserved to avoid cutting or distortion
209
+ scaled_pixmap = pixmap.scaled(
210
+ self.image_label.size()
211
+ - QSize(20, 20), # Add 10px padding on all sides
212
+ Qt.KeepAspectRatio,
213
+ Qt.SmoothTransformation,
214
+ )
215
+ self.image_label.setPixmap(scaled_pixmap)
216
+ except Exception as e:
217
+ logger.error(f"Image update failed: {e}")
218
+
92
219
 
93
220
  class Runner(QRunnable):
94
221
 
95
- def __init__(self, process=None, progress_bar=None, remaining_time_label=None, process_args=None):
96
- QRunnable.__init__(self)
97
-
98
- print(f"{process_args=}")
99
- self.progress_bar = progress_bar
100
- #self.parent_window = parent_window
101
- self.remaining_time_label = remaining_time_label
102
- self.__queue = Queue()
103
- self.__process = process(self.__queue, process_args=process_args)
104
- self.signals = RunnerSignal()
105
-
106
- def run(self):
107
- self.__process.start()
108
- while True:
109
- try:
110
- data = self.__queue.get()
111
- if len(data)==2:
112
- progress, time = data
113
- time = int(time)
114
- remaining_minutes = time // 60
115
- remaining_seconds = int(time % 60)
116
- time_left = "About "+str(remaining_minutes)+" min and "+str(remaining_seconds)+" s remaining"
117
- if remaining_minutes == 0:
118
- time_left = "About "+str(remaining_seconds)+" s remaining"
119
- self.signals.update_time.emit(time_left)
120
- if isinstance(progress, (float,int)):
121
- self.signals.update.emit(math.ceil(progress))
122
- if data == "finished":
123
- self.signals.finished.emit()
124
- break
125
- elif data == "error":
126
- self.signals.error.emit()
127
- except Exception as e:
128
- print(e)
129
- pass
130
-
131
- def close(self):
132
- self.__process.end_process()
222
+ def __init__(
223
+ self,
224
+ process=None,
225
+ process_args=None,
226
+ ):
227
+ QRunnable.__init__(self)
228
+
229
+ logger.info(f"{process_args=}")
230
+ self.__queue = Queue()
231
+ self.__process = process(self.__queue, process_args=process_args)
232
+ self.signals = RunnerSignal()
233
+
234
+ def run(self):
235
+ logger.info("Starting Process (runner-side)...")
236
+ self.__process.start()
237
+ while True:
238
+ try:
239
+ data = self.__queue.get()
240
+
241
+ # Handle dictionary for triple progress
242
+ if isinstance(data, dict):
243
+ if "well_progress" in data:
244
+ self.signals.update_well.emit(int(data["well_progress"]))
245
+ if "well_time" in data:
246
+ self.signals.update_well_time.emit(data["well_time"])
247
+
248
+ if "pos_progress" in data:
249
+ self.signals.update_pos.emit(int(data["pos_progress"]))
250
+ if "pos_time" in data:
251
+ self.signals.update_pos_time.emit(data["pos_time"])
252
+
253
+ if "frame_progress" in data:
254
+ self.signals.update_frame.emit(int(data["frame_progress"]))
255
+ if "frame_time" in data:
256
+ self.signals.update_frame_time.emit(data["frame_time"])
257
+
258
+ if "image_preview" in data:
259
+ self.signals.update_image.emit(data["image_preview"])
260
+ elif "bg_image" in data: # Backward compatibility
261
+ self.signals.update_image.emit(data["bg_image"])
262
+
263
+ if "plot_data" in data:
264
+ self.signals.update_plot.emit(data["plot_data"])
265
+
266
+ if "training_result" in data:
267
+ self.signals.training_result.emit(data["training_result"])
268
+
269
+ if "status" in data: # Moved this block out of frame_time check
270
+ logger.info(
271
+ f"Runner received status: {data['status']}"
272
+ ) # New log as per instruction
273
+ if data["status"] == "finished":
274
+ self.signals.finished.emit()
275
+ break
276
+ elif data["status"] == "error":
277
+ msg = data.get("message", "Unknown error")
278
+ logger.error(f"Runner received error: {msg}")
279
+ self.signals.error.emit(str(msg))
280
+ else:
281
+ self.signals.update_status.emit(data["status"])
282
+
283
+ # Simple fallback for legacy list [progress, time] -> map to POS progress
284
+ elif isinstance(data, list) and len(data) == 2:
285
+ progress, time = data
286
+ self.signals.update_pos.emit(math.ceil(progress))
287
+
288
+ elif data == "finished":
289
+ self.signals.finished.emit()
290
+ break
291
+ elif data == "error":
292
+ self.signals.error.emit("Unknown error")
293
+
294
+ except Exception as e:
295
+ logger.error(e)
296
+ pass
297
+
298
+ def close(self):
299
+ self.__process.end_process()
133
300
 
134
301
 
135
302
  class RunnerSignal(QObject):
136
303
 
137
- update = pyqtSignal(int)
138
- update_time = pyqtSignal(str)
139
- finished = pyqtSignal()
140
- error = pyqtSignal()
304
+ update_well = pyqtSignal(int)
305
+ update_well_time = pyqtSignal(str)
306
+
307
+ update_pos = pyqtSignal(int)
308
+ update_pos_time = pyqtSignal(str)
309
+
310
+ update_frame = pyqtSignal(int)
311
+ update_frame_time = pyqtSignal(str)
312
+ update_image = pyqtSignal(object)
313
+ update_plot = pyqtSignal(dict)
314
+ training_result = pyqtSignal(dict)
315
+ update_status = pyqtSignal(str)
316
+
317
+ finished = pyqtSignal()
318
+ error = pyqtSignal(str)
319
+
320
+
321
+ class GenericProgressWindow(CelldetectiveDialog):
322
+
323
+ def __init__(
324
+ self,
325
+ process=None,
326
+ parent_window=None,
327
+ title="",
328
+ process_args=None,
329
+ label_text="Progress:",
330
+ ):
331
+
332
+ super().__init__()
333
+
334
+ self.setWindowTitle(f"{title}")
335
+ self.__process = process
336
+ self.parent_window = parent_window
337
+
338
+ self.__btn_stp = QPushButton("Cancel")
339
+ self.__label = QLabel("Idle")
340
+ self.progress_label = QLabel(label_text)
341
+
342
+ self.progress_bar = QProgressBar()
343
+ self.progress_bar.setValue(0)
344
+ self.progress_bar.setFormat("%p%")
345
+
346
+ self.__runner = Runner(
347
+ process=self.__process,
348
+ process_args=process_args,
349
+ )
350
+ logger.info("Runner initialized...")
351
+ self.pool = QThreadPool.globalInstance()
352
+
353
+ self.__btn_stp.clicked.connect(self.__stp_net)
354
+ self.__runner.signals.finished.connect(self.__on_finished)
355
+ self.__runner.signals.error.connect(self.__on_error)
356
+ self.__runner.signals.update_status.connect(self.__label.setText)
357
+
358
+ # Connect update_pos for generic progress (Runner maps generic list progress to update_pos)
359
+ self.__runner.signals.update_pos.connect(self.progress_bar.setValue)
360
+
361
+ self.__btn_stp.setDisabled(True)
362
+
363
+ self.layout = QVBoxLayout()
364
+ self.layout.addWidget(self.progress_label)
365
+ self.layout.addWidget(self.progress_bar)
366
+
367
+ self.btn_layout = QHBoxLayout()
368
+ self.btn_layout.addWidget(self.__btn_stp)
369
+ self.btn_layout.addWidget(self.__label)
370
+
371
+ self.layout.addLayout(self.btn_layout)
372
+
373
+ self.setLayout(self.layout)
374
+ self.setFixedSize(QSize(400, 150))
375
+ self.show()
376
+ self.raise_()
377
+ self.activateWindow()
378
+ logger.info("GenericProgressWindow initialized and shown.")
379
+ self.__run_net()
380
+ self.setModal(True)
381
+
382
+ def closeEvent(self, evnt):
383
+ evnt.ignore()
384
+ self.setWindowState(Qt.WindowMinimized)
385
+
386
+ def __run_net(self):
387
+ self.__btn_stp.setEnabled(True)
388
+ self.__label.setText("Running...")
389
+ self.pool.start(self.__runner)
390
+
391
+ def __stp_net(self):
392
+ self.__runner.close()
393
+ logger.info("\n Job cancelled... Abort.")
394
+ self.reject()
395
+
396
+ def __on_finished(self):
397
+ self.__btn_stp.setDisabled(True)
398
+ self.__label.setText("\nFinished!")
399
+ self.__runner.close()
400
+ self.accept()
401
+
402
+ def __on_error(self, message="Error"):
403
+ self.__btn_stp.setDisabled(True)
404
+ self.__label.setText("\nError")
405
+ self.__runner.close()
406
+
407
+ # Show error in a message box to ensure it's seen
408
+ from PyQt5.QtWidgets import QMessageBox
409
+
410
+ msg = QMessageBox()
411
+ msg.setIcon(QMessageBox.Critical)
412
+ msg.setText("Process failed")
413
+ msg.setInformativeText(str(message))
414
+ msg.setWindowTitle("Error")
415
+ msg.exec_()
416
+
417
+ self.reject()
@@ -0,0 +1,92 @@
1
+ import logging
2
+ import os
3
+ import sys
4
+
5
+ # from contextlib import contextmanager
6
+
7
+ # Default formatters
8
+ CONSOLE_FORMAT = "[%(levelname)s] %(message)s"
9
+ FILE_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
10
+
11
+
12
+ def setup_global_logging(level=logging.INFO, log_file=None):
13
+ """
14
+ Sets up the global logger for the application.
15
+ """
16
+ root_logger = logging.getLogger("celldetective")
17
+ root_logger.setLevel(level)
18
+ root_logger.propagate = False # Prevent double logging if attached to root
19
+
20
+ # Clear existing handlers to avoid duplicates on reload
21
+ if root_logger.handlers:
22
+ root_logger.handlers.clear()
23
+
24
+ # Console Handler
25
+ console_handler = logging.StreamHandler(sys.__stdout__)
26
+ console_handler.setFormatter(logging.Formatter(CONSOLE_FORMAT))
27
+ root_logger.addHandler(console_handler)
28
+
29
+ # Optional Global File Handler
30
+ if log_file:
31
+ os.makedirs(os.path.dirname(log_file), exist_ok=True)
32
+ file_handler = logging.FileHandler(log_file)
33
+ file_handler.setFormatter(logging.Formatter(FILE_FORMAT))
34
+ root_logger.addHandler(file_handler)
35
+
36
+ for lib in ["trackpy", "btrack", "cellpose", "stardist"]:
37
+ lib_logger = logging.getLogger(lib)
38
+ lib_logger.setLevel(logging.INFO)
39
+ if file_handler not in lib_logger.handlers:
40
+ lib_logger.addHandler(file_handler)
41
+
42
+ # Hook to capture uncaught exceptions
43
+ def handle_exception(exc_type, exc_value, exc_traceback):
44
+ if issubclass(exc_type, KeyboardInterrupt):
45
+ sys.__excepthook__(exc_type, exc_value, exc_traceback)
46
+ return
47
+ root_logger.error(
48
+ "Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
49
+ )
50
+
51
+ sys.excepthook = handle_exception
52
+
53
+ return root_logger
54
+
55
+
56
+ def get_logger(name="celldetective"):
57
+ """
58
+ Returns a logger with the specified name, defaulting to the package logger.
59
+ """
60
+ return logging.getLogger(name)
61
+
62
+
63
+ # @contextmanager
64
+ # def positionlogger(position_path, logger_name="celldetective"):
65
+ # """
66
+ # context manager to dynamically route logs to a file within a specific position folder.
67
+ #
68
+ # args:
69
+ # position_path (str): path to the position folder.
70
+ # logger_name (str): name of the logger to attach the handler to.
71
+ # """
72
+ # logger = logging.getlogger(logger_name)
73
+ #
74
+ # # ensure logs/ directory exists in the position folder
75
+ # log_dir = os.path.join(position_path, "logs")
76
+ # os.makedirs(log_dir, exist_ok=true)
77
+ #
78
+ # log_file = os.path.join(log_dir, "process.log")
79
+ #
80
+ # # create file handler
81
+ # file_handler = logging.filehandler(log_file)
82
+ # file_handler.setformatter(logging.formatter(file_format))
83
+ #
84
+ # # add handler
85
+ # logger.addhandler(file_handler)
86
+ #
87
+ # try:
88
+ # yield logger
89
+ # finally:
90
+ # # remove handler to stop logging to this file
91
+ # file_handler.close()
92
+ # logger.removehandler(file_handler)