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.
Files changed (38) hide show
  1. OTVision/__init__.py +0 -10
  2. OTVision/application/detect/current_object_detector.py +1 -2
  3. OTVision/application/detect/detected_frame_factory.py +4 -3
  4. OTVision/application/detect/detected_frame_producer.py +1 -2
  5. OTVision/detect/builder.py +1 -1
  6. OTVision/detect/detected_frame_buffer.py +1 -1
  7. OTVision/detect/otdet.py +3 -2
  8. OTVision/detect/yolo.py +2 -2
  9. OTVision/domain/detect_producer_consumer.py +1 -1
  10. OTVision/domain/detection.py +128 -7
  11. OTVision/domain/frame.py +146 -1
  12. OTVision/domain/object_detection.py +1 -2
  13. OTVision/helpers/files.py +10 -2
  14. OTVision/helpers/input_types.py +15 -0
  15. OTVision/track/exporter/__init__.py +0 -0
  16. OTVision/track/exporter/filebased_exporter.py +24 -0
  17. OTVision/track/model/__init__.py +0 -0
  18. OTVision/track/model/filebased/__init__.py +0 -0
  19. OTVision/track/model/filebased/frame_chunk.py +203 -0
  20. OTVision/track/model/filebased/frame_group.py +95 -0
  21. OTVision/track/model/track_exporter.py +119 -0
  22. OTVision/track/model/tracking_interfaces.py +303 -0
  23. OTVision/track/parser/__init__.py +0 -0
  24. OTVision/track/parser/chunk_parser_plugins.py +99 -0
  25. OTVision/track/parser/frame_group_parser_plugins.py +127 -0
  26. OTVision/track/track.py +54 -332
  27. OTVision/track/tracker/__init__.py +0 -0
  28. OTVision/track/tracker/filebased_tracking.py +192 -0
  29. OTVision/track/tracker/tracker_plugin_iou.py +224 -0
  30. OTVision/version.py +1 -1
  31. OTVision/view/view_track.py +1 -1
  32. {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/METADATA +8 -6
  33. {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/RECORD +35 -23
  34. OTVision/track/iou.py +0 -282
  35. OTVision/track/iou_util.py +0 -140
  36. OTVision/track/preprocess.py +0 -453
  37. {otvision-0.6.1.dist-info → otvision-0.6.3.dist-info}/WHEEL +0 -0
  38. {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