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.
Files changed (34) hide show
  1. OTVision/abstraction/pipes_and_filter.py +4 -4
  2. OTVision/application/buffer.py +5 -12
  3. OTVision/application/config_parser.py +51 -2
  4. OTVision/application/detect/current_object_detector.py +2 -4
  5. OTVision/application/event/new_otvision_config.py +6 -0
  6. OTVision/application/{detect/detection_file_save_path_provider.py → otvision_save_path_provider.py} +8 -7
  7. OTVision/application/track/ottrk.py +203 -0
  8. OTVision/application/track/tracking_run_id.py +35 -0
  9. OTVision/detect/builder.py +8 -14
  10. OTVision/detect/detected_frame_buffer.py +13 -1
  11. OTVision/detect/detected_frame_producer.py +2 -2
  12. OTVision/detect/detected_frame_producer_factory.py +4 -8
  13. OTVision/detect/otdet.py +109 -41
  14. OTVision/detect/otdet_file_writer.py +52 -29
  15. OTVision/detect/rtsp_input_source.py +15 -3
  16. OTVision/detect/video_input_source.py +8 -8
  17. OTVision/detect/yolo.py +3 -7
  18. OTVision/domain/detect_producer_consumer.py +3 -3
  19. OTVision/domain/frame.py +12 -0
  20. OTVision/domain/input_source_detect.py +3 -3
  21. OTVision/domain/object_detection.py +4 -6
  22. OTVision/helpers/date.py +16 -0
  23. OTVision/plugin/ffmpeg_video_writer.py +2 -4
  24. OTVision/track/builder.py +2 -5
  25. OTVision/track/id_generator.py +1 -3
  26. OTVision/track/parser/chunk_parser_plugins.py +1 -19
  27. OTVision/track/parser/frame_group_parser_plugins.py +19 -74
  28. OTVision/track/stream_ottrk_file_writer.py +116 -0
  29. OTVision/track/track.py +2 -1
  30. OTVision/version.py +1 -1
  31. {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/METADATA +9 -2
  32. {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/RECORD +34 -30
  33. {otvision-0.6.8.dist-info → otvision-0.6.10.dist-info}/WHEEL +0 -0
  34. {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 OtdetBuilder:
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 add_config(self, config: OtdetBuilderConfig) -> Self:
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
- def _build_data(self, frames: list[DetectedFrame]) -> dict:
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 (DetectionFileSavePathProvider): determines the save path for
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: DetectionFileSavePathProvider,
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
- otdet = self._builder.add_config(
72
- OtdetBuilderConfig(
73
- conf=detect_config.confidence,
74
- iou=detect_config.iou,
75
- source=source_metadata.output,
76
- video_width=source_metadata.width,
77
- video_height=source_metadata.height,
78
- expected_duration=expected_duration,
79
- actual_duration=source_metadata.duration,
80
- recorded_fps=source_metadata.fps,
81
- recorded_start_date=source_metadata.start_time,
82
- actual_fps=actual_fps,
83
- actual_frames=actual_frames,
84
- detection_img_size=detect_config.img_size,
85
- normalized=detect_config.normalized,
86
- detection_model=detect_config.weights,
87
- half_precision=detect_config.half_precision,
88
- chunksize=1,
89
- classifications=class_mapping,
90
- detect_start=detect_config.detect_start,
91
- detect_end=detect_config.detect_end,
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(source_metadata.output)
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 Generator
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 ValueError("Stream config not found in config")
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) -> Generator[Frame, None, None]:
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 Generator
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 (DetectionFileSavePathProvider): Provider for detection
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: DetectionFileSavePathProvider,
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) -> Generator[Frame, None, None]:
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(str(video_file))
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 Generator
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 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,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 Frame
5
5
 
@@ -14,7 +14,7 @@ class InputSourceDetect(ABC):
14
14
  """
15
15
 
16
16
  @abstractmethod
17
- def produce(self) -> Generator[Frame, None, None]:
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
- Generator[Frame, None, None]: A generator yielding Frame objects
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 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/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, Generator
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
@@ -1,7 +1,5 @@
1
1
  import uuid
2
- from typing import Callable, Iterator
3
-
4
- StrIdGenerator = Callable[[], str]
2
+ from typing import Iterator
5
3
 
6
4
 
7
5
  def tracking_run_uuid_generator() -> str:
@@ -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)