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.
Files changed (49) hide show
  1. OTVision/abstraction/pipes_and_filter.py +4 -4
  2. OTVision/application/buffer.py +5 -12
  3. OTVision/application/config.py +18 -0
  4. OTVision/application/config_parser.py +68 -11
  5. OTVision/application/detect/current_object_detector.py +2 -4
  6. OTVision/application/detect/update_detect_config_with_cli_args.py +16 -0
  7. OTVision/application/event/__init__.py +0 -0
  8. OTVision/application/event/new_otvision_config.py +6 -0
  9. OTVision/application/event/new_video_start.py +9 -0
  10. OTVision/application/{detect/detection_file_save_path_provider.py → otvision_save_path_provider.py} +8 -7
  11. OTVision/application/track/ottrk.py +203 -0
  12. OTVision/application/track/tracking_run_id.py +35 -0
  13. OTVision/application/video/__init__.py +0 -0
  14. OTVision/application/video/generate_video.py +15 -0
  15. OTVision/config.py +2 -0
  16. OTVision/detect/builder.py +35 -22
  17. OTVision/detect/cli.py +44 -1
  18. OTVision/detect/detected_frame_buffer.py +13 -1
  19. OTVision/detect/detected_frame_producer.py +14 -0
  20. OTVision/detect/detected_frame_producer_factory.py +39 -0
  21. OTVision/detect/file_based_detect_builder.py +39 -3
  22. OTVision/detect/otdet.py +109 -41
  23. OTVision/detect/otdet_file_writer.py +52 -29
  24. OTVision/detect/rtsp_based_detect_builder.py +35 -3
  25. OTVision/detect/rtsp_input_source.py +134 -37
  26. OTVision/detect/video_input_source.py +46 -14
  27. OTVision/detect/yolo.py +12 -8
  28. OTVision/domain/cli.py +10 -0
  29. OTVision/domain/detect_producer_consumer.py +3 -3
  30. OTVision/domain/frame.py +12 -0
  31. OTVision/domain/input_source_detect.py +4 -6
  32. OTVision/domain/object_detection.py +4 -6
  33. OTVision/domain/time.py +2 -2
  34. OTVision/domain/video_writer.py +30 -0
  35. OTVision/helpers/date.py +16 -0
  36. OTVision/plugin/ffmpeg_video_writer.py +298 -0
  37. OTVision/plugin/generate_video.py +24 -0
  38. OTVision/track/builder.py +2 -5
  39. OTVision/track/id_generator.py +1 -3
  40. OTVision/track/parser/chunk_parser_plugins.py +1 -19
  41. OTVision/track/parser/frame_group_parser_plugins.py +19 -74
  42. OTVision/track/stream_ottrk_file_writer.py +116 -0
  43. OTVision/track/track.py +2 -1
  44. OTVision/version.py +1 -1
  45. {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/METADATA +6 -5
  46. {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/RECORD +48 -36
  47. OTVision/application/detect/detected_frame_producer.py +0 -24
  48. {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/WHEEL +0 -0
  49. {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) -> InputSourceDetect:
34
+ def input_source(self) -> RtspInputSource:
28
35
  return RtspInputSource(
29
- subject=Subject[FlushEvent](),
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 Generator
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 = 1
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 ValueError("Stream config not found in config")
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
- subject: Subject[FlushEvent],
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
- super().__init__(subject)
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) -> Generator[Frame, None, None]:
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
- while not self.should_stop():
127
- if (frame := self._read_next_frame()) is not None:
128
- self._frame_counter.increment()
129
- occurrence = self._datetime_provider.provide()
130
-
131
- if self._outdated:
132
- self._current_video_start_time = occurrence
133
- self._outdated = False
134
-
135
- yield Frame(
136
- data=convert_frame_to_rgb(frame), # YOLO expects RGB
137
- frame=self.current_frame_number,
138
- source=self.rtsp_url,
139
- output=self.create_output(),
140
- occurrence=occurrence,
141
- )
142
- if self.flush_condition_met():
143
- self._notify()
144
- self._outdated = True
145
- self._frame_counter.reset()
146
-
147
- self._notify()
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 _notify(self) -> None:
193
- frame_width = int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
194
- frame_height = int(self._video_capture.get(CAP_PROP_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._subject.notify(
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 Generator
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 (DetectionFileSavePathProvider): Provider for detection
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
- subject: Subject[FlushEvent],
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: DetectionFileSavePathProvider,
67
+ save_path_provider: OtvisionSavePathProvider,
64
68
  ) -> None:
65
- super().__init__(subject)
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) -> Generator[Frame, None, None]:
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(str(video_file))
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.streams.video[0].side_data
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.notify_observers(video_file, video_fps)
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 notify_observers(self, current_video_file: Path, video_fps: float) -> None:
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._subject.notify(
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 Generator
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
- self, frames: Generator[Frame, None, None]
144
- ) -> Generator[DetectedFrame, None, None]:
145
- for frame in tqdm(frames, desc="Detected frames", unit=" frames"):
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 Generator
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) -> Generator[DetectedFrame, None, None]:
24
+ def produce(self) -> Iterator[DetectedFrame]:
25
25
  """Generate a stream of detected frames.
26
26
 
27
27
  Returns:
28
- Generator[DetectedFrame, None, None]: A stream of detected frames.
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 Generator
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(Observable[FlushEvent], ABC):
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) -> Generator[Frame, None, None]:
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
- Generator[Frame, None, None]: A generator yielding Frame objects
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 Generator
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 (Generator[Frame, None, None]): the source to read frames from.
32
+ frames (Iterator[Frame]): the source to read frames from.
35
33
 
36
34
  Returns:
37
- Generator[DetectedFrame, None, None]: nested list of detections.
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, timezone
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(tz=timezone.utc)
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