Simple-Track 2.0.3__tar.gz → 2.0.5__tar.gz

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 (29) hide show
  1. {simple_track-2.0.3 → simple_track-2.0.5}/PKG-INFO +9 -7
  2. {simple_track-2.0.3 → simple_track-2.0.5}/README.md +8 -6
  3. {simple_track-2.0.3 → simple_track-2.0.5}/pyproject.toml +1 -1
  4. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/PKG-INFO +9 -7
  5. {simple_track-2.0.3 → simple_track-2.0.5}/src/run_simple_track.py +7 -10
  6. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/feature.py +22 -10
  7. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/flow_solver.py +2 -3
  8. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/frame.py +6 -3
  9. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/frame_output.py +1 -2
  10. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/frame_tracker.py +2 -4
  11. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/load.py +1 -2
  12. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/track.py +3 -4
  13. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_frame.py +28 -3
  14. {simple_track-2.0.3 → simple_track-2.0.5}/LICENSE +0 -0
  15. {simple_track-2.0.3 → simple_track-2.0.5}/setup.cfg +0 -0
  16. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/SOURCES.txt +0 -0
  17. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/dependency_links.txt +0 -0
  18. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/entry_points.txt +0 -0
  19. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/requires.txt +0 -0
  20. {simple_track-2.0.3 → simple_track-2.0.5}/src/Simple_Track.egg-info/top_level.txt +0 -0
  21. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/__init__.py +0 -0
  22. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/exceptions.py +0 -0
  23. {simple_track-2.0.3 → simple_track-2.0.5}/src/simpletrack/utils.py +0 -0
  24. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_feature.py +0 -0
  25. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_flow_solver.py +0 -0
  26. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_frame_tracker.py +0 -0
  27. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_mwe_output.py +0 -0
  28. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_simple_track_and_load.py +0 -0
  29. {simple_track-2.0.3 → simple_track-2.0.5}/tests/test_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Simple-Track
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Threshold-based object tracking algorithm for 2D data
5
5
  Author-email: Adam Gainford <adam.gainford@reading.ac.uk>, Thorwald Stein <t.h.m.stein@reading.ac.uk>
6
6
  License-Expression: MPL-2.0
@@ -33,13 +33,15 @@ Features are tracked between consecutive frames of data by projecting feature fi
33
33
 
34
34
  # Installation
35
35
 
36
- Simple-Track can be installed using PyPi:
36
+ Simple-Track can be installed using PyPi or conda-forge:
37
37
 
38
38
  ```
39
39
  python3 -m pip install simple-track
40
40
  ```
41
-
42
- Coming soon to conda-forge and uv
41
+ ```
42
+ conda install conda-forge::simple-track
43
+ ```
44
+ Coming soon to uv
43
45
 
44
46
  # User Guide
45
47
 
@@ -177,10 +179,10 @@ Fields (`.field` files):
177
179
 
178
180
  Features (`.csv` or `.txt` files):
179
181
  * ID: Unique feature identifier that persists between frames (i.e., a feature retains the same id across all frames that it is tracked).
180
- * Centroid: (y, x) tuple containing central location of feature.
181
- * Size: Number of pixels spanned by the feature.
182
+ * centroid: (y, x) tuple containing central location of feature.
183
+ * size: Number of pixels spanned by the feature.
182
184
  * dydx: (dy, dx) tuple containing motion vector that translated feature to its location in the current frame from the previous frame.
183
- * extreme: Maximum value contained within the feature in the input data.
185
+ * max: Maximum value contained within the feature in the input data.
184
186
  * lifetime: Number of timesteps the feature has existed for.
185
187
  * accreted: List of IDs of features that were accreted by this feature, if applicable.
186
188
  * parent: ID of parent feature that this feature split from, if applicable
@@ -11,13 +11,15 @@ Features are tracked between consecutive frames of data by projecting feature fi
11
11
 
12
12
  # Installation
13
13
 
14
- Simple-Track can be installed using PyPi:
14
+ Simple-Track can be installed using PyPi or conda-forge:
15
15
 
16
16
  ```
17
17
  python3 -m pip install simple-track
18
18
  ```
19
-
20
- Coming soon to conda-forge and uv
19
+ ```
20
+ conda install conda-forge::simple-track
21
+ ```
22
+ Coming soon to uv
21
23
 
22
24
  # User Guide
23
25
 
@@ -155,10 +157,10 @@ Fields (`.field` files):
155
157
 
156
158
  Features (`.csv` or `.txt` files):
157
159
  * ID: Unique feature identifier that persists between frames (i.e., a feature retains the same id across all frames that it is tracked).
158
- * Centroid: (y, x) tuple containing central location of feature.
159
- * Size: Number of pixels spanned by the feature.
160
+ * centroid: (y, x) tuple containing central location of feature.
161
+ * size: Number of pixels spanned by the feature.
160
162
  * dydx: (dy, dx) tuple containing motion vector that translated feature to its location in the current frame from the previous frame.
161
- * extreme: Maximum value contained within the feature in the input data.
163
+ * max: Maximum value contained within the feature in the input data.
162
164
  * lifetime: Number of timesteps the feature has existed for.
163
165
  * accreted: List of IDs of features that were accreted by this feature, if applicable.
164
166
  * parent: ID of parent feature that this feature split from, if applicable
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "Simple-Track"
7
- version = "2.0.3"
7
+ version = "2.0.5"
8
8
  authors = [
9
9
  { name="Adam Gainford", email="adam.gainford@reading.ac.uk" },
10
10
  { name="Thorwald Stein", email="t.h.m.stein@reading.ac.uk"}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Simple-Track
3
- Version: 2.0.3
3
+ Version: 2.0.5
4
4
  Summary: Threshold-based object tracking algorithm for 2D data
5
5
  Author-email: Adam Gainford <adam.gainford@reading.ac.uk>, Thorwald Stein <t.h.m.stein@reading.ac.uk>
6
6
  License-Expression: MPL-2.0
@@ -33,13 +33,15 @@ Features are tracked between consecutive frames of data by projecting feature fi
33
33
 
34
34
  # Installation
35
35
 
36
- Simple-Track can be installed using PyPi:
36
+ Simple-Track can be installed using PyPi or conda-forge:
37
37
 
38
38
  ```
39
39
  python3 -m pip install simple-track
40
40
  ```
41
-
42
- Coming soon to conda-forge and uv
41
+ ```
42
+ conda install conda-forge::simple-track
43
+ ```
44
+ Coming soon to uv
43
45
 
44
46
  # User Guide
45
47
 
@@ -177,10 +179,10 @@ Fields (`.field` files):
177
179
 
178
180
  Features (`.csv` or `.txt` files):
179
181
  * ID: Unique feature identifier that persists between frames (i.e., a feature retains the same id across all frames that it is tracked).
180
- * Centroid: (y, x) tuple containing central location of feature.
181
- * Size: Number of pixels spanned by the feature.
182
+ * centroid: (y, x) tuple containing central location of feature.
183
+ * size: Number of pixels spanned by the feature.
182
184
  * dydx: (dy, dx) tuple containing motion vector that translated feature to its location in the current frame from the previous frame.
183
- * extreme: Maximum value contained within the feature in the input data.
185
+ * max: Maximum value contained within the feature in the input data.
184
186
  * lifetime: Number of timesteps the feature has existed for.
185
187
  * accreted: List of IDs of features that were accreted by this feature, if applicable.
186
188
  * parent: ID of parent feature that this feature split from, if applicable
@@ -1,23 +1,20 @@
1
1
  import argparse
2
- import sys
3
2
 
4
3
  from simpletrack import Tracker
5
4
 
6
5
 
7
6
  def run_tracking():
8
- if len(sys.argv) < 2:
9
- raise Exception("Running SimpleTrack requires path to at least one config")
7
+ # TODO: make argparser default way of handling inputs, including configs and loaders
8
+ # Need to make sure that changes don't affect pyproject.toml entry points
9
+ # easiest just to pass the parser in to run_tracking.
10
+ parser = argparse.ArgumentParser(description="Run Simple-Track")
11
+ parser.add_argument("configs", help="Path to one or more yaml config files.")
12
+ args = parser.parse_args()
10
13
 
11
- config_paths = sys.argv[1:]
12
- for config_path in config_paths:
14
+ for config_path in args.configs:
13
15
  # With None passed into run method, uses input path in config
14
16
  Tracker(config_path).run()
15
17
 
16
18
 
17
19
  if __name__ == "__main__":
18
- # TODO: make argparser default way of handling inputs, including configs and loaders
19
- # Need to make sure that changes don't affect pyproject.toml entry points
20
- # easiest just to pass the parser in to run_tracking.
21
- msg = "Run Simple-Track. Requires path to at least one yaml config file"
22
- parser = argparse.ArgumentParser(description=msg)
23
20
  run_tracking()
@@ -1,5 +1,4 @@
1
1
  import datetime as dt
2
- from typing import Union
3
2
 
4
3
  import numpy as np
5
4
  from numpy.typing import NDArray
@@ -10,7 +9,7 @@ from simpletrack.utils import check_arrays, check_valid_ids, native
10
9
  class Feature:
11
10
  """
12
11
  Object containing details about a specific feature, including its id, time,
13
- centroid, extreme value, lifetime, and whether it has undergone any
12
+ centroid, maximum value, lifetime, and whether it has undergone any
14
13
  mergers of splits in the current timestep.
15
14
  """
16
15
 
@@ -31,7 +30,8 @@ class Feature:
31
30
  self._parent = None
32
31
  self._children = []
33
32
  self._dydx = ()
34
- self._extreme = None
33
+ self._max = None
34
+ self._mean = None
35
35
 
36
36
  def __repr__(self) -> str:
37
37
  repr_str = f"Feature id: {self._id} (provisionally {self._provisional_id}), "
@@ -141,11 +141,18 @@ class Feature:
141
141
  return native(self._dydx)
142
142
 
143
143
  @property
144
- def extreme(self) -> float:
144
+ def max(self) -> float:
145
145
  """
146
146
  Maximum value of the Feature in the raw input data
147
147
  """
148
- return self._extreme
148
+ return self._max
149
+
150
+ @property
151
+ def mean(self) -> float:
152
+ """
153
+ Mean value of the Feature in the raw input data
154
+ """
155
+ return self._mean
149
156
 
150
157
  @coords.setter
151
158
  def coords(self, new_coords: NDArray[np.integer]) -> None:
@@ -185,9 +192,13 @@ class Feature:
185
192
  id_of_accreting_feature = check_valid_ids(id_of_accreting_feature)
186
193
  self._accreted_in_next_frame_by = id_of_accreting_feature
187
194
 
188
- @extreme.setter
189
- def extreme(self, extreme_val: float) -> None:
190
- self._extreme = extreme_val
195
+ @max.setter
196
+ def max(self, max_val: float) -> None:
197
+ self._max = max_val
198
+
199
+ @mean.setter
200
+ def mean(self, mean_val: float) -> None:
201
+ self._mean = mean_val
191
202
 
192
203
  def calculate_centroid(self) -> tuple:
193
204
  """
@@ -261,7 +272,7 @@ class Feature:
261
272
 
262
273
  def summarise(
263
274
  self, output_type: str = "str", headers_only: bool = False
264
- ) -> Union[str, dict, list]:
275
+ ) -> str | dict | list:
265
276
  """
266
277
  Return a summary of the Feature properties
267
278
 
@@ -280,7 +291,8 @@ class Feature:
280
291
  "size": self.get_size(),
281
292
  # native() does not convert dydx to python type for some reason
282
293
  "dydx": tuple([val.item() for val in self._dydx]),
283
- "extreme": self._extreme,
294
+ "max": self._max,
295
+ "mean": self._mean,
284
296
  "lifetime": self._lifetime,
285
297
  "accreted": self._accreted,
286
298
  # This will not be output properly in the current workflow, since each
@@ -1,7 +1,6 @@
1
1
  import itertools
2
2
  import warnings
3
3
  from collections.abc import Iterable
4
- from typing import Union
5
4
 
6
5
  import numpy as np
7
6
  import scipy.ndimage as ndimage
@@ -69,7 +68,7 @@ class FlowSolver:
69
68
  self.apply_tukey_filtering = apply_tukey_filtering
70
69
 
71
70
  def analyse_flow(
72
- self, prev_field: Union[Frame, NDArray], current_field: Union[Frame, NDArray]
71
+ self, prev_field: Frame | NDArray, current_field: Frame | NDArray
73
72
  ) -> list[NDArray, NDArray]:
74
73
  """
75
74
  Analyses previous field and current field to identify flow field. Uses phase
@@ -330,7 +329,7 @@ class FlowSolver:
330
329
 
331
330
  subdomain_check = [
332
331
  dim % sd_shape / 2
333
- for dim, sd_shape in zip(feature_field_shape, subdomain_shape)
332
+ for dim, sd_shape in zip(feature_field_shape, subdomain_shape, strict=True)
334
333
  ]
335
334
  return not any([remainder != 0 for remainder in subdomain_check])
336
335
 
@@ -1,5 +1,4 @@
1
1
  import datetime as dt
2
- from typing import Union
3
2
 
4
3
  import numpy as np
5
4
  import scipy.ndimage as ndimage
@@ -123,7 +122,7 @@ class Frame:
123
122
  else:
124
123
  return None
125
124
 
126
- def get_flow(self) -> Union[NDArray, None]:
125
+ def get_flow(self) -> NDArray | None:
127
126
  """
128
127
  Get a list of the y-flow and x-flow fields derived by comparing features between
129
128
  this frame and a frame from a previous timestep. Flow fields are both numpy
@@ -212,7 +211,8 @@ class Frame:
212
211
  )
213
212
  # If raw field is not None, use this to find max value within Feature
214
213
  if self.raw_field is not None:
215
- feature.extreme = max(self.raw_field[feature_mask])
214
+ feature.max = np.max(self.raw_field[feature_mask])
215
+ feature.mean = np.mean(self.raw_field[feature_mask])
216
216
  self._features[feature_id] = feature
217
217
 
218
218
  def assign_displacements(self, y_flow: NDArray, x_flow: NDArray) -> None:
@@ -388,6 +388,9 @@ class Timeline:
388
388
  def __init__(self):
389
389
  self.timeline = {}
390
390
 
391
+ def __len__(self) -> int:
392
+ return len(self.timeline)
393
+
391
394
  def add_to_timelime(self, frame: Frame) -> None:
392
395
  """
393
396
  Add the input frame to the timeline, using the frame.get_time() to
@@ -2,7 +2,6 @@ import csv
2
2
  import datetime
3
3
  from ast import literal_eval
4
4
  from pathlib import Path
5
- from typing import Union
6
5
 
7
6
  import numpy as np
8
7
 
@@ -152,7 +151,7 @@ class LoadOutput:
152
151
  (contanining Frames of field and Feature data) for further inspection and analysis.
153
152
  """
154
153
 
155
- def __init__(self, st_data_path: Union[str | Path]):
154
+ def __init__(self, st_data_path: str | Path):
156
155
  self.path = Path(st_data_path)
157
156
  self.strftime = "%Y%m%d_%H%M"
158
157
  # Links field type names in outputs to attribute names in Frame
@@ -1,5 +1,3 @@
1
- from typing import Union
2
-
3
1
  import numpy as np
4
2
  from numpy.typing import NDArray
5
3
 
@@ -493,7 +491,7 @@ class FrameTracker:
493
491
  advected_feature_field: NDArray,
494
492
  current_feature_field: NDArray,
495
493
  current_feature_id: int,
496
- ) -> list[Union[int, None], Union[NDArray, None]]:
494
+ ) -> list[int | None, NDArray | None]:
497
495
  """
498
496
  Use overlap histogram to find the closest matching feature id in the advected
499
497
  field for the current_feature_id in the current_field. Any other ids that are
@@ -865,7 +863,7 @@ def advect_field_using_motion_vectors(
865
863
  dx = np.mean(x_flow[feature_mask], dtype=int)
866
864
 
867
865
  # Now, advect the feature to the new position
868
- for y_coord, x_coord in zip(*feature_mask):
866
+ for y_coord, x_coord in zip(*feature_mask, strict=True):
869
867
  advected_y_coord = y_coord + dy
870
868
  advected_x_coord = x_coord + dx
871
869
 
@@ -1,5 +1,4 @@
1
1
  import datetime as dt
2
- from typing import Union
3
2
 
4
3
  from numpy.typing import NDArray
5
4
 
@@ -39,7 +38,7 @@ class BaseLoader:
39
38
  Loaders should be
40
39
  """
41
40
 
42
- def __init__(self, input_data: Union[list[str] | dict]) -> None:
41
+ def __init__(self, input_data: list[str] | dict) -> None:
43
42
  self.domain_shape = None
44
43
  self.input_data = input_data
45
44
  # Set the iterating list
@@ -3,7 +3,6 @@ Run the SimpleTrack algorithm to track objects through a sequence of images
3
3
  """
4
4
 
5
5
  from pathlib import Path
6
- from typing import Union
7
6
 
8
7
  from yaml import safe_load
9
8
 
@@ -19,7 +18,7 @@ class Tracker:
19
18
  Simple-Track manager controlling inputs, processing, outputs
20
19
  """
21
20
 
22
- def __init__(self, config_input: Union[str | dict]) -> None:
21
+ def __init__(self, config_input: str | dict) -> None:
23
22
  """
24
23
  Initialize SimpleTrack with configuration file
25
24
 
@@ -59,7 +58,7 @@ class Tracker:
59
58
  self.frame_tracker = FrameTracker()
60
59
 
61
60
  if "OUTPUT" in self.config:
62
- self.skip_tracking = self.config["TRACKING"].get("skip_tracking", False)
61
+ self.skip_tracking = self.config["OUTPUT"].get("skip_tracking", False)
63
62
  output_path = self.config["OUTPUT"].get("path", "./output")
64
63
  expt_name = self.config["OUTPUT"].get(
65
64
  "experiment_name", "Simple-Track Experiment"
@@ -80,7 +79,7 @@ class Tracker:
80
79
  config_path,
81
80
  )
82
81
 
83
- def run(self, input_data: Union[list[str] | dict] = None) -> Timeline:
82
+ def run(self, input_data: list[str] | dict = None) -> Timeline:
84
83
  """
85
84
  Runs SimpleTrack using the designated config options.
86
85
 
@@ -103,7 +103,7 @@ def test_populate_features_valid_feature_field():
103
103
  assert test_frame.features == expected_dict
104
104
 
105
105
 
106
- def test_populate_features_sets_extreme_property():
106
+ def test_populate_features_sets_max_property():
107
107
  test_time = dt.datetime.now()
108
108
  test_frame = Frame()
109
109
  test_frame.time = test_time
@@ -123,8 +123,33 @@ def test_populate_features_sets_extreme_property():
123
123
  test_frame.raw_field = test_raw_field
124
124
  test_frame.populate_features()
125
125
 
126
- assert test_frame.get_feature(1).extreme == 10
127
- assert test_frame.get_feature(2).extreme == 20
126
+ assert test_frame.get_feature(1).max == 10
127
+ assert test_frame.get_feature(2).max == 20
128
+
129
+
130
+ def test_populate_features_sets_mean_property():
131
+ test_time = dt.datetime.now()
132
+ test_frame = Frame()
133
+ test_frame.time = test_time
134
+
135
+ test_feature_field = test_field.copy()
136
+ test_feature_field[2:6, 2:6] = 1
137
+ test_feature_field[6:9, 6:9] = 2
138
+
139
+ test_raw_field = test_field.copy()
140
+ test_raw_field[2:6, 2:6] = 10
141
+ test_raw_field[2:6, 4:6] = 50
142
+ test_raw_field[6:9, 6:9] = 20
143
+ # Set another higher maximum within the field to check mean
144
+ # only picks up the values within the feature mask
145
+ test_raw_field[0:2, 0:2] = 100
146
+
147
+ test_frame.feature_field = test_feature_field
148
+ test_frame.raw_field = test_raw_field
149
+ test_frame.populate_features()
150
+
151
+ assert test_frame.get_feature(1).mean == 30
152
+ assert test_frame.get_feature(2).mean == 20
128
153
 
129
154
 
130
155
  def test_populate_features_invalid_negative_features():
File without changes
File without changes