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.
- 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 +403 -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/downloader.py +137 -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 +235 -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.0b1.dist-info}/METADATA +1 -1
- celldetective-1.5.0b1.dist-info/RECORD +187 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.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/downloader.py +0 -111
- 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-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/entry_points.txt +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/licenses/LICENSE +0 -0
- {celldetective-1.4.2.dist-info → celldetective-1.5.0b1.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,565 @@
|
|
|
1
|
+
from multiprocessing import Process
|
|
2
|
+
import time
|
|
3
|
+
import datetime
|
|
4
|
+
import os
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path, PurePath
|
|
7
|
+
|
|
8
|
+
from celldetective.utils.image_loaders import (
|
|
9
|
+
auto_load_number_of_frames,
|
|
10
|
+
load_frames,
|
|
11
|
+
_get_img_num_per_channel,
|
|
12
|
+
)
|
|
13
|
+
from celldetective.utils.experiment import extract_experiment_channels
|
|
14
|
+
from celldetective.utils.parsing import config_section_to_dict
|
|
15
|
+
from celldetective.utils.data_cleaning import (
|
|
16
|
+
_extract_coordinates_from_features,
|
|
17
|
+
remove_trajectory_measurements,
|
|
18
|
+
)
|
|
19
|
+
from glob import glob
|
|
20
|
+
from tqdm import tqdm
|
|
21
|
+
import numpy as np
|
|
22
|
+
import concurrent.futures
|
|
23
|
+
from natsort import natsorted
|
|
24
|
+
from art import tprint
|
|
25
|
+
from typing import Optional, Union
|
|
26
|
+
import gc
|
|
27
|
+
from celldetective.measure import (
|
|
28
|
+
measure_features,
|
|
29
|
+
measure_isotropic_intensity,
|
|
30
|
+
center_of_mass_to_abs_coordinates,
|
|
31
|
+
measure_radial_distance_to_center,
|
|
32
|
+
drop_tonal_features,
|
|
33
|
+
)
|
|
34
|
+
import pandas as pd
|
|
35
|
+
from celldetective.utils.image_loaders import locate_labels
|
|
36
|
+
|
|
37
|
+
from celldetective.log_manager import get_logger
|
|
38
|
+
|
|
39
|
+
logger = get_logger(__name__)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class MeasurementProcess(Process):
|
|
43
|
+
|
|
44
|
+
pos: Optional[Union[str, Path]] = None
|
|
45
|
+
mode: Optional[str] = None
|
|
46
|
+
n_threads: int = 1
|
|
47
|
+
|
|
48
|
+
def __init__(self, queue=None, process_args=None):
|
|
49
|
+
|
|
50
|
+
super().__init__()
|
|
51
|
+
|
|
52
|
+
self.queue = queue
|
|
53
|
+
|
|
54
|
+
if process_args is not None:
|
|
55
|
+
for key, value in process_args.items():
|
|
56
|
+
setattr(self, key, value)
|
|
57
|
+
|
|
58
|
+
self.column_labels = {
|
|
59
|
+
"track": "TRACK_ID",
|
|
60
|
+
"time": "FRAME",
|
|
61
|
+
"x": "POSITION_X",
|
|
62
|
+
"y": "POSITION_Y",
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
self.sum_done = 0
|
|
66
|
+
self.t0 = time.time()
|
|
67
|
+
|
|
68
|
+
def check_possible_measurements(self):
|
|
69
|
+
|
|
70
|
+
if (self.file is None) or (self.intensity_measurement_radii is None):
|
|
71
|
+
self.do_iso_intensities = False
|
|
72
|
+
logger.warning(
|
|
73
|
+
"Either no image, no positions or no radii were provided... Isotropic intensities will not be computed..."
|
|
74
|
+
)
|
|
75
|
+
else:
|
|
76
|
+
self.do_iso_intensities = True
|
|
77
|
+
|
|
78
|
+
if self.label_path is None:
|
|
79
|
+
self.do_features = False
|
|
80
|
+
logger.warning(
|
|
81
|
+
"No labels were provided... Features will not be computed..."
|
|
82
|
+
)
|
|
83
|
+
else:
|
|
84
|
+
self.do_features = True
|
|
85
|
+
|
|
86
|
+
if self.trajectories is None:
|
|
87
|
+
logger.info("Use features as a substitute for the trajectory table.")
|
|
88
|
+
if "label" not in self.features:
|
|
89
|
+
self.features.append("label")
|
|
90
|
+
|
|
91
|
+
def read_measurement_instructions(self):
|
|
92
|
+
|
|
93
|
+
logger.info("Looking for measurement instruction file...")
|
|
94
|
+
instr_path = PurePath(self.exp_dir, Path(f"{self.instruction_file}"))
|
|
95
|
+
if os.path.exists(instr_path):
|
|
96
|
+
with open(instr_path, "r") as f:
|
|
97
|
+
self.instructions = json.load(f)
|
|
98
|
+
logger.info(f"Measurement instruction file successfully loaded...")
|
|
99
|
+
logger.info(f"Instructions: {self.instructions}...")
|
|
100
|
+
|
|
101
|
+
if "background_correction" in self.instructions:
|
|
102
|
+
self.background_correction = self.instructions["background_correction"]
|
|
103
|
+
else:
|
|
104
|
+
self.background_correction = None
|
|
105
|
+
|
|
106
|
+
if "features" in self.instructions:
|
|
107
|
+
self.features = self.instructions["features"]
|
|
108
|
+
else:
|
|
109
|
+
self.features = None
|
|
110
|
+
|
|
111
|
+
if "border_distances" in self.instructions:
|
|
112
|
+
self.border_distances = self.instructions["border_distances"]
|
|
113
|
+
else:
|
|
114
|
+
self.border_distances = None
|
|
115
|
+
|
|
116
|
+
if "spot_detection" in self.instructions:
|
|
117
|
+
self.spot_detection = self.instructions["spot_detection"]
|
|
118
|
+
else:
|
|
119
|
+
self.spot_detection = None
|
|
120
|
+
|
|
121
|
+
if "haralick_options" in self.instructions:
|
|
122
|
+
self.haralick_options = self.instructions["haralick_options"]
|
|
123
|
+
else:
|
|
124
|
+
self.haralick_options = None
|
|
125
|
+
|
|
126
|
+
if "intensity_measurement_radii" in self.instructions:
|
|
127
|
+
self.intensity_measurement_radii = self.instructions[
|
|
128
|
+
"intensity_measurement_radii"
|
|
129
|
+
]
|
|
130
|
+
else:
|
|
131
|
+
self.intensity_measurement_radii = None
|
|
132
|
+
|
|
133
|
+
if "isotropic_operations" in self.instructions:
|
|
134
|
+
self.isotropic_operations = self.instructions["isotropic_operations"]
|
|
135
|
+
else:
|
|
136
|
+
self.isotropic_operations = None
|
|
137
|
+
|
|
138
|
+
if "clear_previous" in self.instructions:
|
|
139
|
+
self.clear_previous = self.instructions["clear_previous"]
|
|
140
|
+
else:
|
|
141
|
+
self.clear_previous = True
|
|
142
|
+
|
|
143
|
+
else:
|
|
144
|
+
logger.info("No measurement instructions found. Use default measurements.")
|
|
145
|
+
self.features = ["area", "intensity_mean"]
|
|
146
|
+
self.border_distances = None
|
|
147
|
+
self.haralick_options = None
|
|
148
|
+
self.clear_previous = False
|
|
149
|
+
self.background_correction = None
|
|
150
|
+
self.spot_detection = None
|
|
151
|
+
self.intensity_measurement_radii = 10
|
|
152
|
+
self.isotropic_operations = ["mean"]
|
|
153
|
+
|
|
154
|
+
if self.features is None:
|
|
155
|
+
self.features = []
|
|
156
|
+
|
|
157
|
+
def detect_channels(self):
|
|
158
|
+
self.img_num_channels = _get_img_num_per_channel(
|
|
159
|
+
self.channel_indices, self.len_movie, self.nbr_channels
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
def write_log(self):
|
|
163
|
+
|
|
164
|
+
features_log = f"features: {self.features}"
|
|
165
|
+
border_distances_log = f"border_distances: {self.border_distances}"
|
|
166
|
+
haralick_options_log = f"haralick_options: {self.haralick_options}"
|
|
167
|
+
background_correction_log = (
|
|
168
|
+
f"background_correction: {self.background_correction}"
|
|
169
|
+
)
|
|
170
|
+
spot_detection_log = f"spot_detection: {self.spot_detection}"
|
|
171
|
+
intensity_measurement_radii_log = (
|
|
172
|
+
f"intensity_measurement_radii: {self.intensity_measurement_radii}"
|
|
173
|
+
)
|
|
174
|
+
isotropic_options_log = f"isotropic_operations: {self.isotropic_operations} \n"
|
|
175
|
+
log = "\n".join(
|
|
176
|
+
[
|
|
177
|
+
features_log,
|
|
178
|
+
border_distances_log,
|
|
179
|
+
haralick_options_log,
|
|
180
|
+
background_correction_log,
|
|
181
|
+
spot_detection_log,
|
|
182
|
+
intensity_measurement_radii_log,
|
|
183
|
+
isotropic_options_log,
|
|
184
|
+
]
|
|
185
|
+
)
|
|
186
|
+
with open(self.pos + f"log_{self.mode}.txt", "a") as f:
|
|
187
|
+
f.write(f"{datetime.datetime.now()} MEASURE \n")
|
|
188
|
+
f.write(log + "\n")
|
|
189
|
+
|
|
190
|
+
def prepare_folders(self):
|
|
191
|
+
|
|
192
|
+
if self.mode.lower() == "target" or self.mode.lower() == "targets":
|
|
193
|
+
self.label_folder = "labels_targets"
|
|
194
|
+
self.table_name = "trajectories_targets.csv"
|
|
195
|
+
self.instruction_file = os.sep.join(
|
|
196
|
+
["configs", "measurement_instructions_targets.json"]
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
elif self.mode.lower() == "effector" or self.mode.lower() == "effectors":
|
|
200
|
+
self.label_folder = "labels_effectors"
|
|
201
|
+
self.table_name = "trajectories_effectors.csv"
|
|
202
|
+
self.instruction_file = os.sep.join(
|
|
203
|
+
["configs", "measurement_instructions_effectors.json"]
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
else:
|
|
207
|
+
self.label_folder = f"labels_{self.mode}"
|
|
208
|
+
self.table_name = f"trajectories_{self.mode}.csv"
|
|
209
|
+
self.instruction_file = os.sep.join(
|
|
210
|
+
["configs", f"measurement_instructions_{self.mode}.json"]
|
|
211
|
+
)
|
|
212
|
+
|
|
213
|
+
def extract_experiment_parameters(self):
|
|
214
|
+
|
|
215
|
+
self.movie_prefix = config_section_to_dict(self.config, "MovieSettings")[
|
|
216
|
+
"movie_prefix"
|
|
217
|
+
]
|
|
218
|
+
self.spatial_calibration = float(
|
|
219
|
+
config_section_to_dict(self.config, "MovieSettings")["pxtoum"]
|
|
220
|
+
)
|
|
221
|
+
self.time_calibration = float(
|
|
222
|
+
config_section_to_dict(self.config, "MovieSettings")["frametomin"]
|
|
223
|
+
)
|
|
224
|
+
self.len_movie = float(
|
|
225
|
+
config_section_to_dict(self.config, "MovieSettings")["len_movie"]
|
|
226
|
+
)
|
|
227
|
+
self.shape_x = int(
|
|
228
|
+
config_section_to_dict(self.config, "MovieSettings")["shape_x"]
|
|
229
|
+
)
|
|
230
|
+
self.shape_y = int(
|
|
231
|
+
config_section_to_dict(self.config, "MovieSettings")["shape_y"]
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
self.channel_names, self.channel_indices = extract_experiment_channels(
|
|
235
|
+
self.exp_dir
|
|
236
|
+
)
|
|
237
|
+
self.nbr_channels = len(self.channel_names)
|
|
238
|
+
|
|
239
|
+
def locate_experiment_config(self):
|
|
240
|
+
|
|
241
|
+
parent1 = Path(self.pos).parent
|
|
242
|
+
self.exp_dir = parent1.parent
|
|
243
|
+
self.config = PurePath(self.exp_dir, Path("config.ini"))
|
|
244
|
+
|
|
245
|
+
if not os.path.exists(self.config):
|
|
246
|
+
logger.error("The configuration file for the experiment was not found...")
|
|
247
|
+
self.abort_process()
|
|
248
|
+
|
|
249
|
+
def detect_tracks(self):
|
|
250
|
+
|
|
251
|
+
# Load trajectories, add centroid if not in trajectory
|
|
252
|
+
self.trajectories = self.pos + os.sep.join(
|
|
253
|
+
["output", "tables", self.table_name]
|
|
254
|
+
)
|
|
255
|
+
if os.path.exists(self.trajectories):
|
|
256
|
+
logger.info("Previous table detected...")
|
|
257
|
+
self.trajectories = pd.read_csv(self.trajectories)
|
|
258
|
+
if "TRACK_ID" not in list(self.trajectories.columns):
|
|
259
|
+
logger.info("Static measurements detected...")
|
|
260
|
+
self.do_iso_intensities = False
|
|
261
|
+
self.intensity_measurement_radii = None
|
|
262
|
+
if self.clear_previous:
|
|
263
|
+
logger.info("Clear previous measurements...")
|
|
264
|
+
self.trajectories = None # remove_trajectory_measurements(trajectories, column_labels)
|
|
265
|
+
self.do_features = True
|
|
266
|
+
self.features += ["centroid"]
|
|
267
|
+
self.column_labels.update({"track": "ID"})
|
|
268
|
+
else:
|
|
269
|
+
logger.info("Time series detected...")
|
|
270
|
+
if self.clear_previous:
|
|
271
|
+
logger.info("TRACK_ID found... Clear previous measurements...")
|
|
272
|
+
self.trajectories = remove_trajectory_measurements(
|
|
273
|
+
self.trajectories, self.column_labels
|
|
274
|
+
)
|
|
275
|
+
else:
|
|
276
|
+
self.trajectories = None
|
|
277
|
+
self.do_features = True
|
|
278
|
+
self.features += ["centroid"]
|
|
279
|
+
self.do_iso_intensities = False
|
|
280
|
+
|
|
281
|
+
def detect_movie_and_labels(self):
|
|
282
|
+
|
|
283
|
+
self.label_path = natsorted(
|
|
284
|
+
glob(os.sep.join([self.pos, self.label_folder, "*.tif"]))
|
|
285
|
+
)
|
|
286
|
+
if len(self.label_path) > 0:
|
|
287
|
+
logger.info(f"Found {len(self.label_path)} segmented frames...")
|
|
288
|
+
else:
|
|
289
|
+
self.features = None
|
|
290
|
+
self.haralick_options = None
|
|
291
|
+
self.border_distances = None
|
|
292
|
+
self.label_path = None
|
|
293
|
+
|
|
294
|
+
try:
|
|
295
|
+
self.file = glob(
|
|
296
|
+
self.pos + os.sep.join(["movie", f"{self.movie_prefix}*.tif"])
|
|
297
|
+
)[0]
|
|
298
|
+
except IndexError:
|
|
299
|
+
self.file = None
|
|
300
|
+
self.haralick_option = None
|
|
301
|
+
self.features = drop_tonal_features(self.features)
|
|
302
|
+
|
|
303
|
+
len_movie_auto = auto_load_number_of_frames(self.file)
|
|
304
|
+
if len_movie_auto is not None:
|
|
305
|
+
self.len_movie = len_movie_auto
|
|
306
|
+
|
|
307
|
+
def parallel_job(self, indices):
|
|
308
|
+
|
|
309
|
+
measurements = []
|
|
310
|
+
|
|
311
|
+
for t in tqdm(indices, desc="frame"):
|
|
312
|
+
|
|
313
|
+
if self.file is not None:
|
|
314
|
+
img = load_frames(
|
|
315
|
+
self.img_num_channels[:, t],
|
|
316
|
+
self.file,
|
|
317
|
+
scale=None,
|
|
318
|
+
normalize_input=False,
|
|
319
|
+
)
|
|
320
|
+
|
|
321
|
+
if self.label_path is not None:
|
|
322
|
+
lbl = locate_labels(self.pos, population=self.mode, frames=t)
|
|
323
|
+
if lbl is None:
|
|
324
|
+
continue
|
|
325
|
+
|
|
326
|
+
if self.trajectories is not None:
|
|
327
|
+
# Optimized access
|
|
328
|
+
if self.frame_slices is not None:
|
|
329
|
+
# Check if frame t is in our precomputed slices
|
|
330
|
+
if t in self.frame_slices:
|
|
331
|
+
start, end = self.frame_slices[t]
|
|
332
|
+
positions_at_t = self.trajectories.iloc[start:end].copy()
|
|
333
|
+
else:
|
|
334
|
+
# Empty frame for trajectories
|
|
335
|
+
positions_at_t = pd.DataFrame(columns=self.trajectories.columns)
|
|
336
|
+
else:
|
|
337
|
+
# Fallback or original method (should not be reached if optimized)
|
|
338
|
+
positions_at_t = self.trajectories.loc[
|
|
339
|
+
self.trajectories[self.column_labels["time"]] == t
|
|
340
|
+
].copy()
|
|
341
|
+
|
|
342
|
+
if self.do_features:
|
|
343
|
+
feature_table = measure_features(
|
|
344
|
+
img,
|
|
345
|
+
lbl,
|
|
346
|
+
features=self.features,
|
|
347
|
+
border_dist=self.border_distances,
|
|
348
|
+
channels=self.channel_names,
|
|
349
|
+
haralick_options=self.haralick_options,
|
|
350
|
+
verbose=False,
|
|
351
|
+
normalisation_list=self.background_correction,
|
|
352
|
+
spot_detection=self.spot_detection,
|
|
353
|
+
)
|
|
354
|
+
if self.trajectories is None:
|
|
355
|
+
positions_at_t = _extract_coordinates_from_features(
|
|
356
|
+
feature_table, timepoint=t
|
|
357
|
+
)
|
|
358
|
+
column_labels = {
|
|
359
|
+
"track": "ID",
|
|
360
|
+
"time": self.column_labels["time"],
|
|
361
|
+
"x": self.column_labels["x"],
|
|
362
|
+
"y": self.column_labels["y"],
|
|
363
|
+
}
|
|
364
|
+
feature_table.rename(
|
|
365
|
+
columns={"centroid-1": "POSITION_X", "centroid-0": "POSITION_Y"},
|
|
366
|
+
inplace=True,
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
if self.do_iso_intensities and not self.trajectories is None:
|
|
370
|
+
iso_table = measure_isotropic_intensity(
|
|
371
|
+
positions_at_t,
|
|
372
|
+
img,
|
|
373
|
+
channels=self.channel_names,
|
|
374
|
+
intensity_measurement_radii=self.intensity_measurement_radii,
|
|
375
|
+
column_labels=self.column_labels,
|
|
376
|
+
operations=self.isotropic_operations,
|
|
377
|
+
verbose=False,
|
|
378
|
+
)
|
|
379
|
+
|
|
380
|
+
if (
|
|
381
|
+
self.do_iso_intensities
|
|
382
|
+
and self.do_features
|
|
383
|
+
and not self.trajectories is None
|
|
384
|
+
):
|
|
385
|
+
measurements_at_t = iso_table.merge(
|
|
386
|
+
feature_table, how="outer", on="class_id", suffixes=("_delme", "")
|
|
387
|
+
)
|
|
388
|
+
measurements_at_t = measurements_at_t[
|
|
389
|
+
[c for c in measurements_at_t.columns if not c.endswith("_delme")]
|
|
390
|
+
]
|
|
391
|
+
elif (
|
|
392
|
+
self.do_iso_intensities
|
|
393
|
+
* (not self.do_features)
|
|
394
|
+
* (not self.trajectories is None)
|
|
395
|
+
):
|
|
396
|
+
measurements_at_t = iso_table
|
|
397
|
+
elif self.do_features:
|
|
398
|
+
measurements_at_t = positions_at_t.merge(
|
|
399
|
+
feature_table, how="outer", on="class_id", suffixes=("_delme", "")
|
|
400
|
+
)
|
|
401
|
+
measurements_at_t = measurements_at_t[
|
|
402
|
+
[c for c in measurements_at_t.columns if not c.endswith("_delme")]
|
|
403
|
+
]
|
|
404
|
+
|
|
405
|
+
measurements_at_t = center_of_mass_to_abs_coordinates(measurements_at_t)
|
|
406
|
+
|
|
407
|
+
measurements_at_t = measure_radial_distance_to_center(
|
|
408
|
+
measurements_at_t, volume=img.shape, column_labels=self.column_labels
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
self.sum_done += 1
|
|
412
|
+
data = {}
|
|
413
|
+
|
|
414
|
+
# Frame Progress
|
|
415
|
+
frame_progress = (self.sum_done / self.len_movie) * 100
|
|
416
|
+
if frame_progress > 100:
|
|
417
|
+
frame_progress = 100
|
|
418
|
+
data["frame_progress"] = frame_progress
|
|
419
|
+
|
|
420
|
+
# Frame Time Estimation
|
|
421
|
+
elapsed = time.time() - getattr(self, "t0_frame", time.time())
|
|
422
|
+
if self.sum_done > 0:
|
|
423
|
+
avg = elapsed / self.sum_done
|
|
424
|
+
rem = self.len_movie - self.sum_done
|
|
425
|
+
rem_t = rem * avg
|
|
426
|
+
mins = int(rem_t // 60)
|
|
427
|
+
secs = int(rem_t % 60)
|
|
428
|
+
data["frame_time"] = f"Measurement: {mins} m {secs} s"
|
|
429
|
+
else:
|
|
430
|
+
data["frame_time"] = "Measuring..."
|
|
431
|
+
|
|
432
|
+
self.queue.put(data)
|
|
433
|
+
|
|
434
|
+
if measurements_at_t is not None:
|
|
435
|
+
measurements_at_t[self.column_labels["time"]] = t
|
|
436
|
+
else:
|
|
437
|
+
measurements_at_t = pd.DataFrame()
|
|
438
|
+
|
|
439
|
+
measurements.append(measurements_at_t)
|
|
440
|
+
|
|
441
|
+
return measurements
|
|
442
|
+
|
|
443
|
+
def setup_for_position(self, pos):
|
|
444
|
+
|
|
445
|
+
self.pos = pos
|
|
446
|
+
# Experiment
|
|
447
|
+
self.prepare_folders()
|
|
448
|
+
self.locate_experiment_config()
|
|
449
|
+
self.extract_experiment_parameters()
|
|
450
|
+
self.read_measurement_instructions()
|
|
451
|
+
self.detect_movie_and_labels()
|
|
452
|
+
self.detect_tracks()
|
|
453
|
+
self.detect_channels()
|
|
454
|
+
self.check_possible_measurements()
|
|
455
|
+
self.write_log()
|
|
456
|
+
|
|
457
|
+
def process_position(self):
|
|
458
|
+
tprint("Measure")
|
|
459
|
+
|
|
460
|
+
self.indices = list(range(self.img_num_channels.shape[1]))
|
|
461
|
+
chunks = np.array_split(self.indices, self.n_threads)
|
|
462
|
+
|
|
463
|
+
self.timestep_dataframes = []
|
|
464
|
+
self.t0_frame = time.time()
|
|
465
|
+
self.sum_done = 0
|
|
466
|
+
|
|
467
|
+
# Optimize: Group trajectories by frame for O(1) access inside the loop
|
|
468
|
+
self.frame_slices = None
|
|
469
|
+
if self.trajectories is not None:
|
|
470
|
+
# Sort by FRAME to enable searchsorted
|
|
471
|
+
self.trajectories = self.trajectories.sort_values(
|
|
472
|
+
self.column_labels["time"]
|
|
473
|
+
)
|
|
474
|
+
frames = self.trajectories[self.column_labels["time"]].values
|
|
475
|
+
|
|
476
|
+
# Find unique frames and their indices
|
|
477
|
+
unique_frames = np.unique(frames)
|
|
478
|
+
|
|
479
|
+
# searchsorted returns the indices where elements should be inserted to maintain order
|
|
480
|
+
# 'left' gives the start index, 'right' gives the end index
|
|
481
|
+
start_indices = np.searchsorted(frames, unique_frames, side="left")
|
|
482
|
+
end_indices = np.searchsorted(frames, unique_frames, side="right")
|
|
483
|
+
|
|
484
|
+
self.frame_slices = {
|
|
485
|
+
frame: (start, end)
|
|
486
|
+
for frame, start, end in zip(unique_frames, start_indices, end_indices)
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
if self.n_threads > 1:
|
|
490
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
491
|
+
max_workers=self.n_threads
|
|
492
|
+
) as executor:
|
|
493
|
+
results = executor.map(
|
|
494
|
+
self.parallel_job, chunks
|
|
495
|
+
) # list(map(lambda x: executor.submit(self.parallel_job, x), chunks))
|
|
496
|
+
try:
|
|
497
|
+
for i, return_value in enumerate(results):
|
|
498
|
+
logger.info(f"Thread {i} completed...")
|
|
499
|
+
self.timestep_dataframes.extend(return_value)
|
|
500
|
+
except Exception as e:
|
|
501
|
+
logger.error("Exception: ", e)
|
|
502
|
+
raise e
|
|
503
|
+
else:
|
|
504
|
+
try:
|
|
505
|
+
# Avoid thread pool overhead for single thread
|
|
506
|
+
results = [self.parallel_job(chunks[0])]
|
|
507
|
+
for i, return_value in enumerate(results):
|
|
508
|
+
logger.info(f"Job {i} completed...")
|
|
509
|
+
self.timestep_dataframes.extend(return_value)
|
|
510
|
+
except Exception as e:
|
|
511
|
+
logger.error("Exception: ", e)
|
|
512
|
+
raise e
|
|
513
|
+
|
|
514
|
+
logger.info("Measurements successfully performed...")
|
|
515
|
+
|
|
516
|
+
if len(self.timestep_dataframes) > 0:
|
|
517
|
+
|
|
518
|
+
df = pd.concat(self.timestep_dataframes)
|
|
519
|
+
|
|
520
|
+
if self.trajectories is not None:
|
|
521
|
+
df = df.sort_values(
|
|
522
|
+
by=[self.column_labels["track"], self.column_labels["time"]]
|
|
523
|
+
)
|
|
524
|
+
df = df.dropna(subset=[self.column_labels["track"]])
|
|
525
|
+
else:
|
|
526
|
+
df["ID"] = np.arange(len(df))
|
|
527
|
+
df = df.sort_values(by=[self.column_labels["time"], "ID"])
|
|
528
|
+
|
|
529
|
+
df = df.reset_index(drop=True)
|
|
530
|
+
# df = _remove_invalid_cols(df)
|
|
531
|
+
logger.info(f"Final columns before export: {df.columns.tolist()}")
|
|
532
|
+
df = df.replace([np.inf, -np.inf], np.nan)
|
|
533
|
+
|
|
534
|
+
df.to_csv(
|
|
535
|
+
self.pos + os.sep.join(["output", "tables", self.table_name]),
|
|
536
|
+
index=False,
|
|
537
|
+
)
|
|
538
|
+
logger.info(
|
|
539
|
+
f'Measurement table successfully exported in {os.sep.join(["output", "tables"])}...'
|
|
540
|
+
)
|
|
541
|
+
logger.info("Done.")
|
|
542
|
+
else:
|
|
543
|
+
logger.error("No measurement could be performed. Check your inputs.")
|
|
544
|
+
|
|
545
|
+
logger.info("Done.")
|
|
546
|
+
gc.collect()
|
|
547
|
+
|
|
548
|
+
def run(self):
|
|
549
|
+
|
|
550
|
+
self.setup_for_position(self.pos)
|
|
551
|
+
self.process_position()
|
|
552
|
+
|
|
553
|
+
# Send end signal
|
|
554
|
+
self.queue.put("finished")
|
|
555
|
+
self.queue.close()
|
|
556
|
+
|
|
557
|
+
def end_process(self):
|
|
558
|
+
|
|
559
|
+
self.terminate()
|
|
560
|
+
self.queue.put("finished")
|
|
561
|
+
|
|
562
|
+
def abort_process(self):
|
|
563
|
+
|
|
564
|
+
self.terminate()
|
|
565
|
+
self.queue.put("error")
|