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