OTVision 0.6.8__py3-none-any.whl → 0.6.10__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/abstraction/pipes_and_filter.py +4 -4
- OTVision/application/buffer.py +5 -12
- OTVision/application/config_parser.py +51 -2
- OTVision/application/detect/current_object_detector.py +2 -4
- OTVision/application/event/new_otvision_config.py +6 -0
- OTVision/application/{detect/detection_file_save_path_provider.py → otvision_save_path_provider.py} +8 -7
- OTVision/application/track/ottrk.py +203 -0
- OTVision/application/track/tracking_run_id.py +35 -0
- OTVision/detect/builder.py +8 -14
- OTVision/detect/detected_frame_buffer.py +13 -1
- OTVision/detect/detected_frame_producer.py +2 -2
- OTVision/detect/detected_frame_producer_factory.py +4 -8
- OTVision/detect/otdet.py +109 -41
- OTVision/detect/otdet_file_writer.py +52 -29
- OTVision/detect/rtsp_input_source.py +15 -3
- OTVision/detect/video_input_source.py +8 -8
- OTVision/detect/yolo.py +3 -7
- OTVision/domain/detect_producer_consumer.py +3 -3
- OTVision/domain/frame.py +12 -0
- OTVision/domain/input_source_detect.py +3 -3
- OTVision/domain/object_detection.py +4 -6
- OTVision/helpers/date.py +16 -0
- OTVision/plugin/ffmpeg_video_writer.py +2 -4
- OTVision/track/builder.py +2 -5
- OTVision/track/id_generator.py +1 -3
- OTVision/track/parser/chunk_parser_plugins.py +1 -19
- OTVision/track/parser/frame_group_parser_plugins.py +19 -74
- OTVision/track/stream_ottrk_file_writer.py +116 -0
- OTVision/track/track.py +2 -1
- OTVision/version.py +1 -1
- {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/METADATA +9 -2
- {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/RECORD +34 -30
- {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/WHEEL +0 -0
- {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/licenses/LICENSE +0 -0
OTVision/detect/otdet.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
4
|
from pathlib import Path
|
|
@@ -6,6 +7,14 @@ from typing import Self
|
|
|
6
7
|
from OTVision import dataformat, version
|
|
7
8
|
from OTVision.domain.detection import Detection
|
|
8
9
|
from OTVision.domain.frame import DetectedFrame
|
|
10
|
+
from OTVision.helpers.date import parse_datetime
|
|
11
|
+
from OTVision.helpers.files import (
|
|
12
|
+
FULL_FILE_NAME_PATTERN,
|
|
13
|
+
HOSTNAME,
|
|
14
|
+
InproperFormattedFilename,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
MISSING_START_DATE = datetime(1900, 1, 1)
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
@dataclass
|
|
@@ -35,7 +44,7 @@ class OtdetBuilderError(Exception):
|
|
|
35
44
|
pass
|
|
36
45
|
|
|
37
46
|
|
|
38
|
-
class
|
|
47
|
+
class OtdetMetadataBuilder:
|
|
39
48
|
@property
|
|
40
49
|
def config(self) -> OtdetBuilderConfig:
|
|
41
50
|
if self._config is None:
|
|
@@ -45,51 +54,14 @@ class OtdetBuilder:
|
|
|
45
54
|
def __init__(self) -> None:
|
|
46
55
|
self._config: OtdetBuilderConfig | None = None
|
|
47
56
|
|
|
48
|
-
def
|
|
49
|
-
self._config = config
|
|
50
|
-
return self
|
|
51
|
-
|
|
52
|
-
def reset(self) -> Self:
|
|
53
|
-
self._config = None
|
|
54
|
-
return self
|
|
55
|
-
|
|
56
|
-
def build(self, detections: list[DetectedFrame]) -> dict:
|
|
57
|
-
number_of_frames = len(detections)
|
|
57
|
+
def build(self, number_of_frames: int) -> dict:
|
|
58
58
|
result = {
|
|
59
|
-
dataformat.METADATA: self._build_metadata(number_of_frames),
|
|
60
|
-
dataformat.DATA: self._build_data(detections),
|
|
61
|
-
}
|
|
62
|
-
self.reset()
|
|
63
|
-
return result
|
|
64
|
-
|
|
65
|
-
def _build_metadata(self, number_of_frames: int) -> dict:
|
|
66
|
-
return {
|
|
67
59
|
dataformat.OTDET_VERSION: version.otdet_version(),
|
|
68
60
|
dataformat.VIDEO: self._build_video_config(number_of_frames),
|
|
69
61
|
dataformat.DETECTION: self._build_detection_config(),
|
|
70
62
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
data = {}
|
|
74
|
-
for frame in frames:
|
|
75
|
-
converted_detections = [
|
|
76
|
-
self.__convert_detection(detection) for detection in frame.detections
|
|
77
|
-
]
|
|
78
|
-
data[str(frame.no)] = {
|
|
79
|
-
dataformat.DETECTIONS: converted_detections,
|
|
80
|
-
dataformat.OCCURRENCE: frame.occurrence.timestamp(),
|
|
81
|
-
}
|
|
82
|
-
return data
|
|
83
|
-
|
|
84
|
-
def __convert_detection(self, detection: Detection) -> dict:
|
|
85
|
-
return {
|
|
86
|
-
dataformat.CLASS: detection.label,
|
|
87
|
-
dataformat.CONFIDENCE: detection.conf,
|
|
88
|
-
dataformat.X: detection.x,
|
|
89
|
-
dataformat.Y: detection.y,
|
|
90
|
-
dataformat.W: detection.w,
|
|
91
|
-
dataformat.H: detection.h,
|
|
92
|
-
}
|
|
63
|
+
self.reset()
|
|
64
|
+
return result
|
|
93
65
|
|
|
94
66
|
def _build_video_config(self, number_of_frames: int) -> dict:
|
|
95
67
|
source = Path(self.config.source)
|
|
@@ -128,6 +100,69 @@ class OtdetBuilder:
|
|
|
128
100
|
dataformat.DETECT_END: self.config.detect_end,
|
|
129
101
|
}
|
|
130
102
|
|
|
103
|
+
def add_config(self, config: OtdetBuilderConfig) -> Self:
|
|
104
|
+
self._config = config
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def reset(self) -> Self:
|
|
108
|
+
self._config = None
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class OtdetBuilder:
|
|
113
|
+
@property
|
|
114
|
+
def config(self) -> OtdetBuilderConfig:
|
|
115
|
+
if self._config is None:
|
|
116
|
+
raise OtdetBuilderError("Otdet builder config is not set")
|
|
117
|
+
return self._config
|
|
118
|
+
|
|
119
|
+
def __init__(self, metadata_builder: OtdetMetadataBuilder) -> None:
|
|
120
|
+
self._config: OtdetBuilderConfig | None = None
|
|
121
|
+
self._metadata_builder = metadata_builder
|
|
122
|
+
|
|
123
|
+
def add_config(self, config: OtdetBuilderConfig) -> Self:
|
|
124
|
+
self._config = config
|
|
125
|
+
self._metadata_builder.add_config(config)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def reset(self) -> Self:
|
|
129
|
+
self._config = None
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def build(self, detections: list[DetectedFrame]) -> dict:
|
|
133
|
+
number_of_frames = len(detections)
|
|
134
|
+
result = {
|
|
135
|
+
dataformat.METADATA: self._build_metadata(number_of_frames),
|
|
136
|
+
dataformat.DATA: self._build_data(detections),
|
|
137
|
+
}
|
|
138
|
+
self.reset()
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def _build_metadata(self, number_of_frames: int) -> dict:
|
|
142
|
+
return self._metadata_builder.build(number_of_frames)
|
|
143
|
+
|
|
144
|
+
def _build_data(self, frames: list[DetectedFrame]) -> dict:
|
|
145
|
+
data = {}
|
|
146
|
+
for frame in frames:
|
|
147
|
+
converted_detections = [
|
|
148
|
+
self.__convert_detection(detection) for detection in frame.detections
|
|
149
|
+
]
|
|
150
|
+
data[str(frame.no)] = {
|
|
151
|
+
dataformat.DETECTIONS: converted_detections,
|
|
152
|
+
dataformat.OCCURRENCE: frame.occurrence.timestamp(),
|
|
153
|
+
}
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
def __convert_detection(self, detection: Detection) -> dict:
|
|
157
|
+
return {
|
|
158
|
+
dataformat.CLASS: detection.label,
|
|
159
|
+
dataformat.CONFIDENCE: detection.conf,
|
|
160
|
+
dataformat.X: detection.x,
|
|
161
|
+
dataformat.Y: detection.y,
|
|
162
|
+
dataformat.W: detection.w,
|
|
163
|
+
dataformat.H: detection.h,
|
|
164
|
+
}
|
|
165
|
+
|
|
131
166
|
|
|
132
167
|
def serialize_video_length(video_length: timedelta) -> str:
|
|
133
168
|
"""Serialize a timedelta object to a video length string in 'H+:MM:SS' format.
|
|
@@ -180,3 +215,36 @@ def parse_video_length(video_length: str) -> timedelta:
|
|
|
180
215
|
f"Could not parse video length '{video_length}'. "
|
|
181
216
|
"Expected format 'HH:MM:SS'."
|
|
182
217
|
) from cause
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def extract_start_date_from_otdet(metadata: dict) -> datetime:
|
|
221
|
+
if dataformat.RECORDED_START_DATE in metadata[dataformat.VIDEO].keys():
|
|
222
|
+
recorded_start_date = metadata[dataformat.VIDEO][dataformat.RECORDED_START_DATE]
|
|
223
|
+
return parse_datetime(recorded_start_date)
|
|
224
|
+
return MISSING_START_DATE
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def extract_expected_duration_from_otdet(metadata: dict) -> timedelta:
|
|
228
|
+
if dataformat.EXPECTED_DURATION in metadata[dataformat.VIDEO].keys():
|
|
229
|
+
if expected_duration := metadata[dataformat.VIDEO][
|
|
230
|
+
dataformat.EXPECTED_DURATION
|
|
231
|
+
]:
|
|
232
|
+
return timedelta(seconds=int(expected_duration))
|
|
233
|
+
return extract_otdet_video_length(metadata)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def extract_otdet_video_length(metadata: dict) -> timedelta:
|
|
237
|
+
video_length = metadata[dataformat.VIDEO][dataformat.LENGTH]
|
|
238
|
+
return parse_video_length(video_length)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def extract_hostname_from_otdet(metadata: dict) -> str:
|
|
242
|
+
video_name = Path(metadata[dataformat.VIDEO][dataformat.FILENAME]).name
|
|
243
|
+
match = re.search(
|
|
244
|
+
FULL_FILE_NAME_PATTERN,
|
|
245
|
+
video_name,
|
|
246
|
+
)
|
|
247
|
+
if match:
|
|
248
|
+
return match.group(HOSTNAME)
|
|
249
|
+
|
|
250
|
+
raise InproperFormattedFilename(f"Could not parse {video_name}.")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
4
|
+
from OTVision.abstraction.observer import Observer, Subject
|
|
3
5
|
from OTVision.application.detect.current_object_detector_metadata import (
|
|
4
6
|
CurrentObjectDetectorMetadata,
|
|
5
7
|
)
|
|
6
|
-
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
7
|
-
DetectionFileSavePathProvider,
|
|
8
|
-
)
|
|
9
8
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
9
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
10
10
|
from OTVision.detect.detected_frame_buffer import DetectedFrameBufferEvent
|
|
11
11
|
from OTVision.detect.otdet import OtdetBuilder, OtdetBuilderConfig
|
|
12
12
|
from OTVision.helpers.files import write_json
|
|
@@ -15,6 +15,14 @@ from OTVision.helpers.log import LOGGER_NAME
|
|
|
15
15
|
log = logging.getLogger(LOGGER_NAME)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class OtdetFileWrittenEvent:
|
|
20
|
+
"""Event that is emitted when an OTDET file is written."""
|
|
21
|
+
|
|
22
|
+
otdet_builder_config: OtdetBuilderConfig
|
|
23
|
+
number_of_frames: int
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
class OtdetFileWriter:
|
|
19
27
|
"""Handles writing object detection results to a file in OTDET format.
|
|
20
28
|
|
|
@@ -28,18 +36,20 @@ class OtdetFileWriter:
|
|
|
28
36
|
settings.
|
|
29
37
|
current_object_detector_metadata (CurrentObjectDetectorMetadata): Provides
|
|
30
38
|
metadata about the current object detector.
|
|
31
|
-
save_path_provider (
|
|
39
|
+
save_path_provider (OtvisionSavePathProvider): determines the save path for
|
|
32
40
|
the otdet file to be written.
|
|
33
41
|
|
|
34
42
|
"""
|
|
35
43
|
|
|
36
44
|
def __init__(
|
|
37
45
|
self,
|
|
46
|
+
subject: Subject[OtdetFileWrittenEvent],
|
|
38
47
|
builder: OtdetBuilder,
|
|
39
48
|
get_current_config: GetCurrentConfig,
|
|
40
49
|
current_object_detector_metadata: CurrentObjectDetectorMetadata,
|
|
41
|
-
save_path_provider:
|
|
50
|
+
save_path_provider: OtvisionSavePathProvider,
|
|
42
51
|
):
|
|
52
|
+
self._subject = subject
|
|
43
53
|
self._builder = builder
|
|
44
54
|
self._get_current_config = get_current_config
|
|
45
55
|
self._current_object_detector_metadata = current_object_detector_metadata
|
|
@@ -68,31 +78,32 @@ class OtdetFileWriter:
|
|
|
68
78
|
actual_fps = actual_frames / source_metadata.duration.total_seconds()
|
|
69
79
|
|
|
70
80
|
class_mapping = self._current_object_detector_metadata.get().classifications
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
).build(event.frames)
|
|
81
|
+
builder_config = OtdetBuilderConfig(
|
|
82
|
+
conf=detect_config.confidence,
|
|
83
|
+
iou=detect_config.iou,
|
|
84
|
+
source=source_metadata.output,
|
|
85
|
+
video_width=source_metadata.width,
|
|
86
|
+
video_height=source_metadata.height,
|
|
87
|
+
expected_duration=expected_duration,
|
|
88
|
+
actual_duration=source_metadata.duration,
|
|
89
|
+
recorded_fps=source_metadata.fps,
|
|
90
|
+
recorded_start_date=source_metadata.start_time,
|
|
91
|
+
actual_fps=actual_fps,
|
|
92
|
+
actual_frames=actual_frames,
|
|
93
|
+
detection_img_size=detect_config.img_size,
|
|
94
|
+
normalized=detect_config.normalized,
|
|
95
|
+
detection_model=detect_config.weights,
|
|
96
|
+
half_precision=detect_config.half_precision,
|
|
97
|
+
chunksize=1,
|
|
98
|
+
classifications=class_mapping,
|
|
99
|
+
detect_start=detect_config.detect_start,
|
|
100
|
+
detect_end=detect_config.detect_end,
|
|
101
|
+
)
|
|
102
|
+
otdet = self._builder.add_config(builder_config).build(event.frames)
|
|
94
103
|
|
|
95
|
-
detections_file = self._save_path_provider.provide(
|
|
104
|
+
detections_file = self._save_path_provider.provide(
|
|
105
|
+
source_metadata.output, config.filetypes.detect
|
|
106
|
+
)
|
|
96
107
|
detections_file.parent.mkdir(parents=True, exist_ok=True)
|
|
97
108
|
write_json(
|
|
98
109
|
otdet,
|
|
@@ -105,3 +116,15 @@ class OtdetFileWriter:
|
|
|
105
116
|
|
|
106
117
|
finished_msg = "Finished detection"
|
|
107
118
|
log.info(finished_msg)
|
|
119
|
+
self.__notify(num_frames=actual_frames, builder_config=builder_config)
|
|
120
|
+
|
|
121
|
+
def __notify(self, num_frames: int, builder_config: OtdetBuilderConfig) -> None:
|
|
122
|
+
self._subject.notify(
|
|
123
|
+
OtdetFileWrittenEvent(
|
|
124
|
+
number_of_frames=num_frames, otdet_builder_config=builder_config
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def register_observer(self, observer: Observer[OtdetFileWrittenEvent]) -> None:
|
|
129
|
+
"""Register an observer to receive notifications about otdet file writes.."""
|
|
130
|
+
self._subject.register(observer)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import socket
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
from time import sleep
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Iterator
|
|
5
5
|
from urllib.parse import urlparse
|
|
6
6
|
|
|
7
7
|
from cv2 import (
|
|
@@ -21,6 +21,7 @@ from OTVision.application.config import (
|
|
|
21
21
|
StreamConfig,
|
|
22
22
|
)
|
|
23
23
|
from OTVision.application.configure_logger import logger
|
|
24
|
+
from OTVision.application.event.new_otvision_config import NewOtvisionConfigEvent
|
|
24
25
|
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
25
26
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
26
27
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
@@ -33,6 +34,10 @@ RETRY_SECONDS = 5
|
|
|
33
34
|
DEFAULT_READ_FAIL_THRESHOLD = 5
|
|
34
35
|
|
|
35
36
|
|
|
37
|
+
class NoConfigurationFoundError(Exception):
|
|
38
|
+
"""Raised when no configuration is found for the RTSP stream."""
|
|
39
|
+
|
|
40
|
+
|
|
36
41
|
class Counter:
|
|
37
42
|
def __init__(self, start_value: int = 0) -> None:
|
|
38
43
|
self._start_value = start_value
|
|
@@ -66,7 +71,7 @@ class RtspInputSource(InputSourceDetect):
|
|
|
66
71
|
def stream_config(self) -> StreamConfig:
|
|
67
72
|
if stream_config := self.config.stream:
|
|
68
73
|
return stream_config
|
|
69
|
-
raise
|
|
74
|
+
raise NoConfigurationFoundError("Stream config not found in config")
|
|
70
75
|
|
|
71
76
|
@property
|
|
72
77
|
def rtsp_url(self) -> str:
|
|
@@ -126,7 +131,7 @@ class RtspInputSource(InputSourceDetect):
|
|
|
126
131
|
self._current_video_capture = self._init_video_capture(self._current_stream)
|
|
127
132
|
return self._current_video_capture
|
|
128
133
|
|
|
129
|
-
def produce(self) ->
|
|
134
|
+
def produce(self) -> Iterator[Frame]:
|
|
130
135
|
self._stream_start_time = self._datetime_provider.provide()
|
|
131
136
|
self._current_video_start_time = self._stream_start_time
|
|
132
137
|
try:
|
|
@@ -248,6 +253,13 @@ class RtspInputSource(InputSourceDetect):
|
|
|
248
253
|
)
|
|
249
254
|
return str(self.stream_config.save_dir / output_filename)
|
|
250
255
|
|
|
256
|
+
def notify_new_config(self, config: NewOtvisionConfigEvent) -> None:
|
|
257
|
+
try:
|
|
258
|
+
logger().debug("New OTVision config detected. Flushing buffers...")
|
|
259
|
+
self._notify_flush_observers()
|
|
260
|
+
except NoConfigurationFoundError:
|
|
261
|
+
logger().info("No configuration found for RTSP stream. Skipping flushing.")
|
|
262
|
+
|
|
251
263
|
|
|
252
264
|
def convert_frame_to_rgb(frame: ndarray) -> ndarray:
|
|
253
265
|
return cvtColor(frame, COLOR_BGR2RGB)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
2
|
from datetime import datetime
|
|
3
3
|
from pathlib import Path
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Iterator
|
|
5
5
|
|
|
6
6
|
import av
|
|
7
7
|
from av.container.input import InputContainer
|
|
@@ -9,12 +9,10 @@ from tqdm import tqdm
|
|
|
9
9
|
|
|
10
10
|
from OTVision.abstraction.observer import Subject
|
|
11
11
|
from OTVision.application.config import DATETIME_FORMAT, Config
|
|
12
|
-
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
13
|
-
DetectionFileSavePathProvider,
|
|
14
|
-
)
|
|
15
12
|
from OTVision.application.detect.timestamper import Timestamper
|
|
16
13
|
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
17
14
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
15
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
18
16
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
19
17
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
20
18
|
from OTVision.detect.timestamper import TimestamperFactory, parse_start_time_from
|
|
@@ -47,7 +45,7 @@ class VideoSource(InputSourceDetect):
|
|
|
47
45
|
configuration.
|
|
48
46
|
frame_rotator (AvVideoFrameRotator): Use to rotate video frames.
|
|
49
47
|
timestamper_factory (Timestamper): Factory for creating timestamp generators.
|
|
50
|
-
save_path_provider (
|
|
48
|
+
save_path_provider (OtvisionSavePathProvider): Provider for detection
|
|
51
49
|
output paths.
|
|
52
50
|
"""
|
|
53
51
|
|
|
@@ -66,7 +64,7 @@ class VideoSource(InputSourceDetect):
|
|
|
66
64
|
get_current_config: GetCurrentConfig,
|
|
67
65
|
frame_rotator: AvVideoFrameRotator,
|
|
68
66
|
timestamper_factory: TimestamperFactory,
|
|
69
|
-
save_path_provider:
|
|
67
|
+
save_path_provider: OtvisionSavePathProvider,
|
|
70
68
|
) -> None:
|
|
71
69
|
self.subject_flush = subject_flush
|
|
72
70
|
self.subject_new_video_start = subject_new_video_start
|
|
@@ -76,7 +74,7 @@ class VideoSource(InputSourceDetect):
|
|
|
76
74
|
self._save_path_provider = save_path_provider
|
|
77
75
|
self.__should_flush = False
|
|
78
76
|
|
|
79
|
-
def produce(self) ->
|
|
77
|
+
def produce(self) -> Iterator[Frame]:
|
|
80
78
|
"""Generate frames from video files that meet detection requirements.
|
|
81
79
|
|
|
82
80
|
Yields frames from valid video files while managing rotation, timestamping,
|
|
@@ -93,7 +91,9 @@ class VideoSource(InputSourceDetect):
|
|
|
93
91
|
print(start_msg)
|
|
94
92
|
|
|
95
93
|
for video_file in tqdm(video_files, desc="Detected video files", unit=" files"):
|
|
96
|
-
detections_file = self._save_path_provider.provide(
|
|
94
|
+
detections_file = self._save_path_provider.provide(
|
|
95
|
+
str(video_file), self._current_config.filetypes.detect
|
|
96
|
+
)
|
|
97
97
|
|
|
98
98
|
if not self.__detection_requirements_are_met(video_file, detections_file):
|
|
99
99
|
continue
|
OTVision/detect/yolo.py
CHANGED
|
@@ -22,7 +22,7 @@ OTVision module to detect objects using yolov5
|
|
|
22
22
|
import logging
|
|
23
23
|
from pathlib import Path
|
|
24
24
|
from time import perf_counter
|
|
25
|
-
from typing import
|
|
25
|
+
from typing import Iterator
|
|
26
26
|
|
|
27
27
|
import torch
|
|
28
28
|
from tqdm import tqdm
|
|
@@ -134,14 +134,10 @@ class YoloDetector(ObjectDetector, Filter[Frame, DetectedFrame]):
|
|
|
134
134
|
self._detection_converter = detection_converter
|
|
135
135
|
self._detected_frame_factory = detected_frame_factory
|
|
136
136
|
|
|
137
|
-
def filter(
|
|
138
|
-
self, pipe: Generator[Frame, None, None]
|
|
139
|
-
) -> Generator[DetectedFrame, None, None]:
|
|
137
|
+
def filter(self, pipe: Iterator[Frame]) -> Iterator[DetectedFrame]:
|
|
140
138
|
return self.detect(pipe)
|
|
141
139
|
|
|
142
|
-
def detect(
|
|
143
|
-
self, frames: Generator[Frame, None, None]
|
|
144
|
-
) -> Generator[DetectedFrame, None, None]:
|
|
140
|
+
def detect(self, frames: Iterator[Frame]) -> Iterator[DetectedFrame]:
|
|
145
141
|
for frame in tqdm(
|
|
146
142
|
frames,
|
|
147
143
|
desc="Detected frames",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
4
|
from OTVision.domain.frame import DetectedFrame
|
|
5
5
|
|
|
@@ -21,10 +21,10 @@ class DetectedFrameProducer(ABC):
|
|
|
21
21
|
"""
|
|
22
22
|
|
|
23
23
|
@abstractmethod
|
|
24
|
-
def produce(self) ->
|
|
24
|
+
def produce(self) -> Iterator[DetectedFrame]:
|
|
25
25
|
"""Generate a stream of detected frames.
|
|
26
26
|
|
|
27
27
|
Returns:
|
|
28
|
-
|
|
28
|
+
Iterator[DetectedFrame, None, None]: A stream of detected frames.
|
|
29
29
|
"""
|
|
30
30
|
raise NotImplementedError
|
OTVision/domain/frame.py
CHANGED
|
@@ -165,6 +165,18 @@ class TrackedFrame(DetectedFrame):
|
|
|
165
165
|
discarded_tracks=discarded_tracks,
|
|
166
166
|
)
|
|
167
167
|
|
|
168
|
+
def without_image(self) -> "TrackedFrame":
|
|
169
|
+
return TrackedFrame(
|
|
170
|
+
no=self.no,
|
|
171
|
+
occurrence=self.occurrence,
|
|
172
|
+
source=self.source,
|
|
173
|
+
output=self.output,
|
|
174
|
+
image=None,
|
|
175
|
+
detections=self.detections,
|
|
176
|
+
finished_tracks=self.finished_tracks,
|
|
177
|
+
discarded_tracks=self.discarded_tracks,
|
|
178
|
+
)
|
|
179
|
+
|
|
168
180
|
|
|
169
181
|
@dataclass(frozen=True, kw_only=True)
|
|
170
182
|
class FinishedFrame(TrackedFrame):
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
4
|
from OTVision.domain.frame import Frame
|
|
5
5
|
|
|
@@ -14,7 +14,7 @@ class InputSourceDetect(ABC):
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|
|
17
|
-
def produce(self) ->
|
|
17
|
+
def produce(self) -> Iterator[Frame]:
|
|
18
18
|
"""Generate a stream of frames from the input source.
|
|
19
19
|
|
|
20
20
|
Implementations should yield Frame objects one at a time from the source,
|
|
@@ -22,7 +22,7 @@ class InputSourceDetect(ABC):
|
|
|
22
22
|
at appropriate points (e.g., end of video segments or buffer boundaries).
|
|
23
23
|
|
|
24
24
|
Returns:
|
|
25
|
-
|
|
25
|
+
Iterator [Frame]: A generator yielding Frame objects
|
|
26
26
|
sequentially from the input source.
|
|
27
27
|
"""
|
|
28
28
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
4
|
from OTVision.application.config import DetectConfig
|
|
5
5
|
from OTVision.domain.frame import DetectedFrame, Frame
|
|
@@ -25,16 +25,14 @@ class ObjectDetectorMetadata(ABC):
|
|
|
25
25
|
class ObjectDetector(ObjectDetectorMetadata):
|
|
26
26
|
|
|
27
27
|
@abstractmethod
|
|
28
|
-
def detect(
|
|
29
|
-
self, frames: Generator[Frame, None, None]
|
|
30
|
-
) -> Generator[DetectedFrame, None, None]:
|
|
28
|
+
def detect(self, frames: Iterator[Frame]) -> Iterator[DetectedFrame]:
|
|
31
29
|
"""Runs object detection on a video.
|
|
32
30
|
|
|
33
31
|
Args:
|
|
34
|
-
frames (
|
|
32
|
+
frames (Iterator[Frame]): the source to read frames from.
|
|
35
33
|
|
|
36
34
|
Returns:
|
|
37
|
-
|
|
35
|
+
Iterator[DetectedFrame]: nested list of detections.
|
|
38
36
|
First level is frames, second level is detections within frame.
|
|
39
37
|
"""
|
|
40
38
|
raise NotImplementedError
|
OTVision/helpers/date.py
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
from datetime import datetime, timezone
|
|
2
2
|
|
|
3
|
+
from OTVision.dataformat import DATE_FORMAT
|
|
4
|
+
|
|
3
5
|
|
|
4
6
|
def parse_date_string_to_utc_datime(date_string: str, date_format: str) -> datetime:
|
|
5
7
|
"""Parse a date string to a datetime object with UTC set as timezone.
|
|
@@ -24,3 +26,17 @@ def parse_timestamp_string_to_utc_datetime(timestamp: str | float) -> datetime:
|
|
|
24
26
|
datetime: the datetime object with UTC as set timezone
|
|
25
27
|
"""
|
|
26
28
|
return datetime.fromtimestamp(float(timestamp), timezone.utc)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def parse_datetime(date: str | float) -> datetime:
|
|
32
|
+
"""Parse a date string or timestamp to a datetime with UTC as timezone.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
date (str | float): the date to parse
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
datetime: the parsed datetime object with UTC set as timezone
|
|
39
|
+
"""
|
|
40
|
+
if isinstance(date, str) and ("-" in date):
|
|
41
|
+
return parse_date_string_to_utc_datime(date, DATE_FORMAT)
|
|
42
|
+
return parse_timestamp_string_to_utc_datetime(date)
|
|
@@ -3,7 +3,7 @@ from enum import IntEnum, StrEnum
|
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
from subprocess import PIPE, Popen, TimeoutExpired
|
|
5
5
|
from threading import Thread
|
|
6
|
-
from typing import Callable,
|
|
6
|
+
from typing import Callable, Iterator
|
|
7
7
|
|
|
8
8
|
import ffmpeg
|
|
9
9
|
from numpy import ndarray
|
|
@@ -282,9 +282,7 @@ class FfmpegVideoWriter(VideoWriter):
|
|
|
282
282
|
log.info(f"Writing new video file to '{save_file}'.")
|
|
283
283
|
return process
|
|
284
284
|
|
|
285
|
-
def filter(
|
|
286
|
-
self, pipe: Generator[Frame, None, None]
|
|
287
|
-
) -> Generator[Frame, None, None]:
|
|
285
|
+
def filter(self, pipe: Iterator[Frame]) -> Iterator[Frame]:
|
|
288
286
|
for frame in pipe:
|
|
289
287
|
if (image := frame.get(FrameKeys.data)) is not None:
|
|
290
288
|
self.write(image)
|
OTVision/track/builder.py
CHANGED
|
@@ -7,6 +7,7 @@ from OTVision.application.configure_logger import ConfigureLogger
|
|
|
7
7
|
from OTVision.application.get_config import GetConfig
|
|
8
8
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
9
9
|
from OTVision.application.track.get_track_cli_args import GetTrackCliArgs
|
|
10
|
+
from OTVision.application.track.tracking_run_id import StrIdGenerator
|
|
10
11
|
from OTVision.application.track.update_current_track_config import (
|
|
11
12
|
UpdateCurrentTrackConfig,
|
|
12
13
|
)
|
|
@@ -20,11 +21,7 @@ from OTVision.domain.serialization import Deserializer
|
|
|
20
21
|
from OTVision.plugin.yaml_serialization import YamlDeserializer
|
|
21
22
|
from OTVision.track.cli import ArgparseTrackCliParser
|
|
22
23
|
from OTVision.track.exporter.filebased_exporter import FinishedChunkTrackExporter
|
|
23
|
-
from OTVision.track.id_generator import
|
|
24
|
-
StrIdGenerator,
|
|
25
|
-
track_id_generator,
|
|
26
|
-
tracking_run_uuid_generator,
|
|
27
|
-
)
|
|
24
|
+
from OTVision.track.id_generator import track_id_generator, tracking_run_uuid_generator
|
|
28
25
|
from OTVision.track.model.filebased.frame_chunk import ChunkParser
|
|
29
26
|
from OTVision.track.model.filebased.frame_group import FrameGroupParser
|
|
30
27
|
from OTVision.track.model.track_exporter import FinishedTracksExporter
|
OTVision/track/id_generator.py
CHANGED
|
@@ -8,7 +8,6 @@ from OTVision.dataformat import (
|
|
|
8
8
|
CLASS,
|
|
9
9
|
CONFIDENCE,
|
|
10
10
|
DATA,
|
|
11
|
-
DATE_FORMAT,
|
|
12
11
|
DETECTIONS,
|
|
13
12
|
OCCURRENCE,
|
|
14
13
|
H,
|
|
@@ -18,10 +17,7 @@ from OTVision.dataformat import (
|
|
|
18
17
|
)
|
|
19
18
|
from OTVision.domain.detection import Detection
|
|
20
19
|
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
|
-
)
|
|
20
|
+
from OTVision.helpers.date import parse_datetime
|
|
25
21
|
from OTVision.helpers.files import denormalize_bbox, read_json
|
|
26
22
|
from OTVision.track.model.filebased.frame_chunk import ChunkParser, FrameChunk
|
|
27
23
|
from OTVision.track.model.filebased.frame_group import FrameGroup
|
|
@@ -84,17 +80,3 @@ class DetectionParser:
|
|
|
84
80
|
)
|
|
85
81
|
detections.append(detected_item)
|
|
86
82
|
return detections
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def parse_datetime(date: str | float) -> datetime:
|
|
90
|
-
"""Parse a date string or timestamp to a datetime with UTC as timezone.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
date (str | float): the date to parse
|
|
94
|
-
|
|
95
|
-
Returns:
|
|
96
|
-
datetime: the parsed datetime object with UTC set as timezone
|
|
97
|
-
"""
|
|
98
|
-
if isinstance(date, str) and ("-" in date):
|
|
99
|
-
return parse_date_string_to_utc_datime(date, DATE_FORMAT)
|
|
100
|
-
return parse_timestamp_string_to_utc_datetime(date)
|