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,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}}
|