Simple-Track 2.0.0__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.
- simple_track-2.0.0.dist-info/METADATA +218 -0
- simple_track-2.0.0.dist-info/RECORD +17 -0
- simple_track-2.0.0.dist-info/WHEEL +5 -0
- simple_track-2.0.0.dist-info/entry_points.txt +2 -0
- simple_track-2.0.0.dist-info/licenses/LICENSE +373 -0
- simple_track-2.0.0.dist-info/top_level.txt +1 -0
- simpletrack/__init__.py +1 -0
- simpletrack/exceptions.py +51 -0
- simpletrack/feature.py +322 -0
- simpletrack/flow_solver.py +589 -0
- simpletrack/frame.py +521 -0
- simpletrack/frame_output.py +295 -0
- simpletrack/frame_tracker.py +962 -0
- simpletrack/load.py +170 -0
- simpletrack/run_simple_track.py +12 -0
- simpletrack/track.py +281 -0
- simpletrack/utils.py +145 -0
simpletrack/feature.py
ADDED
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from typing import Union
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numpy.typing import NDArray
|
|
6
|
+
|
|
7
|
+
from simpletrack.utils import check_arrays, check_valid_ids, native
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class Feature:
|
|
11
|
+
"""
|
|
12
|
+
Object containing details about a specific feature, including its id, time,
|
|
13
|
+
centroid, extreme value, lifetime, and whether it has undergone any
|
|
14
|
+
mergers of splits in the current timestep.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self, id: int, feature_coords: NDArray[np.integer], time: dt.datetime
|
|
19
|
+
) -> None:
|
|
20
|
+
check_arrays(feature_coords, ndim=2, dtype=int)
|
|
21
|
+
id = check_valid_ids(id)
|
|
22
|
+
self._id = native(id)
|
|
23
|
+
self._provisional_id = None
|
|
24
|
+
self._feature_coords = feature_coords
|
|
25
|
+
self._time = time
|
|
26
|
+
self._centroid = None
|
|
27
|
+
self._lifetime = 1
|
|
28
|
+
self._final_timestep = False
|
|
29
|
+
self._accreted = []
|
|
30
|
+
self._accreted_in_next_frame_by = None
|
|
31
|
+
self._parent = None
|
|
32
|
+
self._children = []
|
|
33
|
+
self._dydx = ()
|
|
34
|
+
self._extreme = None
|
|
35
|
+
|
|
36
|
+
def __repr__(self) -> str:
|
|
37
|
+
repr_str = f"Feature id: {self._id} (provisionally {self._provisional_id}), "
|
|
38
|
+
repr_str += f"lifetime: {self._lifetime} timestep(s) at time: {self._time}"
|
|
39
|
+
return repr_str
|
|
40
|
+
|
|
41
|
+
def __eq__(self, other):
|
|
42
|
+
return (
|
|
43
|
+
self._time == other._time
|
|
44
|
+
and self._id == other._id
|
|
45
|
+
and np.array_equal(self._feature_coords, other._feature_coords)
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
@property
|
|
49
|
+
def id(self) -> int:
|
|
50
|
+
"""
|
|
51
|
+
The id of the Feature, a positive nonzero integer
|
|
52
|
+
"""
|
|
53
|
+
return self._id
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def provisional_id(self) -> int:
|
|
57
|
+
"""
|
|
58
|
+
A provisional id for the Feature, used when matching Features between Frames
|
|
59
|
+
before final id assignement.
|
|
60
|
+
"""
|
|
61
|
+
return self._provisional_id
|
|
62
|
+
|
|
63
|
+
@property
|
|
64
|
+
def centroid(self) -> tuple:
|
|
65
|
+
"""
|
|
66
|
+
Central point of the Feature, calculated as the mean of all y, x
|
|
67
|
+
coordinates spanned by the Feature
|
|
68
|
+
"""
|
|
69
|
+
if self._centroid is None:
|
|
70
|
+
self._centroid = self.calculate_centroid()
|
|
71
|
+
return self._centroid
|
|
72
|
+
|
|
73
|
+
@property
|
|
74
|
+
def time(self) -> dt.datetime:
|
|
75
|
+
"""
|
|
76
|
+
Time that the Feature exists at.
|
|
77
|
+
"""
|
|
78
|
+
return self._time
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def coords(self) -> NDArray[np.integer]:
|
|
82
|
+
"""
|
|
83
|
+
Coordinates spanned by the Feature, as a 2D array of shape (2, n),
|
|
84
|
+
where the first row contains y coordinates, and the second
|
|
85
|
+
row contains x coordinates.
|
|
86
|
+
"""
|
|
87
|
+
return self._feature_coords
|
|
88
|
+
|
|
89
|
+
@property
|
|
90
|
+
def lifetime(self) -> int:
|
|
91
|
+
"""
|
|
92
|
+
Lifetime that the current Feature has existed for.
|
|
93
|
+
If lifetime = 1, it initiated at the current timestep.
|
|
94
|
+
"""
|
|
95
|
+
return self._lifetime
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def accreted(self) -> list[int]:
|
|
99
|
+
"""
|
|
100
|
+
List of Feature ids that have been accreted by this Feature in the
|
|
101
|
+
current timestep, if any. Return None if no accreted features
|
|
102
|
+
"""
|
|
103
|
+
if len(self._accreted) < 1:
|
|
104
|
+
return None
|
|
105
|
+
return self._accreted
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def accreted_in_next_frame_by(self) -> int:
|
|
109
|
+
"""
|
|
110
|
+
ID of Feature that accretes this Feature in the next frame, if any.
|
|
111
|
+
This will not be known until the next frame of data has been processed.
|
|
112
|
+
"""
|
|
113
|
+
return self._accreted_in_next_frame_by
|
|
114
|
+
|
|
115
|
+
@property
|
|
116
|
+
def parent(self) -> int:
|
|
117
|
+
"""
|
|
118
|
+
If this Feature split from another Feature in the current timestep,
|
|
119
|
+
this is the ID of the parent Feature, Otherwise, this is None.
|
|
120
|
+
"""
|
|
121
|
+
return self._parent
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def children(self) -> list[int]:
|
|
125
|
+
"""
|
|
126
|
+
If other Features split from this Feature in the current timestep,
|
|
127
|
+
this is the list of IDs of those child Features. Otherwise this is None
|
|
128
|
+
"""
|
|
129
|
+
if len(self._children) < 1:
|
|
130
|
+
return None
|
|
131
|
+
return self._children
|
|
132
|
+
|
|
133
|
+
@property
|
|
134
|
+
def dydx(self) -> tuple:
|
|
135
|
+
"""
|
|
136
|
+
Motion vector that translated the current Feature from its position in the
|
|
137
|
+
previous frame to its position in the current frame. This is calculated from
|
|
138
|
+
the mean of y_flow, x_flow values spanned by the Feature in the
|
|
139
|
+
frame with the same timestamp.
|
|
140
|
+
"""
|
|
141
|
+
return native(self._dydx)
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def extreme(self) -> float:
|
|
145
|
+
"""
|
|
146
|
+
Maximum value of the Feature in the raw input data
|
|
147
|
+
"""
|
|
148
|
+
return self._extreme
|
|
149
|
+
|
|
150
|
+
@coords.setter
|
|
151
|
+
def coords(self, new_coords: NDArray[np.integer]) -> None:
|
|
152
|
+
self._feature_coords = new_coords
|
|
153
|
+
self._centroid = self.calculate_centroid() # Update centroid when coords change
|
|
154
|
+
|
|
155
|
+
@parent.setter
|
|
156
|
+
def parent(self, parent_id: int) -> None:
|
|
157
|
+
if parent_id is None:
|
|
158
|
+
self._parent = None
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
parent_id = check_valid_ids(parent_id)
|
|
162
|
+
self._parent = native(parent_id)
|
|
163
|
+
|
|
164
|
+
@dydx.setter
|
|
165
|
+
def dydx(self, dy_dx: tuple) -> None:
|
|
166
|
+
self._dydx = native(dy_dx)
|
|
167
|
+
|
|
168
|
+
@id.setter
|
|
169
|
+
def id(self, _id: int) -> None:
|
|
170
|
+
_id = check_valid_ids(_id)
|
|
171
|
+
self._id = native(_id)
|
|
172
|
+
|
|
173
|
+
@lifetime.setter
|
|
174
|
+
def lifetime(self, lifetime: int) -> None:
|
|
175
|
+
self._lifetime = native(lifetime)
|
|
176
|
+
|
|
177
|
+
@provisional_id.setter
|
|
178
|
+
def provisional_id(self, _id: int) -> None:
|
|
179
|
+
if _id is not None:
|
|
180
|
+
_id = check_valid_ids(_id)
|
|
181
|
+
self._provisional_id = native(_id)
|
|
182
|
+
|
|
183
|
+
@accreted_in_next_frame_by.setter
|
|
184
|
+
def accreted_in_next_frame_by(self, id_of_accreting_feature: int):
|
|
185
|
+
id_of_accreting_feature = check_valid_ids(id_of_accreting_feature)
|
|
186
|
+
self._accreted_in_next_frame_by = id_of_accreting_feature
|
|
187
|
+
|
|
188
|
+
@extreme.setter
|
|
189
|
+
def extreme(self, extreme_val: float) -> None:
|
|
190
|
+
self._extreme = extreme_val
|
|
191
|
+
|
|
192
|
+
def calculate_centroid(self) -> tuple:
|
|
193
|
+
"""
|
|
194
|
+
Calculate centroid of the Feature as the mean of all y, x coordinates
|
|
195
|
+
spanned by the Feature
|
|
196
|
+
|
|
197
|
+
Returns:
|
|
198
|
+
tuple: (y_centroid, x_centroid)
|
|
199
|
+
"""
|
|
200
|
+
y_centroid = native(np.mean(self._feature_coords[0, :]))
|
|
201
|
+
x_centroid = native(np.mean(self._feature_coords[1, :]))
|
|
202
|
+
return (y_centroid, x_centroid)
|
|
203
|
+
|
|
204
|
+
def accrete_ids(self, feature_ids: int | list[int], replace: bool = False) -> None:
|
|
205
|
+
"""
|
|
206
|
+
Add input ids to the list of accreted_ids contained in this Feature.
|
|
207
|
+
|
|
208
|
+
Args:
|
|
209
|
+
feature_ids (int | list[int]):
|
|
210
|
+
ID or list of IDs of features to be added to the accreted list for
|
|
211
|
+
this Feature
|
|
212
|
+
replace (bool, optional):
|
|
213
|
+
If True, replaces existing accreted ids with the input ids,
|
|
214
|
+
rather than adding inputs to the existing list.
|
|
215
|
+
Defaults to False.
|
|
216
|
+
"""
|
|
217
|
+
feature_ids = check_valid_ids(feature_ids)
|
|
218
|
+
existing_ids = [] if replace else self._accreted
|
|
219
|
+
|
|
220
|
+
if isinstance(feature_ids, int):
|
|
221
|
+
existing_ids.append(native(feature_ids))
|
|
222
|
+
elif isinstance(feature_ids, np.ndarray):
|
|
223
|
+
existing_ids.extend(feature_ids.tolist())
|
|
224
|
+
else:
|
|
225
|
+
existing_ids.extend(feature_ids)
|
|
226
|
+
|
|
227
|
+
self._accreted = existing_ids
|
|
228
|
+
|
|
229
|
+
def spawns(self, feature_ids: int | list[int], replace: bool = False) -> None:
|
|
230
|
+
"""
|
|
231
|
+
Add input ids to the list of child ids for this Feature.
|
|
232
|
+
If replace is True, replaces existing child ids with the input ids,
|
|
233
|
+
rather than adding to existing list.
|
|
234
|
+
"""
|
|
235
|
+
feature_ids = check_valid_ids(feature_ids)
|
|
236
|
+
existing_ids = [] if replace else self._children
|
|
237
|
+
|
|
238
|
+
if isinstance(feature_ids, int):
|
|
239
|
+
existing_ids.append(feature_ids)
|
|
240
|
+
elif isinstance(feature_ids, np.ndarray):
|
|
241
|
+
existing_ids.extend(feature_ids.tolist())
|
|
242
|
+
else:
|
|
243
|
+
existing_ids.extend(feature_ids)
|
|
244
|
+
|
|
245
|
+
self._children = existing_ids
|
|
246
|
+
|
|
247
|
+
def get_size(self) -> int:
|
|
248
|
+
"""
|
|
249
|
+
Get number of pixels spanned by the Feature, calculated as the number of
|
|
250
|
+
coordinate pairs in feature_coords array
|
|
251
|
+
"""
|
|
252
|
+
return len(self._feature_coords[0])
|
|
253
|
+
|
|
254
|
+
def set_as_final_timestep(self) -> None:
|
|
255
|
+
"""
|
|
256
|
+
Set the feature as being in its final timestep, which means it is either
|
|
257
|
+
accreting into another feature in the next frame, or it is dissipating
|
|
258
|
+
in the next frame.
|
|
259
|
+
"""
|
|
260
|
+
self._final_timestep = True
|
|
261
|
+
|
|
262
|
+
def summarise(
|
|
263
|
+
self, output_type: str = "str", headers_only: bool = False
|
|
264
|
+
) -> Union[str, dict, list]:
|
|
265
|
+
"""
|
|
266
|
+
Return a summary of the Feature properties
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
output_type (str, optional):
|
|
270
|
+
Format to return the summary in. Either "str" or "dict"
|
|
271
|
+
Defaults to "str".
|
|
272
|
+
headers_only (bool, optional):
|
|
273
|
+
Whether to return just the headers of the summary
|
|
274
|
+
(i.e., the keys of the summary dict)
|
|
275
|
+
Defaults to False.
|
|
276
|
+
"""
|
|
277
|
+
summary = {
|
|
278
|
+
"id": self._id,
|
|
279
|
+
"centroid": self.centroid,
|
|
280
|
+
"size": self.get_size(),
|
|
281
|
+
# native() does not convert dydx to python type for some reason
|
|
282
|
+
"dydx": tuple([val.item() for val in self._dydx]),
|
|
283
|
+
"extreme": self._extreme,
|
|
284
|
+
"lifetime": self._lifetime,
|
|
285
|
+
"accreted": self._accreted,
|
|
286
|
+
# This will not be output properly in the current workflow, since each
|
|
287
|
+
# output occurs after the current frame analysis has finished, but this
|
|
288
|
+
# can only be set after comparison with the next frame of data.
|
|
289
|
+
# "accredted_in_next_frame_by": self._accreted_in_next_frame_by,
|
|
290
|
+
"parent": self._parent,
|
|
291
|
+
"children": self._children,
|
|
292
|
+
}
|
|
293
|
+
if headers_only:
|
|
294
|
+
return list(summary.keys())
|
|
295
|
+
if output_type == "str":
|
|
296
|
+
return str(summary)
|
|
297
|
+
elif output_type == "dict":
|
|
298
|
+
return summary
|
|
299
|
+
else:
|
|
300
|
+
raise ValueError("output_type must be 'str' or 'dict'")
|
|
301
|
+
|
|
302
|
+
def is_new(self) -> bool:
|
|
303
|
+
"""
|
|
304
|
+
Returns bool whether the Feature is 'new' in the sense that it
|
|
305
|
+
has lifetime of 1 AND it has not split from another Feature
|
|
306
|
+
(i.e., it has no parent)
|
|
307
|
+
"""
|
|
308
|
+
return self._lifetime == 1 and self._parent is None
|
|
309
|
+
|
|
310
|
+
def is_dissipating(self) -> bool:
|
|
311
|
+
"""
|
|
312
|
+
Returns bool whether the Feature is 'dissipating' in the sense that
|
|
313
|
+
this is its final timestep AND it has not been accreted by another Feature
|
|
314
|
+
"""
|
|
315
|
+
return self._final_timestep and self._accreted_in_next_frame_by is None
|
|
316
|
+
|
|
317
|
+
def is_final_timestep(self) -> bool:
|
|
318
|
+
"""
|
|
319
|
+
Returns bool whether this is the final timestep for the Feature, i.e., it is
|
|
320
|
+
either dissipating or accreting into another Feature
|
|
321
|
+
"""
|
|
322
|
+
return self._final_timestep
|