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 +2 -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/detection.py +141 -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/frame.py +149 -0
- OTVision/track/model/track_exporter.py +119 -0
- OTVision/track/model/tracking_interfaces.py +309 -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 +197 -0
- OTVision/track/tracker/tracker_plugin_iou.py +228 -0
- OTVision/version.py +1 -1
- OTVision/view/view_track.py +1 -1
- {otvision-0.6.0.dist-info → otvision-0.6.2.dist-info}/METADATA +4 -3
- {otvision-0.6.0.dist-info → otvision-0.6.2.dist-info}/RECORD +26 -12
- OTVision/track/iou.py +0 -282
- OTVision/track/iou_util.py +0 -140
- OTVision/track/preprocess.py +0 -453
- {otvision-0.6.0.dist-info → otvision-0.6.2.dist-info}/WHEEL +0 -0
- {otvision-0.6.0.dist-info → otvision-0.6.2.dist-info}/licenses/LICENSE +0 -0
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=
|
|
114
|
-
|
|
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(
|
|
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
|