OTVision 0.6.7__py3-none-any.whl → 0.6.9__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.py +18 -0
- OTVision/application/config_parser.py +68 -11
- OTVision/application/detect/current_object_detector.py +2 -4
- OTVision/application/detect/update_detect_config_with_cli_args.py +16 -0
- OTVision/application/event/__init__.py +0 -0
- OTVision/application/event/new_otvision_config.py +6 -0
- OTVision/application/event/new_video_start.py +9 -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/application/video/__init__.py +0 -0
- OTVision/application/video/generate_video.py +15 -0
- OTVision/config.py +2 -0
- OTVision/detect/builder.py +35 -22
- OTVision/detect/cli.py +44 -1
- OTVision/detect/detected_frame_buffer.py +13 -1
- OTVision/detect/detected_frame_producer.py +14 -0
- OTVision/detect/detected_frame_producer_factory.py +39 -0
- OTVision/detect/file_based_detect_builder.py +39 -3
- OTVision/detect/otdet.py +109 -41
- OTVision/detect/otdet_file_writer.py +52 -29
- OTVision/detect/rtsp_based_detect_builder.py +35 -3
- OTVision/detect/rtsp_input_source.py +134 -37
- OTVision/detect/video_input_source.py +46 -14
- OTVision/detect/yolo.py +12 -8
- OTVision/domain/cli.py +10 -0
- OTVision/domain/detect_producer_consumer.py +3 -3
- OTVision/domain/frame.py +12 -0
- OTVision/domain/input_source_detect.py +4 -6
- OTVision/domain/object_detection.py +4 -6
- OTVision/domain/time.py +2 -2
- OTVision/domain/video_writer.py +30 -0
- OTVision/helpers/date.py +16 -0
- OTVision/plugin/ffmpeg_video_writer.py +298 -0
- OTVision/plugin/generate_video.py +24 -0
- 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.7.dist-info → otvision-0.6.9.dist-info}/METADATA +6 -5
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/RECORD +48 -36
- OTVision/application/detect/detected_frame_producer.py +0 -24
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/WHEEL +0 -0
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/licenses/LICENSE +0 -0
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)
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from enum import IntEnum, StrEnum
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from subprocess import PIPE, Popen, TimeoutExpired
|
|
5
|
+
from threading import Thread
|
|
6
|
+
from typing import Callable, Iterator
|
|
7
|
+
|
|
8
|
+
import ffmpeg
|
|
9
|
+
from numpy import ndarray
|
|
10
|
+
|
|
11
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
12
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
13
|
+
from OTVision.domain.frame import Frame, FrameKeys
|
|
14
|
+
from OTVision.domain.video_writer import VideoWriter
|
|
15
|
+
from OTVision.helpers.log import LOGGER_NAME
|
|
16
|
+
from OTVision.helpers.machine import ON_WINDOWS
|
|
17
|
+
|
|
18
|
+
VideoSaveLocationStrategy = Callable[[str], str]
|
|
19
|
+
|
|
20
|
+
BUFFER_SIZE_100MB = 10**8
|
|
21
|
+
DEFAULT_CRF = 23
|
|
22
|
+
VIDEO_SAVE_FILE_POSTFIX = "_processed"
|
|
23
|
+
|
|
24
|
+
log = logging.getLogger(LOGGER_NAME)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class VideoCodec(StrEnum):
|
|
28
|
+
"""Enum of possible video codecs to be used with ffmpeg
|
|
29
|
+
|
|
30
|
+
Attributes:
|
|
31
|
+
H264_SOFTWARE: Software-based H.264 encoder.
|
|
32
|
+
H264_NVENC: NVIDIA GPU-based H.264 encoder.
|
|
33
|
+
H264_QSV: Intel Quick Sync Video encoder.
|
|
34
|
+
H264_VAAPI: Intel/AMD GPU-based H.264 encoder.
|
|
35
|
+
H264_VIDEOTOOLBOX: macOS hardware encoder.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
H264_SOFTWARE = "libx264"
|
|
39
|
+
H264_NVENC = "h264_nvenc"
|
|
40
|
+
H264_QSV = "h264_qsv"
|
|
41
|
+
H264_VAAPI = "h264_vaapi"
|
|
42
|
+
H264_VIDEOTOOLBOX = "h264_videotoolbox"
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def as_list() -> list[str]:
|
|
46
|
+
return list(VideoCodec.__members__.values())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class VideoFormat(StrEnum):
|
|
50
|
+
RAW = "rawvideo"
|
|
51
|
+
MP4 = "mp4"
|
|
52
|
+
|
|
53
|
+
@staticmethod
|
|
54
|
+
def as_list() -> list[str]:
|
|
55
|
+
return list(VideoFormat.__members__.values())
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PixelFormat(StrEnum):
|
|
59
|
+
YUV420P = "yuv420p" # compatible with most players for H.264
|
|
60
|
+
RGB24 = "rgb24"
|
|
61
|
+
BGR24 = "bgr24"
|
|
62
|
+
|
|
63
|
+
@staticmethod
|
|
64
|
+
def as_list() -> list[str]:
|
|
65
|
+
return list(PixelFormat.__members__.values())
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
class EncodingSpeed(StrEnum):
|
|
69
|
+
ULTRA_FAST = "ultrafast"
|
|
70
|
+
SUPER_FAST = "superfast"
|
|
71
|
+
VERY_FAST = "veryfast"
|
|
72
|
+
FASTER = "faster"
|
|
73
|
+
FAST = "fast"
|
|
74
|
+
MEDIUM = "medium"
|
|
75
|
+
SLOW = "slow"
|
|
76
|
+
SLOWER = "slower"
|
|
77
|
+
VERY_SLOW = "veryslow"
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def as_list() -> list[str]:
|
|
81
|
+
return list(EncodingSpeed.__members__.values())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class ConstantRateFactor(IntEnum):
|
|
85
|
+
"""Adjust quality/size (lower means better quality/larger file).
|
|
86
|
+
Attributes:
|
|
87
|
+
LOSSLESS: Perfect quality, massive file size.
|
|
88
|
+
HIGH_QUALITY: Visually lossless for most eyes.
|
|
89
|
+
GOOD: High quality, slightly compressed.
|
|
90
|
+
DEFAULT: x264 default; good balance of size and quality.
|
|
91
|
+
COMPACT: Acceptable quality for small screens or streaming.
|
|
92
|
+
LOW_QUALITY: Noticeable compression artifacts; smaller file.
|
|
93
|
+
WORST_ACCEPTABLE: Very low quality; only for previews or constrained storage.
|
|
94
|
+
"""
|
|
95
|
+
|
|
96
|
+
LOSSLESS = 0
|
|
97
|
+
HIGH_QUALITY = 18
|
|
98
|
+
GOOD = 20
|
|
99
|
+
DEFAULT = 23
|
|
100
|
+
COMPACT = 26
|
|
101
|
+
LOW_QUALITY = 28
|
|
102
|
+
WORST_ACCEPTABLE = 35
|
|
103
|
+
|
|
104
|
+
@staticmethod
|
|
105
|
+
def as_list() -> list[str]:
|
|
106
|
+
return list(ConstantRateFactor.__members__.keys())
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class FfmpegVideoWriter(VideoWriter):
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def _current_video_metadata(self) -> NewVideoStartEvent:
|
|
113
|
+
if self.__current_video_metadata is None:
|
|
114
|
+
raise ValueError("FfmpegVideoWriter is not configured yet.")
|
|
115
|
+
return self.__current_video_metadata
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def _ffmpeg_process(self) -> Popen:
|
|
119
|
+
if self.__ffmpeg_process is None:
|
|
120
|
+
raise ValueError("FfmpegVideoWriter is not initialized yet.")
|
|
121
|
+
return self.__ffmpeg_process
|
|
122
|
+
|
|
123
|
+
@property
|
|
124
|
+
def is_open(self) -> bool:
|
|
125
|
+
return self.__ffmpeg_process is not None
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def is_closed(self) -> bool:
|
|
129
|
+
return self.is_open is False
|
|
130
|
+
|
|
131
|
+
def __init__(
|
|
132
|
+
self,
|
|
133
|
+
save_location_strategy: VideoSaveLocationStrategy,
|
|
134
|
+
encoding_speed: EncodingSpeed = EncodingSpeed.FAST,
|
|
135
|
+
input_format: VideoFormat = VideoFormat.RAW,
|
|
136
|
+
output_format: VideoFormat = VideoFormat.MP4,
|
|
137
|
+
input_pixel_format: PixelFormat = PixelFormat.RGB24,
|
|
138
|
+
output_pixel_format: PixelFormat = PixelFormat.YUV420P,
|
|
139
|
+
output_video_codec: VideoCodec = VideoCodec.H264_SOFTWARE,
|
|
140
|
+
constant_rate_factor: ConstantRateFactor = ConstantRateFactor.LOSSLESS,
|
|
141
|
+
) -> None:
|
|
142
|
+
if ON_WINDOWS:
|
|
143
|
+
log.warning(
|
|
144
|
+
"Writing every frame into a new video is not supported on Windows."
|
|
145
|
+
)
|
|
146
|
+
self._save_location_strategy = save_location_strategy
|
|
147
|
+
self._encoding_speed = encoding_speed
|
|
148
|
+
self._input_format = input_format
|
|
149
|
+
self._output_format = output_format
|
|
150
|
+
self._input_pixel_format = input_pixel_format
|
|
151
|
+
self._output_pixel_format = output_pixel_format
|
|
152
|
+
self._output_video_codec = output_video_codec
|
|
153
|
+
self.__ffmpeg_process: Popen | None = None
|
|
154
|
+
self.__current_video_metadata: NewVideoStartEvent | None = None
|
|
155
|
+
self._constant_rate_factor = constant_rate_factor
|
|
156
|
+
log.info(
|
|
157
|
+
"FFmpeg video writer settings: "
|
|
158
|
+
f"video_codec='{self._output_video_codec.value}', "
|
|
159
|
+
f"encoding_speed='{self._encoding_speed.value}', "
|
|
160
|
+
f"crf='{self._constant_rate_factor.value}'"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
def open(self, output: str, width: int, height: int, fps: float) -> None:
|
|
164
|
+
self.__ffmpeg_process = self.__create_ffmpeg_process(
|
|
165
|
+
output_file=output,
|
|
166
|
+
width=width,
|
|
167
|
+
height=height,
|
|
168
|
+
fps=fps,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
def write(self, image: ndarray) -> None:
|
|
172
|
+
try:
|
|
173
|
+
if self._ffmpeg_process.stdin:
|
|
174
|
+
self._ffmpeg_process.stdin.write(image.tobytes())
|
|
175
|
+
self._ffmpeg_process.stdin.flush()
|
|
176
|
+
except BrokenPipeError:
|
|
177
|
+
# Check if the process is still running
|
|
178
|
+
if self._ffmpeg_process.poll() is not None:
|
|
179
|
+
# Process has terminated, get the error message
|
|
180
|
+
stderr = (
|
|
181
|
+
self._ffmpeg_process.stderr.read()
|
|
182
|
+
if self._ffmpeg_process.stderr
|
|
183
|
+
else b""
|
|
184
|
+
)
|
|
185
|
+
log.info(stderr.decode("utf-8", errors="ignore"))
|
|
186
|
+
raise RuntimeError(
|
|
187
|
+
"ffmpeg process terminated unexpectedly: "
|
|
188
|
+
f"{stderr.decode('utf-8', errors='ignore')}"
|
|
189
|
+
)
|
|
190
|
+
raise # Re-raise the original exception if the process is still running
|
|
191
|
+
|
|
192
|
+
def close(self) -> None:
|
|
193
|
+
if self.__ffmpeg_process is not None:
|
|
194
|
+
process_to_cleanup = self._ffmpeg_process
|
|
195
|
+
self.__ffmpeg_process = None # Immediately mark as closed
|
|
196
|
+
|
|
197
|
+
# Close stdin synchronously (fast operation)
|
|
198
|
+
try:
|
|
199
|
+
if process_to_cleanup.stdin:
|
|
200
|
+
process_to_cleanup.stdin.flush()
|
|
201
|
+
process_to_cleanup.stdin.close()
|
|
202
|
+
except Exception as cause:
|
|
203
|
+
log.debug(f"Error closing stdin: {cause}")
|
|
204
|
+
|
|
205
|
+
# Handle cleanup in background thread
|
|
206
|
+
def cleanup_process() -> None:
|
|
207
|
+
try:
|
|
208
|
+
# Just check if it's still running and wait for it
|
|
209
|
+
if process_to_cleanup.poll() is None:
|
|
210
|
+
# Still running, close stdin and wait
|
|
211
|
+
try:
|
|
212
|
+
if (
|
|
213
|
+
process_to_cleanup.stdin
|
|
214
|
+
and not process_to_cleanup.stdin.closed
|
|
215
|
+
):
|
|
216
|
+
process_to_cleanup.stdin.close()
|
|
217
|
+
except Exception as cause:
|
|
218
|
+
log.error(f"Error closing stdin: {cause}")
|
|
219
|
+
|
|
220
|
+
# Simple wait with timeout
|
|
221
|
+
try:
|
|
222
|
+
process_to_cleanup.wait(timeout=5.0)
|
|
223
|
+
except TimeoutExpired:
|
|
224
|
+
process_to_cleanup.kill()
|
|
225
|
+
try:
|
|
226
|
+
process_to_cleanup.wait(timeout=1.0)
|
|
227
|
+
except TimeoutExpired:
|
|
228
|
+
log.error("Could not kill FFmpeg process")
|
|
229
|
+
|
|
230
|
+
# Log return code if we care
|
|
231
|
+
if process_to_cleanup.returncode != 0:
|
|
232
|
+
log.debug(
|
|
233
|
+
f"FFmpeg ended with code: {process_to_cleanup.returncode}"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
except Exception as e:
|
|
237
|
+
log.debug(f"Cleanup completed with minor issues: {e}")
|
|
238
|
+
|
|
239
|
+
# Start cleanup in daemon thread (won't prevent program exit)
|
|
240
|
+
cleanup_thread = Thread(target=cleanup_process, daemon=True)
|
|
241
|
+
cleanup_thread.start()
|
|
242
|
+
|
|
243
|
+
self.__current_video_metadata = None
|
|
244
|
+
|
|
245
|
+
def notify_on_flush_event(self, event: FlushEvent) -> None:
|
|
246
|
+
self.close()
|
|
247
|
+
|
|
248
|
+
def notify_on_new_video_start(self, event: NewVideoStartEvent) -> None:
|
|
249
|
+
self.__current_video_metadata = event
|
|
250
|
+
self.open(event.output, event.width, event.height, event.fps)
|
|
251
|
+
|
|
252
|
+
def __create_ffmpeg_process(
|
|
253
|
+
self, output_file: str, width: int, height: int, fps: float
|
|
254
|
+
) -> Popen:
|
|
255
|
+
save_file = self._save_location_strategy(output_file)
|
|
256
|
+
cmd = (
|
|
257
|
+
ffmpeg.input(
|
|
258
|
+
"pipe:0",
|
|
259
|
+
format=self._input_format.value,
|
|
260
|
+
framerate=fps,
|
|
261
|
+
pix_fmt=self._input_pixel_format.value,
|
|
262
|
+
s=f"{width}x{height}",
|
|
263
|
+
)
|
|
264
|
+
.output(
|
|
265
|
+
save_file,
|
|
266
|
+
pix_fmt=self._output_pixel_format.value,
|
|
267
|
+
vcodec=self._output_video_codec.value,
|
|
268
|
+
preset=self._encoding_speed.value,
|
|
269
|
+
crf=self._constant_rate_factor.value,
|
|
270
|
+
format=self._output_format.value,
|
|
271
|
+
)
|
|
272
|
+
.overwrite_output()
|
|
273
|
+
.compile()
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
process = Popen(
|
|
277
|
+
cmd,
|
|
278
|
+
stdin=PIPE,
|
|
279
|
+
stderr=PIPE,
|
|
280
|
+
bufsize=BUFFER_SIZE_100MB,
|
|
281
|
+
)
|
|
282
|
+
log.info(f"Writing new video file to '{save_file}'.")
|
|
283
|
+
return process
|
|
284
|
+
|
|
285
|
+
def filter(self, pipe: Iterator[Frame]) -> Iterator[Frame]:
|
|
286
|
+
for frame in pipe:
|
|
287
|
+
if (image := frame.get(FrameKeys.data)) is not None:
|
|
288
|
+
self.write(image)
|
|
289
|
+
yield frame
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def append_save_suffix_to_save_location(given: str) -> str:
|
|
293
|
+
filepath = Path(given)
|
|
294
|
+
return str(Path(filepath).with_stem(f"{filepath.stem}{VIDEO_SAVE_FILE_POSTFIX}"))
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def keep_original_save_location(given: str) -> str:
|
|
298
|
+
return given
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from OTVision.application.video.generate_video import GenerateVideo
|
|
4
|
+
from OTVision.detect.file_based_detect_builder import FileBasedDetectBuilder
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class GenerateVideoBuilder(FileBasedDetectBuilder):
|
|
8
|
+
@cached_property
|
|
9
|
+
def generate_video(self) -> GenerateVideo:
|
|
10
|
+
return GenerateVideo(
|
|
11
|
+
input_source=self.input_source, video_writer=self.video_file_writer
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
def build_generate_video(self) -> GenerateVideo:
|
|
15
|
+
self.register_observers()
|
|
16
|
+
return self.generate_video
|
|
17
|
+
|
|
18
|
+
def register_observers(self) -> None:
|
|
19
|
+
self.input_source.subject_new_video_start.register(
|
|
20
|
+
self.video_file_writer.notify_on_new_video_start
|
|
21
|
+
)
|
|
22
|
+
self.input_source.subject_flush.register(
|
|
23
|
+
self.video_file_writer.notify_on_flush_event
|
|
24
|
+
)
|
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)
|
|
@@ -1,34 +1,17 @@
|
|
|
1
|
-
import re
|
|
2
1
|
from datetime import datetime, timedelta
|
|
3
2
|
from pathlib import Path
|
|
4
3
|
|
|
5
|
-
from OTVision import dataformat, version
|
|
6
4
|
from OTVision.application.config import TrackConfig
|
|
7
5
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
8
|
-
from OTVision.
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
LENGTH,
|
|
14
|
-
OTTRACK_VERSION,
|
|
15
|
-
OTVISION_VERSION,
|
|
16
|
-
RECORDED_START_DATE,
|
|
17
|
-
TRACKER,
|
|
18
|
-
TRACKING,
|
|
19
|
-
VIDEO,
|
|
20
|
-
)
|
|
21
|
-
from OTVision.detect.otdet import parse_video_length
|
|
22
|
-
from OTVision.helpers.files import (
|
|
23
|
-
FULL_FILE_NAME_PATTERN,
|
|
24
|
-
HOSTNAME,
|
|
25
|
-
InproperFormattedFilename,
|
|
26
|
-
read_json_bz2_metadata,
|
|
6
|
+
from OTVision.application.track.ottrk import create_ottrk_metadata_entry
|
|
7
|
+
from OTVision.detect.otdet import (
|
|
8
|
+
extract_expected_duration_from_otdet,
|
|
9
|
+
extract_hostname_from_otdet,
|
|
10
|
+
extract_start_date_from_otdet,
|
|
27
11
|
)
|
|
12
|
+
from OTVision.helpers.files import read_json_bz2_metadata
|
|
28
13
|
from OTVision.track.model.filebased.frame_group import FrameGroup, FrameGroupParser
|
|
29
|
-
from OTVision.track.parser.chunk_parser_plugins import parse_datetime
|
|
30
14
|
|
|
31
|
-
MISSING_START_DATE = datetime(1900, 1, 1)
|
|
32
15
|
MISSING_EXPECTED_DURATION = timedelta(minutes=15)
|
|
33
16
|
|
|
34
17
|
|
|
@@ -37,16 +20,6 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
|
|
|
37
20
|
def config(self) -> TrackConfig:
|
|
38
21
|
return self._get_current_config.get().track
|
|
39
22
|
|
|
40
|
-
@property
|
|
41
|
-
def _tracker_data(self) -> dict:
|
|
42
|
-
return tracker_metadata(
|
|
43
|
-
sigma_l=self.config.sigma_l,
|
|
44
|
-
sigma_h=self.config.sigma_h,
|
|
45
|
-
sigma_iou=self.config.sigma_iou,
|
|
46
|
-
t_min=self.config.t_min,
|
|
47
|
-
t_miss_max=self.config.t_miss_max,
|
|
48
|
-
)
|
|
49
|
-
|
|
50
23
|
def __init__(
|
|
51
24
|
self,
|
|
52
25
|
get_current_config: GetCurrentConfig,
|
|
@@ -80,43 +53,28 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
|
|
|
80
53
|
)
|
|
81
54
|
|
|
82
55
|
def get_hostname(self, file_metadata: dict) -> str:
|
|
83
|
-
|
|
84
|
-
match = re.search(
|
|
85
|
-
FULL_FILE_NAME_PATTERN,
|
|
86
|
-
video_name,
|
|
87
|
-
)
|
|
88
|
-
if match:
|
|
89
|
-
return match.group(HOSTNAME)
|
|
90
|
-
|
|
91
|
-
raise InproperFormattedFilename(f"Could not parse {video_name}.")
|
|
56
|
+
return extract_hostname_from_otdet(file_metadata)
|
|
92
57
|
|
|
93
58
|
def extract_start_date_from(self, metadata: dict) -> datetime:
|
|
94
|
-
|
|
95
|
-
recorded_start_date = metadata[VIDEO][RECORDED_START_DATE]
|
|
96
|
-
return parse_datetime(recorded_start_date)
|
|
97
|
-
return MISSING_START_DATE
|
|
59
|
+
return extract_start_date_from_otdet(metadata)
|
|
98
60
|
|
|
99
61
|
def extract_expected_duration_from(self, metadata: dict) -> timedelta:
|
|
100
|
-
|
|
101
|
-
if expected_duration := metadata[VIDEO][EXPECTED_DURATION]:
|
|
102
|
-
return timedelta(seconds=int(expected_duration))
|
|
103
|
-
return self.parse_video_length(metadata)
|
|
104
|
-
|
|
105
|
-
def parse_video_length(self, metadata: dict) -> timedelta:
|
|
106
|
-
video_length = metadata[VIDEO][LENGTH]
|
|
107
|
-
return parse_video_length(video_length)
|
|
62
|
+
return extract_expected_duration_from_otdet(metadata)
|
|
108
63
|
|
|
109
64
|
def update_metadata(self, frame_group: FrameGroup) -> dict[Path, dict]:
|
|
110
65
|
metadata_by_file = dict(frame_group.metadata_by_file)
|
|
111
66
|
for filepath in frame_group.files:
|
|
112
67
|
metadata = metadata_by_file[filepath]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
68
|
+
ottrk_metadata = create_ottrk_metadata_entry(
|
|
69
|
+
start_date=frame_group.start_date,
|
|
70
|
+
end_date=frame_group.end_date,
|
|
71
|
+
sigma_l=self.config.sigma_l,
|
|
72
|
+
sigma_h=self.config.sigma_h,
|
|
73
|
+
sigma_iou=self.config.sigma_iou,
|
|
74
|
+
t_min=self.config.t_min,
|
|
75
|
+
t_miss_max=self.config.t_miss_max,
|
|
76
|
+
)
|
|
77
|
+
metadata.update(ottrk_metadata)
|
|
120
78
|
|
|
121
79
|
return metadata_by_file
|
|
122
80
|
|
|
@@ -142,16 +100,3 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
|
|
|
142
100
|
last_group = current_group
|
|
143
101
|
merged_groups.append(last_group)
|
|
144
102
|
return merged_groups
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def tracker_metadata(
|
|
148
|
-
sigma_l: float, sigma_h: float, sigma_iou: float, t_min: float, t_miss_max: float
|
|
149
|
-
) -> dict:
|
|
150
|
-
return {
|
|
151
|
-
dataformat.NAME: "IOU",
|
|
152
|
-
dataformat.SIGMA_L: sigma_l,
|
|
153
|
-
dataformat.SIGMA_H: sigma_h,
|
|
154
|
-
dataformat.SIGMA_IOU: sigma_iou,
|
|
155
|
-
dataformat.T_MIN: t_min,
|
|
156
|
-
dataformat.T_MISS_MAX: t_miss_max,
|
|
157
|
-
}
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from OTVision.application.buffer import Buffer
|
|
4
|
+
from OTVision.application.config import Config, TrackConfig
|
|
5
|
+
from OTVision.application.configure_logger import logger
|
|
6
|
+
from OTVision.application.get_current_config import GetCurrentConfig
|
|
7
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
8
|
+
from OTVision.application.track.ottrk import OttrkBuilder, OttrkBuilderConfig
|
|
9
|
+
from OTVision.application.track.tracking_run_id import GetCurrentTrackingRunId
|
|
10
|
+
from OTVision.detect.otdet import OtdetBuilderConfig
|
|
11
|
+
from OTVision.detect.otdet_file_writer import OtdetFileWrittenEvent
|
|
12
|
+
from OTVision.domain.detection import TrackId
|
|
13
|
+
from OTVision.domain.frame import TrackedFrame
|
|
14
|
+
from OTVision.helpers.files import write_json
|
|
15
|
+
|
|
16
|
+
STREAMING_FRAME_GROUP_ID = 0
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
|
|
20
|
+
@property
|
|
21
|
+
def config(self) -> Config:
|
|
22
|
+
return self._get_current_config.get()
|
|
23
|
+
|
|
24
|
+
@property
|
|
25
|
+
def track_config(self) -> TrackConfig:
|
|
26
|
+
return self.config.track
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def build_condition_fulfilled(self) -> bool:
|
|
30
|
+
return len(self._ottrk_unfinished_tracks) == 0
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def current_output_file(self) -> Path:
|
|
34
|
+
if self._current_output_file is None:
|
|
35
|
+
raise ValueError("Output file has not been set yet.")
|
|
36
|
+
return self._current_output_file
|
|
37
|
+
|
|
38
|
+
def __init__(
|
|
39
|
+
self,
|
|
40
|
+
builder: OttrkBuilder,
|
|
41
|
+
get_current_config: GetCurrentConfig,
|
|
42
|
+
get_current_tracking_run_id: GetCurrentTrackingRunId,
|
|
43
|
+
save_path_provider: OtvisionSavePathProvider,
|
|
44
|
+
) -> None:
|
|
45
|
+
Buffer.__init__(self)
|
|
46
|
+
self._builder = builder
|
|
47
|
+
self._get_current_config = get_current_config
|
|
48
|
+
self._current_tracking_run_id = get_current_tracking_run_id
|
|
49
|
+
self._save_path_provider = save_path_provider
|
|
50
|
+
|
|
51
|
+
self._in_writing_state: bool = False
|
|
52
|
+
self._ottrk_unfinished_tracks: set[TrackId] = set()
|
|
53
|
+
self._current_output_file: Path | None = None
|
|
54
|
+
|
|
55
|
+
def on_flush(self, event: OtdetFileWrittenEvent) -> None:
|
|
56
|
+
tracked_frames = self._get_buffered_elements()
|
|
57
|
+
if not tracked_frames:
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
self._in_writing_state = True
|
|
61
|
+
self._current_output_file = self._save_path_provider.provide(
|
|
62
|
+
event.otdet_builder_config.source, self.config.filetypes.track
|
|
63
|
+
)
|
|
64
|
+
builder_config = self._create_ottrk_builder_config(
|
|
65
|
+
event.otdet_builder_config, event.number_of_frames
|
|
66
|
+
)
|
|
67
|
+
self._builder.set_config(builder_config)
|
|
68
|
+
last_frame = tracked_frames[-1]
|
|
69
|
+
self._builder.add_tracked_frames(self._get_buffered_elements())
|
|
70
|
+
self._ottrk_unfinished_tracks = last_frame.unfinished_tracks
|
|
71
|
+
self.reset()
|
|
72
|
+
|
|
73
|
+
def _create_ottrk_builder_config(
|
|
74
|
+
self,
|
|
75
|
+
otdet_builder_config: OtdetBuilderConfig,
|
|
76
|
+
number_of_frames: int,
|
|
77
|
+
) -> OttrkBuilderConfig:
|
|
78
|
+
return OttrkBuilderConfig(
|
|
79
|
+
otdet_builder_config=otdet_builder_config,
|
|
80
|
+
number_of_frames=number_of_frames,
|
|
81
|
+
sigma_l=self.track_config.sigma_l,
|
|
82
|
+
sigma_h=self.track_config.sigma_h,
|
|
83
|
+
sigma_iou=self.track_config.sigma_iou,
|
|
84
|
+
t_min=self.track_config.t_min,
|
|
85
|
+
t_miss_max=self.track_config.t_miss_max,
|
|
86
|
+
tracking_run_id=self._current_tracking_run_id.get(),
|
|
87
|
+
frame_group=STREAMING_FRAME_GROUP_ID,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
def reset(self) -> None:
|
|
91
|
+
self._reset_buffer()
|
|
92
|
+
|
|
93
|
+
def buffer(self, to_buffer: TrackedFrame) -> None:
|
|
94
|
+
self._buffer.append(to_buffer.without_image())
|
|
95
|
+
|
|
96
|
+
if self._in_writing_state:
|
|
97
|
+
self._builder.finish_tracks(to_buffer.finished_tracks)
|
|
98
|
+
self._builder.discard_tracks(to_buffer.discarded_tracks)
|
|
99
|
+
self._ottrk_unfinished_tracks = (
|
|
100
|
+
self._ottrk_unfinished_tracks.difference(to_buffer.unfinished_tracks)
|
|
101
|
+
.difference(to_buffer.finished_tracks)
|
|
102
|
+
.difference(to_buffer.discarded_tracks)
|
|
103
|
+
)
|
|
104
|
+
logger().warning(f"Unfinished tracks: {self._ottrk_unfinished_tracks}")
|
|
105
|
+
if self.build_condition_fulfilled:
|
|
106
|
+
ottrk_data = self._builder.build()
|
|
107
|
+
self.write(ottrk_data)
|
|
108
|
+
self._in_writing_state = False
|
|
109
|
+
|
|
110
|
+
def write(self, ottrk: dict) -> None:
|
|
111
|
+
write_json(
|
|
112
|
+
dict_to_write=ottrk,
|
|
113
|
+
file=self.current_output_file,
|
|
114
|
+
filetype=self.config.filetypes.track,
|
|
115
|
+
overwrite=True,
|
|
116
|
+
)
|
OTVision/track/track.py
CHANGED
|
@@ -4,10 +4,11 @@ from tqdm import tqdm
|
|
|
4
4
|
|
|
5
5
|
from OTVision.application.config import Config
|
|
6
6
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
7
|
+
from OTVision.application.track.tracking_run_id import StrIdGenerator
|
|
7
8
|
from OTVision.helpers.files import get_files
|
|
8
9
|
from OTVision.helpers.input_types import check_types
|
|
9
10
|
from OTVision.helpers.log import LOGGER_NAME
|
|
10
|
-
from OTVision.track.id_generator import
|
|
11
|
+
from OTVision.track.id_generator import tracking_run_uuid_generator
|
|
11
12
|
from OTVision.track.model.track_exporter import FinishedTracksExporter
|
|
12
13
|
from OTVision.track.tracker.filebased_tracking import UnfinishedChunksBuffer
|
|
13
14
|
|
OTVision/version.py
CHANGED