celldetective 1.4.1.post1__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 +642 -554
  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 -1700
  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 +1332 -1011
  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.1.post1.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.1.post1.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 +425 -144
  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.1.post1.dist-info/RECORD +0 -123
  148. /celldetective/{gui/processes → processes}/downloader.py +0 -0
  149. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/entry_points.txt +0 -0
  150. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/licenses/LICENSE +0 -0
  151. {celldetective-1.4.1.post1.dist-info → celldetective-1.5.0b0.dist-info}/top_level.txt +0 -0
tests/test_tracking.py CHANGED
@@ -1,164 +1,445 @@
1
1
  import unittest
2
2
  import numpy as np
3
3
  import pandas as pd
4
- from celldetective.tracking import filter_by_endpoints, extrapolate_tracks, filter_by_tracklength, interpolate_time_gaps
4
+ from celldetective.tracking import (
5
+ filter_by_endpoints,
6
+ extrapolate_tracks,
7
+ filter_by_tracklength,
8
+ interpolate_time_gaps,
9
+ interpolate_nan_properties,
10
+ compute_instantaneous_velocity,
11
+ compute_instantaneous_diffusion,
12
+ write_first_detection_class,
13
+ clean_trajectories,
14
+ )
5
15
 
6
- class TestTrackFilteringByEndpoint(unittest.TestCase):
7
16
 
8
- @classmethod
9
- def setUpClass(self):
10
- self.tracks = pd.DataFrame([{"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
11
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
12
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 30, "POSITION_Y": 5},
13
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 40, "POSITION_Y": 0},
14
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
15
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
16
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
17
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 10, "POSITION_Y": 25}
17
+ class TestTrackFilteringByEndpoint(unittest.TestCase):
18
18
 
19
- ])
19
+ @classmethod
20
+ def setUpClass(self):
21
+ self.tracks = pd.DataFrame(
22
+ [
23
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
24
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
25
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 30, "POSITION_Y": 5},
26
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 40, "POSITION_Y": 0},
27
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
28
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
29
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
30
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 10, "POSITION_Y": 25},
31
+ ]
32
+ )
20
33
 
21
- def test_filter_not_in_last(self):
22
- self.filtered_tracks = filter_by_endpoints(self.tracks, remove_not_in_first=False, remove_not_in_last=True)
23
- track_ids = list(self.filtered_tracks['TRACK_ID'].unique())
24
- self.assertEqual(track_ids,[0.])
34
+ def test_filter_not_in_last(self):
35
+ self.filtered_tracks = filter_by_endpoints(
36
+ self.tracks, remove_not_in_first=False, remove_not_in_last=True
37
+ )
38
+ track_ids = list(self.filtered_tracks["TRACK_ID"].unique())
39
+ self.assertEqual(track_ids, [0.0])
25
40
 
26
- def test_filter_not_in_first(self):
27
- self.filtered_tracks = filter_by_endpoints(self.tracks, remove_not_in_first=True, remove_not_in_last=False)
28
- track_ids = list(self.filtered_tracks['TRACK_ID'].unique())
29
- self.assertEqual(track_ids,[0.,2.])
41
+ def test_filter_not_in_first(self):
42
+ self.filtered_tracks = filter_by_endpoints(
43
+ self.tracks, remove_not_in_first=True, remove_not_in_last=False
44
+ )
45
+ track_ids = list(self.filtered_tracks["TRACK_ID"].unique())
46
+ self.assertEqual(track_ids, [0.0, 2.0])
30
47
 
31
- def test_no_filter_does_nothing(self):
32
- self.filtered_tracks = filter_by_endpoints(self.tracks, remove_not_in_first=False, remove_not_in_last=False)
33
- track_ids = list(self.filtered_tracks['TRACK_ID'].unique())
34
- self.assertEqual(track_ids,list(self.tracks['TRACK_ID'].unique()))
48
+ def test_no_filter_does_nothing(self):
49
+ self.filtered_tracks = filter_by_endpoints(
50
+ self.tracks, remove_not_in_first=False, remove_not_in_last=False
51
+ )
52
+ track_ids = list(self.filtered_tracks["TRACK_ID"].unique())
53
+ self.assertEqual(track_ids, list(self.tracks["TRACK_ID"].unique()))
35
54
 
36
55
 
37
56
  class TestTrackFilteringByLength(unittest.TestCase):
38
57
 
39
- @classmethod
40
- def setUpClass(self):
41
- self.tracks = pd.DataFrame([{"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
42
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
43
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 30, "POSITION_Y": 5},
44
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 40, "POSITION_Y": 0},
45
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
46
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
47
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
48
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 10, "POSITION_Y": 25}
49
- ])
50
-
51
- def test_filter_by_tracklength_of_zero(self):
52
- self.filtered_tracks = filter_by_tracklength(self.tracks, minimum_tracklength=0)
53
- track_ids = list(self.filtered_tracks['TRACK_ID'].unique())
54
- self.assertEqual(track_ids,[0.,1.,2.])
55
-
56
- def test_filter_by_tracklength_of_three(self):
57
- self.filtered_tracks = filter_by_tracklength(self.tracks, minimum_tracklength=3)
58
- track_ids = list(self.filtered_tracks['TRACK_ID'].unique())
59
- self.assertEqual(track_ids,[0.])
58
+ @classmethod
59
+ def setUpClass(self):
60
+ self.tracks = pd.DataFrame(
61
+ [
62
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
63
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
64
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 30, "POSITION_Y": 5},
65
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 40, "POSITION_Y": 0},
66
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
67
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
68
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
69
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 10, "POSITION_Y": 25},
70
+ ]
71
+ )
72
+
73
+ def test_filter_by_tracklength_of_zero(self):
74
+ self.filtered_tracks = filter_by_tracklength(self.tracks, minimum_tracklength=0)
75
+ track_ids = list(self.filtered_tracks["TRACK_ID"].unique())
76
+ self.assertEqual(track_ids, [0.0, 1.0, 2.0])
77
+
78
+ def test_filter_by_tracklength_of_three(self):
79
+ self.filtered_tracks = filter_by_tracklength(self.tracks, minimum_tracklength=3)
80
+ track_ids = list(self.filtered_tracks["TRACK_ID"].unique())
81
+ self.assertEqual(track_ids, [0.0])
60
82
 
61
83
 
62
84
  class TestTrackInterpolation(unittest.TestCase):
63
85
 
64
- @classmethod
65
- def setUpClass(self):
66
-
67
- self.tracks = pd.DataFrame([{"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
68
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
69
- #{"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
70
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
71
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
72
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
73
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
74
- #{"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
75
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25}
76
- ])
77
- self.tracks_real_intep = pd.DataFrame([{"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
78
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
79
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
80
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
81
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
82
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
83
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
84
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
85
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25}
86
- ])
87
-
88
-
89
- def test_interpolate_tracks_as_expected(self):
90
- self.interpolated_tracks = interpolate_time_gaps(self.tracks)
91
- self.assertTrue(np.array_equal(self.interpolated_tracks.to_numpy(), self.tracks_real_intep.to_numpy(), equal_nan=True))
86
+ @classmethod
87
+ def setUpClass(self):
88
+
89
+ self.tracks = pd.DataFrame(
90
+ [
91
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
92
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
93
+ # {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
94
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
95
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
96
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
97
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
98
+ # {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
99
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
100
+ ]
101
+ )
102
+ self.tracks_real_intep = pd.DataFrame(
103
+ [
104
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
105
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
106
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
107
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
108
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
109
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
110
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
111
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
112
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
113
+ ]
114
+ )
115
+
116
+ def test_interpolate_tracks_as_expected(self):
117
+ self.interpolated_tracks = interpolate_time_gaps(self.tracks)
118
+ # We use allclose because interpolation returns floats and strict equality might fail on some platforms
119
+ self.assertTrue(
120
+ np.allclose(
121
+ self.interpolated_tracks.to_numpy().astype(float),
122
+ self.tracks_real_intep.to_numpy().astype(float),
123
+ equal_nan=True,
124
+ )
125
+ )
126
+
92
127
 
93
128
  class TestTrackExtrapolation(unittest.TestCase):
94
129
 
95
- @classmethod
96
- def setUpClass(self):
97
-
98
- self.tracks = pd.DataFrame([{"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
99
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
100
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
101
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
102
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
103
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
104
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
105
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
106
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25}
107
- ])
108
- self.tracks_pre_extrapol = pd.DataFrame([
109
- {"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
110
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
111
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
112
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
113
- {"TRACK_ID": 1., "FRAME": 0, "POSITION_X": 5, "POSITION_Y": 20},
114
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
115
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
116
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
117
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
118
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25}
119
- ])
120
- self.tracks_post_extrapol = pd.DataFrame([
121
- {"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
122
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
123
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
124
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
125
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
126
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
127
- {"TRACK_ID": 1., "FRAME": 3, "POSITION_X": 10, "POSITION_Y": 25},
128
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
129
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
130
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
131
- {"TRACK_ID": 2., "FRAME": 3, "POSITION_X": 0, "POSITION_Y": 25}
132
- ])
133
-
134
- self.tracks_full_extrapol = pd.DataFrame([
135
- {"TRACK_ID": 0., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
136
- {"TRACK_ID": 0., "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
137
- {"TRACK_ID": 0., "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
138
- {"TRACK_ID": 0., "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
139
- {"TRACK_ID": 1., "FRAME": 0, "POSITION_X": 5, "POSITION_Y": 20},
140
- {"TRACK_ID": 1., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
141
- {"TRACK_ID": 1., "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
142
- {"TRACK_ID": 1., "FRAME": 3, "POSITION_X": 10, "POSITION_Y": 25},
143
- {"TRACK_ID": 2., "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
144
- {"TRACK_ID": 2., "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
145
- {"TRACK_ID": 2., "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
146
- {"TRACK_ID": 2., "FRAME": 3, "POSITION_X": 0, "POSITION_Y": 25}
147
- ])
148
-
149
-
150
- def test_pre_extrapolate(self):
151
- self.extrapolated_tracks = extrapolate_tracks(self.tracks,post=False, pre=True)
152
- self.assertTrue(np.array_equal(self.extrapolated_tracks.to_numpy(), self.tracks_pre_extrapol.to_numpy(), equal_nan=True))
153
-
154
- def test_post_extrapolate(self):
155
- self.extrapolated_tracks = extrapolate_tracks(self.tracks,post=True, pre=False)
156
- self.assertTrue(np.array_equal(self.extrapolated_tracks.to_numpy(), self.tracks_post_extrapol.to_numpy(), equal_nan=True))
157
-
158
- def test_full_extrapolate(self):
159
- self.extrapolated_tracks = extrapolate_tracks(self.tracks,post=True, pre=True)
160
- self.assertTrue(np.array_equal(self.extrapolated_tracks.to_numpy(), self.tracks_full_extrapol.to_numpy(), equal_nan=True))
161
-
162
-
163
- if __name__=="__main__":
164
- unittest.main()
130
+ @classmethod
131
+ def setUpClass(self):
132
+
133
+ self.tracks = pd.DataFrame(
134
+ [
135
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
136
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
137
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
138
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
139
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
140
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
141
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
142
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
143
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
144
+ ]
145
+ )
146
+ self.tracks_pre_extrapol = pd.DataFrame(
147
+ [
148
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
149
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
150
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
151
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
152
+ {"TRACK_ID": 1.0, "FRAME": 0, "POSITION_X": 5, "POSITION_Y": 20},
153
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
154
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
155
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
156
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
157
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
158
+ ]
159
+ )
160
+ self.tracks_post_extrapol = pd.DataFrame(
161
+ [
162
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
163
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
164
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
165
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
166
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
167
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
168
+ {"TRACK_ID": 1.0, "FRAME": 3, "POSITION_X": 10, "POSITION_Y": 25},
169
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
170
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
171
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
172
+ {"TRACK_ID": 2.0, "FRAME": 3, "POSITION_X": 0, "POSITION_Y": 25},
173
+ ]
174
+ )
175
+
176
+ self.tracks_full_extrapol = pd.DataFrame(
177
+ [
178
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 15},
179
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
180
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": 5},
181
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
182
+ {"TRACK_ID": 1.0, "FRAME": 0, "POSITION_X": 5, "POSITION_Y": 20},
183
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
184
+ {"TRACK_ID": 1.0, "FRAME": 2, "POSITION_X": 10, "POSITION_Y": 25},
185
+ {"TRACK_ID": 1.0, "FRAME": 3, "POSITION_X": 10, "POSITION_Y": 25},
186
+ {"TRACK_ID": 2.0, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 25},
187
+ {"TRACK_ID": 2.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 25},
188
+ {"TRACK_ID": 2.0, "FRAME": 2, "POSITION_X": 0, "POSITION_Y": 25},
189
+ {"TRACK_ID": 2.0, "FRAME": 3, "POSITION_X": 0, "POSITION_Y": 25},
190
+ ]
191
+ )
192
+
193
+ def test_pre_extrapolate(self):
194
+ self.extrapolated_tracks = extrapolate_tracks(self.tracks, post=False, pre=True)
195
+ self.assertTrue(
196
+ np.array_equal(
197
+ self.extrapolated_tracks.to_numpy(),
198
+ self.tracks_pre_extrapol.to_numpy(),
199
+ equal_nan=True,
200
+ )
201
+ )
202
+
203
+ def test_post_extrapolate(self):
204
+ self.extrapolated_tracks = extrapolate_tracks(self.tracks, post=True, pre=False)
205
+ self.assertTrue(
206
+ np.array_equal(
207
+ self.extrapolated_tracks.to_numpy(),
208
+ self.tracks_post_extrapol.to_numpy(),
209
+ equal_nan=True,
210
+ )
211
+ )
212
+
213
+ def test_full_extrapolate(self):
214
+ self.extrapolated_tracks = extrapolate_tracks(self.tracks, post=True, pre=True)
215
+ self.assertTrue(
216
+ np.array_equal(
217
+ self.extrapolated_tracks.to_numpy(),
218
+ self.tracks_full_extrapol.to_numpy(),
219
+ equal_nan=True,
220
+ )
221
+ )
222
+
223
+
224
+ class TestTrackInterpolationNaN(unittest.TestCase):
225
+ @classmethod
226
+ def setUpClass(self):
227
+ self.tracks = pd.DataFrame(
228
+ [
229
+ {"TRACK_ID": 0.0, "FRAME": 0, "POSITION_X": np.nan, "POSITION_Y": 15},
230
+ {"TRACK_ID": 0.0, "FRAME": 1, "POSITION_X": 15, "POSITION_Y": 10},
231
+ {"TRACK_ID": 0.0, "FRAME": 2, "POSITION_X": 20, "POSITION_Y": np.nan},
232
+ {"TRACK_ID": 0.0, "FRAME": 3, "POSITION_X": 25, "POSITION_Y": 0},
233
+ {"TRACK_ID": 1.0, "FRAME": 1, "POSITION_X": 5, "POSITION_Y": 20},
234
+ {
235
+ "TRACK_ID": 1.0,
236
+ "FRAME": 2,
237
+ "POSITION_X": np.nan,
238
+ "POSITION_Y": np.nan,
239
+ },
240
+ {"TRACK_ID": 1.0, "FRAME": 3, "POSITION_X": 15, "POSITION_Y": 30},
241
+ ]
242
+ )
243
+
244
+ def test_interpolate_nan(self):
245
+ interpolated = interpolate_nan_properties(self.tracks.copy())
246
+
247
+ # Track 0: Start NaN should be filled by first valid (bfill), End NaN should be filled by last valid (ffill)
248
+ # But `interpolate_per_track` uses limit_direction="both", so it acts as ffill+bfill
249
+
250
+ # Track 0, Frame 0, Pos X: Should be 15 (bfill from next)
251
+ self.assertEqual(
252
+ interpolated.loc[
253
+ (interpolated.TRACK_ID == 0) & (interpolated.FRAME == 0), "POSITION_X"
254
+ ].values[0],
255
+ 15.0,
256
+ )
257
+
258
+ # Track 0, Frame 2, Pos Y: Should be (10 + 0) / 2 = 5 (linear interp)
259
+ self.assertEqual(
260
+ interpolated.loc[
261
+ (interpolated.TRACK_ID == 0) & (interpolated.FRAME == 2), "POSITION_Y"
262
+ ].values[0],
263
+ 5.0,
264
+ )
265
+
266
+ # Track 1, Frame 2, Pos X: (5 + 15) / 2 = 10
267
+ self.assertEqual(
268
+ interpolated.loc[
269
+ (interpolated.TRACK_ID == 1) & (interpolated.FRAME == 2), "POSITION_X"
270
+ ].values[0],
271
+ 10.0,
272
+ )
273
+
274
+
275
+ class TestPhysics(unittest.TestCase):
276
+ @classmethod
277
+ def setUpClass(self):
278
+ # Linear motion: dx=1, dy=0, dt=1 -> v=1
279
+ self.tracks_linear = pd.DataFrame(
280
+ [
281
+ {"TRACK_ID": 0, "FRAME": 0, "POSITION_X": 0, "POSITION_Y": 0},
282
+ {"TRACK_ID": 0, "FRAME": 1, "POSITION_X": 1, "POSITION_Y": 0},
283
+ {"TRACK_ID": 0, "FRAME": 2, "POSITION_X": 2, "POSITION_Y": 0},
284
+ ]
285
+ )
286
+
287
+ # Stationary: v=0
288
+ self.tracks_static = pd.DataFrame(
289
+ [
290
+ {"TRACK_ID": 1, "FRAME": 0, "POSITION_X": 10, "POSITION_Y": 10},
291
+ {"TRACK_ID": 1, "FRAME": 1, "POSITION_X": 10, "POSITION_Y": 10},
292
+ ]
293
+ )
294
+
295
+ def test_velocity(self):
296
+ v_linear = compute_instantaneous_velocity(self.tracks_linear.copy())
297
+ # First point has NaN velocity ideally or 0 depending on implementation.
298
+ # Looking at code: diff() produces NaN for first element.
299
+ self.assertTrue(np.isnan(v_linear.iloc[0]["velocity"]))
300
+ self.assertTrue(np.allclose(v_linear.iloc[1:]["velocity"], 1.0))
301
+
302
+ v_static = compute_instantaneous_velocity(self.tracks_static.copy())
303
+ self.assertTrue(np.allclose(v_static.iloc[1:]["velocity"], 0.0))
304
+
305
+ def test_diffusion(self):
306
+ # Simple diffusion test
307
+ # Track 0: Linear motion should have diffusion related to displacement
308
+ d_linear = compute_instantaneous_diffusion(self.tracks_linear.copy())
309
+ # Diffusion computation requires 3 points (t-1, t, t+1)
310
+ # So for 3 points, only the middle one (index 1) might have value
311
+
312
+ # We need more points for a meaningful test or check code logic
313
+ # Code: if len(x) > 3: ...
314
+
315
+ tracks_long = pd.DataFrame(
316
+ {
317
+ "TRACK_ID": [0] * 5,
318
+ "FRAME": [0, 1, 2, 3, 4],
319
+ "POSITION_X": [0, 1, 2, 3, 4],
320
+ "POSITION_Y": [0, 0, 0, 0, 0],
321
+ }
322
+ )
323
+ d_long = compute_instantaneous_diffusion(tracks_long)
324
+ self.assertIn("diffusion", d_long.columns)
325
+ # Check that we have some non-nan values
326
+ valid_diff = d_long["diffusion"].dropna()
327
+ self.assertGreater(len(valid_diff), 0)
328
+
329
+
330
+ class TestFirstDetection(unittest.TestCase):
331
+ def test_first_detection_start(self):
332
+ df = pd.DataFrame(
333
+ [
334
+ {
335
+ "TRACK_ID": 0,
336
+ "FRAME": 0,
337
+ "POSITION_X": 10,
338
+ "POSITION_Y": 10,
339
+ "class_id": 1,
340
+ },
341
+ {
342
+ "TRACK_ID": 0,
343
+ "FRAME": 1,
344
+ "POSITION_X": 11,
345
+ "POSITION_Y": 11,
346
+ "class_id": 1,
347
+ },
348
+ ]
349
+ )
350
+ # Track starts at frame 0 -> class 2 (invalid/start)
351
+ res = write_first_detection_class(df.copy())
352
+ self.assertEqual(res["class_firstdetection"].iloc[0], 2)
353
+ self.assertEqual(res["t_firstdetection"].iloc[0], -1)
354
+
355
+ def test_first_detection_middle(self):
356
+ df = pd.DataFrame(
357
+ [
358
+ {
359
+ "TRACK_ID": 1,
360
+ "FRAME": 5,
361
+ "POSITION_X": 50,
362
+ "POSITION_Y": 50,
363
+ "class_id": 1,
364
+ },
365
+ {
366
+ "TRACK_ID": 1,
367
+ "FRAME": 6,
368
+ "POSITION_X": 51,
369
+ "POSITION_Y": 51,
370
+ "class_id": 1,
371
+ },
372
+ ]
373
+ )
374
+ # Track starts at frame 5 -> class 0 (valid)
375
+ res = write_first_detection_class(df.copy())
376
+ self.assertEqual(res["class_firstdetection"].iloc[0], 0)
377
+ # t_first should be float(t_first) - dt (dt=1) => 5 - 1 = 4.0
378
+ self.assertEqual(res["t_firstdetection"].iloc[0], 4.0)
379
+
380
+ def test_first_detection_edge(self):
381
+ df = pd.DataFrame(
382
+ [
383
+ {
384
+ "TRACK_ID": 2,
385
+ "FRAME": 5,
386
+ "POSITION_X": 5,
387
+ "POSITION_Y": 50,
388
+ "class_id": 1,
389
+ }, # Near edge x=5 < 20
390
+ ]
391
+ )
392
+ # Edge threshold default 20
393
+ res = write_first_detection_class(df.copy(), img_shape=(100, 100))
394
+ self.assertEqual(res["class_firstdetection"].iloc[0], 2)
395
+
396
+
397
+ class TestCleanTrajectories(unittest.TestCase):
398
+ def test_clean_pipeline(self):
399
+ # A mix of short tracks, nan gaps, time gaps
400
+ tracks = pd.DataFrame(
401
+ [
402
+ # Short track (length 2)
403
+ {"TRACK_ID": 0, "FRAME": 0, "POSITION_X": 0, "POSITION_Y": 0},
404
+ {"TRACK_ID": 0, "FRAME": 1, "POSITION_X": 1, "POSITION_Y": 1},
405
+ # Good track with gap
406
+ {"TRACK_ID": 1, "FRAME": 0, "POSITION_X": 0, "POSITION_Y": 0},
407
+ {
408
+ "TRACK_ID": 1,
409
+ "FRAME": 2,
410
+ "POSITION_X": 2,
411
+ "POSITION_Y": 2,
412
+ }, # Time gap
413
+ # Track with NaN
414
+ {"TRACK_ID": 2, "FRAME": 0, "POSITION_X": 0, "POSITION_Y": 0},
415
+ {"TRACK_ID": 2, "FRAME": 1, "POSITION_X": np.nan, "POSITION_Y": 1},
416
+ {"TRACK_ID": 2, "FRAME": 2, "POSITION_X": 2, "POSITION_Y": 2},
417
+ ]
418
+ )
419
+
420
+ # Clean: min length 3, interpolate time, interpolate nan
421
+ cleaned = clean_trajectories(
422
+ tracks.copy(),
423
+ minimum_tracklength=2,
424
+ interpolate_position_gaps=True,
425
+ interpolate_na=True,
426
+ remove_not_in_first=False,
427
+ remove_not_in_last=False,
428
+ )
429
+
430
+ # Track 0 (len 2) is NOT > 2, so it should be gone.
431
+ self.assertNotIn(0, cleaned["TRACK_ID"].unique())
432
+
433
+ # Track 1 should have frame 1 filled
434
+ self.assertIn(1, cleaned["TRACK_ID"].unique())
435
+ t1 = cleaned[cleaned.TRACK_ID == 1]
436
+ self.assertIn(1.0, t1.FRAME.values) # interpolated frame
437
+
438
+ # Track 2 should have nan filled
439
+ self.assertIn(2, cleaned["TRACK_ID"].unique())
440
+ t2_f1 = cleaned[(cleaned.TRACK_ID == 2) & (cleaned.FRAME == 1)]
441
+ self.assertFalse(np.isnan(t2_f1.POSITION_X.values[0]))
442
+
443
+
444
+ if __name__ == "__main__":
445
+ unittest.main()