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,203 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, field, replace
3
+ from pathlib import Path
4
+ from typing import Sequence
5
+
6
+ from tqdm import tqdm
7
+
8
+ from OTVision.dataformat import FRAME, INPUT_FILE_PATH, TRACK_ID
9
+ from OTVision.domain.detection import TrackId
10
+ from OTVision.domain.frame import (
11
+ DetectedFrame,
12
+ FinishedFrame,
13
+ FrameNo,
14
+ IsLastFrame,
15
+ TrackedFrame,
16
+ )
17
+ from OTVision.track.model.filebased.frame_group import FrameGroup, get_output_file
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class FrameChunk:
22
+ """
23
+ A chunk of Frames obtained from a common file path source.
24
+
25
+ Attributes:
26
+ file (Path): common file path source of Frames.
27
+ metadata (dict): otdet metadata.
28
+ frames (Sequence[DetectedFrame]): a sequence of untracked Frames.
29
+ frame_group_id (int): id of FrameGroup this FrameCHunk is part of.
30
+ """
31
+
32
+ file: Path
33
+ metadata: dict
34
+ frames: Sequence[DetectedFrame]
35
+ frame_group_id: int
36
+
37
+ def check_output_file_exists(self, with_suffix: str) -> bool:
38
+ return get_output_file(self.file, with_suffix).is_file()
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class TrackedChunk(FrameChunk):
43
+ """A chunk of TrackedFrames extends FrameChunk.
44
+ Aggregates observed/finished/unfinished tracking metadata.
45
+
46
+ If is_last_chunk is true, all unfinished tracks
47
+ are marked as finished in last frame.
48
+
49
+ Attributes:
50
+ is_last_chunk (bool): whether this chunk is the last of
51
+ subsequently (related/connected) chunks.
52
+ frames (Sequence[TrackedFrame]): overrides frames
53
+ with more specific frame type.
54
+ finished_tracks (set[TrackId]): aggregates finished tracks
55
+ of given TrackedFrames.
56
+ observed_tracks (set[TrackId]): aggregates observed tracks
57
+ of given TrackedFrames.
58
+ unfinished_tracks (set[TrackId]): aggregates unfinished tracks
59
+ of given TrackedFrames as observed but not finished tracks in chunk.
60
+ last_track_frame (dict[TrackId, int]): mapping of track id
61
+ to frame number in which it last occurs.
62
+ """
63
+
64
+ is_last_chunk: bool
65
+ frames: Sequence[TrackedFrame] = field(init=False)
66
+
67
+ finished_tracks: set[TrackId] = field(init=False)
68
+ observed_tracks: set[TrackId] = field(init=False)
69
+ unfinished_tracks: set[TrackId] = field(init=False)
70
+ discarded_tracks: set[TrackId] = field(init=False)
71
+ last_track_frame: dict[TrackId, int] = field(init=False)
72
+
73
+ def __init__(
74
+ self,
75
+ file: Path,
76
+ metadata: dict,
77
+ is_last_chunk: bool,
78
+ frames: Sequence[TrackedFrame],
79
+ frame_group_id: int,
80
+ ) -> None:
81
+
82
+ object.__setattr__(self, "file", file)
83
+ object.__setattr__(self, "metadata", metadata)
84
+ object.__setattr__(self, "is_last_chunk", is_last_chunk)
85
+ object.__setattr__(self, "frame_group_id", frame_group_id)
86
+
87
+ observed = set().union(*(f.observed_tracks for f in frames))
88
+ finished = set().union(
89
+ *(f.finished_tracks for f in frames)
90
+ ) # TODO remove discarded?
91
+ discarded = set().union(*(f.discarded_tracks for f in frames))
92
+ unfinished = {o for o in observed if o not in finished and o not in discarded}
93
+
94
+ # set all unfinished tracks as finished, as this is the last track
95
+ if self.is_last_chunk:
96
+ frames_list = list(frames)
97
+ # assume frames sorted by occurrence
98
+
99
+ last_frame = frames_list[-1]
100
+ frames_list[-1] = replace(
101
+ last_frame,
102
+ finished_tracks=last_frame.finished_tracks.union(unfinished),
103
+ )
104
+
105
+ unfinished = set()
106
+ finished = set().union(*(f.finished_tracks for f in frames_list))
107
+ else:
108
+ frames_list = list(frames)
109
+
110
+ object.__setattr__(self, "frames", frames_list)
111
+
112
+ object.__setattr__(self, "finished_tracks", finished)
113
+ object.__setattr__(self, "observed_tracks", observed)
114
+ object.__setattr__(self, "unfinished_tracks", unfinished)
115
+ object.__setattr__(self, "discarded_tracks", discarded)
116
+
117
+ # assume frames sorted by occurrence
118
+ last_track_frame: dict[TrackId, FrameNo] = {
119
+ detection.track_id: frame.no
120
+ for frame in self.frames
121
+ for detection in frame.detections
122
+ }
123
+ object.__setattr__(self, "last_track_frame", last_track_frame)
124
+
125
+ def finish(
126
+ self,
127
+ is_last: IsLastFrame,
128
+ discarded_tracks: set[TrackId],
129
+ keep_discarded: bool = False,
130
+ ) -> "FinishedChunk":
131
+ """Turn this TrackedChunk into a FinishedChunk
132
+ by adding is_finished information to all its detections.
133
+
134
+ Args:
135
+ is_last (IsLastFrame): function to determine whether
136
+ a track is finished in a certain frame.
137
+ discarded_tracks (set[TrackId]): list of tracks considered discarded.
138
+ Used to mark corresponding tracks.
139
+ keep_discarded (bool): whether FinishedDetections marked as discarded
140
+ should be kept in detections list. Defaults to False.
141
+
142
+ Returns:
143
+ FinishedChunk: chunk of FinishedFrames
144
+ """
145
+ return FinishedChunk(
146
+ file=self.file,
147
+ metadata=self.metadata,
148
+ is_last_chunk=self.is_last_chunk,
149
+ frames=[
150
+ frame.finish(is_last, discarded_tracks, keep_discarded)
151
+ for frame in self.frames
152
+ ],
153
+ frame_group_id=self.frame_group_id,
154
+ )
155
+
156
+ def __repr__(self) -> str:
157
+ return self.__str__()
158
+
159
+ def __str__(self) -> str:
160
+ return f"FG [{self.frame_group_id}] - {self.file}"
161
+
162
+
163
+ @dataclass(frozen=True)
164
+ class FinishedChunk(TrackedChunk):
165
+ """A chunk of FinishedFrames.
166
+
167
+ Attributes:
168
+ frames (Sequence[FinishedFrame]): overrides frames
169
+ with more specific frame type.
170
+ """
171
+
172
+ frames: Sequence[FinishedFrame]
173
+
174
+ def to_detection_dicts(self) -> list[dict]:
175
+ chunk_metadata = {INPUT_FILE_PATH: self.file.as_posix()}
176
+
177
+ frames_progress = tqdm(
178
+ self.frames, desc="Frames to_dict", total=len(self.frames), leave=False
179
+ )
180
+
181
+ detection_dict_list = [
182
+ {**det_dict, **chunk_metadata}
183
+ for frame in frames_progress
184
+ for det_dict in frame.to_detection_dicts()
185
+ ]
186
+
187
+ detection_dict_list.sort(
188
+ key=lambda detection: (
189
+ detection[FRAME],
190
+ detection[TRACK_ID],
191
+ )
192
+ )
193
+ return detection_dict_list
194
+
195
+
196
+ class ChunkParser(ABC):
197
+ """A parser for file path to FrameChunk."""
198
+
199
+ @abstractmethod
200
+ def parse(
201
+ self, file: Path, frame_group: FrameGroup, frame_offset: int
202
+ ) -> FrameChunk:
203
+ pass
@@ -0,0 +1,95 @@
1
+ from abc import ABC, abstractmethod
2
+ from dataclasses import dataclass, replace
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+
6
+ from tqdm import tqdm
7
+
8
+
9
+ def get_output_file(file: Path, with_suffix: str) -> Path:
10
+ return file.with_suffix(with_suffix)
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class FrameGroup:
15
+ id: int
16
+ start_date: datetime
17
+ end_date: datetime
18
+ hostname: str
19
+ files: list[Path]
20
+ metadata_by_file: dict[Path, dict]
21
+
22
+ def merge(self, other: "FrameGroup") -> "FrameGroup":
23
+ if self.start_date < other.start_date:
24
+ return FrameGroup._merge(self, other)
25
+ else:
26
+ return FrameGroup._merge(other, self)
27
+
28
+ @staticmethod
29
+ def _merge(first: "FrameGroup", second: "FrameGroup") -> "FrameGroup":
30
+ if first.hostname != second.hostname:
31
+ raise ValueError("Hostname of FrameGroups does not match")
32
+
33
+ files = first.files + second.files
34
+ metadata = dict(first.metadata_by_file)
35
+ metadata.update(second.metadata_by_file)
36
+
37
+ merged = FrameGroup(
38
+ id=first.id,
39
+ start_date=first.start_date,
40
+ end_date=second.end_date,
41
+ hostname=first.hostname,
42
+ files=files,
43
+ metadata_by_file=metadata,
44
+ )
45
+
46
+ return merged
47
+
48
+ def check_any_output_file_exists(self, with_suffix: str) -> bool:
49
+ return len(self.get_existing_output_files(with_suffix)) > 0
50
+
51
+ def get_existing_output_files(self, with_suffix: str) -> list[Path]:
52
+ return [file for file in self.get_output_files(with_suffix) if file.is_file()]
53
+
54
+ def get_output_files(self, with_suffix: str) -> list[Path]:
55
+ return [get_output_file(file, with_suffix) for file in self.files]
56
+
57
+ def with_id(self, new_id: int) -> "FrameGroup":
58
+ return replace(self, id=new_id)
59
+
60
+ def __repr__(self) -> str:
61
+ return self.__str__()
62
+
63
+ def __str__(self) -> str:
64
+ return f"FrameGroup[{self.id}] = [{self.start_date} - {self.end_date}]"
65
+
66
+
67
+ class FrameGroupParser(ABC):
68
+
69
+ def process_all(self, files: list[Path]) -> list[FrameGroup]:
70
+ files_progress = tqdm(files, desc="parse FrameGroups", total=len(files))
71
+
72
+ parsed: list[FrameGroup] = [self.parse(file) for file in files_progress]
73
+ merged: list[FrameGroup] = self.merge(parsed)
74
+ updated: list[FrameGroup] = [
75
+ self.updated_metadata_copy(group).with_id(i)
76
+ for i, group in enumerate(merged)
77
+ ]
78
+
79
+ return updated
80
+
81
+ @abstractmethod
82
+ def parse(self, file: Path) -> FrameGroup:
83
+ pass
84
+
85
+ @abstractmethod
86
+ def merge(self, frame_groups: list[FrameGroup]) -> list[FrameGroup]:
87
+ pass
88
+
89
+ def updated_metadata_copy(self, frame_group: FrameGroup) -> FrameGroup:
90
+ new_metadata = self.update_metadata(frame_group)
91
+ return replace(frame_group, metadata_by_file=new_metadata)
92
+
93
+ @abstractmethod
94
+ def update_metadata(self, frame_group: FrameGroup) -> dict[Path, dict]:
95
+ pass
@@ -0,0 +1,119 @@
1
+ import logging
2
+ from abc import ABC, abstractmethod
3
+ from pathlib import Path
4
+ from typing import Generic, Iterator, TypeVar
5
+
6
+ from tqdm import tqdm
7
+
8
+ from OTVision.config import CONFIG, DEFAULT_FILETYPE, TRACK
9
+ from OTVision.dataformat import (
10
+ DATA,
11
+ DETECTIONS,
12
+ FRAME,
13
+ FRAME_GROUP,
14
+ INPUT_FILE_PATH,
15
+ METADATA,
16
+ TRACK_ID,
17
+ TRACKING,
18
+ TRACKING_RUN_ID,
19
+ )
20
+ from OTVision.helpers.files import write_json
21
+ from OTVision.helpers.log import LOGGER_NAME
22
+
23
+ log = logging.getLogger(LOGGER_NAME)
24
+
25
+
26
+ F = TypeVar("F") # Finished container: e.g. FinishedFrame or FinishedChunk
27
+
28
+
29
+ class FinishedTracksExporter(ABC, Generic[F]):
30
+
31
+ def __init__(self, file_type: str = CONFIG[DEFAULT_FILETYPE][TRACK]):
32
+ self.file_type = file_type
33
+
34
+ @abstractmethod
35
+ def get_detection_dicts(self, container: F) -> list[dict]:
36
+ pass
37
+
38
+ @abstractmethod
39
+ def get_result_path(self, container: F) -> Path:
40
+ pass
41
+
42
+ @abstractmethod
43
+ def get_metadata(self, container: F) -> dict:
44
+ pass
45
+
46
+ @abstractmethod
47
+ def get_frame_group_id(self, container: F) -> int:
48
+ pass
49
+
50
+ def export(
51
+ self, tracking_run_id: str, stream: Iterator[F], overwrite: bool
52
+ ) -> None:
53
+ for container in stream:
54
+ self.export_frames(container, tracking_run_id, overwrite)
55
+
56
+ def export_frames(
57
+ self, container: F, tracking_run_id: str, overwrite: bool
58
+ ) -> None:
59
+ file_path = self.get_result_path(container)
60
+
61
+ det_dicts = self.reindex(self.get_detection_dicts(container))
62
+
63
+ output = self.build_output(
64
+ det_dicts,
65
+ self.get_metadata(container),
66
+ tracking_run_id,
67
+ self.get_frame_group_id(container),
68
+ )
69
+
70
+ write_json(
71
+ dict_to_write=output,
72
+ file=Path(file_path),
73
+ filetype=self.file_type,
74
+ overwrite=overwrite,
75
+ )
76
+
77
+ log.info(f"Successfully tracked and wrote {file_path}")
78
+
79
+ @staticmethod
80
+ def reindex(det_dicts: list[dict]) -> list[dict]:
81
+ min_frame_no = min(det[FRAME] for det in det_dicts)
82
+
83
+ det_dicts_progress = tqdm(
84
+ det_dicts,
85
+ desc="reindex TrackedDetections",
86
+ total=len(det_dicts),
87
+ leave=False,
88
+ )
89
+ reindexed_dets = [
90
+ {**det, **{FRAME: det[FRAME] - min_frame_no + 1}}
91
+ for det in det_dicts_progress
92
+ ]
93
+
94
+ if len(reindexed_dets) == 0:
95
+ return []
96
+
97
+ if len({detection[INPUT_FILE_PATH] for detection in reindexed_dets}) > 1:
98
+ raise ValueError("Expect detections from only a single source file")
99
+
100
+ reindexed_dets.sort(
101
+ key=lambda detection: (
102
+ detection[INPUT_FILE_PATH],
103
+ detection[FRAME],
104
+ detection[TRACK_ID],
105
+ )
106
+ )
107
+
108
+ return reindexed_dets
109
+
110
+ @staticmethod
111
+ def build_output(
112
+ detections: list[dict],
113
+ metadata: dict,
114
+ tracking_run_id: str,
115
+ frame_group_id: int,
116
+ ) -> dict:
117
+ metadata[TRACKING][TRACKING_RUN_ID] = tracking_run_id
118
+ metadata[TRACKING][FRAME_GROUP] = frame_group_id
119
+ return {METADATA: metadata, DATA: {DETECTIONS: detections}}