OTVision 0.6.4__py3-none-any.whl → 0.6.5__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 (51) hide show
  1. OTVision/abstraction/defaults.py +15 -0
  2. OTVision/application/config.py +475 -0
  3. OTVision/application/config_parser.py +280 -0
  4. OTVision/application/configure_logger.py +1 -1
  5. OTVision/application/detect/factory.py +1 -1
  6. OTVision/application/detect/update_detect_config_with_cli_args.py +2 -1
  7. OTVision/application/get_config.py +2 -1
  8. OTVision/application/get_current_config.py +1 -1
  9. OTVision/application/track/__init__.py +0 -0
  10. OTVision/application/track/get_track_cli_args.py +20 -0
  11. OTVision/application/track/update_current_track_config.py +42 -0
  12. OTVision/application/track/update_track_config_with_cli_args.py +52 -0
  13. OTVision/application/update_current_config.py +1 -1
  14. OTVision/config.py +61 -668
  15. OTVision/convert/convert.py +3 -3
  16. OTVision/detect/builder.py +27 -20
  17. OTVision/detect/cli.py +3 -3
  18. OTVision/detect/detected_frame_buffer.py +3 -0
  19. OTVision/detect/file_based_detect_builder.py +19 -0
  20. OTVision/detect/otdet.py +54 -1
  21. OTVision/detect/otdet_file_writer.py +3 -2
  22. OTVision/detect/rtsp_based_detect_builder.py +37 -0
  23. OTVision/detect/rtsp_input_source.py +199 -0
  24. OTVision/detect/timestamper.py +1 -1
  25. OTVision/detect/video_input_source.py +2 -1
  26. OTVision/detect/yolo.py +17 -1
  27. OTVision/domain/cli.py +31 -1
  28. OTVision/domain/current_config.py +1 -1
  29. OTVision/domain/object_detection.py +6 -1
  30. OTVision/domain/serialization.py +12 -0
  31. OTVision/domain/time.py +13 -0
  32. OTVision/helpers/files.py +14 -15
  33. OTVision/plugin/__init__.py +0 -0
  34. OTVision/plugin/yaml_serialization.py +20 -0
  35. OTVision/track/builder.py +132 -0
  36. OTVision/track/cli.py +128 -0
  37. OTVision/track/exporter/filebased_exporter.py +2 -1
  38. OTVision/track/id_generator.py +15 -0
  39. OTVision/track/model/track_exporter.py +2 -1
  40. OTVision/track/model/tracking_interfaces.py +6 -6
  41. OTVision/track/parser/frame_group_parser_plugins.py +35 -5
  42. OTVision/track/track.py +54 -133
  43. OTVision/track/tracker/filebased_tracking.py +8 -7
  44. OTVision/track/tracker/tracker_plugin_iou.py +14 -9
  45. OTVision/transform/transform.py +2 -2
  46. OTVision/version.py +1 -1
  47. otvision-0.6.5.dist-info/METADATA +182 -0
  48. {otvision-0.6.4.dist-info → otvision-0.6.5.dist-info}/RECORD +50 -33
  49. otvision-0.6.4.dist-info/METADATA +0 -49
  50. {otvision-0.6.4.dist-info → otvision-0.6.5.dist-info}/WHEEL +0 -0
  51. {otvision-0.6.4.dist-info → otvision-0.6.5.dist-info}/licenses/LICENSE +0 -0
@@ -27,8 +27,7 @@ from typing import Optional
27
27
 
28
28
  from tqdm import tqdm
29
29
 
30
- from OTVision.config import (
31
- CONFIG,
30
+ from OTVision.application.config import (
32
31
  CONVERT,
33
32
  DELETE_INPUT,
34
33
  FILETYPES,
@@ -40,6 +39,7 @@ from OTVision.config import (
40
39
  VID,
41
40
  VID_ROTATABLE,
42
41
  )
42
+ from OTVision.config import CONFIG
43
43
  from OTVision.helpers.files import get_files
44
44
  from OTVision.helpers.formats import _get_fps_from_filename
45
45
  from OTVision.helpers.log import LOGGER_NAME
@@ -140,7 +140,7 @@ def convert(
140
140
  overwrite (bool, optional): Whether to overwrite existing video files.
141
141
  Defaults to CONFIG["CONVERT"]["OVERWRITE"].
142
142
  delete_input (bool, optional): Whether to delete the input h264.
143
- Defaults to CONFIG["CONVERT"]["DELETE_INPUT"].
143
+ Defaults to CONFIG["CONVERT"]["DELETE_INPUT"].
144
144
 
145
145
  Raises:
146
146
  TypeError: If output video filetype is not supported.
@@ -1,8 +1,11 @@
1
+ from abc import ABC, abstractmethod
1
2
  from argparse import ArgumentParser
2
3
  from functools import cached_property
3
4
 
4
5
  from OTVision.abstraction.observer import Subject
5
6
  from OTVision.application.buffer import Buffer
7
+ from OTVision.application.config import Config
8
+ from OTVision.application.config_parser import ConfigParser
6
9
  from OTVision.application.configure_logger import ConfigureLogger
7
10
  from OTVision.application.detect.current_object_detector import CurrentObjectDetector
8
11
  from OTVision.application.detect.current_object_detector_metadata import (
@@ -24,7 +27,6 @@ from OTVision.application.frame_count_provider import FrameCountProvider
24
27
  from OTVision.application.get_config import GetConfig
25
28
  from OTVision.application.get_current_config import GetCurrentConfig
26
29
  from OTVision.application.update_current_config import UpdateCurrentConfig
27
- from OTVision.config import Config, ConfigParser
28
30
  from OTVision.detect.cli import ArgparseDetectCliParser
29
31
  from OTVision.detect.detect import OTVisionVideoDetect
30
32
  from OTVision.detect.detected_frame_buffer import (
@@ -37,7 +39,6 @@ from OTVision.detect.otdet_file_writer import OtdetFileWriter
37
39
  from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
38
40
  from OTVision.detect.pyav_frame_count_provider import PyAVFrameCountProvider
39
41
  from OTVision.detect.timestamper import TimestamperFactory
40
- from OTVision.detect.video_input_source import VideoSource
41
42
  from OTVision.detect.yolo import YoloDetectionConverter, YoloFactory
42
43
  from OTVision.domain.cli import DetectCliParser
43
44
  from OTVision.domain.current_config import CurrentConfig
@@ -45,17 +46,15 @@ from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
45
46
  from OTVision.domain.frame import DetectedFrame
46
47
  from OTVision.domain.input_source_detect import InputSourceDetect
47
48
  from OTVision.domain.object_detection import ObjectDetectorFactory
49
+ from OTVision.domain.serialization import Deserializer
50
+ from OTVision.plugin.yaml_serialization import YamlDeserializer
48
51
 
49
52
 
50
- class DetectBuilder:
53
+ class DetectBuilder(ABC):
51
54
  @cached_property
52
55
  def get_config(self) -> GetConfig:
53
56
  return GetConfig(self.config_parser)
54
57
 
55
- @cached_property
56
- def config_parser(self) -> ConfigParser:
57
- return ConfigParser()
58
-
59
58
  @cached_property
60
59
  def get_detect_cli_args(self) -> GetDetectCliArgs:
61
60
  return GetDetectCliArgs(self.detect_cli_parser)
@@ -108,19 +107,6 @@ class DetectBuilder:
108
107
  def update_current_config(self) -> UpdateCurrentConfig:
109
108
  return UpdateCurrentConfig(self.current_config)
110
109
 
111
- def __init__(self, argv: list[str] | None = None) -> None:
112
- self.argv = argv
113
-
114
- @cached_property
115
- def input_source(self) -> InputSourceDetect:
116
- return VideoSource(
117
- subject=Subject[FlushEvent](),
118
- get_current_config=self.get_current_config,
119
- frame_rotator=self.frame_rotator,
120
- timestamper_factory=self.timestamper_factory,
121
- save_path_provider=self.detection_file_save_path_provider,
122
- )
123
-
124
110
  @cached_property
125
111
  def frame_rotator(self) -> AvVideoFrameRotator:
126
112
  return AvVideoFrameRotator()
@@ -171,10 +157,31 @@ class DetectBuilder:
171
157
  detected_frame_buffer=self.detected_frame_buffer,
172
158
  )
173
159
 
160
+ @cached_property
161
+ def config_parser(self) -> ConfigParser:
162
+ return ConfigParser(self.yaml_deserializer)
163
+
164
+ @cached_property
165
+ def yaml_deserializer(self) -> Deserializer:
166
+ return YamlDeserializer()
167
+
168
+ def __init__(self, argv: list[str] | None = None) -> None:
169
+ self.argv = argv
170
+
171
+ @property
172
+ @abstractmethod
173
+ def input_source(self) -> InputSourceDetect:
174
+ raise NotImplementedError
175
+
174
176
  def build(self) -> OTVisionVideoDetect:
175
177
  self.register_observers()
178
+ self._preload_object_detection_model()
176
179
  return OTVisionVideoDetect(self.detected_frame_producer)
177
180
 
181
+ def _preload_object_detection_model(self) -> None:
182
+ model = self.current_object_detector.get()
183
+ model.preload()
184
+
178
185
  def register_observers(self) -> None:
179
186
  self.input_source.register(self.detected_frame_buffer.on_flush)
180
187
  self.detected_frame_buffer.register(self.otdet_file_writer.write)
OTVision/detect/cli.py CHANGED
@@ -2,7 +2,7 @@ from argparse import ArgumentParser, BooleanOptionalAction, Namespace
2
2
  from datetime import datetime, timedelta
3
3
  from pathlib import Path
4
4
 
5
- from OTVision.config import DATETIME_FORMAT
5
+ from OTVision.application.config import DATETIME_FORMAT
6
6
  from OTVision.domain.cli import CliParseError, DetectCliArgs, DetectCliParser
7
7
  from OTVision.helpers.files import check_if_all_paths_exist
8
8
  from OTVision.helpers.log import DEFAULT_LOG_FILE, VALID_LOG_LEVELS
@@ -172,10 +172,10 @@ class ArgparseDetectCliParser(DetectCliParser):
172
172
  )
173
173
  )
174
174
 
175
- def _parse_files(self, files: list[str] | None) -> list[Path] | None:
175
+ def _parse_files(self, files: list[str] | None) -> list[str] | None:
176
176
  if files is None:
177
177
  return None
178
178
 
179
179
  result = [Path(file).expanduser() for file in files]
180
180
  check_if_all_paths_exist(result)
181
- return result
181
+ return list(map(str, result))
@@ -8,6 +8,7 @@ from OTVision.domain.frame import DetectedFrame
8
8
  @dataclass
9
9
  class SourceMetadata:
10
10
  source: str
11
+ output: str
11
12
  duration: timedelta
12
13
  height: int
13
14
  width: int
@@ -22,6 +23,7 @@ class FlushEvent:
22
23
  @staticmethod
23
24
  def create(
24
25
  source: str,
26
+ output: str,
25
27
  duration: timedelta,
26
28
  source_height: int,
27
29
  source_width: int,
@@ -31,6 +33,7 @@ class FlushEvent:
31
33
  return FlushEvent(
32
34
  SourceMetadata(
33
35
  source,
36
+ output,
34
37
  duration,
35
38
  source_height,
36
39
  source_width,
@@ -0,0 +1,19 @@
1
+ from functools import cached_property
2
+
3
+ from OTVision.abstraction.observer import Subject
4
+ from OTVision.detect.builder import DetectBuilder
5
+ from OTVision.detect.detected_frame_buffer import FlushEvent
6
+ from OTVision.detect.video_input_source import VideoSource
7
+ from OTVision.domain.input_source_detect import InputSourceDetect
8
+
9
+
10
+ class FileBasedDetectBuilder(DetectBuilder):
11
+ @cached_property
12
+ def input_source(self) -> InputSourceDetect:
13
+ return VideoSource(
14
+ subject=Subject[FlushEvent](),
15
+ get_current_config=self.get_current_config,
16
+ frame_rotator=self.frame_rotator,
17
+ timestamper_factory=self.timestamper_factory,
18
+ save_path_provider=self.detection_file_save_path_provider,
19
+ )
OTVision/detect/otdet.py CHANGED
@@ -102,7 +102,7 @@ class OtdetBuilder:
102
102
  dataformat.ACTUAL_FPS: self.config.actual_fps,
103
103
  dataformat.NUMBER_OF_FRAMES: number_of_frames,
104
104
  dataformat.RECORDED_START_DATE: self.config.recorded_start_date.timestamp(),
105
- dataformat.LENGTH: str(self.config.actual_duration),
105
+ dataformat.LENGTH: serialize_video_length(self.config.actual_duration),
106
106
  }
107
107
  if self.config.expected_duration is not None:
108
108
  video_config[dataformat.EXPECTED_DURATION] = int(
@@ -127,3 +127,56 @@ class OtdetBuilder:
127
127
  dataformat.DETECT_START: self.config.detect_start,
128
128
  dataformat.DETECT_END: self.config.detect_end,
129
129
  }
130
+
131
+
132
+ def serialize_video_length(video_length: timedelta) -> str:
133
+ """Serialize a timedelta object to a video length string in 'H+:MM:SS' format.
134
+
135
+ Args:
136
+ video_length (timedelta): The video length to serialize.
137
+
138
+ Returns:
139
+ str: The video length represented in 'H+:MM:SS' format.
140
+ """
141
+ seconds_per_hour = 3600
142
+ seconds_per_minute = 60
143
+
144
+ total_seconds = int(video_length.total_seconds())
145
+ hours = total_seconds // seconds_per_hour
146
+ minutes = (total_seconds % seconds_per_hour) // seconds_per_minute
147
+ seconds = total_seconds % seconds_per_minute
148
+ return f"{hours}:{minutes:02}:{seconds:02}"
149
+
150
+
151
+ class VideoLengthParseError(Exception):
152
+ """Exception raised for errors in parsing video length strings."""
153
+
154
+ pass
155
+
156
+
157
+ def parse_video_length(video_length: str) -> timedelta:
158
+ """Parse a video length string that is in either 'H+:MM:SS' or 'H+:MM:SS.mmmuuu'
159
+ format into a timedelta object ignoring milliseconds and microseconds.
160
+
161
+ Args:
162
+ video_length (str): A string representing the video length in or
163
+ 'H+:MM:SS.mmmuuu' or 'H+:MM:SS.mmmuuu' format.
164
+
165
+ Returns:
166
+ timedelta: A timedelta object representing the parsed video length ignoring
167
+ milliseconds and microseconds.
168
+
169
+ Raises:
170
+ VideoLengthParseError: If the input string is not in the expected format.
171
+ """
172
+
173
+ try:
174
+ hours, minutes, seconds = video_length.strip().split(":")
175
+ return timedelta(
176
+ hours=int(hours), minutes=int(minutes), seconds=int(float(seconds))
177
+ )
178
+ except ValueError as cause:
179
+ raise VideoLengthParseError(
180
+ f"Could not parse video length '{video_length}'. "
181
+ "Expected format 'HH:MM:SS'."
182
+ ) from cause
@@ -72,7 +72,7 @@ class OtdetFileWriter:
72
72
  OtdetBuilderConfig(
73
73
  conf=detect_config.confidence,
74
74
  iou=detect_config.iou,
75
- source=source_metadata.source,
75
+ source=source_metadata.output,
76
76
  video_width=source_metadata.width,
77
77
  video_height=source_metadata.height,
78
78
  expected_duration=expected_duration,
@@ -92,7 +92,8 @@ class OtdetFileWriter:
92
92
  )
93
93
  ).build(event.frames)
94
94
 
95
- detections_file = self._save_path_provider.provide(source_metadata.source)
95
+ detections_file = self._save_path_provider.provide(source_metadata.output)
96
+ detections_file.parent.mkdir(parents=True, exist_ok=True)
96
97
  write_json(
97
98
  otdet,
98
99
  file=detections_file,
@@ -0,0 +1,37 @@
1
+ from functools import cached_property
2
+
3
+ from OTVision.abstraction.observer import Subject
4
+ from OTVision.application.config import StreamConfig
5
+ from OTVision.detect.builder import DetectBuilder
6
+ from OTVision.detect.detected_frame_buffer import FlushEvent
7
+ from OTVision.detect.rtsp_input_source import Counter, RtspInputSource
8
+ from OTVision.domain.input_source_detect import InputSourceDetect
9
+ from OTVision.domain.time import CurrentDatetimeProvider, DatetimeProvider
10
+
11
+ FLUSH_BUFFER_SIZE = 18000
12
+ FLUSH_BUFFER_SIZE = 1200
13
+
14
+
15
+ class RtspBasedDetectBuilder(DetectBuilder):
16
+ @property
17
+ def stream_config(self) -> StreamConfig:
18
+ config = self.get_current_config.get()
19
+ if config.stream is None:
20
+ raise ValueError(
21
+ "Stream config is not provided. "
22
+ "Running OTVision in streaming mode requires stream config"
23
+ )
24
+ return config.stream
25
+
26
+ @cached_property
27
+ def input_source(self) -> InputSourceDetect:
28
+ return RtspInputSource(
29
+ subject=Subject[FlushEvent](),
30
+ datetime_provider=self.datetime_provider,
31
+ frame_counter=Counter(),
32
+ get_current_config=self.get_current_config,
33
+ )
34
+
35
+ @cached_property
36
+ def datetime_provider(self) -> DatetimeProvider:
37
+ return CurrentDatetimeProvider()
@@ -0,0 +1,199 @@
1
+ from datetime import datetime, timedelta
2
+ from time import sleep
3
+ from typing import Generator
4
+
5
+ from cv2 import (
6
+ CAP_PROP_FRAME_HEIGHT,
7
+ CAP_PROP_FRAME_WIDTH,
8
+ COLOR_BGR2RGB,
9
+ VideoCapture,
10
+ cvtColor,
11
+ )
12
+ from numpy import ndarray
13
+
14
+ from OTVision.abstraction.observer import Subject
15
+ from OTVision.application.config import (
16
+ DATETIME_FORMAT,
17
+ Config,
18
+ DetectConfig,
19
+ StreamConfig,
20
+ )
21
+ from OTVision.application.configure_logger import logger
22
+ from OTVision.application.get_current_config import GetCurrentConfig
23
+ from OTVision.detect.detected_frame_buffer import FlushEvent
24
+ from OTVision.domain.frame import Frame
25
+ from OTVision.domain.input_source_detect import InputSourceDetect
26
+ from OTVision.domain.time import DatetimeProvider
27
+
28
+ RTSP_URL = "rtsp://127.0.0.1:8554/test"
29
+ RETRY_SECONDS = 1
30
+
31
+
32
+ class Counter:
33
+ def __init__(self, start_value: int = 0) -> None:
34
+ self.__counter = start_value
35
+
36
+ def increment(self) -> None:
37
+ self.__counter += 1
38
+
39
+ def get(self) -> int:
40
+ return self.__counter
41
+
42
+
43
+ class RtspInputSource(InputSourceDetect):
44
+
45
+ @property
46
+ def current_frame_number(self) -> int:
47
+ return self._frame_counter.get()
48
+
49
+ @property
50
+ def config(self) -> Config:
51
+ return self._get_current_config.get()
52
+
53
+ @property
54
+ def detect_config(self) -> DetectConfig:
55
+ return self.config.detect
56
+
57
+ @property
58
+ def stream_config(self) -> StreamConfig:
59
+ if stream_config := self.config.stream:
60
+ return stream_config
61
+ raise ValueError("Stream config not found in config")
62
+
63
+ @property
64
+ def rtsp_url(self) -> str:
65
+ return self.stream_config.source
66
+
67
+ @property
68
+ def flush_buffer_size(self) -> int:
69
+ return self.stream_config.flush_buffer_size
70
+
71
+ def __init__(
72
+ self,
73
+ subject: Subject[FlushEvent],
74
+ datetime_provider: DatetimeProvider,
75
+ frame_counter: Counter,
76
+ get_current_config: GetCurrentConfig,
77
+ ) -> None:
78
+ super().__init__(subject)
79
+ self._datetime_provider = datetime_provider
80
+ self._stop_capture = False
81
+ self._frame_counter = frame_counter
82
+ self._get_current_config = get_current_config
83
+ self._current_stream: str | None = None
84
+ self._current_video_capture: VideoCapture | None = None
85
+
86
+ @property
87
+ def _video_capture(self) -> VideoCapture:
88
+ # Property is moved below __init__ otherwise mypy is somehow unable to determine
89
+ # types of self._current_stream and self._current_video_capture
90
+ new_source = self.stream_config.source
91
+ if (
92
+ self._current_stream is not None
93
+ and self._current_stream == new_source
94
+ and self._current_video_capture
95
+ ):
96
+ # current source has not changed
97
+ return self._current_video_capture
98
+
99
+ # Stream changed or has not been initialized
100
+ if self._current_video_capture is not None:
101
+ # If the stream changed and there's an existing capture, release it
102
+ self._current_video_capture.release()
103
+
104
+ self._current_stream = new_source
105
+ self._current_video_capture = self._init_video_capture(self._current_stream)
106
+ return self._current_video_capture
107
+
108
+ def produce(self) -> Generator[Frame, None, None]:
109
+ start_time = self._datetime_provider.provide()
110
+ while not self.should_stop():
111
+ if (frame := self._read_next_frame()) is not None:
112
+ self._frame_counter.increment()
113
+
114
+ yield Frame(
115
+ data=convert_frame_to_rgb(frame), # YOLO expects RGB
116
+ frame=self.current_frame_number,
117
+ source=self.rtsp_url,
118
+ occurrence=self._datetime_provider.provide(),
119
+ )
120
+ if self.flush_condition_met():
121
+ self._notify(start_time)
122
+ self._notify(start_time)
123
+
124
+ def _init_video_capture(self, source: str) -> VideoCapture:
125
+ cap = VideoCapture(source)
126
+ while not self.should_stop() and not cap.isOpened():
127
+ logger().warning(
128
+ f"Couldn't open the RTSP stream: {source}. "
129
+ f"Trying again in {RETRY_SECONDS}s..."
130
+ )
131
+ sleep(RETRY_SECONDS)
132
+ cap.release()
133
+ cap = VideoCapture(source)
134
+ return cap
135
+
136
+ def _read_next_frame(self) -> ndarray | None:
137
+ successful, frame = self._video_capture.read()
138
+ if successful:
139
+ return frame
140
+ logger().debug("Failed to grab frame")
141
+ return None
142
+
143
+ def should_stop(self) -> bool:
144
+ return self._stop_capture
145
+
146
+ def stop(self) -> None:
147
+ self._stop_capture = True
148
+
149
+ def start(self) -> None:
150
+ self._stop_capture = False
151
+
152
+ def flush_condition_met(self) -> bool:
153
+ return self.current_frame_number % self.flush_buffer_size == 0
154
+
155
+ def _notify(self, start_time: datetime) -> None:
156
+ frame_width = int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
157
+ frame_height = int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
158
+ fps = self.config.convert.output_fps
159
+ _start_time = calculate_start_time(
160
+ start_time, self.current_frame_number, fps, self.flush_buffer_size
161
+ )
162
+ frames = (
163
+ self.flush_buffer_size
164
+ if self.current_frame_number % self.flush_buffer_size == 0
165
+ else self.current_frame_number % self.flush_buffer_size
166
+ )
167
+ duration = timedelta(seconds=round(frames / fps))
168
+ output_filename = (
169
+ f"{self.stream_config.name}_FR{round(fps)}"
170
+ f"_{_start_time.strftime(DATETIME_FORMAT)}.mp4"
171
+ )
172
+ output = str(self.stream_config.save_dir / output_filename)
173
+ self._subject.notify(
174
+ FlushEvent.create(
175
+ source=self.rtsp_url,
176
+ output=output,
177
+ duration=duration,
178
+ source_width=frame_width,
179
+ source_height=frame_height,
180
+ source_fps=fps,
181
+ start_time=_start_time,
182
+ )
183
+ )
184
+
185
+
186
+ def calculate_start_time(
187
+ start: datetime, current_frame_number: int, fps: float, flush_buffer_size: int
188
+ ) -> datetime:
189
+ offset_in_frames = (
190
+ current_frame_number // flush_buffer_size - 1
191
+ ) * flush_buffer_size
192
+ if offset_in_frames == 0:
193
+ return start
194
+ offset_in_seconds = offset_in_frames / fps
195
+ return start + timedelta(seconds=offset_in_seconds)
196
+
197
+
198
+ def convert_frame_to_rgb(frame: ndarray) -> ndarray:
199
+ return cvtColor(frame, COLOR_BGR2RGB)
@@ -2,10 +2,10 @@ import re
2
2
  from datetime import datetime, timedelta, timezone
3
3
  from pathlib import Path
4
4
 
5
+ from OTVision.application.config import DATETIME_FORMAT
5
6
  from OTVision.application.detect.timestamper import Timestamper
6
7
  from OTVision.application.frame_count_provider import FrameCountProvider
7
8
  from OTVision.application.get_current_config import GetCurrentConfig
8
- from OTVision.config import DATETIME_FORMAT
9
9
  from OTVision.dataformat import FRAME
10
10
  from OTVision.domain.frame import Frame, FrameKeys
11
11
  from OTVision.helpers.date import parse_date_string_to_utc_datime
@@ -7,12 +7,12 @@ import av
7
7
  from tqdm import tqdm
8
8
 
9
9
  from OTVision.abstraction.observer import Subject
10
+ from OTVision.application.config import DATETIME_FORMAT, Config
10
11
  from OTVision.application.detect.detection_file_save_path_provider import (
11
12
  DetectionFileSavePathProvider,
12
13
  )
13
14
  from OTVision.application.detect.timestamper import Timestamper
14
15
  from OTVision.application.get_current_config import GetCurrentConfig
15
- from OTVision.config import DATETIME_FORMAT, Config
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
@@ -181,6 +181,7 @@ class VideoSource(InputSourceDetect):
181
181
  self._subject.notify(
182
182
  FlushEvent.create(
183
183
  source=str(current_video_file),
184
+ output=str(current_video_file),
184
185
  duration=duration,
185
186
  source_height=height,
186
187
  source_width=width,
OTVision/detect/yolo.py CHANGED
@@ -30,9 +30,9 @@ from ultralytics import YOLO
30
30
  from ultralytics.engine.results import Boxes
31
31
 
32
32
  from OTVision.abstraction.pipes_and_filter import Filter
33
+ from OTVision.application.config import DetectConfig
33
34
  from OTVision.application.detect.detected_frame_factory import DetectedFrameFactory
34
35
  from OTVision.application.get_current_config import GetCurrentConfig
35
- from OTVision.config import DetectConfig
36
36
  from OTVision.domain.detection import Detection
37
37
  from OTVision.domain.frame import DetectedFrame, Frame, FrameKeys
38
38
  from OTVision.domain.object_detection import ObjectDetector, ObjectDetectorFactory
@@ -184,6 +184,22 @@ class YoloDetector(ObjectDetector, Filter[Frame, DetectedFrame]):
184
184
  """Create a DetectedFrame with no detections."""
185
185
  return self._detected_frame_factory.create(frame, detections=[])
186
186
 
187
+ def preload(self) -> None:
188
+ model_name = Path(self.config.weights).name
189
+ log.info(f"Preloading YOLO model '{model_name}...'")
190
+ self._model.predict(
191
+ source=None,
192
+ conf=self.config.confidence,
193
+ iou=self.config.iou,
194
+ half=self.config.half_precision,
195
+ imgsz=self.config.img_size,
196
+ device=0 if torch.cuda.is_available() else "cpu",
197
+ stream=False,
198
+ verbose=False,
199
+ agnostic_nms=True,
200
+ )
201
+ log.info(f"YOLO model '{model_name}' loaded and ready for inference.'")
202
+
187
203
 
188
204
  class YoloFactory(ObjectDetectorFactory):
189
205
  """
OTVision/domain/cli.py CHANGED
@@ -17,7 +17,7 @@ class CliParseError(Exception):
17
17
  @dataclass
18
18
  class DetectCliArgs(CliArgs):
19
19
  expected_duration: timedelta | None
20
- paths: list[Path] | None
20
+ paths: list[str] | None
21
21
  config_file: Path | None
22
22
  logfile: Path
23
23
  logfile_overwrite: bool
@@ -41,3 +41,33 @@ class DetectCliParser(ABC):
41
41
  @abstractmethod
42
42
  def parse(self) -> DetectCliArgs:
43
43
  raise NotImplementedError
44
+
45
+
46
+ @dataclass
47
+ class TrackCliArgs(CliArgs):
48
+ paths: list[str] | None
49
+ config_file: Path | None
50
+ logfile: Path
51
+ logfile_overwrite: bool
52
+ log_level_console: str | None
53
+ log_level_file: str | None
54
+ overwrite: bool | None = None
55
+ sigma_l: float | None = None
56
+ sigma_h: float | None = None
57
+ sigma_iou: float | None = None
58
+ t_min: int | None = None
59
+ t_miss_max: int | None = None
60
+
61
+ def get_config_file(self) -> Path | None:
62
+ return self.config_file
63
+
64
+
65
+ class TrackCliParser(ABC):
66
+ @abstractmethod
67
+ def parse(self) -> TrackCliArgs:
68
+ """Parse track CLI arguments.
69
+
70
+ Returns:
71
+ TrackCliArgs: the parsed track CLI arguments.
72
+ """
73
+ raise NotImplementedError
@@ -1,4 +1,4 @@
1
- from OTVision.config import Config
1
+ from OTVision.application.config import Config
2
2
 
3
3
 
4
4
  class CurrentConfig:
@@ -1,7 +1,7 @@
1
1
  from abc import ABC, abstractmethod
2
2
  from typing import Generator
3
3
 
4
- from OTVision.config import DetectConfig
4
+ from OTVision.application.config import DetectConfig
5
5
  from OTVision.domain.frame import DetectedFrame, Frame
6
6
 
7
7
 
@@ -39,6 +39,11 @@ class ObjectDetector(ObjectDetectorMetadata):
39
39
  """
40
40
  raise NotImplementedError
41
41
 
42
+ @abstractmethod
43
+ def preload(self) -> None:
44
+ """Preload the model if possible."""
45
+ raise NotImplementedError
46
+
42
47
 
43
48
  class ObjectDetectorFactory(ABC):
44
49
  @abstractmethod
@@ -0,0 +1,12 @@
1
+ from abc import ABC
2
+ from pathlib import Path
3
+
4
+
5
+ class Serializer(ABC):
6
+ def serialize(self, file: Path) -> dict:
7
+ raise NotImplementedError
8
+
9
+
10
+ class Deserializer(ABC):
11
+ def deserialize(self, file: Path) -> dict:
12
+ raise NotImplementedError
@@ -0,0 +1,13 @@
1
+ from abc import ABC, abstractmethod
2
+ from datetime import datetime, timezone
3
+
4
+
5
+ class DatetimeProvider(ABC):
6
+ @abstractmethod
7
+ def provide(self) -> datetime:
8
+ raise NotImplementedError
9
+
10
+
11
+ class CurrentDatetimeProvider(DatetimeProvider):
12
+ def provide(self) -> datetime:
13
+ return datetime.now(tz=timezone.utc)