celldetective 1.4.2__py3-none-any.whl → 1.5.0b0__py3-none-any.whl

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