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.
- celldetective/__init__.py +25 -0
- celldetective/__main__.py +62 -43
- celldetective/_version.py +1 -1
- celldetective/extra_properties.py +477 -399
- celldetective/filters.py +192 -97
- celldetective/gui/InitWindow.py +541 -411
- celldetective/gui/__init__.py +0 -15
- celldetective/gui/about.py +44 -39
- celldetective/gui/analyze_block.py +120 -84
- celldetective/gui/base/__init__.py +0 -0
- celldetective/gui/base/channel_norm_generator.py +335 -0
- celldetective/gui/base/components.py +249 -0
- celldetective/gui/base/feature_choice.py +92 -0
- celldetective/gui/base/figure_canvas.py +52 -0
- celldetective/gui/base/list_widget.py +133 -0
- celldetective/gui/{styles.py → base/styles.py} +92 -36
- celldetective/gui/base/utils.py +33 -0
- celldetective/gui/base_annotator.py +900 -767
- celldetective/gui/classifier_widget.py +6 -22
- celldetective/gui/configure_new_exp.py +777 -671
- celldetective/gui/control_panel.py +635 -524
- celldetective/gui/dynamic_progress.py +449 -0
- celldetective/gui/event_annotator.py +2023 -1662
- celldetective/gui/generic_signal_plot.py +1292 -944
- celldetective/gui/gui_utils.py +899 -1289
- celldetective/gui/interactions_block.py +658 -0
- celldetective/gui/interactive_timeseries_viewer.py +447 -0
- celldetective/gui/json_readers.py +48 -15
- celldetective/gui/layouts/__init__.py +5 -0
- celldetective/gui/layouts/background_model_free_layout.py +537 -0
- celldetective/gui/layouts/channel_offset_layout.py +134 -0
- celldetective/gui/layouts/local_correction_layout.py +91 -0
- celldetective/gui/layouts/model_fit_layout.py +372 -0
- celldetective/gui/layouts/operation_layout.py +68 -0
- celldetective/gui/layouts/protocol_designer_layout.py +96 -0
- celldetective/gui/pair_event_annotator.py +3130 -2435
- celldetective/gui/plot_measurements.py +586 -267
- celldetective/gui/plot_signals_ui.py +724 -506
- celldetective/gui/preprocessing_block.py +395 -0
- celldetective/gui/process_block.py +1678 -1831
- celldetective/gui/seg_model_loader.py +580 -473
- celldetective/gui/settings/__init__.py +0 -7
- celldetective/gui/settings/_cellpose_model_params.py +181 -0
- celldetective/gui/settings/_event_detection_model_params.py +95 -0
- celldetective/gui/settings/_segmentation_model_params.py +159 -0
- celldetective/gui/settings/_settings_base.py +77 -65
- celldetective/gui/settings/_settings_event_model_training.py +752 -526
- celldetective/gui/settings/_settings_measurements.py +1133 -964
- celldetective/gui/settings/_settings_neighborhood.py +574 -488
- celldetective/gui/settings/_settings_segmentation_model_training.py +779 -564
- celldetective/gui/settings/_settings_signal_annotator.py +329 -305
- celldetective/gui/settings/_settings_tracking.py +1304 -1094
- celldetective/gui/settings/_stardist_model_params.py +98 -0
- celldetective/gui/survival_ui.py +422 -312
- celldetective/gui/tableUI.py +1665 -1701
- celldetective/gui/table_ops/_maths.py +295 -0
- celldetective/gui/table_ops/_merge_groups.py +140 -0
- celldetective/gui/table_ops/_merge_one_hot.py +95 -0
- celldetective/gui/table_ops/_query_table.py +43 -0
- celldetective/gui/table_ops/_rename_col.py +44 -0
- celldetective/gui/thresholds_gui.py +382 -179
- celldetective/gui/viewers/__init__.py +0 -0
- celldetective/gui/viewers/base_viewer.py +700 -0
- celldetective/gui/viewers/channel_offset_viewer.py +331 -0
- celldetective/gui/viewers/contour_viewer.py +394 -0
- celldetective/gui/viewers/size_viewer.py +153 -0
- celldetective/gui/viewers/spot_detection_viewer.py +341 -0
- celldetective/gui/viewers/threshold_viewer.py +309 -0
- celldetective/gui/workers.py +304 -126
- celldetective/log_manager.py +92 -0
- celldetective/measure.py +1895 -1478
- celldetective/napari/__init__.py +0 -0
- celldetective/napari/utils.py +1025 -0
- celldetective/neighborhood.py +1914 -1448
- celldetective/preprocessing.py +1620 -1220
- celldetective/processes/__init__.py +0 -0
- celldetective/processes/background_correction.py +271 -0
- celldetective/processes/compute_neighborhood.py +894 -0
- celldetective/processes/detect_events.py +246 -0
- celldetective/processes/measure_cells.py +565 -0
- celldetective/processes/segment_cells.py +760 -0
- celldetective/processes/track_cells.py +435 -0
- celldetective/processes/train_segmentation_model.py +694 -0
- celldetective/processes/train_signal_model.py +265 -0
- celldetective/processes/unified_process.py +292 -0
- celldetective/regionprops/_regionprops.py +358 -317
- celldetective/relative_measurements.py +987 -710
- celldetective/scripts/measure_cells.py +313 -212
- celldetective/scripts/measure_relative.py +90 -46
- celldetective/scripts/segment_cells.py +165 -104
- celldetective/scripts/segment_cells_thresholds.py +96 -68
- celldetective/scripts/track_cells.py +198 -149
- celldetective/scripts/train_segmentation_model.py +324 -201
- celldetective/scripts/train_signal_model.py +87 -45
- celldetective/segmentation.py +844 -749
- celldetective/signals.py +3514 -2861
- celldetective/tracking.py +30 -15
- celldetective/utils/__init__.py +0 -0
- celldetective/utils/cellpose_utils/__init__.py +133 -0
- celldetective/utils/color_mappings.py +42 -0
- celldetective/utils/data_cleaning.py +630 -0
- celldetective/utils/data_loaders.py +450 -0
- celldetective/utils/dataset_helpers.py +207 -0
- celldetective/utils/downloaders.py +197 -0
- celldetective/utils/event_detection/__init__.py +8 -0
- celldetective/utils/experiment.py +1782 -0
- celldetective/utils/image_augmenters.py +308 -0
- celldetective/utils/image_cleaning.py +74 -0
- celldetective/utils/image_loaders.py +926 -0
- celldetective/utils/image_transforms.py +335 -0
- celldetective/utils/io.py +62 -0
- celldetective/utils/mask_cleaning.py +348 -0
- celldetective/utils/mask_transforms.py +5 -0
- celldetective/utils/masks.py +184 -0
- celldetective/utils/maths.py +351 -0
- celldetective/utils/model_getters.py +325 -0
- celldetective/utils/model_loaders.py +296 -0
- celldetective/utils/normalization.py +380 -0
- celldetective/utils/parsing.py +465 -0
- celldetective/utils/plots/__init__.py +0 -0
- celldetective/utils/plots/regression.py +53 -0
- celldetective/utils/resources.py +34 -0
- celldetective/utils/stardist_utils/__init__.py +104 -0
- celldetective/utils/stats.py +90 -0
- celldetective/utils/types.py +21 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/METADATA +1 -1
- celldetective-1.5.0b0.dist-info/RECORD +187 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/WHEEL +1 -1
- tests/gui/test_new_project.py +129 -117
- tests/gui/test_project.py +127 -79
- tests/test_filters.py +39 -15
- tests/test_notebooks.py +8 -0
- tests/test_tracking.py +232 -13
- tests/test_utils.py +123 -77
- celldetective/gui/base_components.py +0 -23
- celldetective/gui/layouts.py +0 -1602
- celldetective/gui/processes/compute_neighborhood.py +0 -594
- celldetective/gui/processes/measure_cells.py +0 -360
- celldetective/gui/processes/segment_cells.py +0 -499
- celldetective/gui/processes/track_cells.py +0 -303
- celldetective/gui/processes/train_segmentation_model.py +0 -270
- celldetective/gui/processes/train_signal_model.py +0 -108
- celldetective/gui/table_ops/merge_groups.py +0 -118
- celldetective/gui/viewers.py +0 -1354
- celldetective/io.py +0 -3663
- celldetective/utils.py +0 -3108
- celldetective-1.4.2.dist-info/RECORD +0 -123
- /celldetective/{gui/processes → processes}/downloader.py +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
celldetective/gui/workers.py
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
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)
|