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
tests/gui/test_project.py CHANGED
@@ -1,99 +1,147 @@
1
1
  import pytest
2
2
  from PyQt5 import QtCore
3
+
4
+ import celldetective.gui.preprocessing_block
3
5
  from celldetective.gui.InitWindow import AppInitWindow
4
- from celldetective.utils import get_software_location
6
+ from celldetective import get_software_location
5
7
  import os
6
8
 
7
9
  software_location = get_software_location()
8
10
 
11
+
9
12
  @pytest.fixture
10
13
  def app(qtbot):
11
- test_app = AppInitWindow(software_location=software_location)
12
- qtbot.addWidget(test_app)
13
- return test_app
14
+ test_app = AppInitWindow(software_location=software_location)
15
+ qtbot.addWidget(test_app)
16
+ return test_app
17
+
14
18
 
15
19
  def test_open_project(app, qtbot):
16
- app.experiment_path_selection.setText(software_location + os.sep + 'examples/demo')
17
- qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
18
- qtbot.wait(10000)
20
+ app.experiment_path_selection.setText(software_location + os.sep + "examples/demo")
21
+ qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
22
+ qtbot.wait(10000)
23
+
19
24
 
20
25
  def test_launch_demo(app, qtbot):
21
- app.experiment_path_selection.setText(software_location + os.sep + 'examples/demo')
22
- qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
26
+ app.experiment_path_selection.setText(software_location + os.sep + "examples/demo")
27
+ qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
28
+
23
29
 
24
30
  def test_preprocessing_panel(app, qtbot):
25
31
 
26
- app.experiment_path_selection.setText(software_location + os.sep + 'examples/demo')
27
- qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
32
+ app.experiment_path_selection.setText(software_location + os.sep + "examples/demo")
33
+ qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
34
+
35
+ qtbot.mouseClick(
36
+ app.control_panel.PreprocessingPanel.collapse_btn,
37
+ QtCore.Qt.LeftButton,
38
+ )
39
+ qtbot.mouseClick(
40
+ app.control_panel.PreprocessingPanel.fit_correction_layout.add_correction_btn,
41
+ QtCore.Qt.LeftButton,
42
+ )
43
+ qtbot.mouseClick(
44
+ app.control_panel.PreprocessingPanel.collapse_btn,
45
+ QtCore.Qt.LeftButton,
46
+ )
28
47
 
29
- qtbot.mouseClick(app.control_panel.PreprocessingPanel.collapse_btn, QtCore.Qt.LeftButton)
30
- qtbot.mouseClick(app.control_panel.PreprocessingPanel.fit_correction_layout.add_correction_btn, QtCore.Qt.LeftButton)
31
- qtbot.mouseClick(app.control_panel.PreprocessingPanel.collapse_btn, QtCore.Qt.LeftButton)
32
48
 
33
49
  def test_app(app, qtbot):
34
50
 
35
- # Set an experiment folder and open
36
- app.experiment_path_selection.setText(os.sep.join([software_location,'examples','demo']))
37
- qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
38
-
39
- # Set a position
40
- #app.control_panel.position_list.setCurrentIndex(0)
41
- #app.control_panel.update_position_options()
42
-
43
- # View stacl
44
- qtbot.mouseClick(app.control_panel.view_stack_btn, QtCore.Qt.LeftButton)
45
- #qtbot.wait(1000)
46
- app.control_panel.viewer.close()
47
-
48
- # Expand process block
49
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].collapse_btn, QtCore.Qt.LeftButton)
50
-
51
- # Use Threshold Config Wizard
52
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].upload_model_btn, QtCore.Qt.LeftButton)
53
- qtbot.wait(1000)
54
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].SegModelLoader.threshold_config_button, QtCore.Qt.LeftButton)
55
- app.control_panel.ProcessPopulations[0].SegModelLoader.ThreshWizard.close()
56
- app.control_panel.ProcessPopulations[0].SegModelLoader.close()
57
-
58
- # Check segmentation with napari
59
- #qtbot.mouseClick(app.control_panel.ProcessEffectors.check_seg_btn, QtCore.Qt.LeftButton)
60
- # close napari?
61
-
62
- # Train model
63
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].train_btn, QtCore.Qt.LeftButton)
64
- qtbot.wait(1000)
65
- app.control_panel.ProcessPopulations[0].settings_segmentation_training.close()
66
-
67
- # Config tracking
68
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].track_config_btn, QtCore.Qt.LeftButton)
69
- qtbot.wait(1000)
70
- app.control_panel.ProcessPopulations[0].settings_tracking.close()
71
-
72
- # Config measurements
73
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].measurements_config_btn, QtCore.Qt.LeftButton)
74
- qtbot.wait(1000)
75
- app.control_panel.ProcessPopulations[0].settings_measurements.close()
76
-
77
- # Classifier widget
78
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].classify_btn, QtCore.Qt.LeftButton)
79
- qtbot.wait(1000)
80
- app.control_panel.ProcessPopulations[0].ClassifierWidget.close()
81
-
82
- # Config signal annotator
83
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].config_signal_annotator_btn, QtCore.Qt.LeftButton)
84
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].settings_signal_annotator.rgb_btn, QtCore.Qt.LeftButton)
85
- qtbot.wait(1000)
86
- app.control_panel.ProcessPopulations[0].settings_signal_annotator.close()
87
-
88
- # Signal annotator widget
89
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].check_signals_btn, QtCore.Qt.LeftButton)
90
- qtbot.wait(1000)
91
- app.control_panel.ProcessPopulations[0].event_annotator.close()
92
-
93
- # Table widget
94
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].view_tab_btn, QtCore.Qt.LeftButton)
95
- qtbot.wait(1000)
96
- app.control_panel.ProcessPopulations[0].tab_ui.close()
97
-
98
- #qtbot.mouseClick(app.control_panel.PreprocessingPanel.fit_correction_layout.add_correction_btn, QtCore.Qt.LeftButton)
99
- qtbot.mouseClick(app.control_panel.ProcessPopulations[0].collapse_btn, QtCore.Qt.LeftButton)
51
+ # Set an experiment folder and open
52
+ app.experiment_path_selection.setText(
53
+ os.sep.join([software_location, "examples", "demo"])
54
+ )
55
+ qtbot.mouseClick(app.validate_button, QtCore.Qt.LeftButton)
56
+
57
+ # Set a position
58
+ # app.control_panel.position_list.setCurrentIndex(0)
59
+ # app.control_panel.update_position_options()
60
+
61
+ # View stacl
62
+ qtbot.mouseClick(app.control_panel.view_stack_btn, QtCore.Qt.LeftButton)
63
+ # qtbot.wait(1000)
64
+ app.control_panel.viewer.close()
65
+
66
+ # Expand process block
67
+ qtbot.mouseClick(
68
+ app.control_panel.ProcessPopulations[0].collapse_btn, QtCore.Qt.LeftButton
69
+ )
70
+
71
+ # Use Threshold Config Wizard
72
+ qtbot.mouseClick(
73
+ app.control_panel.ProcessPopulations[0].upload_model_btn, QtCore.Qt.LeftButton
74
+ )
75
+ qtbot.wait(1000)
76
+ qtbot.mouseClick(
77
+ app.control_panel.ProcessPopulations[
78
+ 0
79
+ ].seg_model_loader.threshold_config_button,
80
+ QtCore.Qt.LeftButton,
81
+ )
82
+ app.control_panel.ProcessPopulations[0].seg_model_loader.thresh_wizard.close()
83
+ app.control_panel.ProcessPopulations[0].seg_model_loader.close()
84
+
85
+ # Check segmentation with napari
86
+ # qtbot.mouseClick(app.control_panel.ProcessEffectors.check_seg_btn, QtCore.Qt.LeftButton)
87
+ # close napari?
88
+
89
+ # Train model
90
+ qtbot.mouseClick(
91
+ app.control_panel.ProcessPopulations[0].train_btn, QtCore.Qt.LeftButton
92
+ )
93
+ qtbot.wait(1000)
94
+ app.control_panel.ProcessPopulations[0].settings_segmentation_training.close()
95
+
96
+ # Config tracking
97
+ qtbot.mouseClick(
98
+ app.control_panel.ProcessPopulations[0].track_config_btn, QtCore.Qt.LeftButton
99
+ )
100
+ qtbot.wait(1000)
101
+ app.control_panel.ProcessPopulations[0].settings_tracking.close()
102
+
103
+ # Config measurements
104
+ qtbot.mouseClick(
105
+ app.control_panel.ProcessPopulations[0].measurements_config_btn,
106
+ QtCore.Qt.LeftButton,
107
+ )
108
+ qtbot.wait(1000)
109
+ app.control_panel.ProcessPopulations[0].settings_measurements.close()
110
+
111
+ # Classifier widget
112
+ qtbot.mouseClick(
113
+ app.control_panel.ProcessPopulations[0].classify_btn, QtCore.Qt.LeftButton
114
+ )
115
+ qtbot.wait(1000)
116
+ app.control_panel.ProcessPopulations[0].classifier_widget.close()
117
+
118
+ # Config signal annotator
119
+ qtbot.mouseClick(
120
+ app.control_panel.ProcessPopulations[0].config_signal_annotator_btn,
121
+ QtCore.Qt.LeftButton,
122
+ )
123
+ qtbot.mouseClick(
124
+ app.control_panel.ProcessPopulations[0].settings_signal_annotator.rgb_btn,
125
+ QtCore.Qt.LeftButton,
126
+ )
127
+ qtbot.wait(1000)
128
+ app.control_panel.ProcessPopulations[0].settings_signal_annotator.close()
129
+
130
+ # Signal annotator widget
131
+ qtbot.mouseClick(
132
+ app.control_panel.ProcessPopulations[0].check_signals_btn, QtCore.Qt.LeftButton
133
+ )
134
+ qtbot.wait(1000)
135
+ app.control_panel.ProcessPopulations[0].event_annotator.close()
136
+
137
+ # Table widget
138
+ qtbot.mouseClick(
139
+ app.control_panel.ProcessPopulations[0].view_tab_btn, QtCore.Qt.LeftButton
140
+ )
141
+ qtbot.wait(1000)
142
+ app.control_panel.ProcessPopulations[0].tab_ui.close()
143
+
144
+ # qtbot.mouseClick(app.control_panel.PreprocessingPanel.fit_correction_layout.add_correction_btn, QtCore.Qt.LeftButton)
145
+ qtbot.mouseClick(
146
+ app.control_panel.ProcessPopulations[0].collapse_btn, QtCore.Qt.LeftButton
147
+ )
tests/test_filters.py CHANGED
@@ -1,24 +1,48 @@
1
1
  import unittest
2
2
  import numpy as np
3
- from celldetective.filters import gauss_filter, abs_filter
3
+ from celldetective.filters import gauss_filter, abs_filter, filter_image
4
4
 
5
5
 
6
6
  class TestFilters(unittest.TestCase):
7
7
 
8
- @classmethod
9
- def setUpClass(self):
10
- self.img = np.ones((256,256),dtype=int)
11
- self.img[100:110,100:110] = 0
12
- self.gauss_sigma = 1.6
8
+ @classmethod
9
+ def setUpClass(self):
10
+ self.img = np.ones((256, 256), dtype=int)
11
+ self.img[100:110, 100:110] = 0
12
+ self.gauss_sigma = 1.6
13
13
 
14
- def test_gauss_filter_is_float(self):
15
- self.assertIsInstance(gauss_filter(self.img, self.gauss_sigma)[0,0], float)
16
-
17
- def test_gauss_filter_has_same_shape(self):
18
- self.assertEqual(gauss_filter(self.img, self.gauss_sigma).shape, self.img.shape)
14
+ def test_gauss_filter_is_float(self):
15
+ self.assertIsInstance(gauss_filter(self.img, self.gauss_sigma)[0, 0], float)
19
16
 
20
- def test_abs_filter_is_positive(self):
21
- self.assertTrue(np.all(abs_filter(self.img) >= 0.))
17
+ def test_gauss_filter_has_same_shape(self):
18
+ self.assertEqual(gauss_filter(self.img, self.gauss_sigma).shape, self.img.shape)
22
19
 
23
- if __name__=="__main__":
24
- unittest.main()
20
+ def test_abs_filter_is_positive(self):
21
+ self.assertTrue(np.all(abs_filter(self.img) >= 0.0))
22
+
23
+ def test_filter_image_none(self):
24
+ # Should return original image if filters is None
25
+ res = filter_image(self.img, filters=None)
26
+ np.testing.assert_array_equal(res, self.img)
27
+
28
+ def test_filter_image_single(self):
29
+ # Test with a single filter: e.g. abs
30
+ # Create an image with negatives
31
+ img_neg = self.img.copy() * -1
32
+ res = filter_image(img_neg, filters=[("abs",)])
33
+ self.assertTrue(np.all(res >= 0))
34
+ np.testing.assert_array_almost_equal(res, np.abs(img_neg))
35
+
36
+ def test_filter_image_chain(self):
37
+ # Test chaining: subtract 10 then abs
38
+ # Start with ones. Subtract 10 -> -9. Abs -> 9.
39
+ img = np.ones((5, 5), dtype=float)
40
+ filters = [("subtract", 10), ("abs",)]
41
+ res = filter_image(img, filters=filters)
42
+ expected = np.abs(img - 10)
43
+ np.testing.assert_array_almost_equal(res, expected)
44
+ self.assertTrue(np.allclose(res, 9.0))
45
+
46
+
47
+ if __name__ == "__main__":
48
+ unittest.main()
@@ -0,0 +1,8 @@
1
+ # test_notebooks.py
2
+ import nbformat
3
+ from nbclient import NotebookClient
4
+
5
+ def test_notebook_runs():
6
+ nb = nbformat.read("../demos/ADCC_analysis_demo.ipynb", as_version=4)
7
+ client = NotebookClient(nb, timeout=600, kernel_name="python3")
8
+ client.execute() # raises exception if any cell fails
tests/test_tracking.py CHANGED
@@ -6,6 +6,11 @@ from celldetective.tracking import (
6
6
  extrapolate_tracks,
7
7
  filter_by_tracklength,
8
8
  interpolate_time_gaps,
9
+ interpolate_nan_properties,
10
+ compute_instantaneous_velocity,
11
+ compute_instantaneous_diffusion,
12
+ write_first_detection_class,
13
+ clean_trajectories,
9
14
  )
10
15
 
11
16
 
@@ -110,19 +115,13 @@ class TestTrackInterpolation(unittest.TestCase):
110
115
 
111
116
  def test_interpolate_tracks_as_expected(self):
112
117
  self.interpolated_tracks = interpolate_time_gaps(self.tracks)
113
- # Sort columns to ensure order is consistent
114
- self.interpolated_tracks = self.interpolated_tracks.sort_index(axis=1)
115
- self.tracks_real_intep = self.tracks_real_intep.sort_index(axis=1)
116
-
117
- # Allow for some floating point tolerance and ignore int/float differences
118
- pd.testing.assert_frame_equal(
119
- self.interpolated_tracks,
120
- self.tracks_real_intep,
121
- check_dtype=False,
122
- check_index_type=False,
123
- check_column_type=False,
124
- rtol=1e-5,
125
- atol=1e-8,
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
+ )
126
125
  )
127
126
 
128
127
 
@@ -222,5 +221,225 @@ class TestTrackExtrapolation(unittest.TestCase):
222
221
  )
223
222
 
224
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
+
225
444
  if __name__ == "__main__":
226
445
  unittest.main()