OTVision 0.6.0__py3-none-any.whl → 0.6.2__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/detect/cli.py CHANGED
@@ -110,8 +110,8 @@ class ArgparseDetectCliParser(DetectCliParser):
110
110
  "--start-time",
111
111
  default=None,
112
112
  type=str,
113
- help=f"Specify start date and time of the recording in format "
114
- f"{DATETIME_FORMAT}.",
113
+ help="Specify start date and time of the recording in format "
114
+ "YYYY-MM-DD_HH-MM-SS",
115
115
  required=False,
116
116
  )
117
117
  self._parser.add_argument(
OTVision/helpers/files.py CHANGED
@@ -329,6 +329,7 @@ def get_metadata(otdict: dict) -> dict:
329
329
  # TODO: Type hint nested dict during refactoring
330
330
  def denormalize_bbox(
331
331
  otdict: dict,
332
+ file_path: Path | None = None,
332
333
  keys_width: Union[list[str], None] = None,
333
334
  keys_height: Union[list[str], None] = None,
334
335
  metadata: dict[str, dict] = {},
@@ -337,6 +338,8 @@ def denormalize_bbox(
337
338
 
338
339
  Args:
339
340
  otdict (dict): dict of detections or tracks
341
+ file_path (Path): file path source of the given otdict
342
+ if all detections stem from the same file.
340
343
  keys_width (list[str], optional): list of keys describing horizontal position.
341
344
  Defaults to ["x", "w"].
342
345
  keys_height (list[str], optional): list of keys describing vertical position.
@@ -351,7 +354,9 @@ def denormalize_bbox(
351
354
  if keys_height is None:
352
355
  keys_height = [dataformat.Y, dataformat.H]
353
356
  log.debug("Denormalize frame wise")
354
- otdict = _denormalize_transformation(otdict, keys_width, keys_height, metadata)
357
+ otdict = _denormalize_transformation(
358
+ otdict, keys_width, keys_height, metadata, file_path
359
+ )
355
360
  return otdict
356
361
 
357
362
 
@@ -361,6 +366,7 @@ def _denormalize_transformation(
361
366
  keys_width: list[str],
362
367
  keys_height: list[str],
363
368
  metadata: dict[str, dict] = {},
369
+ file_path: Path | None = None,
364
370
  ) -> dict:
365
371
  """Helper to do the actual denormalization.
366
372
 
@@ -371,6 +377,8 @@ def _denormalize_transformation(
371
377
  keys_height (list[str]): list of keys describing vertical position.
372
378
  Defaults to ["y", "h"].
373
379
  metadata (dict[str, dict]): dict of metadata per input file.
380
+ file_path (Path): file path source of otdict
381
+ if all detections stem from the same file.
374
382
 
375
383
  Returns:
376
384
  dict: denormalized dict
@@ -378,7 +386,7 @@ def _denormalize_transformation(
378
386
  changed_files = set()
379
387
 
380
388
  for frame in otdict[dataformat.DATA].values():
381
- input_file = frame[INPUT_FILE_PATH]
389
+ input_file = file_path.as_posix() if file_path else frame[INPUT_FILE_PATH]
382
390
  metadate = metadata[input_file]
383
391
  width = metadate[dataformat.VIDEO][dataformat.WIDTH]
384
392
  height = metadate[dataformat.VIDEO][dataformat.HEIGHT]
@@ -0,0 +1,15 @@
1
+ def check_types(
2
+ sigma_l: float, sigma_h: float, sigma_iou: float, t_min: int, t_miss_max: int
3
+ ) -> None:
4
+ """Raise ValueErrors if wrong types"""
5
+
6
+ if not isinstance(sigma_l, (int, float)):
7
+ raise ValueError("sigma_l has to be int or float")
8
+ if not isinstance(sigma_h, (int, float)):
9
+ raise ValueError("sigma_h has to be int or float")
10
+ if not isinstance(sigma_iou, (int, float)):
11
+ raise ValueError("sigma_iou has to be int or float")
12
+ if not isinstance(t_min, int):
13
+ raise ValueError("t_min has to be int")
14
+ if not isinstance(t_miss_max, int):
15
+ raise ValueError("t_miss_max has to be int")
File without changes
@@ -0,0 +1,24 @@
1
+ from pathlib import Path
2
+
3
+ from OTVision.config import CONFIG, DEFAULT_FILETYPE, TRACK
4
+ from OTVision.track.model.filebased.frame_chunk import FinishedChunk
5
+ from OTVision.track.model.filebased.frame_group import get_output_file
6
+ from OTVision.track.model.track_exporter import FinishedTracksExporter
7
+
8
+
9
+ class FinishedChunkTrackExporter(FinishedTracksExporter[FinishedChunk]):
10
+
11
+ def __init__(self, file_type: str = CONFIG[DEFAULT_FILETYPE][TRACK]) -> None:
12
+ super().__init__(file_type)
13
+
14
+ def get_detection_dicts(self, container: FinishedChunk) -> list[dict]:
15
+ return container.to_detection_dicts()
16
+
17
+ def get_result_path(self, container: FinishedChunk) -> Path:
18
+ return get_output_file(container.file, self.file_type)
19
+
20
+ def get_metadata(self, container: FinishedChunk) -> dict:
21
+ return container.metadata
22
+
23
+ def get_frame_group_id(self, container: FinishedChunk) -> int:
24
+ return container.frame_group_id
File without changes
@@ -0,0 +1,141 @@
1
+ from dataclasses import dataclass
2
+
3
+ from OTVision.dataformat import (
4
+ CLASS,
5
+ CONFIDENCE,
6
+ FINISHED,
7
+ FIRST,
8
+ INTERPOLATED_DETECTION,
9
+ TRACK_ID,
10
+ H,
11
+ W,
12
+ X,
13
+ Y,
14
+ )
15
+
16
+ TrackId = int
17
+
18
+
19
+ @dataclass(frozen=True, repr=True)
20
+ class Detection:
21
+ """Detection data without track context data.
22
+
23
+ Attributes:
24
+ label (str): Assigned label, e.g. vehicle class.
25
+ conf (float): Confidence of detected class.
26
+ x (float): X-coordinate of detection center.
27
+ y (float): Y-coordinate of detection center.
28
+ w (float): Width of detection.
29
+ h (float): Height of detection.
30
+ """
31
+
32
+ label: str
33
+ conf: float
34
+ x: float
35
+ y: float
36
+ w: float
37
+ h: float
38
+
39
+ def of_track(self, id: TrackId, is_first: bool) -> "TrackedDetection":
40
+ """Convert to TrackedDetection by adding track information.
41
+
42
+ Args:
43
+ id (TrackId): id of assigned track.
44
+ is_first (bool): whether this detection is first of track.
45
+
46
+ Returns:
47
+ TrackedDetection: This detection data with additional track information.
48
+ """
49
+ return TrackedDetection(
50
+ self.label,
51
+ self.conf,
52
+ self.x,
53
+ self.y,
54
+ self.w,
55
+ self.h,
56
+ is_first,
57
+ id,
58
+ )
59
+
60
+ def to_otdet(self) -> dict:
61
+ return {
62
+ CLASS: self.label,
63
+ CONFIDENCE: self.conf,
64
+ X: self.x,
65
+ Y: self.y,
66
+ W: self.w,
67
+ H: self.h,
68
+ }
69
+
70
+
71
+ @dataclass(frozen=True, repr=True)
72
+ class TrackedDetection(Detection):
73
+ """Detection with additional track data.
74
+ At the time a detection is tracked,
75
+ it might not be known whether it is the last of a track.
76
+
77
+ Attributes:
78
+ is_first (bool): whether this detection is the first in the track.
79
+ track_id (TrackId): id of the assigned track.
80
+ """
81
+
82
+ is_first: bool
83
+ track_id: TrackId
84
+
85
+ def finish(self, is_last: bool, is_discarded: bool) -> "FinishedDetection":
86
+ return FinishedDetection.from_tracked_detection(self, is_last, is_discarded)
87
+
88
+ def as_last_detection(self, is_discarded: bool) -> "FinishedDetection":
89
+ return FinishedDetection.from_tracked_detection(
90
+ self, is_last=True, is_discarded=is_discarded
91
+ )
92
+
93
+ def as_intermediate_detection(self, is_discarded: bool) -> "FinishedDetection":
94
+ return FinishedDetection.from_tracked_detection(
95
+ self, is_last=False, is_discarded=is_discarded
96
+ )
97
+
98
+
99
+ @dataclass(frozen=True, repr=True)
100
+ class FinishedDetection(TrackedDetection):
101
+ """Detection data with extended track information including is_finished.
102
+
103
+ Attributes:
104
+ is_last (bool): whether this detection is the last in the track.
105
+ is_discarded (bool): whether the detections's track was discarded.
106
+ """
107
+
108
+ is_last: bool
109
+ is_discarded: bool
110
+
111
+ @classmethod
112
+ def from_tracked_detection(
113
+ cls, tracked_detection: TrackedDetection, is_last: bool, is_discarded: bool
114
+ ) -> "FinishedDetection":
115
+ td = tracked_detection
116
+ return cls(
117
+ label=td.label,
118
+ conf=td.conf,
119
+ x=td.x,
120
+ y=td.y,
121
+ w=td.w,
122
+ h=td.h,
123
+ is_first=td.is_first,
124
+ track_id=td.track_id,
125
+ is_last=is_last,
126
+ is_discarded=is_discarded,
127
+ )
128
+
129
+ def to_dict(self) -> dict:
130
+ return {
131
+ CLASS: self.label,
132
+ CONFIDENCE: self.conf,
133
+ X: self.x,
134
+ Y: self.y,
135
+ W: self.w,
136
+ H: self.h,
137
+ INTERPOLATED_DETECTION: False,
138
+ FIRST: self.is_first,
139
+ FINISHED: self.is_last,
140
+ TRACK_ID: self.track_id,
141
+ }
File without changes
@@ -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.track.model.filebased.frame_group import FrameGroup, get_output_file
10
+ from OTVision.track.model.frame import (
11
+ FinishedFrame,
12
+ Frame,
13
+ FrameNo,
14
+ IsLastFrame,
15
+ TrackedFrame,
16
+ TrackId,
17
+ )
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[Frame[Path]]): 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[Frame[Path]]
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[Path]]): 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[Path]] = 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[Path]],
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[Path]]): overrides frames
169
+ with more specific frame type.
170
+ """
171
+
172
+ frames: Sequence[FinishedFrame[Path]]
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,149 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import datetime
3
+ from typing import Callable, Generic, Optional, Sequence, TypeVar
4
+
5
+ from PIL.Image import Image
6
+
7
+ from OTVision.dataformat import FRAME, OCCURRENCE, TRACK_ID
8
+ from OTVision.track.model.detection import (
9
+ Detection,
10
+ FinishedDetection,
11
+ TrackedDetection,
12
+ TrackId,
13
+ )
14
+
15
+ FrameNo = int
16
+ S = TypeVar("S")
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class Frame(Generic[S]):
21
+ """Frame metadata, optional image and respective detections.
22
+
23
+ Attributes:
24
+ no (FrameNo): Frame number.
25
+ occurrence (datetime): Time stamp, at which frame was recorded.
26
+ source (S): Generic source from where frame was obtained, e.g. video file path.
27
+ detections (Sequence[Detection]): A sequence of Detections occurring in frame.
28
+ image (Optional[Image]): Optional image data of frame.
29
+ """
30
+
31
+ no: FrameNo
32
+ occurrence: datetime
33
+ source: S
34
+ detections: Sequence[Detection]
35
+ image: Optional[Image]
36
+
37
+
38
+ IsLastFrame = Callable[[FrameNo, TrackId], bool]
39
+
40
+
41
+ @dataclass(frozen=True)
42
+ class TrackedFrame(Frame[S]):
43
+ """Frame metadata with tracked detections.
44
+ Also provides additional aggregated information about:
45
+ observed, finished and unfinished tracks.
46
+
47
+ Attributes:
48
+ detections (Sequence[TrackedDetection]): overrides Frame.detections with more
49
+ specific type of detection.
50
+ observed_tracks (set[TrackId]): set of tracks of which detection occur in this
51
+ frame.
52
+ finished_tracks (set[TrackId]): track ids of tracks observed in this or prior
53
+ to this frame that can now be considered finished. These track ids should
54
+ no longer be observed/assigned in future frames. (successfully completed)
55
+ discarded_tracks (set[TrackId]): track ids, that are now considered discarded.
56
+ The corresponding tracks are no longer pursued, previous TrackedDetections
57
+ of these tracks are also considered discarded. Discarded tracks may be
58
+ observed but not finished.(unsuccessful, incomplete)
59
+ unfinished_tracks (set[TrackId]): observed tracks that are not yet finished
60
+ and were not discarded.
61
+ """
62
+
63
+ detections: Sequence[TrackedDetection]
64
+ finished_tracks: set[TrackId]
65
+ discarded_tracks: set[TrackId]
66
+ observed_tracks: set[TrackId] = field(init=False)
67
+ unfinished_tracks: set[TrackId] = field(init=False)
68
+
69
+ def __post_init__(self) -> None:
70
+ """
71
+ Derive observed and unfinished tracks from tracked detections and finished
72
+ track information.
73
+ """
74
+ observed = {d.track_id for d in self.detections}
75
+ object.__setattr__(self, "observed_tracks", observed)
76
+
77
+ unfinished = {
78
+ o
79
+ for o in self.observed_tracks
80
+ if o not in self.finished_tracks and o not in self.discarded_tracks
81
+ }
82
+ object.__setattr__(self, "unfinished_tracks", unfinished)
83
+
84
+ def finish(
85
+ self,
86
+ is_last: IsLastFrame,
87
+ discarded_tracks: set[TrackId],
88
+ keep_discarded: bool = False,
89
+ ) -> "FinishedFrame":
90
+ """Turn this TrackedFrame into a finished frame
91
+ by adding is_finished information to all its detections.
92
+
93
+ Args:
94
+ is_last (IsLastFrame): function to determine whether
95
+ a track is finished in a certain frame.
96
+ discarded_tracks (set[TrackId]): list of tracks considered discarded.
97
+ Used to mark corresponding tracks.
98
+ keep_discarded (bool): whether FinishedDetections marked as discarded
99
+ should be kept in detections list. Defaults to False.
100
+ Returns:
101
+ FinishedFrame: frame with FinishedDetections
102
+ """
103
+ if keep_discarded:
104
+ detections = [
105
+ det.finish(
106
+ is_last=is_last(self.no, det.track_id),
107
+ is_discarded=(det.track_id in discarded_tracks),
108
+ )
109
+ for det in self.detections
110
+ ]
111
+ else:
112
+ detections = [
113
+ det.finish(is_last=is_last(self.no, det.track_id), is_discarded=False)
114
+ for det in self.detections
115
+ if (det.track_id not in discarded_tracks)
116
+ ]
117
+
118
+ return FinishedFrame(
119
+ no=self.no,
120
+ occurrence=self.occurrence,
121
+ source=self.source,
122
+ finished_tracks=self.finished_tracks,
123
+ detections=detections,
124
+ image=self.image,
125
+ discarded_tracks=discarded_tracks,
126
+ )
127
+
128
+
129
+ @dataclass(frozen=True)
130
+ class FinishedFrame(TrackedFrame[S]):
131
+ """TrackedFrame with FinishedDetections.
132
+
133
+ Args:
134
+ detections (Sequence[FinishedDetection]): overrides TrackedFrame.detections
135
+ with more specific detection type.
136
+ """
137
+
138
+ detections: Sequence[FinishedDetection]
139
+
140
+ def to_detection_dicts(self) -> list[dict]:
141
+ frame_metadata = {FRAME: self.no, OCCURRENCE: self.occurrence.timestamp()}
142
+
143
+ # add frame metadata to each detection dict
144
+ detection_dict_list = [
145
+ {**detection.to_dict(), **frame_metadata} for detection in self.detections
146
+ ]
147
+
148
+ detection_dict_list.sort(key=lambda det: det[TRACK_ID])
149
+ return detection_dict_list