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,140 +1,318 @@
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)
@@ -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)