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,630 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Optional, List
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _remove_invalid_cols(df: pd.DataFrame) -> pd.DataFrame:
|
|
9
|
+
"""
|
|
10
|
+
Removes invalid columns from a DataFrame.
|
|
11
|
+
|
|
12
|
+
This function identifies and removes columns in the DataFrame whose names
|
|
13
|
+
start with "Unnamed", or that contain only NaN values.
|
|
14
|
+
|
|
15
|
+
Parameters
|
|
16
|
+
----------
|
|
17
|
+
df : pandas.DataFrame
|
|
18
|
+
The input DataFrame from which invalid columns will be removed.
|
|
19
|
+
|
|
20
|
+
Returns
|
|
21
|
+
-------
|
|
22
|
+
pandas.DataFrame
|
|
23
|
+
A new DataFrame with the invalid columns removed. If no invalid
|
|
24
|
+
columns are found, the original DataFrame is returned unchanged.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
invalid_cols = [c for c in list(df.columns) if c.startswith("Unnamed")]
|
|
28
|
+
if len(invalid_cols) > 0:
|
|
29
|
+
df = df.drop(invalid_cols, axis=1)
|
|
30
|
+
df = df.dropna(axis=1, how="all")
|
|
31
|
+
return df
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _extract_coordinates_from_features(
|
|
35
|
+
df: pd.DataFrame, timepoint: int
|
|
36
|
+
) -> pd.DataFrame:
|
|
37
|
+
"""
|
|
38
|
+
Re-format coordinates from a regionprops table to tracking/measurement table format.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
df : pandas.DataFrame
|
|
43
|
+
A DataFrame containing feature data, including columns for centroids
|
|
44
|
+
(`'centroid-1'` and `'centroid-0'`) and feature classes (`'class_id'`).
|
|
45
|
+
timepoint : int
|
|
46
|
+
The timepoint (frame) to assign to all features. This is used to populate
|
|
47
|
+
the `'FRAME'` column in the output.
|
|
48
|
+
|
|
49
|
+
Returns
|
|
50
|
+
-------
|
|
51
|
+
pandas.DataFrame
|
|
52
|
+
A DataFrame containing the extracted coordinates and additional metadata,
|
|
53
|
+
with the following columns:
|
|
54
|
+
- `'POSITION_X'`: X-coordinate of the centroid.
|
|
55
|
+
- `'POSITION_Y'`: Y-coordinate of the centroid.
|
|
56
|
+
- `'class_id'`: The label associated to the cell mask.
|
|
57
|
+
- `'ID'`: A unique identifier for each cell (index-based).
|
|
58
|
+
- `'FRAME'`: The timepoint associated with the features.
|
|
59
|
+
|
|
60
|
+
Notes
|
|
61
|
+
-----
|
|
62
|
+
- The function assumes that the input DataFrame contains columns `'centroid-1'`,
|
|
63
|
+
`'centroid-0'`, and `'class_id'`. Missing columns will raise a KeyError.
|
|
64
|
+
- The `'ID'` column is created based on the index of the input DataFrame.
|
|
65
|
+
- This function renames `'centroid-1'` to `'POSITION_X'` and `'centroid-0'`
|
|
66
|
+
to `'POSITION_Y'`.
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
coords = df[["centroid-1", "centroid-0", "class_id"]].copy()
|
|
70
|
+
coords["ID"] = np.arange(len(coords))
|
|
71
|
+
coords.rename(
|
|
72
|
+
columns={"centroid-1": "POSITION_X", "centroid-0": "POSITION_Y"}, inplace=True
|
|
73
|
+
)
|
|
74
|
+
coords["FRAME"] = int(timepoint)
|
|
75
|
+
|
|
76
|
+
return coords
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _mask_intensity_measurements(df: pd.DataFrame, mask_channels: Optional[List[str]]):
|
|
80
|
+
"""
|
|
81
|
+
Removes columns from a DataFrame that match specific channel name patterns.
|
|
82
|
+
|
|
83
|
+
This function filters out intensity measurement columns in a DataFrame based on
|
|
84
|
+
specified channel names. It identifies columns containing the channel
|
|
85
|
+
names as substrings and drops them from the DataFrame.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
df : pandas.DataFrame
|
|
90
|
+
The input DataFrame containing intensity measurement data. Column names should
|
|
91
|
+
include the mask channel names if they are to be filtered.
|
|
92
|
+
mask_channels : list of str or None
|
|
93
|
+
A list of channel names (as substrings) to use for identifying columns
|
|
94
|
+
to remove. If `None`, no filtering is applied, and the original DataFrame is
|
|
95
|
+
returned.
|
|
96
|
+
|
|
97
|
+
Returns
|
|
98
|
+
-------
|
|
99
|
+
pandas.DataFrame
|
|
100
|
+
The modified DataFrame with specified columns removed. If no columns match
|
|
101
|
+
the mask channels, the original DataFrame is returned.
|
|
102
|
+
|
|
103
|
+
Notes
|
|
104
|
+
-----
|
|
105
|
+
- The function searches for mask channel substrings in column names.
|
|
106
|
+
Partial matches are sufficient to mark a column for removal.
|
|
107
|
+
- If no mask channels are specified (`mask_channels` is `None`), the function
|
|
108
|
+
does not modify the input DataFrame.
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
if isinstance(mask_channels, str):
|
|
112
|
+
mask_channels = [mask_channels]
|
|
113
|
+
|
|
114
|
+
if mask_channels is not None:
|
|
115
|
+
|
|
116
|
+
cols_to_drop = []
|
|
117
|
+
columns = list(df.columns)
|
|
118
|
+
|
|
119
|
+
for mc in mask_channels:
|
|
120
|
+
cols_to_remove = [c for c in columns if mc in c]
|
|
121
|
+
cols_to_drop.extend(cols_to_remove)
|
|
122
|
+
|
|
123
|
+
if len(cols_to_drop) > 0:
|
|
124
|
+
df = df.drop(cols_to_drop, axis=1)
|
|
125
|
+
return df
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def extract_cols_from_table_list(tables, nrows=1):
|
|
129
|
+
"""
|
|
130
|
+
Extracts a unique list of column names from a list of CSV tables.
|
|
131
|
+
|
|
132
|
+
Parameters
|
|
133
|
+
----------
|
|
134
|
+
tables : list of str
|
|
135
|
+
A list of file paths to the CSV tables from which to extract column names.
|
|
136
|
+
nrows : int, optional
|
|
137
|
+
The number of rows to read from each table to identify the columns.
|
|
138
|
+
Default is 1.
|
|
139
|
+
|
|
140
|
+
Returns
|
|
141
|
+
-------
|
|
142
|
+
numpy.ndarray
|
|
143
|
+
An array of unique column names found across all the tables.
|
|
144
|
+
|
|
145
|
+
Notes
|
|
146
|
+
-----
|
|
147
|
+
- This function reads only the first `nrows` rows of each table to improve performance when dealing with large files.
|
|
148
|
+
- The function ensures that column names are unique by consolidating them using `numpy.unique`.
|
|
149
|
+
|
|
150
|
+
Examples
|
|
151
|
+
--------
|
|
152
|
+
>>> tables = ["table1.csv", "table2.csv"]
|
|
153
|
+
>>> extract_cols_from_table_list(tables)
|
|
154
|
+
array(['Column1', 'Column2', 'Column3'], dtype='<U8')
|
|
155
|
+
"""
|
|
156
|
+
|
|
157
|
+
all_columns = []
|
|
158
|
+
for tab in tables:
|
|
159
|
+
cols = pd.read_csv(tab, nrows=1).columns.tolist()
|
|
160
|
+
all_columns.extend(cols)
|
|
161
|
+
all_columns = np.unique(all_columns)
|
|
162
|
+
return all_columns
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def extract_identity_col(trajectories):
|
|
166
|
+
"""
|
|
167
|
+
Determines the identity column name in a DataFrame of trajectories.
|
|
168
|
+
|
|
169
|
+
This function checks the provided DataFrame for the presence of a column
|
|
170
|
+
that can serve as the identity column. It first looks for the column
|
|
171
|
+
'TRACK_ID'. If 'TRACK_ID' exists but contains only null values, it checks
|
|
172
|
+
for the column 'ID' instead. If neither column is found, the function
|
|
173
|
+
returns `None` and prints a message indicating the issue.
|
|
174
|
+
|
|
175
|
+
Parameters
|
|
176
|
+
----------
|
|
177
|
+
trajectories : pandas.DataFrame
|
|
178
|
+
A DataFrame containing trajectory data. The function assumes that
|
|
179
|
+
the identity of each trajectory might be stored in either the
|
|
180
|
+
'TRACK_ID' or 'ID' column.
|
|
181
|
+
|
|
182
|
+
Returns
|
|
183
|
+
-------
|
|
184
|
+
str or None
|
|
185
|
+
The name of the identity column ('TRACK_ID' or 'ID') if found;
|
|
186
|
+
otherwise, `None`.
|
|
187
|
+
"""
|
|
188
|
+
|
|
189
|
+
for col in ["TRACK_ID", "ID"]:
|
|
190
|
+
if col in trajectories.columns and not trajectories[col].isnull().all():
|
|
191
|
+
return col
|
|
192
|
+
|
|
193
|
+
print("ID or TRACK_ID column could not be found in the table...")
|
|
194
|
+
return None
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def rename_intensity_column(df, channels):
|
|
198
|
+
"""
|
|
199
|
+
|
|
200
|
+
Rename intensity columns in a DataFrame based on the provided channel names.
|
|
201
|
+
|
|
202
|
+
Parameters
|
|
203
|
+
----------
|
|
204
|
+
df : pandas DataFrame
|
|
205
|
+
The DataFrame containing the intensity columns.
|
|
206
|
+
channels : list
|
|
207
|
+
A list of channel names corresponding to the intensity columns.
|
|
208
|
+
|
|
209
|
+
Returns
|
|
210
|
+
-------
|
|
211
|
+
pandas DataFrame
|
|
212
|
+
The DataFrame with renamed intensity columns.
|
|
213
|
+
|
|
214
|
+
Notes
|
|
215
|
+
-----
|
|
216
|
+
This function renames the intensity columns in a DataFrame based on the provided channel names.
|
|
217
|
+
It searches for columns containing the substring 'intensity' in their names and replaces it with
|
|
218
|
+
the respective channel name. The renaming is performed according to the order of the channels
|
|
219
|
+
provided in the `channels` list. If multiple channels are provided, the function assumes that the
|
|
220
|
+
intensity columns have a naming pattern that includes a numerical index indicating the channel.
|
|
221
|
+
If only one channel is provided, the function replaces 'intensity' with the single channel name.
|
|
222
|
+
|
|
223
|
+
Examples
|
|
224
|
+
--------
|
|
225
|
+
>>> data = {'intensity_0': [1, 2, 3], 'intensity_1': [4, 5, 6]}
|
|
226
|
+
>>> df = pd.DataFrame(data)
|
|
227
|
+
>>> channels = ['channel1', 'channel2']
|
|
228
|
+
>>> renamed_df = rename_intensity_column(df, channels)
|
|
229
|
+
# Rename the intensity columns in the DataFrame based on the provided channel names.
|
|
230
|
+
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
channel_names = np.array(channels)
|
|
234
|
+
channel_indices = np.arange(len(channel_names), dtype=int)
|
|
235
|
+
intensity_cols = [s for s in list(df.columns) if "intensity" in s]
|
|
236
|
+
|
|
237
|
+
to_rename = {}
|
|
238
|
+
|
|
239
|
+
for k in range(len(intensity_cols)):
|
|
240
|
+
|
|
241
|
+
# identify if digit in section
|
|
242
|
+
sections = np.array(re.split("-|_", intensity_cols[k]))
|
|
243
|
+
test_digit = np.array([False for s in sections])
|
|
244
|
+
for j, s in enumerate(sections):
|
|
245
|
+
if str(s).isdigit():
|
|
246
|
+
if int(s) < len(channel_names):
|
|
247
|
+
test_digit[j] = True
|
|
248
|
+
|
|
249
|
+
if np.any(test_digit):
|
|
250
|
+
index = int(sections[np.where(test_digit)[0]][-1])
|
|
251
|
+
else:
|
|
252
|
+
print(
|
|
253
|
+
f"No valid channel index found for {intensity_cols[k]}... Skipping the renaming for {intensity_cols[k]}..."
|
|
254
|
+
)
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
channel_name = channel_names[np.where(channel_indices == index)[0]][0]
|
|
258
|
+
new_name = np.delete(
|
|
259
|
+
sections, np.where(test_digit)[0]
|
|
260
|
+
) # np.where(test_digit)[0]
|
|
261
|
+
new_name = "_".join(list(new_name))
|
|
262
|
+
new_name = new_name.replace("intensity", channel_name)
|
|
263
|
+
new_name = new_name.replace("-", "_")
|
|
264
|
+
new_name = new_name.replace("_nanmean", "_mean")
|
|
265
|
+
|
|
266
|
+
to_rename.update({intensity_cols[k]: new_name})
|
|
267
|
+
|
|
268
|
+
if "centre" in intensity_cols[k]:
|
|
269
|
+
|
|
270
|
+
measure = np.array(re.split("-|_", new_name))
|
|
271
|
+
|
|
272
|
+
if sections[-2] == "0":
|
|
273
|
+
new_name = np.delete(measure, -1)
|
|
274
|
+
new_name = "_".join(list(new_name))
|
|
275
|
+
if "edge" in intensity_cols[k]:
|
|
276
|
+
new_name = new_name.replace(
|
|
277
|
+
"center_of_mass_displacement",
|
|
278
|
+
"edge_center_of_mass_displacement_in_px",
|
|
279
|
+
)
|
|
280
|
+
else:
|
|
281
|
+
new_name = new_name.replace(
|
|
282
|
+
"center_of_mass", "center_of_mass_displacement_in_px"
|
|
283
|
+
)
|
|
284
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
285
|
+
|
|
286
|
+
elif sections[-2] == "1":
|
|
287
|
+
new_name = np.delete(measure, -1)
|
|
288
|
+
new_name = "_".join(list(new_name))
|
|
289
|
+
if "edge" in intensity_cols[k]:
|
|
290
|
+
new_name = new_name.replace(
|
|
291
|
+
"center_of_mass_displacement", "edge_center_of_mass_orientation"
|
|
292
|
+
)
|
|
293
|
+
else:
|
|
294
|
+
new_name = new_name.replace(
|
|
295
|
+
"center_of_mass", "center_of_mass_orientation"
|
|
296
|
+
)
|
|
297
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
298
|
+
|
|
299
|
+
elif sections[-2] == "2":
|
|
300
|
+
new_name = np.delete(measure, -1)
|
|
301
|
+
new_name = "_".join(list(new_name))
|
|
302
|
+
if "edge" in intensity_cols[k]:
|
|
303
|
+
new_name = new_name.replace(
|
|
304
|
+
"center_of_mass_displacement", "edge_center_of_mass_x"
|
|
305
|
+
)
|
|
306
|
+
else:
|
|
307
|
+
new_name = new_name.replace("center_of_mass", "center_of_mass_x")
|
|
308
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
309
|
+
|
|
310
|
+
elif sections[-2] == "3":
|
|
311
|
+
new_name = np.delete(measure, -1)
|
|
312
|
+
new_name = "_".join(list(new_name))
|
|
313
|
+
if "edge" in intensity_cols[k]:
|
|
314
|
+
new_name = new_name.replace(
|
|
315
|
+
"center_of_mass_displacement", "edge_center_of_mass_y"
|
|
316
|
+
)
|
|
317
|
+
else:
|
|
318
|
+
new_name = new_name.replace("center_of_mass", "center_of_mass_y")
|
|
319
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
320
|
+
|
|
321
|
+
if "radial_gradient" in intensity_cols[k]:
|
|
322
|
+
# sections = np.array(re.split('-|_', intensity_columns[k]))
|
|
323
|
+
measure = np.array(re.split("-|_", new_name))
|
|
324
|
+
|
|
325
|
+
if sections[-2] == "0":
|
|
326
|
+
new_name = np.delete(measure, -1)
|
|
327
|
+
new_name = "_".join(list(measure))
|
|
328
|
+
new_name = new_name.replace("radial_gradient", "radial_gradient")
|
|
329
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
330
|
+
|
|
331
|
+
elif sections[-2] == "1":
|
|
332
|
+
new_name = np.delete(measure, -1)
|
|
333
|
+
new_name = "_".join(list(measure))
|
|
334
|
+
new_name = new_name.replace("radial_gradient", "radial_intercept")
|
|
335
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
336
|
+
|
|
337
|
+
elif sections[-2] == "2":
|
|
338
|
+
new_name = np.delete(measure, -1)
|
|
339
|
+
new_name = "_".join(list(measure))
|
|
340
|
+
new_name = new_name.replace(
|
|
341
|
+
"radial_gradient", "radial_gradient_r2_score"
|
|
342
|
+
)
|
|
343
|
+
to_rename.update({intensity_cols[k]: new_name.replace("-", "_")})
|
|
344
|
+
|
|
345
|
+
df = df.rename(columns=to_rename)
|
|
346
|
+
|
|
347
|
+
return df
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def remove_redundant_features(features, reference_features, channel_names=None):
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
Remove redundant features from a list of features based on a reference feature list.
|
|
354
|
+
|
|
355
|
+
Parameters
|
|
356
|
+
----------
|
|
357
|
+
features : list
|
|
358
|
+
The list of features to be filtered.
|
|
359
|
+
reference_features : list
|
|
360
|
+
The reference list of features.
|
|
361
|
+
channel_names : list or None, optional
|
|
362
|
+
The list of channel names. If provided, it is used to identify and remove redundant intensity features.
|
|
363
|
+
Default is None.
|
|
364
|
+
|
|
365
|
+
Returns
|
|
366
|
+
-------
|
|
367
|
+
list
|
|
368
|
+
The filtered list of features without redundant entries.
|
|
369
|
+
|
|
370
|
+
Notes
|
|
371
|
+
-----
|
|
372
|
+
This function removes redundant features from the input list based on a reference list of features. Features that
|
|
373
|
+
appear in the reference list are removed from the input list. Additionally, if the channel_names parameter is provided,
|
|
374
|
+
it is used to identify and remove redundant intensity features. Intensity features that have the same mode (e.g., 'mean',
|
|
375
|
+
'min', 'max') as any of the channel names in the reference list are also removed.
|
|
376
|
+
|
|
377
|
+
Examples
|
|
378
|
+
--------
|
|
379
|
+
>>> features = ['area', 'intensity_mean', 'intensity_max', 'eccentricity']
|
|
380
|
+
>>> reference_features = ['area', 'eccentricity']
|
|
381
|
+
>>> filtered_features = remove_redundant_features(features, reference_features)
|
|
382
|
+
>>> filtered_features
|
|
383
|
+
['intensity_mean', 'intensity_max']
|
|
384
|
+
|
|
385
|
+
>>> channel_names = ['brightfield', 'channel1', 'channel2']
|
|
386
|
+
>>> filtered_features = remove_redundant_features(features, reference_features, channel_names)
|
|
387
|
+
>>> filtered_features
|
|
388
|
+
['area', 'eccentricity']
|
|
389
|
+
|
|
390
|
+
"""
|
|
391
|
+
|
|
392
|
+
new_features = features[:]
|
|
393
|
+
|
|
394
|
+
for f in features:
|
|
395
|
+
|
|
396
|
+
if f in reference_features:
|
|
397
|
+
new_features.remove(f)
|
|
398
|
+
|
|
399
|
+
if ("intensity" in f) and (channel_names is not None):
|
|
400
|
+
|
|
401
|
+
mode = f.split("_")[-1]
|
|
402
|
+
pattern = [a + "_" + mode for a in channel_names]
|
|
403
|
+
|
|
404
|
+
for p in pattern:
|
|
405
|
+
if p in reference_features:
|
|
406
|
+
try:
|
|
407
|
+
new_features.remove(f)
|
|
408
|
+
except:
|
|
409
|
+
pass
|
|
410
|
+
return new_features
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def remove_trajectory_measurements(
|
|
414
|
+
trajectories,
|
|
415
|
+
column_labels={
|
|
416
|
+
"track": "TRACK_ID",
|
|
417
|
+
"time": "FRAME",
|
|
418
|
+
"x": "POSITION_X",
|
|
419
|
+
"y": "POSITION_Y",
|
|
420
|
+
},
|
|
421
|
+
):
|
|
422
|
+
"""
|
|
423
|
+
Clear a measurement table, while keeping the tracking information.
|
|
424
|
+
|
|
425
|
+
Parameters
|
|
426
|
+
----------
|
|
427
|
+
trajectories : pandas.DataFrame
|
|
428
|
+
The measurement table where each line is a cell at a timepoint and each column a tracking feature or measurement.
|
|
429
|
+
column_labels : dict, optional
|
|
430
|
+
The column labels to use in the output DataFrame. Default is {'track': "TRACK_ID", 'time': 'FRAME', 'x': 'POSITION_X', 'y': 'POSITION_Y'}.
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
Returns
|
|
434
|
+
-------
|
|
435
|
+
pandas.DataFrame
|
|
436
|
+
A filtered DataFrame containing only the tracking columns.
|
|
437
|
+
|
|
438
|
+
Examples
|
|
439
|
+
--------
|
|
440
|
+
>>> trajectories_df = pd.DataFrame({
|
|
441
|
+
... 'TRACK_ID': [1, 1, 2],
|
|
442
|
+
... 'FRAME': [0, 1, 0],
|
|
443
|
+
... 'POSITION_X': [100, 105, 200],
|
|
444
|
+
... 'POSITION_Y': [150, 155, 250],
|
|
445
|
+
... 'area': [10,100,100], # Additional column to be removed
|
|
446
|
+
... })
|
|
447
|
+
>>> filtered_df = remove_trajectory_measurements(trajectories_df)
|
|
448
|
+
>>> print(filtered_df)
|
|
449
|
+
# pd.DataFrame({
|
|
450
|
+
# 'TRACK_ID': [1, 1, 2],
|
|
451
|
+
# 'FRAME': [0, 1, 0],
|
|
452
|
+
# 'POSITION_X': [100, 105, 200],
|
|
453
|
+
# 'POSITION_Y': [150, 155, 250],
|
|
454
|
+
# })
|
|
455
|
+
"""
|
|
456
|
+
|
|
457
|
+
tracks = trajectories.copy()
|
|
458
|
+
|
|
459
|
+
columns_to_keep = [
|
|
460
|
+
column_labels["track"],
|
|
461
|
+
column_labels["time"],
|
|
462
|
+
column_labels["x"],
|
|
463
|
+
column_labels["y"],
|
|
464
|
+
column_labels["x"] + "_um",
|
|
465
|
+
column_labels["y"] + "_um",
|
|
466
|
+
"class_id",
|
|
467
|
+
"t",
|
|
468
|
+
"state",
|
|
469
|
+
"generation",
|
|
470
|
+
"root",
|
|
471
|
+
"parent",
|
|
472
|
+
"ID",
|
|
473
|
+
"t0",
|
|
474
|
+
"class",
|
|
475
|
+
"status",
|
|
476
|
+
"class_color",
|
|
477
|
+
"status_color",
|
|
478
|
+
"class_firstdetection",
|
|
479
|
+
"t_firstdetection",
|
|
480
|
+
"status_firstdetection",
|
|
481
|
+
"velocity",
|
|
482
|
+
]
|
|
483
|
+
cols = list(tracks.columns)
|
|
484
|
+
for c in columns_to_keep:
|
|
485
|
+
if c not in cols:
|
|
486
|
+
columns_to_keep.remove(c)
|
|
487
|
+
|
|
488
|
+
keep = [x for x in columns_to_keep if x in cols]
|
|
489
|
+
tracks = tracks[keep]
|
|
490
|
+
|
|
491
|
+
return tracks
|
|
492
|
+
|
|
493
|
+
|
|
494
|
+
def collapse_trajectories_by_status(
|
|
495
|
+
df,
|
|
496
|
+
status=None,
|
|
497
|
+
projection="mean",
|
|
498
|
+
population="effectors",
|
|
499
|
+
groupby_columns=["position", "TRACK_ID"],
|
|
500
|
+
):
|
|
501
|
+
|
|
502
|
+
static_columns = [
|
|
503
|
+
"well_index",
|
|
504
|
+
"well_name",
|
|
505
|
+
"pos_name",
|
|
506
|
+
"position",
|
|
507
|
+
"well",
|
|
508
|
+
"status",
|
|
509
|
+
"t0",
|
|
510
|
+
"class",
|
|
511
|
+
"cell_type",
|
|
512
|
+
"concentration",
|
|
513
|
+
"antibody",
|
|
514
|
+
"pharmaceutical_agent",
|
|
515
|
+
"TRACK_ID",
|
|
516
|
+
"position",
|
|
517
|
+
"neighbor_population",
|
|
518
|
+
"reference_population",
|
|
519
|
+
"NEIGHBOR_ID",
|
|
520
|
+
"REFERENCE_ID",
|
|
521
|
+
"FRAME",
|
|
522
|
+
]
|
|
523
|
+
|
|
524
|
+
if status is None or status not in list(df.columns):
|
|
525
|
+
print("invalid status selection...")
|
|
526
|
+
return None
|
|
527
|
+
|
|
528
|
+
df = df.dropna(subset=status, ignore_index=True)
|
|
529
|
+
unique_statuses = np.unique(df[status].to_numpy())
|
|
530
|
+
|
|
531
|
+
df_sections = []
|
|
532
|
+
for s in unique_statuses:
|
|
533
|
+
subtab = df.loc[df[status] == s, :]
|
|
534
|
+
op = getattr(subtab.groupby(groupby_columns), projection)
|
|
535
|
+
subtab_projected = op(subtab.groupby(groupby_columns))
|
|
536
|
+
frame_duration = subtab.groupby(groupby_columns).size().to_numpy()
|
|
537
|
+
for c in static_columns:
|
|
538
|
+
try:
|
|
539
|
+
subtab_projected[c] = subtab.groupby(groupby_columns)[c].apply(
|
|
540
|
+
lambda x: x.unique()[0]
|
|
541
|
+
)
|
|
542
|
+
except Exception as e:
|
|
543
|
+
print(e)
|
|
544
|
+
pass
|
|
545
|
+
subtab_projected["duration_in_state"] = frame_duration
|
|
546
|
+
df_sections.append(subtab_projected)
|
|
547
|
+
|
|
548
|
+
group_table = pd.concat(df_sections, axis=0, ignore_index=True)
|
|
549
|
+
if population == "pairs":
|
|
550
|
+
for col in [
|
|
551
|
+
"duration_in_state",
|
|
552
|
+
status,
|
|
553
|
+
"neighbor_population",
|
|
554
|
+
"reference_population",
|
|
555
|
+
"NEIGHBOR_ID",
|
|
556
|
+
"REFERENCE_ID",
|
|
557
|
+
]:
|
|
558
|
+
first_column = group_table.pop(col)
|
|
559
|
+
group_table.insert(0, col, first_column)
|
|
560
|
+
else:
|
|
561
|
+
for col in ["duration_in_state", status, "TRACK_ID"]:
|
|
562
|
+
first_column = group_table.pop(col)
|
|
563
|
+
group_table.insert(0, col, first_column)
|
|
564
|
+
|
|
565
|
+
group_table.pop("FRAME")
|
|
566
|
+
group_table = group_table.sort_values(
|
|
567
|
+
by=groupby_columns + [status], ignore_index=True
|
|
568
|
+
)
|
|
569
|
+
group_table = group_table.reset_index(drop=True)
|
|
570
|
+
|
|
571
|
+
return group_table
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def tracks_to_btrack(df, exclude_nans=False):
|
|
575
|
+
"""
|
|
576
|
+
Converts a dataframe of tracked objects into the bTrack output format.
|
|
577
|
+
The function prepares tracking data, properties, and an empty graph structure for further processing.
|
|
578
|
+
|
|
579
|
+
Parameters
|
|
580
|
+
----------
|
|
581
|
+
df : pandas.DataFrame
|
|
582
|
+
A dataframe containing tracking information. The dataframe must have columns for `TRACK_ID`,
|
|
583
|
+
`FRAME`, `POSITION_Y`, `POSITION_X`, and `class_id` (among others).
|
|
584
|
+
|
|
585
|
+
exclude_nans : bool, optional, default=False
|
|
586
|
+
If True, rows with NaN values in the `class_id` column will be excluded from the dataset.
|
|
587
|
+
If False, the dataframe will retain all rows, including those with NaN in `class_id`.
|
|
588
|
+
|
|
589
|
+
Returns
|
|
590
|
+
-------
|
|
591
|
+
data : numpy.ndarray
|
|
592
|
+
A 2D numpy array containing the tracking data with columns `[TRACK_ID, FRAME, z, POSITION_Y, POSITION_X]`.
|
|
593
|
+
The `z` column is set to zero for all rows.
|
|
594
|
+
|
|
595
|
+
properties : dict
|
|
596
|
+
A dictionary where keys are property names (e.g., 'FRAME', 'state', 'generation', etc.) and values are numpy arrays
|
|
597
|
+
containing the corresponding values from the dataframe.
|
|
598
|
+
|
|
599
|
+
graph : dict
|
|
600
|
+
An empty dictionary intended to store graph-related information for the tracking data. It can be extended
|
|
601
|
+
later to represent relationships between different tracking objects.
|
|
602
|
+
|
|
603
|
+
Notes
|
|
604
|
+
-----
|
|
605
|
+
- The function assumes that the dataframe contains specific columns: `TRACK_ID`, `FRAME`, `POSITION_Y`, `POSITION_X`,
|
|
606
|
+
and `class_id`. These columns are used to construct the tracking data and properties.
|
|
607
|
+
- The `z` coordinate is set to 0 for all tracks since the function does not process 3D data.
|
|
608
|
+
- This function is useful for transforming tracking data into a format that can be used by tracking graph algorithms.
|
|
609
|
+
|
|
610
|
+
Example
|
|
611
|
+
-------
|
|
612
|
+
>>> data, properties, graph = tracks_to_btrack(df, exclude_nans=True)
|
|
613
|
+
|
|
614
|
+
"""
|
|
615
|
+
|
|
616
|
+
graph = {}
|
|
617
|
+
if exclude_nans:
|
|
618
|
+
df.dropna(subset="class_id", inplace=True)
|
|
619
|
+
df.dropna(subset="TRACK_ID", inplace=True)
|
|
620
|
+
|
|
621
|
+
df["z"] = 0.0
|
|
622
|
+
data = df[["TRACK_ID", "FRAME", "z", "POSITION_Y", "POSITION_X"]].to_numpy()
|
|
623
|
+
|
|
624
|
+
df["dummy"] = False
|
|
625
|
+
prop_cols = ["FRAME", "state", "generation", "root", "parent", "dummy", "class_id"]
|
|
626
|
+
properties = {}
|
|
627
|
+
for col in prop_cols:
|
|
628
|
+
properties.update({col: df[col].to_numpy()})
|
|
629
|
+
|
|
630
|
+
return data, properties, graph
|