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
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import datetime
|
|
3
|
+
from ast import literal_eval
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Union
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
|
|
9
|
+
from simpletrack.feature import Feature
|
|
10
|
+
from simpletrack.frame import Frame, Timeline
|
|
11
|
+
from simpletrack.utils import check_arrays
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FrameOutputManager:
|
|
15
|
+
"""
|
|
16
|
+
Class for outputting contents of Frames to .txt, .csv and .field files.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(
|
|
20
|
+
self,
|
|
21
|
+
output_path: str = "./output",
|
|
22
|
+
expt_name: str = "default",
|
|
23
|
+
start_time: str = None,
|
|
24
|
+
config_path: str = None,
|
|
25
|
+
):
|
|
26
|
+
output_path = Path(output_path)
|
|
27
|
+
output_path.mkdir(parents=True, exist_ok=True)
|
|
28
|
+
self.output_path = output_path
|
|
29
|
+
self.expt_name = expt_name
|
|
30
|
+
self.start_time = start_time
|
|
31
|
+
self.config_path = config_path
|
|
32
|
+
self.strftime = "%Y%m%d_%H%M"
|
|
33
|
+
|
|
34
|
+
def features_to_txt(self, frame: Frame) -> None:
|
|
35
|
+
"""
|
|
36
|
+
Outputs contents of input Frame to a text file
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
frame (Frame): _description_
|
|
40
|
+
"""
|
|
41
|
+
frame_time = frame.time
|
|
42
|
+
frame_time_str = frame_time.strftime(self.strftime)
|
|
43
|
+
output_fnm = f"{self.output_path}/frame_{frame_time_str}.txt"
|
|
44
|
+
|
|
45
|
+
with open(output_fnm, "w") as output_file:
|
|
46
|
+
output_file.write(self.expt_name + "\n")
|
|
47
|
+
if self.config_path is not None:
|
|
48
|
+
output_file.write(f"Config path: {self.config_path}\n")
|
|
49
|
+
if self.start_time is not None:
|
|
50
|
+
output_file.write(f"Start time: {self.start_time}\n")
|
|
51
|
+
output_file.write(f"Frame time: {frame_time_str}\n")
|
|
52
|
+
output_file.write(f"Total tracked features: {frame.max_id}\n")
|
|
53
|
+
|
|
54
|
+
frame_features_dict = frame.features
|
|
55
|
+
|
|
56
|
+
for feature_id in sorted(frame_features_dict):
|
|
57
|
+
feature = frame_features_dict[feature_id]
|
|
58
|
+
output_line = feature.summarise(output_type="str")
|
|
59
|
+
output_file.write(output_line + "\n")
|
|
60
|
+
|
|
61
|
+
def features_to_csv(self, frame: Frame) -> None:
|
|
62
|
+
frame_time = frame.time
|
|
63
|
+
frame_time_str = frame_time.strftime(self.strftime)
|
|
64
|
+
output_fnm = f"{self.output_path}/frame_{frame_time_str}.csv"
|
|
65
|
+
|
|
66
|
+
with open(output_fnm, "w") as output_file:
|
|
67
|
+
# Write headers
|
|
68
|
+
writer = csv.writer(output_file)
|
|
69
|
+
writer.writerow([self.expt_name])
|
|
70
|
+
if self.config_path is not None:
|
|
71
|
+
writer.writerow([f"Config path: {self.config_path}"])
|
|
72
|
+
if self.start_time is not None:
|
|
73
|
+
writer.writerow([f"Start time: {self.start_time}"])
|
|
74
|
+
writer.writerow([f"Frame time: {frame_time_str}"])
|
|
75
|
+
writer.writerow([f"Total tracked features: {frame.max_id}"])
|
|
76
|
+
|
|
77
|
+
# Write data
|
|
78
|
+
frame_features_dict = frame.features
|
|
79
|
+
# Get data headers by looking at any feature in the frame features dict
|
|
80
|
+
random_feature = frame_features_dict[list(frame_features_dict.keys())[0]]
|
|
81
|
+
data_headers = random_feature.summarise("dict").keys()
|
|
82
|
+
dict_writer = csv.DictWriter(output_file, fieldnames=data_headers)
|
|
83
|
+
dict_writer.writeheader()
|
|
84
|
+
|
|
85
|
+
for feature_id in sorted(frame_features_dict):
|
|
86
|
+
feature = frame_features_dict[feature_id]
|
|
87
|
+
dict_writer.writerow(feature.summarise("dict"))
|
|
88
|
+
|
|
89
|
+
def fields_to_npy(self, frame: Frame) -> None:
|
|
90
|
+
"""
|
|
91
|
+
Output feature and lifetime fields to .npy files
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
frame (Frame): _description_
|
|
95
|
+
"""
|
|
96
|
+
outputs = {
|
|
97
|
+
"features": [frame.feature_field, "%.6e"],
|
|
98
|
+
"lifetime": [frame.lifetime_field, "%.4e"],
|
|
99
|
+
"y-flow": [frame.get_flow()[0], "%.2e"],
|
|
100
|
+
"x-flow": [frame.get_flow()[1], "%.2e"],
|
|
101
|
+
}
|
|
102
|
+
frame_time = frame.time
|
|
103
|
+
frame_time_str = frame_time.strftime("%Y%m%d_%H%M")
|
|
104
|
+
for output_fnm, [output, output_fmt] in outputs.items():
|
|
105
|
+
if output is None:
|
|
106
|
+
continue
|
|
107
|
+
full_fnm = f"{self.output_path}/{output_fnm}_{frame_time_str}.field"
|
|
108
|
+
np.savetxt(full_fnm, output, fmt=output_fmt)
|
|
109
|
+
|
|
110
|
+
def output_density_field(
|
|
111
|
+
self, timeline: Timeline, field_type: str, centroid_only: bool = True
|
|
112
|
+
):
|
|
113
|
+
"""
|
|
114
|
+
Loops over all Frames in Timeline, makes density plot of areas
|
|
115
|
+
where new Features are being created
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
timeline (Timeline): _description_
|
|
119
|
+
"""
|
|
120
|
+
valid_types = ["init", "dissipation"]
|
|
121
|
+
if field_type not in valid_types:
|
|
122
|
+
raise ValueError(
|
|
123
|
+
f"field_type ({field_type}) not in valid_types {valid_types}"
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
all_frames = list(timeline.get_timeline().values())
|
|
127
|
+
if not all(isinstance(frame, Frame) for frame in all_frames):
|
|
128
|
+
return TypeError(f"Expected all Frames, got {all_frames}")
|
|
129
|
+
|
|
130
|
+
check_arrays(
|
|
131
|
+
*[frame.feature_field for frame in all_frames],
|
|
132
|
+
ndim=2,
|
|
133
|
+
equal_shape=True,
|
|
134
|
+
non_negative=True,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
# If above check passes, can make storage array from first frame field
|
|
138
|
+
field_shape = (len(all_frames), *all_frames[0].feature_field.shape)
|
|
139
|
+
field_density = np.zeros(field_shape)
|
|
140
|
+
|
|
141
|
+
for frame_idx, frame in enumerate(all_frames):
|
|
142
|
+
field = frame.get_field(field_type, centroid_only=centroid_only)
|
|
143
|
+
field_density[frame_idx, ...] = field
|
|
144
|
+
|
|
145
|
+
output_fnm = f"{self.output_path}/{field_type}_density.npy"
|
|
146
|
+
np.save(output_fnm, field_density)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class LoadOutput:
|
|
150
|
+
"""
|
|
151
|
+
Contains functionality for reading previous outputs back into a Timeline object
|
|
152
|
+
(contanining Frames of field and Feature data) for further inspection and analysis.
|
|
153
|
+
"""
|
|
154
|
+
|
|
155
|
+
def __init__(self, st_data_path: Union[str | Path]):
|
|
156
|
+
self.path = Path(st_data_path)
|
|
157
|
+
self.strftime = "%Y%m%d_%H%M"
|
|
158
|
+
# Links field type names in outputs to attribute names in Frame
|
|
159
|
+
self.field_attributes = {
|
|
160
|
+
"features": "feature_field",
|
|
161
|
+
"lifetime": "lifetime_field",
|
|
162
|
+
"x-flow": "x_flow",
|
|
163
|
+
"y-flow": "y_flow",
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
def load_to_timeline(self) -> Timeline:
|
|
167
|
+
timeline = Timeline()
|
|
168
|
+
|
|
169
|
+
# Get list of times from output fields.
|
|
170
|
+
frame_times = self.get_frame_times_from_field_filenames()
|
|
171
|
+
|
|
172
|
+
# Load blank Frames into Timeline
|
|
173
|
+
for frame_time in frame_times:
|
|
174
|
+
frame = Frame()
|
|
175
|
+
frame.set_time(frame_time)
|
|
176
|
+
timeline.add_to_timelime(frame)
|
|
177
|
+
|
|
178
|
+
# Load fields into blank Frames
|
|
179
|
+
self.load_frame_fields(timeline)
|
|
180
|
+
# Load raw data into frame
|
|
181
|
+
# TODO: will need config file for this to determine
|
|
182
|
+
# if a Loader has been used, and location of input data
|
|
183
|
+
# self.load_raw_fields(timeline)
|
|
184
|
+
|
|
185
|
+
# Populate features in each Frame
|
|
186
|
+
for frame in timeline.get_timeline().values():
|
|
187
|
+
frame.populate_features()
|
|
188
|
+
|
|
189
|
+
# Finally, fill these Features with data loaded from outputs
|
|
190
|
+
self.load_feature_data(timeline)
|
|
191
|
+
return timeline
|
|
192
|
+
|
|
193
|
+
def load_feature_data(self, timeline: Timeline) -> None:
|
|
194
|
+
# Get list of headers from blank Feature class
|
|
195
|
+
blank_feature = Feature(
|
|
196
|
+
id=1,
|
|
197
|
+
feature_coords=np.array(((0, 0), (0, 0))),
|
|
198
|
+
time=datetime.datetime.now(),
|
|
199
|
+
)
|
|
200
|
+
headers = blank_feature.summarise(headers_only=True)
|
|
201
|
+
# Set the number of headers to skip in each csv file
|
|
202
|
+
number_header_rows = 5
|
|
203
|
+
|
|
204
|
+
for frame_time, frame in timeline.get_timeline().items():
|
|
205
|
+
# Load all data for the current time
|
|
206
|
+
frame_time_str = frame_time.strftime(self.strftime)
|
|
207
|
+
frame_time_fnames = self.path.rglob(f"*{frame_time_str}.csv")
|
|
208
|
+
|
|
209
|
+
for fname in frame_time_fnames:
|
|
210
|
+
# Read data from output
|
|
211
|
+
with open(fname) as csv_file:
|
|
212
|
+
reader = csv.DictReader(csv_file, fieldnames=headers)
|
|
213
|
+
all_feature_data = [
|
|
214
|
+
row
|
|
215
|
+
for row_idx, row in enumerate(reader)
|
|
216
|
+
if row_idx > number_header_rows
|
|
217
|
+
]
|
|
218
|
+
|
|
219
|
+
# Add data to feature object in Frame
|
|
220
|
+
for feature_data in all_feature_data:
|
|
221
|
+
id = int(feature_data["id"])
|
|
222
|
+
feature = frame.get_feature(id)
|
|
223
|
+
|
|
224
|
+
# Loop over all features, set attribute to value
|
|
225
|
+
for property, value in feature_data.items():
|
|
226
|
+
if property == "id" or property == "centroid":
|
|
227
|
+
continue
|
|
228
|
+
|
|
229
|
+
if len(value) == 0:
|
|
230
|
+
setattr(feature, property, None)
|
|
231
|
+
else:
|
|
232
|
+
# Literal eval converts str to inferred python type
|
|
233
|
+
setattr(feature, property, literal_eval(value))
|
|
234
|
+
|
|
235
|
+
def load_frame_fields(self, timeline: Timeline) -> None:
|
|
236
|
+
for frame_time, frame in timeline.get_timeline().items():
|
|
237
|
+
# Load all data for the current time
|
|
238
|
+
frame_time_str = frame_time.strftime(self.strftime)
|
|
239
|
+
frame_time_fnames = self.path.rglob(f"*{frame_time_str}.field")
|
|
240
|
+
|
|
241
|
+
for fname in frame_time_fnames:
|
|
242
|
+
ftype = fname.name.split("_")[0]
|
|
243
|
+
# Set the relevant attribute of frame, as mapped using
|
|
244
|
+
# self.field_attributes
|
|
245
|
+
setattr(frame, self.field_attributes[ftype], np.loadtxt(fname))
|
|
246
|
+
|
|
247
|
+
def get_frame_times_from_field_filenames(self) -> list:
|
|
248
|
+
"""
|
|
249
|
+
Using a list of all field filenames from a given run of SimpleTrack,
|
|
250
|
+
determine the frame times. Checks whether data is present for all
|
|
251
|
+
field types
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
list: list of all frame times as datetime.datetime object
|
|
255
|
+
"""
|
|
256
|
+
# Containing dict for these times
|
|
257
|
+
times_from_each_field_type = {}
|
|
258
|
+
|
|
259
|
+
# Iterate over each field type to find time the data is available for
|
|
260
|
+
for ftype in self.field_attributes:
|
|
261
|
+
# Setup containing array for times
|
|
262
|
+
field_filenames = self.path.rglob(f"{ftype}*.field")
|
|
263
|
+
field_times = []
|
|
264
|
+
|
|
265
|
+
for field_fname in sorted(field_filenames):
|
|
266
|
+
fname_parts = str(field_fname.name).split("_")
|
|
267
|
+
yyyymmdd = fname_parts[1]
|
|
268
|
+
hhmm = fname_parts[2]
|
|
269
|
+
field_times.append(
|
|
270
|
+
datetime.datetime(
|
|
271
|
+
year=int(yyyymmdd[0:4]),
|
|
272
|
+
month=int(yyyymmdd[4:6]),
|
|
273
|
+
day=int(yyyymmdd[6:8]),
|
|
274
|
+
hour=int(hhmm[0:2]),
|
|
275
|
+
minute=int(hhmm[2:4]),
|
|
276
|
+
)
|
|
277
|
+
)
|
|
278
|
+
times_from_each_field_type[ftype] = field_times
|
|
279
|
+
|
|
280
|
+
# Check times from each field type to check consistency
|
|
281
|
+
# If all times are the same (have previously been sorted), should only
|
|
282
|
+
# be one set of unqiue dict values
|
|
283
|
+
times_list = times_from_each_field_type.values()
|
|
284
|
+
times_set = [set(tuple(times)) for times in times_list]
|
|
285
|
+
if len(times_set) == 1:
|
|
286
|
+
# All times are the same, can return any list
|
|
287
|
+
return times_from_each_field_type["features"]
|
|
288
|
+
|
|
289
|
+
else:
|
|
290
|
+
# Get the longest list to return
|
|
291
|
+
max_arr_key = max(
|
|
292
|
+
times_from_each_field_type,
|
|
293
|
+
key=lambda x: len(times_from_each_field_type[x]),
|
|
294
|
+
)
|
|
295
|
+
return times_from_each_field_type[max_arr_key]
|