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
|
@@ -2,11 +2,18 @@ from functools import cached_property
|
|
|
2
2
|
|
|
3
3
|
from OTVision.abstraction.observer import Subject
|
|
4
4
|
from OTVision.application.config import StreamConfig
|
|
5
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
5
6
|
from OTVision.detect.builder import DetectBuilder
|
|
6
7
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
7
8
|
from OTVision.detect.rtsp_input_source import Counter, RtspInputSource
|
|
8
|
-
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
9
9
|
from OTVision.domain.time import CurrentDatetimeProvider, DatetimeProvider
|
|
10
|
+
from OTVision.domain.video_writer import VideoWriter
|
|
11
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
12
|
+
FfmpegVideoWriter,
|
|
13
|
+
PixelFormat,
|
|
14
|
+
VideoFormat,
|
|
15
|
+
keep_original_save_location,
|
|
16
|
+
)
|
|
10
17
|
|
|
11
18
|
FLUSH_BUFFER_SIZE = 18000
|
|
12
19
|
FLUSH_BUFFER_SIZE = 1200
|
|
@@ -24,9 +31,10 @@ class RtspBasedDetectBuilder(DetectBuilder):
|
|
|
24
31
|
return config.stream
|
|
25
32
|
|
|
26
33
|
@cached_property
|
|
27
|
-
def input_source(self) ->
|
|
34
|
+
def input_source(self) -> RtspInputSource:
|
|
28
35
|
return RtspInputSource(
|
|
29
|
-
|
|
36
|
+
subject_flush=Subject[FlushEvent](),
|
|
37
|
+
subject_new_video_start=Subject[NewVideoStartEvent](),
|
|
30
38
|
datetime_provider=self.datetime_provider,
|
|
31
39
|
frame_counter=Counter(),
|
|
32
40
|
get_current_config=self.get_current_config,
|
|
@@ -35,3 +43,27 @@ class RtspBasedDetectBuilder(DetectBuilder):
|
|
|
35
43
|
@cached_property
|
|
36
44
|
def datetime_provider(self) -> DatetimeProvider:
|
|
37
45
|
return CurrentDatetimeProvider()
|
|
46
|
+
|
|
47
|
+
@cached_property
|
|
48
|
+
def video_file_writer(self) -> VideoWriter:
|
|
49
|
+
return FfmpegVideoWriter(
|
|
50
|
+
save_location_strategy=keep_original_save_location,
|
|
51
|
+
encoding_speed=self.detect_config.encoding_speed,
|
|
52
|
+
input_format=VideoFormat.RAW,
|
|
53
|
+
output_format=VideoFormat.MP4,
|
|
54
|
+
input_pixel_format=PixelFormat.RGB24,
|
|
55
|
+
output_pixel_format=PixelFormat.YUV420P,
|
|
56
|
+
output_video_codec=self.detect_config.video_codec,
|
|
57
|
+
constant_rate_factor=self.detect_config.crf,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
def register_observers(self) -> None:
|
|
61
|
+
if self.detect_config.write_video:
|
|
62
|
+
self.input_source.subject_new_video_start.register(
|
|
63
|
+
self.video_file_writer.notify_on_new_video_start
|
|
64
|
+
)
|
|
65
|
+
self.input_source.subject_flush.register(
|
|
66
|
+
self.video_file_writer.notify_on_flush_event
|
|
67
|
+
)
|
|
68
|
+
self.input_source.subject_flush.register(self.detected_frame_buffer.on_flush)
|
|
69
|
+
self.detected_frame_buffer.register(self.otdet_file_writer.write)
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import socket
|
|
1
2
|
from datetime import datetime, timedelta
|
|
2
3
|
from time import sleep
|
|
3
|
-
from typing import
|
|
4
|
+
from typing import Iterator
|
|
5
|
+
from urllib.parse import urlparse
|
|
4
6
|
|
|
5
7
|
from cv2 import (
|
|
6
8
|
CAP_PROP_FRAME_HEIGHT,
|
|
@@ -19,6 +21,8 @@ from OTVision.application.config import (
|
|
|
19
21
|
StreamConfig,
|
|
20
22
|
)
|
|
21
23
|
from OTVision.application.configure_logger import logger
|
|
24
|
+
from OTVision.application.event.new_otvision_config import NewOtvisionConfigEvent
|
|
25
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
22
26
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
23
27
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
24
28
|
from OTVision.domain.frame import Frame
|
|
@@ -26,10 +30,14 @@ from OTVision.domain.input_source_detect import InputSourceDetect
|
|
|
26
30
|
from OTVision.domain.time import DatetimeProvider
|
|
27
31
|
|
|
28
32
|
RTSP_URL = "rtsp://127.0.0.1:8554/test"
|
|
29
|
-
RETRY_SECONDS =
|
|
33
|
+
RETRY_SECONDS = 5
|
|
30
34
|
DEFAULT_READ_FAIL_THRESHOLD = 5
|
|
31
35
|
|
|
32
36
|
|
|
37
|
+
class NoConfigurationFoundError(Exception):
|
|
38
|
+
"""Raised when no configuration is found for the RTSP stream."""
|
|
39
|
+
|
|
40
|
+
|
|
33
41
|
class Counter:
|
|
34
42
|
def __init__(self, start_value: int = 0) -> None:
|
|
35
43
|
self._start_value = start_value
|
|
@@ -63,7 +71,7 @@ class RtspInputSource(InputSourceDetect):
|
|
|
63
71
|
def stream_config(self) -> StreamConfig:
|
|
64
72
|
if stream_config := self.config.stream:
|
|
65
73
|
return stream_config
|
|
66
|
-
raise
|
|
74
|
+
raise NoConfigurationFoundError("Stream config not found in config")
|
|
67
75
|
|
|
68
76
|
@property
|
|
69
77
|
def rtsp_url(self) -> str:
|
|
@@ -79,13 +87,16 @@ class RtspInputSource(InputSourceDetect):
|
|
|
79
87
|
|
|
80
88
|
def __init__(
|
|
81
89
|
self,
|
|
82
|
-
|
|
90
|
+
subject_flush: Subject[FlushEvent],
|
|
91
|
+
subject_new_video_start: Subject[NewVideoStartEvent],
|
|
83
92
|
datetime_provider: DatetimeProvider,
|
|
84
93
|
frame_counter: Counter,
|
|
85
94
|
get_current_config: GetCurrentConfig,
|
|
86
95
|
read_fail_threshold: int = DEFAULT_READ_FAIL_THRESHOLD,
|
|
87
96
|
) -> None:
|
|
88
|
-
|
|
97
|
+
|
|
98
|
+
self.subject_flush = subject_flush
|
|
99
|
+
self.subject_new_video_start = subject_new_video_start
|
|
89
100
|
self._datetime_provider = datetime_provider
|
|
90
101
|
self._stop_capture = False
|
|
91
102
|
self._frame_counter = frame_counter
|
|
@@ -120,44 +131,53 @@ class RtspInputSource(InputSourceDetect):
|
|
|
120
131
|
self._current_video_capture = self._init_video_capture(self._current_stream)
|
|
121
132
|
return self._current_video_capture
|
|
122
133
|
|
|
123
|
-
def produce(self) ->
|
|
134
|
+
def produce(self) -> Iterator[Frame]:
|
|
124
135
|
self._stream_start_time = self._datetime_provider.provide()
|
|
125
136
|
self._current_video_start_time = self._stream_start_time
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
self.
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
self.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self.
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
137
|
+
try:
|
|
138
|
+
while not self.should_stop():
|
|
139
|
+
if (frame := self._read_next_frame()) is not None:
|
|
140
|
+
self._frame_counter.increment()
|
|
141
|
+
occurrence = self._datetime_provider.provide()
|
|
142
|
+
|
|
143
|
+
if self._outdated:
|
|
144
|
+
self._current_video_start_time = occurrence
|
|
145
|
+
self._outdated = False
|
|
146
|
+
self._notify_new_video_start_observers()
|
|
147
|
+
|
|
148
|
+
yield Frame(
|
|
149
|
+
data=convert_frame_to_rgb(frame), # YOLO expects RGB
|
|
150
|
+
frame=self.current_frame_number,
|
|
151
|
+
source=self.rtsp_url,
|
|
152
|
+
output=self.create_output(),
|
|
153
|
+
occurrence=occurrence,
|
|
154
|
+
)
|
|
155
|
+
if self.flush_condition_met():
|
|
156
|
+
self._notify_flush_observers()
|
|
157
|
+
self._outdated = True
|
|
158
|
+
self._frame_counter.reset()
|
|
159
|
+
self._notify_flush_observers()
|
|
160
|
+
except InvalidRtspUrlError as cause:
|
|
161
|
+
logger().error(cause)
|
|
148
162
|
|
|
149
163
|
def _init_video_capture(self, source: str) -> VideoCapture:
|
|
164
|
+
self._wait_for_connection(source)
|
|
165
|
+
|
|
150
166
|
cap = VideoCapture(source)
|
|
151
167
|
while not self.should_stop() and not cap.isOpened():
|
|
152
|
-
logger().warning(
|
|
153
|
-
f"Couldn't open the RTSP stream: {source}. "
|
|
154
|
-
f"Trying again in {RETRY_SECONDS}s..."
|
|
155
|
-
)
|
|
156
|
-
sleep(RETRY_SECONDS)
|
|
157
168
|
cap.release()
|
|
169
|
+
self._wait_for_connection(source)
|
|
158
170
|
cap = VideoCapture(source)
|
|
159
171
|
return cap
|
|
160
172
|
|
|
173
|
+
def _wait_for_connection(self, connection: str) -> None:
|
|
174
|
+
while not self.should_stop() and not is_connection_available(connection):
|
|
175
|
+
logger().debug(
|
|
176
|
+
f"Couldn't open the RTSP stream: {connection}. "
|
|
177
|
+
f"Trying again in {RETRY_SECONDS}s..."
|
|
178
|
+
)
|
|
179
|
+
sleep(RETRY_SECONDS)
|
|
180
|
+
|
|
161
181
|
def _read_next_frame(self) -> ndarray | None:
|
|
162
182
|
successful, frame = self._video_capture.read()
|
|
163
183
|
if successful:
|
|
@@ -189,9 +209,9 @@ class RtspInputSource(InputSourceDetect):
|
|
|
189
209
|
def flush_condition_met(self) -> bool:
|
|
190
210
|
return self.current_frame_number % self.flush_buffer_size == 0
|
|
191
211
|
|
|
192
|
-
def
|
|
193
|
-
frame_width =
|
|
194
|
-
frame_height =
|
|
212
|
+
def _notify_flush_observers(self) -> None:
|
|
213
|
+
frame_width = self._get_width()
|
|
214
|
+
frame_height = self._get_height()
|
|
195
215
|
frames = (
|
|
196
216
|
self.flush_buffer_size
|
|
197
217
|
if self.current_frame_number % self.flush_buffer_size == 0
|
|
@@ -199,7 +219,7 @@ class RtspInputSource(InputSourceDetect):
|
|
|
199
219
|
)
|
|
200
220
|
duration = timedelta(seconds=round(frames / self.fps))
|
|
201
221
|
output = self.create_output()
|
|
202
|
-
self.
|
|
222
|
+
self.subject_flush.notify(
|
|
203
223
|
FlushEvent.create(
|
|
204
224
|
source=self.rtsp_url,
|
|
205
225
|
output=output,
|
|
@@ -211,6 +231,21 @@ class RtspInputSource(InputSourceDetect):
|
|
|
211
231
|
)
|
|
212
232
|
)
|
|
213
233
|
|
|
234
|
+
def _get_width(self) -> int:
|
|
235
|
+
return int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
|
|
236
|
+
|
|
237
|
+
def _get_height(self) -> int:
|
|
238
|
+
return int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
|
|
239
|
+
|
|
240
|
+
def _notify_new_video_start_observers(self) -> None:
|
|
241
|
+
event = NewVideoStartEvent(
|
|
242
|
+
output=self.create_output(),
|
|
243
|
+
width=self._get_width(),
|
|
244
|
+
height=self._get_height(),
|
|
245
|
+
fps=self.fps,
|
|
246
|
+
)
|
|
247
|
+
self.subject_new_video_start.notify(event)
|
|
248
|
+
|
|
214
249
|
def create_output(self) -> str:
|
|
215
250
|
output_filename = (
|
|
216
251
|
f"{self.stream_config.name}_FR{round(self.fps)}"
|
|
@@ -218,6 +253,68 @@ class RtspInputSource(InputSourceDetect):
|
|
|
218
253
|
)
|
|
219
254
|
return str(self.stream_config.save_dir / output_filename)
|
|
220
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
|
+
|
|
221
263
|
|
|
222
264
|
def convert_frame_to_rgb(frame: ndarray) -> ndarray:
|
|
223
265
|
return cvtColor(frame, COLOR_BGR2RGB)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class InvalidRtspUrlError(Exception):
|
|
269
|
+
"""Raised when the RTSP URL is invalid."""
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def is_connection_available(rtsp_url: str) -> bool:
|
|
273
|
+
"""
|
|
274
|
+
Check if RTSP connection is available by sending a DESCRIBE request.
|
|
275
|
+
|
|
276
|
+
Args:
|
|
277
|
+
rtsp_url: The RTSP URL to check
|
|
278
|
+
|
|
279
|
+
Returns:
|
|
280
|
+
bool: True if stream is available, False otherwise
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
parsed = urlparse(rtsp_url)
|
|
284
|
+
if parsed.hostname is None and parsed.port is None:
|
|
285
|
+
raise InvalidRtspUrlError(
|
|
286
|
+
f"Invalid RTSP URL: {rtsp_url}. Missing hostname or port."
|
|
287
|
+
)
|
|
288
|
+
|
|
289
|
+
host = parsed.hostname
|
|
290
|
+
port = parsed.port
|
|
291
|
+
|
|
292
|
+
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
|
293
|
+
sock.settimeout(5)
|
|
294
|
+
|
|
295
|
+
if sock.connect_ex((host, port)) != 0:
|
|
296
|
+
sock.close()
|
|
297
|
+
return False
|
|
298
|
+
|
|
299
|
+
# Send RTSP DESCRIBE request to get stream info
|
|
300
|
+
rtsp_request = (
|
|
301
|
+
f"DESCRIBE {rtsp_url} RTSP/1.0\r\n"
|
|
302
|
+
f"CSeq: 1\r\n"
|
|
303
|
+
f"Accept: application/sdp\r\n\r\n"
|
|
304
|
+
)
|
|
305
|
+
sock.send(rtsp_request.encode())
|
|
306
|
+
|
|
307
|
+
# Read response
|
|
308
|
+
response = sock.recv(4096).decode()
|
|
309
|
+
sock.close()
|
|
310
|
+
|
|
311
|
+
# Check if we got a valid RTSP response with SDP content
|
|
312
|
+
return (
|
|
313
|
+
response.startswith("RTSP/1.0 200 OK")
|
|
314
|
+
and "application/sdp" in response
|
|
315
|
+
and "m=video" in response
|
|
316
|
+
)
|
|
317
|
+
except InvalidRtspUrlError:
|
|
318
|
+
raise
|
|
319
|
+
except Exception:
|
|
320
|
+
return False
|
|
@@ -1,18 +1,18 @@
|
|
|
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
|
+
from av.container.input import InputContainer
|
|
7
8
|
from tqdm import tqdm
|
|
8
9
|
|
|
9
10
|
from OTVision.abstraction.observer import Subject
|
|
10
11
|
from OTVision.application.config import DATETIME_FORMAT, Config
|
|
11
|
-
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
12
|
-
DetectionFileSavePathProvider,
|
|
13
|
-
)
|
|
14
12
|
from OTVision.application.detect.timestamper import Timestamper
|
|
13
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
15
14
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
15
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
16
16
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
17
17
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
18
18
|
from OTVision.detect.timestamper import TimestamperFactory, parse_start_time_from
|
|
@@ -38,11 +38,14 @@ class VideoSource(InputSourceDetect):
|
|
|
38
38
|
and selective frame processing based on configuration parameters.
|
|
39
39
|
|
|
40
40
|
Args:
|
|
41
|
+
subject_flush: (Subject[FlushEvent]): Subject for notifying about flush events.
|
|
42
|
+
subject_new_video_start (Subject[NewVideoStartEvent): Subject for notifying
|
|
43
|
+
about new video start events.
|
|
41
44
|
get_current_config (GetCurrentConfig): Use case to retrieve current
|
|
42
45
|
configuration.
|
|
43
46
|
frame_rotator (AvVideoFrameRotator): Use to rotate video frames.
|
|
44
47
|
timestamper_factory (Timestamper): Factory for creating timestamp generators.
|
|
45
|
-
save_path_provider (
|
|
48
|
+
save_path_provider (OtvisionSavePathProvider): Provider for detection
|
|
46
49
|
output paths.
|
|
47
50
|
"""
|
|
48
51
|
|
|
@@ -56,20 +59,22 @@ class VideoSource(InputSourceDetect):
|
|
|
56
59
|
|
|
57
60
|
def __init__(
|
|
58
61
|
self,
|
|
59
|
-
|
|
62
|
+
subject_flush: Subject[FlushEvent],
|
|
63
|
+
subject_new_video_start: Subject[NewVideoStartEvent],
|
|
60
64
|
get_current_config: GetCurrentConfig,
|
|
61
65
|
frame_rotator: AvVideoFrameRotator,
|
|
62
66
|
timestamper_factory: TimestamperFactory,
|
|
63
|
-
save_path_provider:
|
|
67
|
+
save_path_provider: OtvisionSavePathProvider,
|
|
64
68
|
) -> None:
|
|
65
|
-
|
|
69
|
+
self.subject_flush = subject_flush
|
|
70
|
+
self.subject_new_video_start = subject_new_video_start
|
|
66
71
|
self._frame_rotator = frame_rotator
|
|
67
72
|
self._get_current_config = get_current_config
|
|
68
73
|
self._timestamper_factory = timestamper_factory
|
|
69
74
|
self._save_path_provider = save_path_provider
|
|
70
75
|
self.__should_flush = False
|
|
71
76
|
|
|
72
|
-
def produce(self) ->
|
|
77
|
+
def produce(self) -> Iterator[Frame]:
|
|
73
78
|
"""Generate frames from video files that meet detection requirements.
|
|
74
79
|
|
|
75
80
|
Yields frames from valid video files while managing rotation, timestamping,
|
|
@@ -86,7 +91,9 @@ class VideoSource(InputSourceDetect):
|
|
|
86
91
|
print(start_msg)
|
|
87
92
|
|
|
88
93
|
for video_file in tqdm(video_files, desc="Detected video files", unit=" files"):
|
|
89
|
-
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
|
+
)
|
|
90
97
|
|
|
91
98
|
if not self.__detection_requirements_are_met(video_file, detections_file):
|
|
92
99
|
continue
|
|
@@ -97,13 +104,14 @@ class VideoSource(InputSourceDetect):
|
|
|
97
104
|
expected_duration=self._current_config.detect.expected_duration,
|
|
98
105
|
)
|
|
99
106
|
video_fps = get_fps(video_file)
|
|
107
|
+
self.notify_new_video_start_observers(video_file, video_fps)
|
|
100
108
|
detect_start = self.__get_detect_start_in_frames(video_fps)
|
|
101
109
|
detect_end = self.__get_detect_end_in_frames(video_fps)
|
|
102
110
|
counter = 0
|
|
103
111
|
try:
|
|
104
112
|
with av.open(str(video_file.absolute())) as container:
|
|
105
113
|
container.streams.video[0].thread_type = "AUTO"
|
|
106
|
-
side_data = container
|
|
114
|
+
side_data = self._extract_side_data(container)
|
|
107
115
|
for frame_number, frame in enumerate(
|
|
108
116
|
container.decode(video=0), start=1
|
|
109
117
|
):
|
|
@@ -129,10 +137,20 @@ class VideoSource(InputSourceDetect):
|
|
|
129
137
|
}
|
|
130
138
|
)
|
|
131
139
|
counter += 1
|
|
132
|
-
self.
|
|
140
|
+
self.notify_flush_event_observers(video_file, video_fps)
|
|
133
141
|
except Exception as e:
|
|
134
142
|
log.error(f"Error processing {video_file}", exc_info=e)
|
|
135
143
|
|
|
144
|
+
def _extract_side_data(self, container: InputContainer) -> dict:
|
|
145
|
+
try:
|
|
146
|
+
return container.streams.video[0].side_data
|
|
147
|
+
except AttributeError:
|
|
148
|
+
log.warning(
|
|
149
|
+
"No side_data found in video stream. "
|
|
150
|
+
"Existing rotation will not be applied."
|
|
151
|
+
)
|
|
152
|
+
return {}
|
|
153
|
+
|
|
136
154
|
def __collect_files_to_detect(self) -> list[Path]:
|
|
137
155
|
filetypes = self._current_config.filetypes.video_filetypes.to_list()
|
|
138
156
|
video_files = get_files(
|
|
@@ -169,7 +187,9 @@ class VideoSource(InputSourceDetect):
|
|
|
169
187
|
return False
|
|
170
188
|
return True
|
|
171
189
|
|
|
172
|
-
def
|
|
190
|
+
def notify_flush_event_observers(
|
|
191
|
+
self, current_video_file: Path, video_fps: float
|
|
192
|
+
) -> None:
|
|
173
193
|
if expected_duration := self._current_config.detect.expected_duration:
|
|
174
194
|
duration = expected_duration
|
|
175
195
|
else:
|
|
@@ -180,7 +200,7 @@ class VideoSource(InputSourceDetect):
|
|
|
180
200
|
current_video_file, start_time=self._start_time
|
|
181
201
|
)
|
|
182
202
|
|
|
183
|
-
self.
|
|
203
|
+
self.subject_flush.notify(
|
|
184
204
|
FlushEvent.create(
|
|
185
205
|
source=str(current_video_file),
|
|
186
206
|
output=str(current_video_file),
|
|
@@ -192,6 +212,18 @@ class VideoSource(InputSourceDetect):
|
|
|
192
212
|
)
|
|
193
213
|
)
|
|
194
214
|
|
|
215
|
+
def notify_new_video_start_observers(
|
|
216
|
+
self, current_video_file: Path, video_fps: float
|
|
217
|
+
) -> None:
|
|
218
|
+
width, height = get_video_dimensions(current_video_file)
|
|
219
|
+
event = NewVideoStartEvent(
|
|
220
|
+
output=str(current_video_file),
|
|
221
|
+
width=width,
|
|
222
|
+
height=height,
|
|
223
|
+
fps=video_fps,
|
|
224
|
+
)
|
|
225
|
+
self.subject_new_video_start.notify(event)
|
|
226
|
+
|
|
195
227
|
def __get_detect_start_in_frames(self, video_fps: float) -> int:
|
|
196
228
|
detect_start = convert_seconds_to_frames(
|
|
197
229
|
self._current_config.detect.detect_start, video_fps
|
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,17 +134,21 @@ 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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
140
|
+
def detect(self, frames: Iterator[Frame]) -> Iterator[DetectedFrame]:
|
|
141
|
+
for frame in tqdm(
|
|
142
|
+
frames,
|
|
143
|
+
desc="Detected frames",
|
|
144
|
+
unit=" frames",
|
|
145
|
+
disable=self.disable_tqdm_logging(),
|
|
146
|
+
):
|
|
146
147
|
yield self._predict(frame)
|
|
147
148
|
|
|
149
|
+
def disable_tqdm_logging(self) -> bool:
|
|
150
|
+
return log.level > logging.INFO
|
|
151
|
+
|
|
148
152
|
def _predict(self, frame: Frame) -> DetectedFrame:
|
|
149
153
|
if frame[FrameKeys.data] is None:
|
|
150
154
|
return self._create_empty_detection(frame)
|
OTVision/domain/cli.py
CHANGED
|
@@ -3,6 +3,12 @@ from dataclasses import dataclass
|
|
|
3
3
|
from datetime import datetime, timedelta
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
7
|
+
ConstantRateFactor,
|
|
8
|
+
EncodingSpeed,
|
|
9
|
+
VideoCodec,
|
|
10
|
+
)
|
|
11
|
+
|
|
6
12
|
|
|
7
13
|
class CliArgs(ABC):
|
|
8
14
|
@abstractmethod
|
|
@@ -32,6 +38,10 @@ class DetectCliArgs(CliArgs):
|
|
|
32
38
|
start_time: datetime | None = None
|
|
33
39
|
detect_start: int | None = None
|
|
34
40
|
detect_end: int | None = None
|
|
41
|
+
write_video: bool | None = None
|
|
42
|
+
video_codec: VideoCodec | None = None
|
|
43
|
+
encoding_speed: EncodingSpeed | None = None
|
|
44
|
+
crf: ConstantRateFactor | None = None
|
|
35
45
|
|
|
36
46
|
def get_config_file(self) -> Path | None:
|
|
37
47
|
return self.config_file
|
|
@@ -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,12 +1,10 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
|
-
from OTVision.abstraction.observer import Observable
|
|
5
|
-
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
6
4
|
from OTVision.domain.frame import Frame
|
|
7
5
|
|
|
8
6
|
|
|
9
|
-
class InputSourceDetect(
|
|
7
|
+
class InputSourceDetect(ABC):
|
|
10
8
|
"""Interface for input sources that generate frames and notify about flush events.
|
|
11
9
|
|
|
12
10
|
This class combines the Observable pattern for flush events with frame generation
|
|
@@ -16,7 +14,7 @@ class InputSourceDetect(Observable[FlushEvent], ABC):
|
|
|
16
14
|
"""
|
|
17
15
|
|
|
18
16
|
@abstractmethod
|
|
19
|
-
def produce(self) ->
|
|
17
|
+
def produce(self) -> Iterator[Frame]:
|
|
20
18
|
"""Generate a stream of frames from the input source.
|
|
21
19
|
|
|
22
20
|
Implementations should yield Frame objects one at a time from the source,
|
|
@@ -24,7 +22,7 @@ class InputSourceDetect(Observable[FlushEvent], ABC):
|
|
|
24
22
|
at appropriate points (e.g., end of video segments or buffer boundaries).
|
|
25
23
|
|
|
26
24
|
Returns:
|
|
27
|
-
|
|
25
|
+
Iterator [Frame]: A generator yielding Frame objects
|
|
28
26
|
sequentially from the input source.
|
|
29
27
|
"""
|
|
30
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/domain/time.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from datetime import datetime
|
|
2
|
+
from datetime import datetime
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class DatetimeProvider(ABC):
|
|
@@ -10,4 +10,4 @@ class DatetimeProvider(ABC):
|
|
|
10
10
|
|
|
11
11
|
class CurrentDatetimeProvider(DatetimeProvider):
|
|
12
12
|
def provide(self) -> datetime:
|
|
13
|
-
return datetime.now(
|
|
13
|
+
return datetime.now()
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from numpy import ndarray
|
|
4
|
+
|
|
5
|
+
from OTVision.abstraction.pipes_and_filter import Filter
|
|
6
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
7
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
8
|
+
from OTVision.domain.frame import Frame
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class VideoWriter(Filter[Frame, Frame], ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
def write(self, image: ndarray) -> None:
|
|
14
|
+
raise NotImplementedError
|
|
15
|
+
|
|
16
|
+
@abstractmethod
|
|
17
|
+
def open(self, output: str, width: int, height: int, fps: float) -> None:
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
|
|
20
|
+
@abstractmethod
|
|
21
|
+
def close(self) -> None:
|
|
22
|
+
raise NotImplementedError
|
|
23
|
+
|
|
24
|
+
@abstractmethod
|
|
25
|
+
def notify_on_flush_event(self, event: FlushEvent) -> None:
|
|
26
|
+
raise NotImplementedError
|
|
27
|
+
|
|
28
|
+
@abstractmethod
|
|
29
|
+
def notify_on_new_video_start(self, event: NewVideoStartEvent) -> None:
|
|
30
|
+
raise NotImplementedError
|