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
|
@@ -0,0 +1,435 @@
|
|
|
1
|
+
from multiprocessing import Process
|
|
2
|
+
import time
|
|
3
|
+
from pathlib import Path, PurePath
|
|
4
|
+
from glob import glob
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
import numpy as np
|
|
7
|
+
import gc
|
|
8
|
+
import concurrent.futures
|
|
9
|
+
import datetime
|
|
10
|
+
import os
|
|
11
|
+
import json
|
|
12
|
+
from celldetective.measure import drop_tonal_features, measure_features
|
|
13
|
+
from celldetective.tracking import track
|
|
14
|
+
import pandas as pd
|
|
15
|
+
from natsort import natsorted
|
|
16
|
+
from art import tprint
|
|
17
|
+
from celldetective.log_manager import get_logger
|
|
18
|
+
import traceback
|
|
19
|
+
from skimage.io import imread
|
|
20
|
+
|
|
21
|
+
from celldetective.utils.data_cleaning import _mask_intensity_measurements
|
|
22
|
+
from celldetective.utils.data_loaders import interpret_tracking_configuration
|
|
23
|
+
from celldetective.utils.experiment import extract_experiment_channels
|
|
24
|
+
from celldetective.utils.image_loaders import (
|
|
25
|
+
_get_img_num_per_channel,
|
|
26
|
+
auto_load_number_of_frames,
|
|
27
|
+
_load_frames_to_measure,
|
|
28
|
+
)
|
|
29
|
+
from celldetective.utils.io import remove_file_if_exists
|
|
30
|
+
from celldetective.utils.parsing import config_section_to_dict
|
|
31
|
+
|
|
32
|
+
logger = get_logger(__name__)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class TrackingProcess(Process):
|
|
36
|
+
|
|
37
|
+
def __init__(self, queue=None, process_args=None, *args, **kwargs):
|
|
38
|
+
|
|
39
|
+
super().__init__(*args, **kwargs)
|
|
40
|
+
|
|
41
|
+
self.queue = queue
|
|
42
|
+
|
|
43
|
+
if process_args is not None:
|
|
44
|
+
for key, value in process_args.items():
|
|
45
|
+
setattr(self, key, value)
|
|
46
|
+
|
|
47
|
+
self.timestep_dataframes = []
|
|
48
|
+
|
|
49
|
+
self.sum_done = 0
|
|
50
|
+
self.t0 = time.time()
|
|
51
|
+
|
|
52
|
+
def read_tracking_instructions(self):
|
|
53
|
+
|
|
54
|
+
instr_path = PurePath(self.exp_dir, Path(f"{self.instruction_file}"))
|
|
55
|
+
if os.path.exists(instr_path):
|
|
56
|
+
logger.info(
|
|
57
|
+
f"Tracking instructions for the {self.mode} population have been successfully loaded..."
|
|
58
|
+
)
|
|
59
|
+
with open(instr_path, "r") as f:
|
|
60
|
+
self.instructions = json.load(f)
|
|
61
|
+
|
|
62
|
+
self.btrack_config = interpret_tracking_configuration(
|
|
63
|
+
self.instructions["btrack_config_path"]
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
if "features" in self.instructions:
|
|
67
|
+
self.features = self.instructions["features"]
|
|
68
|
+
else:
|
|
69
|
+
self.features = None
|
|
70
|
+
|
|
71
|
+
if "mask_channels" in self.instructions:
|
|
72
|
+
self.mask_channels = self.instructions["mask_channels"]
|
|
73
|
+
else:
|
|
74
|
+
self.mask_channels = None
|
|
75
|
+
|
|
76
|
+
if "haralick_options" in self.instructions:
|
|
77
|
+
self.haralick_options = self.instructions["haralick_options"]
|
|
78
|
+
else:
|
|
79
|
+
self.haralick_options = None
|
|
80
|
+
|
|
81
|
+
if "post_processing_options" in self.instructions:
|
|
82
|
+
self.post_processing_options = self.instructions[
|
|
83
|
+
"post_processing_options"
|
|
84
|
+
]
|
|
85
|
+
else:
|
|
86
|
+
self.post_processing_options = None
|
|
87
|
+
|
|
88
|
+
self.btrack_option = True
|
|
89
|
+
if "btrack_option" in self.instructions:
|
|
90
|
+
self.btrack_option = self.instructions["btrack_option"]
|
|
91
|
+
self.search_range = None
|
|
92
|
+
if "search_range" in self.instructions:
|
|
93
|
+
self.search_range = self.instructions["search_range"]
|
|
94
|
+
self.memory = None
|
|
95
|
+
if "memory" in self.instructions:
|
|
96
|
+
self.memory = self.instructions["memory"]
|
|
97
|
+
else:
|
|
98
|
+
logger.info(
|
|
99
|
+
"Tracking instructions could not be located... Using a standard bTrack motion model instead..."
|
|
100
|
+
)
|
|
101
|
+
self.btrack_config = interpret_tracking_configuration(None)
|
|
102
|
+
self.features = None
|
|
103
|
+
self.mask_channels = None
|
|
104
|
+
self.haralick_options = None
|
|
105
|
+
self.post_processing_options = None
|
|
106
|
+
self.btrack_option = True
|
|
107
|
+
self.memory = None
|
|
108
|
+
self.search_range = None
|
|
109
|
+
|
|
110
|
+
if self.features is None:
|
|
111
|
+
self.features = []
|
|
112
|
+
|
|
113
|
+
def detect_channels(self):
|
|
114
|
+
self.img_num_channels = _get_img_num_per_channel(
|
|
115
|
+
self.channel_indices, self.len_movie, self.nbr_channels
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
def write_log(self):
|
|
119
|
+
|
|
120
|
+
features_log = f"features: {self.features}"
|
|
121
|
+
mask_channels_log = f"mask_channels: {self.mask_channels}"
|
|
122
|
+
haralick_option_log = f"haralick_options: {self.haralick_options}"
|
|
123
|
+
post_processing_option_log = (
|
|
124
|
+
f"post_processing_options: {self.post_processing_options}"
|
|
125
|
+
)
|
|
126
|
+
log_list = [
|
|
127
|
+
features_log,
|
|
128
|
+
mask_channels_log,
|
|
129
|
+
haralick_option_log,
|
|
130
|
+
post_processing_option_log,
|
|
131
|
+
]
|
|
132
|
+
log = "\n".join(log_list)
|
|
133
|
+
|
|
134
|
+
with open(self.pos + f"log_{self.mode}.txt", "a") as f:
|
|
135
|
+
f.write(f"{datetime.datetime.now()} TRACK \n")
|
|
136
|
+
f.write(log + "\n")
|
|
137
|
+
|
|
138
|
+
def prepare_folders(self):
|
|
139
|
+
|
|
140
|
+
if not os.path.exists(self.pos + "output"):
|
|
141
|
+
os.mkdir(self.pos + "output")
|
|
142
|
+
|
|
143
|
+
if not os.path.exists(self.pos + os.sep.join(["output", "tables"])):
|
|
144
|
+
os.mkdir(self.pos + os.sep.join(["output", "tables"]))
|
|
145
|
+
|
|
146
|
+
if self.mode.lower() == "target" or self.mode.lower() == "targets":
|
|
147
|
+
self.label_folder = "labels_targets"
|
|
148
|
+
self.instruction_file = os.sep.join(
|
|
149
|
+
["configs", "tracking_instructions_targets.json"]
|
|
150
|
+
)
|
|
151
|
+
self.napari_name = "napari_target_trajectories.npy"
|
|
152
|
+
self.table_name = "trajectories_targets.csv"
|
|
153
|
+
|
|
154
|
+
elif self.mode.lower() == "effector" or self.mode.lower() == "effectors":
|
|
155
|
+
self.label_folder = "labels_effectors"
|
|
156
|
+
self.instruction_file = os.sep.join(
|
|
157
|
+
["configs", "tracking_instructions_effectors.json"]
|
|
158
|
+
)
|
|
159
|
+
self.napari_name = "napari_effector_trajectories.npy"
|
|
160
|
+
self.table_name = "trajectories_effectors.csv"
|
|
161
|
+
|
|
162
|
+
else:
|
|
163
|
+
self.label_folder = f"labels_{self.mode}"
|
|
164
|
+
self.instruction_file = os.sep.join(
|
|
165
|
+
["configs", f"tracking_instructions_{self.mode}.json"]
|
|
166
|
+
)
|
|
167
|
+
self.napari_name = f"napari_{self.mode}_trajectories.npy"
|
|
168
|
+
self.table_name = f"trajectories_{self.mode}.csv"
|
|
169
|
+
|
|
170
|
+
def extract_experiment_parameters(self):
|
|
171
|
+
|
|
172
|
+
self.movie_prefix = config_section_to_dict(self.config, "MovieSettings")[
|
|
173
|
+
"movie_prefix"
|
|
174
|
+
]
|
|
175
|
+
self.spatial_calibration = float(
|
|
176
|
+
config_section_to_dict(self.config, "MovieSettings")["pxtoum"]
|
|
177
|
+
)
|
|
178
|
+
self.time_calibration = float(
|
|
179
|
+
config_section_to_dict(self.config, "MovieSettings")["frametomin"]
|
|
180
|
+
)
|
|
181
|
+
self.len_movie = float(
|
|
182
|
+
config_section_to_dict(self.config, "MovieSettings")["len_movie"]
|
|
183
|
+
)
|
|
184
|
+
self.shape_x = int(
|
|
185
|
+
config_section_to_dict(self.config, "MovieSettings")["shape_x"]
|
|
186
|
+
)
|
|
187
|
+
self.shape_y = int(
|
|
188
|
+
config_section_to_dict(self.config, "MovieSettings")["shape_y"]
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
self.channel_names, self.channel_indices = extract_experiment_channels(
|
|
192
|
+
self.exp_dir
|
|
193
|
+
)
|
|
194
|
+
self.nbr_channels = len(self.channel_names)
|
|
195
|
+
|
|
196
|
+
def locate_experiment_config(self):
|
|
197
|
+
|
|
198
|
+
parent1 = Path(self.pos).parent
|
|
199
|
+
self.exp_dir = parent1.parent
|
|
200
|
+
self.config = PurePath(self.exp_dir, Path("config.ini"))
|
|
201
|
+
|
|
202
|
+
if not os.path.exists(self.config):
|
|
203
|
+
logger.info("The configuration file for the experiment was not found...")
|
|
204
|
+
self.abort_process()
|
|
205
|
+
|
|
206
|
+
def detect_movie_and_labels(self):
|
|
207
|
+
|
|
208
|
+
self.label_path = natsorted(
|
|
209
|
+
glob(self.pos + f"{self.label_folder}" + os.sep + "*.tif")
|
|
210
|
+
)
|
|
211
|
+
if len(self.label_path) > 0:
|
|
212
|
+
logger.info(f"Found {len(self.label_path)} segmented frames...")
|
|
213
|
+
# Optimize: Create a map of frame index to file path
|
|
214
|
+
self.label_map = {}
|
|
215
|
+
for path in self.label_path:
|
|
216
|
+
try:
|
|
217
|
+
# Assumes format ####.tif, e.g., 0001.tif
|
|
218
|
+
frame_idx = int(os.path.basename(path).split(".")[0])
|
|
219
|
+
self.label_map[frame_idx] = path
|
|
220
|
+
except ValueError:
|
|
221
|
+
continue
|
|
222
|
+
else:
|
|
223
|
+
logger.error(
|
|
224
|
+
f"No segmented frames have been found. Please run segmentation first. Abort..."
|
|
225
|
+
)
|
|
226
|
+
self.abort_process()
|
|
227
|
+
|
|
228
|
+
try:
|
|
229
|
+
self.file = glob(self.pos + f"movie/{self.movie_prefix}*.tif")[0]
|
|
230
|
+
except IndexError:
|
|
231
|
+
self.file = None
|
|
232
|
+
self.haralick_option = None
|
|
233
|
+
self.features = drop_tonal_features(self.features)
|
|
234
|
+
logger.warning("Movie could not be found. Check the prefix.")
|
|
235
|
+
|
|
236
|
+
len_movie_auto = auto_load_number_of_frames(self.file)
|
|
237
|
+
if len_movie_auto is not None:
|
|
238
|
+
self.len_movie = len_movie_auto
|
|
239
|
+
|
|
240
|
+
def parallel_job(self, indices):
|
|
241
|
+
|
|
242
|
+
props = []
|
|
243
|
+
|
|
244
|
+
try:
|
|
245
|
+
|
|
246
|
+
for t in tqdm(indices, desc="frame"):
|
|
247
|
+
|
|
248
|
+
# Load channels at time t
|
|
249
|
+
img = _load_frames_to_measure(
|
|
250
|
+
self.file, indices=self.img_num_channels[:, t]
|
|
251
|
+
)
|
|
252
|
+
# Optimize: Direct lookup instead of glob
|
|
253
|
+
if t in self.label_map:
|
|
254
|
+
try:
|
|
255
|
+
# Load image from path in map
|
|
256
|
+
lbl = np.array(imread(self.label_map[t]))
|
|
257
|
+
except Exception as e:
|
|
258
|
+
logger.error(f"Failed to load label for frame {t}: {e}")
|
|
259
|
+
continue
|
|
260
|
+
else:
|
|
261
|
+
# Fallback or skip if not in map
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
df_props = measure_features(
|
|
265
|
+
img,
|
|
266
|
+
lbl,
|
|
267
|
+
features=self.features + ["centroid"],
|
|
268
|
+
border_dist=None,
|
|
269
|
+
channels=self.channel_names,
|
|
270
|
+
haralick_options=self.haralick_options,
|
|
271
|
+
verbose=False,
|
|
272
|
+
)
|
|
273
|
+
df_props.rename(
|
|
274
|
+
columns={"centroid-1": "x", "centroid-0": "y"}, inplace=True
|
|
275
|
+
)
|
|
276
|
+
df_props["t"] = int(t)
|
|
277
|
+
|
|
278
|
+
props.append(df_props)
|
|
279
|
+
|
|
280
|
+
# Progress Update
|
|
281
|
+
self.loop_count += 1
|
|
282
|
+
|
|
283
|
+
data = {}
|
|
284
|
+
|
|
285
|
+
# Frame Progress
|
|
286
|
+
frame_progress = (self.loop_count / self.len_movie) * 100
|
|
287
|
+
if frame_progress > 100:
|
|
288
|
+
frame_progress = 100
|
|
289
|
+
data["frame_progress"] = frame_progress
|
|
290
|
+
|
|
291
|
+
# Frame Time Estimation
|
|
292
|
+
elapsed = time.time() - getattr(self, "t0_frame", time.time())
|
|
293
|
+
if self.loop_count > 0:
|
|
294
|
+
avg = elapsed / self.loop_count
|
|
295
|
+
rem = self.len_movie - self.loop_count
|
|
296
|
+
rem_t = rem * avg
|
|
297
|
+
mins = int(rem_t // 60)
|
|
298
|
+
secs = int(rem_t % 60)
|
|
299
|
+
data["frame_time"] = f"Tracking: {mins} m {secs} s"
|
|
300
|
+
else:
|
|
301
|
+
data["frame_time"] = "Tracking..."
|
|
302
|
+
|
|
303
|
+
self.queue.put(data)
|
|
304
|
+
|
|
305
|
+
except Exception as e:
|
|
306
|
+
logger.error(e)
|
|
307
|
+
traceback.print_exc()
|
|
308
|
+
|
|
309
|
+
return props
|
|
310
|
+
|
|
311
|
+
def setup_for_position(self, pos):
|
|
312
|
+
|
|
313
|
+
self.pos = pos
|
|
314
|
+
# Experiment
|
|
315
|
+
self.prepare_folders()
|
|
316
|
+
self.locate_experiment_config()
|
|
317
|
+
self.extract_experiment_parameters()
|
|
318
|
+
self.read_tracking_instructions()
|
|
319
|
+
self.detect_movie_and_labels()
|
|
320
|
+
self.detect_channels()
|
|
321
|
+
self.write_log()
|
|
322
|
+
|
|
323
|
+
def process_position(self):
|
|
324
|
+
|
|
325
|
+
tprint("Track")
|
|
326
|
+
|
|
327
|
+
self.indices = list(range(self.img_num_channels.shape[1]))
|
|
328
|
+
chunks = np.array_split(self.indices, self.n_threads)
|
|
329
|
+
|
|
330
|
+
self.timestep_dataframes = []
|
|
331
|
+
self.t0_frame = time.time()
|
|
332
|
+
self.loop_count = 0
|
|
333
|
+
|
|
334
|
+
with concurrent.futures.ThreadPoolExecutor(
|
|
335
|
+
max_workers=self.n_threads
|
|
336
|
+
) as executor:
|
|
337
|
+
results = executor.map(self.parallel_job, chunks)
|
|
338
|
+
try:
|
|
339
|
+
for i, return_value in enumerate(results):
|
|
340
|
+
logger.info(f"Thread {i} completed...")
|
|
341
|
+
self.timestep_dataframes.extend(return_value)
|
|
342
|
+
except Exception as e:
|
|
343
|
+
logger.error("Exception: ", e)
|
|
344
|
+
|
|
345
|
+
logger.info("Features successfully measured...")
|
|
346
|
+
|
|
347
|
+
if not self.timestep_dataframes:
|
|
348
|
+
logger.warning("No cells detected in any frame. Skipping position.")
|
|
349
|
+
return
|
|
350
|
+
|
|
351
|
+
df = pd.concat(self.timestep_dataframes)
|
|
352
|
+
if df.empty:
|
|
353
|
+
logger.warning("Dataframe is empty. Skipping position.")
|
|
354
|
+
return
|
|
355
|
+
|
|
356
|
+
df = df.replace([np.inf, -np.inf], np.nan)
|
|
357
|
+
|
|
358
|
+
df.reset_index(inplace=True, drop=True)
|
|
359
|
+
df = _mask_intensity_measurements(df, self.mask_channels)
|
|
360
|
+
|
|
361
|
+
# do tracking
|
|
362
|
+
if self.btrack_option:
|
|
363
|
+
tracker = "bTrack"
|
|
364
|
+
else:
|
|
365
|
+
tracker = "trackpy"
|
|
366
|
+
|
|
367
|
+
try:
|
|
368
|
+
trajectories, napari_data = track(
|
|
369
|
+
None,
|
|
370
|
+
configuration=self.btrack_config,
|
|
371
|
+
objects=df,
|
|
372
|
+
spatial_calibration=self.spatial_calibration,
|
|
373
|
+
channel_names=self.channel_names,
|
|
374
|
+
return_napari_data=True,
|
|
375
|
+
optimizer_options={"tm_lim": int(12e4)},
|
|
376
|
+
track_kwargs={"step_size": 100},
|
|
377
|
+
clean_trajectories_kwargs=self.post_processing_options,
|
|
378
|
+
volume=(self.shape_x, self.shape_y),
|
|
379
|
+
btrack_option=self.btrack_option,
|
|
380
|
+
search_range=self.search_range,
|
|
381
|
+
memory=self.memory,
|
|
382
|
+
)
|
|
383
|
+
except Exception as e:
|
|
384
|
+
logger.error(f"Tracking failed: {e}")
|
|
385
|
+
if "search_range" in str(e) or "SubnetOversizeException" in str(e):
|
|
386
|
+
logger.error(
|
|
387
|
+
"Suggestion: Try reducing the 'search_range' (maxdisp) in your tracking configuration. Skipping tracking for this position."
|
|
388
|
+
)
|
|
389
|
+
return
|
|
390
|
+
raise e
|
|
391
|
+
|
|
392
|
+
logger.info("Tracking successfully performed...")
|
|
393
|
+
|
|
394
|
+
# out trajectory table, create POSITION_X_um, POSITION_Y_um, TIME_min (new ones)
|
|
395
|
+
# Save napari data
|
|
396
|
+
np.save(
|
|
397
|
+
self.pos + os.sep.join(["output", "tables", self.napari_name]),
|
|
398
|
+
napari_data,
|
|
399
|
+
allow_pickle=True,
|
|
400
|
+
)
|
|
401
|
+
|
|
402
|
+
trajectories.to_csv(
|
|
403
|
+
self.pos + os.sep.join(["output", "tables", self.table_name]), index=False
|
|
404
|
+
)
|
|
405
|
+
logger.info(
|
|
406
|
+
f"Trajectory table successfully exported in {os.sep.join(['output', 'tables'])}..."
|
|
407
|
+
)
|
|
408
|
+
|
|
409
|
+
remove_file_if_exists(
|
|
410
|
+
self.pos
|
|
411
|
+
+ os.sep.join(["output", "tables", self.table_name.replace(".csv", ".pkl")])
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
del trajectories
|
|
415
|
+
del napari_data
|
|
416
|
+
gc.collect()
|
|
417
|
+
|
|
418
|
+
def run(self):
|
|
419
|
+
|
|
420
|
+
self.setup_for_position(self.pos)
|
|
421
|
+
self.process_position()
|
|
422
|
+
|
|
423
|
+
# Send end signal
|
|
424
|
+
self.queue.put("finished")
|
|
425
|
+
self.queue.close()
|
|
426
|
+
|
|
427
|
+
def end_process(self):
|
|
428
|
+
|
|
429
|
+
self.terminate()
|
|
430
|
+
self.queue.put("finished")
|
|
431
|
+
|
|
432
|
+
def abort_process(self):
|
|
433
|
+
|
|
434
|
+
self.terminate()
|
|
435
|
+
self.queue.put("error")
|