OTVision 0.6.1__py3-none-any.whl → 0.6.3__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.
- OTVision/__init__.py +0 -10
- OTVision/application/detect/current_object_detector.py +1 -2
- OTVision/application/detect/detected_frame_factory.py +4 -3
- OTVision/application/detect/detected_frame_producer.py +1 -2
- OTVision/detect/builder.py +1 -1
- OTVision/detect/detected_frame_buffer.py +1 -1
- OTVision/detect/otdet.py +3 -2
- OTVision/detect/yolo.py +2 -2
- OTVision/domain/detect_producer_consumer.py +1 -1
- OTVision/domain/detection.py +128 -7
- OTVision/domain/frame.py +146 -1
- OTVision/domain/object_detection.py +1 -2
- OTVision/helpers/files.py +10 -2
- OTVision/helpers/input_types.py +15 -0
- OTVision/track/exporter/__init__.py +0 -0
- OTVision/track/exporter/filebased_exporter.py +24 -0
- OTVision/track/model/__init__.py +0 -0
- OTVision/track/model/filebased/__init__.py +0 -0
- OTVision/track/model/filebased/frame_chunk.py +203 -0
- OTVision/track/model/filebased/frame_group.py +95 -0
- OTVision/track/model/track_exporter.py +119 -0
- OTVision/track/model/tracking_interfaces.py +303 -0
- OTVision/track/parser/__init__.py +0 -0
- OTVision/track/parser/chunk_parser_plugins.py +99 -0
- OTVision/track/parser/frame_group_parser_plugins.py +127 -0
- OTVision/track/track.py +54 -332
- OTVision/track/tracker/__init__.py +0 -0
- OTVision/track/tracker/filebased_tracking.py +192 -0
- OTVision/track/tracker/tracker_plugin_iou.py +224 -0
- OTVision/version.py +1 -1
- OTVision/view/view_track.py +1 -1
- {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/METADATA +8 -6
- {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/RECORD +35 -23
- OTVision/track/iou.py +0 -282
- OTVision/track/iou_util.py +0 -140
- OTVision/track/preprocess.py +0 -453
- {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/WHEEL +0 -0
- {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Generic, Iterator, TypeVar
|
|
3
|
+
|
|
4
|
+
from OTVision.domain.detection import TrackId
|
|
5
|
+
from OTVision.domain.frame import (
|
|
6
|
+
DetectedFrame,
|
|
7
|
+
FinishedFrame,
|
|
8
|
+
FrameNo,
|
|
9
|
+
IsLastFrame,
|
|
10
|
+
TrackedFrame,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
ID_GENERATOR = Iterator[TrackId]
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Tracker(ABC):
|
|
17
|
+
"""Tracker interface for processing a stream of Frames
|
|
18
|
+
to add tracking information, creating a lazy stream (generator)
|
|
19
|
+
of TrackedFrames.
|
|
20
|
+
|
|
21
|
+
Implementing class can specify template method:
|
|
22
|
+
track_frame for processing a single frame.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def track(
|
|
26
|
+
self, frames: Iterator[DetectedFrame], id_generator: ID_GENERATOR
|
|
27
|
+
) -> Iterator[TrackedFrame]:
|
|
28
|
+
"""Process the given stream of Frames,
|
|
29
|
+
yielding TrackedFrames one by one as a lazy stream of TrackedFrames.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
frames (Iterator[DetectedFrame]): (lazy) stream of Frames
|
|
33
|
+
with untracked Detections.
|
|
34
|
+
id_generator (ID_GENERATOR): provider of new (unique) track ids.
|
|
35
|
+
|
|
36
|
+
Yields:
|
|
37
|
+
Iterator[TrackedFrame]: (lazy) stream of TrackedFrames with
|
|
38
|
+
TrackedDetections
|
|
39
|
+
"""
|
|
40
|
+
for frame in frames:
|
|
41
|
+
yield self.track_frame(frame, id_generator)
|
|
42
|
+
|
|
43
|
+
@abstractmethod
|
|
44
|
+
def track_frame(
|
|
45
|
+
self,
|
|
46
|
+
frame: DetectedFrame,
|
|
47
|
+
id_generator: ID_GENERATOR,
|
|
48
|
+
) -> TrackedFrame:
|
|
49
|
+
"""Process single Frame with untracked Detections,
|
|
50
|
+
by adding tracking information,
|
|
51
|
+
creating a TrackedFrame with TrackedDetections.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
frame (DetectedFrame): the Frame to be tracked.
|
|
55
|
+
id_generator (ID_GENERATOR): provider of new (unique) track ids.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
TrackedFrame: TrackedFrame with TrackedDetections
|
|
59
|
+
"""
|
|
60
|
+
pass
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
C = TypeVar("C") # Detection container: e.g. TrackedFrame or TrackedChunk
|
|
64
|
+
F = TypeVar("F") # Finished container: e.g. FinishedFrame or FinishedChunk
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
class UnfinishedTracksBuffer(ABC, Generic[C, F]):
|
|
68
|
+
"""UnfinishedTracksBuffer provides functionality
|
|
69
|
+
to add finished information to tracked detections.
|
|
70
|
+
|
|
71
|
+
It processes containers (C) of TrackedDetections, buffers them
|
|
72
|
+
and stores track ids that are reported as finished.
|
|
73
|
+
Only when all tracks of a container (C) were marked as finished,
|
|
74
|
+
it is converted into a finished container (F) and yielded.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
Generic (C): generic type of TrackedDetection container
|
|
78
|
+
(e.g. TrackedFrame or TrackedChunk)
|
|
79
|
+
Generic (F): generic type of FinishedDetection container
|
|
80
|
+
(e.g. FinishedFrame or FinishedChunk)
|
|
81
|
+
keep_discarded (bool): whether detections marked as discarded should
|
|
82
|
+
be kept of filtered when finishing them. Defaults to False.
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
def __init__(self, keep_discarded: bool = False) -> None:
|
|
86
|
+
self._keep_discarded = keep_discarded
|
|
87
|
+
self._unfinished_containers: list[tuple[C, set[TrackId]]] = list()
|
|
88
|
+
self._merged_last_track_frame: dict[TrackId, FrameNo] = dict()
|
|
89
|
+
self._discarded_tracks: set[TrackId] = set()
|
|
90
|
+
|
|
91
|
+
@abstractmethod
|
|
92
|
+
def _get_last_track_frames(self, container: C) -> dict[TrackId, FrameNo]:
|
|
93
|
+
"""Mapping from TrackId to frame no of last detection occurrence.
|
|
94
|
+
Mapping for all tracks in newly tracked container.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
container (C): newly tracked TrackedDetection container
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
dict[TrackId, int]: last frame no by TrackId
|
|
101
|
+
"""
|
|
102
|
+
pass
|
|
103
|
+
|
|
104
|
+
@abstractmethod
|
|
105
|
+
def _get_unfinished_tracks(self, container: C) -> set[TrackId]:
|
|
106
|
+
"""TrackIds of given container, that are marked as unfinished.
|
|
107
|
+
|
|
108
|
+
Args:
|
|
109
|
+
container (C): newly tracked TrackedDetection container
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
set[TrackId]: TrackIds of container marked as unfinished
|
|
113
|
+
"""
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
@abstractmethod
|
|
117
|
+
def _get_observed_tracks(self, container: C) -> set[TrackId]:
|
|
118
|
+
"""TrackIds observed given (newly tracked) container.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
container (C): newly tracked TrackedDetection container
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
set[TrackId]: observed TrackIds of container
|
|
125
|
+
"""
|
|
126
|
+
pass
|
|
127
|
+
|
|
128
|
+
@abstractmethod
|
|
129
|
+
def _get_newly_finished_tracks(self, container: C) -> set[TrackId]:
|
|
130
|
+
"""TrackIds marked as finished in the given (newly tracked) container.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
container (C): newly tracked TrackedDetection container
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
set[TrackId]: finished TrackIds in container
|
|
137
|
+
"""
|
|
138
|
+
pass
|
|
139
|
+
|
|
140
|
+
@abstractmethod
|
|
141
|
+
def _get_newly_discarded_tracks(self, container: C) -> set[TrackId]:
|
|
142
|
+
"""TrackIds marked as discarded in the given (newly tracked) container.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
container (C): newly tracked TrackedDetection container
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
set[TrackId]: discarded TrackIds in container
|
|
149
|
+
"""
|
|
150
|
+
pass
|
|
151
|
+
|
|
152
|
+
@abstractmethod
|
|
153
|
+
def _get_last_frame_of_container(self, container: C) -> FrameNo:
|
|
154
|
+
"""The last FrameNo of the given container.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
container (C): newly tracked TrackedDetection container
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
FrameNo: last FrameNo of the given container
|
|
161
|
+
"""
|
|
162
|
+
pass
|
|
163
|
+
|
|
164
|
+
@abstractmethod
|
|
165
|
+
def _finish(
|
|
166
|
+
self,
|
|
167
|
+
container: C,
|
|
168
|
+
is_last: IsLastFrame,
|
|
169
|
+
discarded_tracks: set[TrackId],
|
|
170
|
+
keep_discarded: bool,
|
|
171
|
+
) -> F:
|
|
172
|
+
"""Transform the given container to a finished container
|
|
173
|
+
by adding is_finished information to all contained TrackedDetections
|
|
174
|
+
turning them into FinishedDetections.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
container (C): container of TrackedDetections
|
|
178
|
+
is_last (IsLastFrame): check whether a track ends in a certain frame
|
|
179
|
+
keep_discarded (bool): whether detections marked as discarded are kept.
|
|
180
|
+
Returns:
|
|
181
|
+
F: a finished container with transformed detections of given container
|
|
182
|
+
"""
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
def track_and_finish(self, containers: Iterator[C]) -> Iterator[F]:
|
|
186
|
+
# TODO template method to obtain containers?
|
|
187
|
+
|
|
188
|
+
for container in containers:
|
|
189
|
+
|
|
190
|
+
# if track is observed in current iteration, update its last observed frame
|
|
191
|
+
new_last_track_frames = self._get_last_track_frames(container)
|
|
192
|
+
self._merged_last_track_frame.update(new_last_track_frames)
|
|
193
|
+
|
|
194
|
+
newly_unfinished_tracks = self._get_unfinished_tracks(container)
|
|
195
|
+
self._unfinished_containers.append((container, newly_unfinished_tracks))
|
|
196
|
+
|
|
197
|
+
# update unfinished track ids of previously tracked containers
|
|
198
|
+
# if containers have no pending tracks, make ready for finishing
|
|
199
|
+
newly_finished_tracks = self._get_newly_finished_tracks(container)
|
|
200
|
+
newly_discarded_tracks = self._get_newly_discarded_tracks(container)
|
|
201
|
+
self._discarded_tracks.update(newly_discarded_tracks)
|
|
202
|
+
|
|
203
|
+
ready_containers: list[C] = []
|
|
204
|
+
for c, track_ids in self._unfinished_containers:
|
|
205
|
+
track_ids.difference_update(newly_finished_tracks)
|
|
206
|
+
track_ids.difference_update(newly_discarded_tracks)
|
|
207
|
+
|
|
208
|
+
if not track_ids:
|
|
209
|
+
ready_containers.append(c)
|
|
210
|
+
|
|
211
|
+
self._unfinished_containers = [
|
|
212
|
+
(c, u)
|
|
213
|
+
for c, u in self._unfinished_containers
|
|
214
|
+
if c not in ready_containers
|
|
215
|
+
]
|
|
216
|
+
|
|
217
|
+
finished_containers: list[F] = self._finish_containers(ready_containers)
|
|
218
|
+
yield from finished_containers
|
|
219
|
+
|
|
220
|
+
# finish remaining containers with pending tracks
|
|
221
|
+
remaining_containers = [c for c, _ in self._unfinished_containers]
|
|
222
|
+
self._unfinished_containers = list()
|
|
223
|
+
|
|
224
|
+
finished_containers = self._finish_containers(remaining_containers)
|
|
225
|
+
self._merged_last_track_frame = dict()
|
|
226
|
+
yield from finished_containers
|
|
227
|
+
|
|
228
|
+
def _finish_containers(self, containers: list[C]) -> list[F]:
|
|
229
|
+
if len(containers) == 0:
|
|
230
|
+
return []
|
|
231
|
+
|
|
232
|
+
def is_last(frame_no: FrameNo, track_id: TrackId) -> bool:
|
|
233
|
+
return frame_no == self._merged_last_track_frame[track_id]
|
|
234
|
+
|
|
235
|
+
keep = self._keep_discarded
|
|
236
|
+
discarded = self._discarded_tracks
|
|
237
|
+
|
|
238
|
+
finished_containers: list[F] = [
|
|
239
|
+
self._finish(c, is_last, discarded, keep) for c in containers
|
|
240
|
+
]
|
|
241
|
+
|
|
242
|
+
# todo check if there are edge cases where track ids in merged_last_track_frame
|
|
243
|
+
# have frame no below containers last frame,
|
|
244
|
+
# but might appear in following containers
|
|
245
|
+
last_frame_of_container = max(
|
|
246
|
+
self._get_last_frame_of_container(c) for c in containers
|
|
247
|
+
)
|
|
248
|
+
ids_to_delete = [
|
|
249
|
+
track_id
|
|
250
|
+
for track_id, frame_no in self._merged_last_track_frame.items()
|
|
251
|
+
if frame_no <= last_frame_of_container
|
|
252
|
+
]
|
|
253
|
+
|
|
254
|
+
self._merged_last_track_frame = {
|
|
255
|
+
track_id: frame_no
|
|
256
|
+
for track_id, frame_no in self._merged_last_track_frame.items()
|
|
257
|
+
if track_id not in ids_to_delete
|
|
258
|
+
}
|
|
259
|
+
self._discarded_tracks.difference_update(ids_to_delete)
|
|
260
|
+
# self._finished_tracks.difference_update(ids_to_delete)
|
|
261
|
+
|
|
262
|
+
return finished_containers
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
class UnfinishedFramesBuffer(UnfinishedTracksBuffer[TrackedFrame, FinishedFrame]):
|
|
266
|
+
"""UnfinishedTracksBuffer implementation for Frames as Detection container."""
|
|
267
|
+
|
|
268
|
+
def __init__(self, tracker: Tracker, keep_discarded: bool = False):
|
|
269
|
+
super().__init__(keep_discarded)
|
|
270
|
+
self._tracker = tracker
|
|
271
|
+
|
|
272
|
+
def track(
|
|
273
|
+
self, frames: Iterator[DetectedFrame], id_generator: ID_GENERATOR
|
|
274
|
+
) -> Iterator[FinishedFrame]:
|
|
275
|
+
tracked_frame_stream = self._tracker.track(frames, id_generator)
|
|
276
|
+
return self.track_and_finish(tracked_frame_stream)
|
|
277
|
+
|
|
278
|
+
def _get_last_track_frames(self, container: TrackedFrame) -> dict[TrackId, int]:
|
|
279
|
+
return {o: container.no for o in container.observed_tracks}
|
|
280
|
+
|
|
281
|
+
def _get_unfinished_tracks(self, container: TrackedFrame) -> set[TrackId]:
|
|
282
|
+
return container.unfinished_tracks
|
|
283
|
+
|
|
284
|
+
def _get_observed_tracks(self, container: TrackedFrame) -> set[TrackId]:
|
|
285
|
+
return container.observed_tracks
|
|
286
|
+
|
|
287
|
+
def _get_newly_finished_tracks(self, container: TrackedFrame) -> set[TrackId]:
|
|
288
|
+
return container.finished_tracks
|
|
289
|
+
|
|
290
|
+
def _get_newly_discarded_tracks(self, container: TrackedFrame) -> set[TrackId]:
|
|
291
|
+
return container.discarded_tracks
|
|
292
|
+
|
|
293
|
+
def _get_last_frame_of_container(self, container: TrackedFrame) -> FrameNo:
|
|
294
|
+
return container.no
|
|
295
|
+
|
|
296
|
+
def _finish(
|
|
297
|
+
self,
|
|
298
|
+
container: TrackedFrame,
|
|
299
|
+
is_last: IsLastFrame,
|
|
300
|
+
discarded_tracks: set[TrackId],
|
|
301
|
+
keep_discarded: bool,
|
|
302
|
+
) -> FinishedFrame:
|
|
303
|
+
return container.finish(is_last, discarded_tracks, keep_discarded)
|
|
File without changes
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
from datetime import datetime
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from tqdm import tqdm
|
|
6
|
+
|
|
7
|
+
from OTVision.dataformat import (
|
|
8
|
+
CLASS,
|
|
9
|
+
CONFIDENCE,
|
|
10
|
+
DATA,
|
|
11
|
+
DATE_FORMAT,
|
|
12
|
+
DETECTIONS,
|
|
13
|
+
OCCURRENCE,
|
|
14
|
+
H,
|
|
15
|
+
W,
|
|
16
|
+
X,
|
|
17
|
+
Y,
|
|
18
|
+
)
|
|
19
|
+
from OTVision.domain.detection import Detection
|
|
20
|
+
from OTVision.domain.frame import DetectedFrame
|
|
21
|
+
from OTVision.helpers.date import (
|
|
22
|
+
parse_date_string_to_utc_datime,
|
|
23
|
+
parse_timestamp_string_to_utc_datetime,
|
|
24
|
+
)
|
|
25
|
+
from OTVision.helpers.files import denormalize_bbox, read_json
|
|
26
|
+
from OTVision.track.model.filebased.frame_chunk import ChunkParser, FrameChunk
|
|
27
|
+
from OTVision.track.model.filebased.frame_group import FrameGroup
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class JsonChunkParser(ChunkParser):
|
|
31
|
+
|
|
32
|
+
def parse(
|
|
33
|
+
self, file: Path, frame_group: FrameGroup, frame_offset: int = 0
|
|
34
|
+
) -> FrameChunk:
|
|
35
|
+
json = read_json(file)
|
|
36
|
+
metadata: dict = frame_group.metadata_by_file[file]
|
|
37
|
+
|
|
38
|
+
denormalized = denormalize_bbox(
|
|
39
|
+
json, file, metadata={file.as_posix(): metadata}
|
|
40
|
+
)
|
|
41
|
+
input: dict[int, dict[str, Any]] = denormalized[DATA]
|
|
42
|
+
|
|
43
|
+
frames = self.convert(file, frame_offset, input)
|
|
44
|
+
|
|
45
|
+
frames.sort(key=lambda frame: (frame.occurrence, frame.no))
|
|
46
|
+
return FrameChunk(file, metadata, frames, frame_group.id)
|
|
47
|
+
|
|
48
|
+
def convert(
|
|
49
|
+
self, file: Path, frame_offset: int, input: dict[int, dict[str, Any]]
|
|
50
|
+
) -> list[DetectedFrame]:
|
|
51
|
+
detection_parser = DetectionParser()
|
|
52
|
+
frames = []
|
|
53
|
+
|
|
54
|
+
input_progress = tqdm(
|
|
55
|
+
input.items(), desc="parse Frames", total=len(input), leave=False
|
|
56
|
+
)
|
|
57
|
+
for key, value in input_progress:
|
|
58
|
+
occurrence: datetime = parse_datetime(value[OCCURRENCE])
|
|
59
|
+
data_detections = value[DETECTIONS]
|
|
60
|
+
detections = detection_parser.convert(data_detections)
|
|
61
|
+
parsed_frame = DetectedFrame(
|
|
62
|
+
no=int(key) + frame_offset,
|
|
63
|
+
occurrence=occurrence,
|
|
64
|
+
source=str(file),
|
|
65
|
+
detections=detections,
|
|
66
|
+
image=None,
|
|
67
|
+
)
|
|
68
|
+
frames.append(parsed_frame)
|
|
69
|
+
return frames
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class DetectionParser:
|
|
73
|
+
def convert(self, detection_data: list[dict[str, str]]) -> list[Detection]:
|
|
74
|
+
detections: list[Detection] = []
|
|
75
|
+
for detection in detection_data:
|
|
76
|
+
detected_item = Detection(
|
|
77
|
+
detection[CLASS],
|
|
78
|
+
float(detection[CONFIDENCE]),
|
|
79
|
+
float(detection[X]),
|
|
80
|
+
float(detection[Y]),
|
|
81
|
+
float(detection[W]),
|
|
82
|
+
float(detection[H]),
|
|
83
|
+
)
|
|
84
|
+
detections.append(detected_item)
|
|
85
|
+
return detections
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def parse_datetime(date: str | float) -> datetime:
|
|
89
|
+
"""Parse a date string or timestamp to a datetime with UTC as timezone.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
date (str | float): the date to parse
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
datetime: the parsed datetime object with UTC set as timezone
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(date, str) and ("-" in date):
|
|
98
|
+
return parse_date_string_to_utc_datime(date, DATE_FORMAT)
|
|
99
|
+
return parse_timestamp_string_to_utc_datetime(date)
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime, timedelta
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from OTVision import version
|
|
6
|
+
from OTVision.dataformat import (
|
|
7
|
+
EXPECTED_DURATION,
|
|
8
|
+
FILENAME,
|
|
9
|
+
FIRST_TRACKED_VIDEO_START,
|
|
10
|
+
LAST_TRACKED_VIDEO_END,
|
|
11
|
+
LENGTH,
|
|
12
|
+
OTTRACK_VERSION,
|
|
13
|
+
OTVISION_VERSION,
|
|
14
|
+
RECORDED_START_DATE,
|
|
15
|
+
TRACKER,
|
|
16
|
+
TRACKING,
|
|
17
|
+
VIDEO,
|
|
18
|
+
)
|
|
19
|
+
from OTVision.helpers.files import (
|
|
20
|
+
FULL_FILE_NAME_PATTERN,
|
|
21
|
+
HOSTNAME,
|
|
22
|
+
InproperFormattedFilename,
|
|
23
|
+
read_json_bz2_metadata,
|
|
24
|
+
)
|
|
25
|
+
from OTVision.track.model.filebased.frame_group import FrameGroup, FrameGroupParser
|
|
26
|
+
from OTVision.track.parser.chunk_parser_plugins import parse_datetime
|
|
27
|
+
|
|
28
|
+
MISSING_START_DATE = datetime(1900, 1, 1)
|
|
29
|
+
MISSING_EXPECTED_DURATION = timedelta(minutes=15)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TimeThresholdFrameGroupParser(FrameGroupParser):
|
|
33
|
+
|
|
34
|
+
def __init__(
|
|
35
|
+
self, tracker_data: dict, time_without_frames: timedelta = timedelta(minutes=1)
|
|
36
|
+
):
|
|
37
|
+
self._time_without_frames = time_without_frames
|
|
38
|
+
self._tracker_data: dict = tracker_data
|
|
39
|
+
self._id_count = 0
|
|
40
|
+
|
|
41
|
+
def new_id(self) -> int:
|
|
42
|
+
self._id_count += 1
|
|
43
|
+
return self._id_count
|
|
44
|
+
|
|
45
|
+
def parse(self, file: Path) -> FrameGroup:
|
|
46
|
+
metadata = read_json_bz2_metadata(file)
|
|
47
|
+
return self.convert(file, metadata)
|
|
48
|
+
|
|
49
|
+
def convert(self, file: Path, metadata: dict) -> FrameGroup:
|
|
50
|
+
start_date: datetime = self.extract_start_date_from(metadata)
|
|
51
|
+
duration: timedelta = self.extract_expected_duration_from(metadata)
|
|
52
|
+
end_date: datetime = start_date + duration
|
|
53
|
+
hostname = self.get_hostname(metadata)
|
|
54
|
+
|
|
55
|
+
return FrameGroup(
|
|
56
|
+
id=self.new_id(),
|
|
57
|
+
start_date=start_date,
|
|
58
|
+
end_date=end_date,
|
|
59
|
+
files=[file],
|
|
60
|
+
metadata_by_file={file: metadata},
|
|
61
|
+
hostname=hostname,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def get_hostname(self, file_metadata: dict) -> str:
|
|
65
|
+
video_name = Path(file_metadata[VIDEO][FILENAME]).name
|
|
66
|
+
match = re.search(
|
|
67
|
+
FULL_FILE_NAME_PATTERN,
|
|
68
|
+
video_name,
|
|
69
|
+
)
|
|
70
|
+
if match:
|
|
71
|
+
return match.group(HOSTNAME)
|
|
72
|
+
|
|
73
|
+
raise InproperFormattedFilename(f"Could not parse {video_name}.")
|
|
74
|
+
|
|
75
|
+
def extract_start_date_from(self, metadata: dict) -> datetime:
|
|
76
|
+
if RECORDED_START_DATE in metadata[VIDEO].keys():
|
|
77
|
+
recorded_start_date = metadata[VIDEO][RECORDED_START_DATE]
|
|
78
|
+
return parse_datetime(recorded_start_date)
|
|
79
|
+
return MISSING_START_DATE
|
|
80
|
+
|
|
81
|
+
def extract_expected_duration_from(self, metadata: dict) -> timedelta:
|
|
82
|
+
if EXPECTED_DURATION in metadata[VIDEO].keys():
|
|
83
|
+
if expected_duration := metadata[VIDEO][EXPECTED_DURATION]:
|
|
84
|
+
return timedelta(seconds=int(expected_duration))
|
|
85
|
+
return self.parse_video_length(metadata)
|
|
86
|
+
|
|
87
|
+
def parse_video_length(self, metadata: dict) -> timedelta:
|
|
88
|
+
video_length = metadata[VIDEO][LENGTH]
|
|
89
|
+
time = datetime.strptime(video_length, "%H:%M:%S")
|
|
90
|
+
return timedelta(hours=time.hour, minutes=time.minute, seconds=time.second)
|
|
91
|
+
|
|
92
|
+
def update_metadata(self, frame_group: FrameGroup) -> dict[Path, dict]:
|
|
93
|
+
metadata_by_file = dict(frame_group.metadata_by_file)
|
|
94
|
+
for filepath in frame_group.files:
|
|
95
|
+
metadata = metadata_by_file[filepath]
|
|
96
|
+
metadata[OTTRACK_VERSION] = version.ottrack_version()
|
|
97
|
+
metadata[TRACKING] = {
|
|
98
|
+
OTVISION_VERSION: version.otvision_version(),
|
|
99
|
+
FIRST_TRACKED_VIDEO_START: frame_group.start_date.timestamp(),
|
|
100
|
+
LAST_TRACKED_VIDEO_END: frame_group.end_date.timestamp(),
|
|
101
|
+
TRACKER: self._tracker_data,
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return metadata_by_file
|
|
105
|
+
|
|
106
|
+
def merge(self, frame_groups: list[FrameGroup]) -> list[FrameGroup]:
|
|
107
|
+
if len(frame_groups) == 0:
|
|
108
|
+
return []
|
|
109
|
+
|
|
110
|
+
merged_groups = []
|
|
111
|
+
sorted_groups = sorted(frame_groups, key=lambda group: group.start_date)
|
|
112
|
+
last_group = sorted_groups[0]
|
|
113
|
+
for current_group in sorted_groups[1:]:
|
|
114
|
+
if last_group.hostname != current_group.hostname:
|
|
115
|
+
merged_groups.append(last_group)
|
|
116
|
+
last_group = current_group
|
|
117
|
+
elif (
|
|
118
|
+
timedelta(seconds=0)
|
|
119
|
+
<= (current_group.start_date - last_group.end_date)
|
|
120
|
+
<= self._time_without_frames
|
|
121
|
+
):
|
|
122
|
+
last_group = last_group.merge(current_group)
|
|
123
|
+
else:
|
|
124
|
+
merged_groups.append(last_group)
|
|
125
|
+
last_group = current_group
|
|
126
|
+
merged_groups.append(last_group)
|
|
127
|
+
return merged_groups
|