OTVision 0.6.4__py3-none-any.whl → 0.6.6__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 (54) 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/detected_frame_factory.py +1 -0
  6. OTVision/application/detect/factory.py +1 -1
  7. OTVision/application/detect/update_detect_config_with_cli_args.py +2 -1
  8. OTVision/application/get_config.py +2 -1
  9. OTVision/application/get_current_config.py +1 -1
  10. OTVision/application/track/__init__.py +0 -0
  11. OTVision/application/track/get_track_cli_args.py +20 -0
  12. OTVision/application/track/update_current_track_config.py +42 -0
  13. OTVision/application/track/update_track_config_with_cli_args.py +52 -0
  14. OTVision/application/update_current_config.py +1 -1
  15. OTVision/config.py +61 -668
  16. OTVision/convert/convert.py +3 -3
  17. OTVision/detect/builder.py +27 -20
  18. OTVision/detect/cli.py +3 -3
  19. OTVision/detect/detected_frame_buffer.py +3 -0
  20. OTVision/detect/file_based_detect_builder.py +19 -0
  21. OTVision/detect/otdet.py +54 -1
  22. OTVision/detect/otdet_file_writer.py +4 -3
  23. OTVision/detect/rtsp_based_detect_builder.py +37 -0
  24. OTVision/detect/rtsp_input_source.py +207 -0
  25. OTVision/detect/timestamper.py +2 -1
  26. OTVision/detect/video_input_source.py +9 -5
  27. OTVision/detect/yolo.py +17 -1
  28. OTVision/domain/cli.py +31 -1
  29. OTVision/domain/current_config.py +1 -1
  30. OTVision/domain/frame.py +6 -0
  31. OTVision/domain/object_detection.py +6 -1
  32. OTVision/domain/serialization.py +12 -0
  33. OTVision/domain/time.py +13 -0
  34. OTVision/helpers/files.py +14 -15
  35. OTVision/plugin/__init__.py +0 -0
  36. OTVision/plugin/yaml_serialization.py +20 -0
  37. OTVision/track/builder.py +132 -0
  38. OTVision/track/cli.py +128 -0
  39. OTVision/track/exporter/filebased_exporter.py +2 -1
  40. OTVision/track/id_generator.py +15 -0
  41. OTVision/track/model/track_exporter.py +2 -1
  42. OTVision/track/model/tracking_interfaces.py +6 -6
  43. OTVision/track/parser/chunk_parser_plugins.py +1 -0
  44. OTVision/track/parser/frame_group_parser_plugins.py +35 -5
  45. OTVision/track/track.py +54 -133
  46. OTVision/track/tracker/filebased_tracking.py +8 -7
  47. OTVision/track/tracker/tracker_plugin_iou.py +15 -9
  48. OTVision/transform/transform.py +2 -2
  49. OTVision/version.py +1 -1
  50. otvision-0.6.6.dist-info/METADATA +182 -0
  51. {otvision-0.6.4.dist-info → otvision-0.6.6.dist-info}/RECORD +53 -36
  52. otvision-0.6.4.dist-info/METADATA +0 -49
  53. {otvision-0.6.4.dist-info → otvision-0.6.6.dist-info}/WHEEL +0 -0
  54. {otvision-0.6.4.dist-info → otvision-0.6.6.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
@@ -62,7 +62,7 @@ class OtdetFileWriter:
62
62
  detect_config = config.detect
63
63
 
64
64
  actual_frames = len(event.frames)
65
- if (expected_duration := detect_config.expected_duration) is not None:
65
+ if expected_duration := detect_config.expected_duration:
66
66
  actual_fps = actual_frames / expected_duration.total_seconds()
67
67
  else:
68
68
  actual_fps = actual_frames / source_metadata.duration.total_seconds()
@@ -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,207 @@
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._start_value = start_value
35
+ self.__counter = start_value
36
+
37
+ def increment(self) -> None:
38
+ self.__counter += 1
39
+
40
+ def get(self) -> int:
41
+ return self.__counter
42
+
43
+ def reset(self) -> None:
44
+ self.__counter = self._start_value
45
+
46
+
47
+ class RtspInputSource(InputSourceDetect):
48
+
49
+ @property
50
+ def current_frame_number(self) -> int:
51
+ return self._frame_counter.get()
52
+
53
+ @property
54
+ def config(self) -> Config:
55
+ return self._get_current_config.get()
56
+
57
+ @property
58
+ def detect_config(self) -> DetectConfig:
59
+ return self.config.detect
60
+
61
+ @property
62
+ def stream_config(self) -> StreamConfig:
63
+ if stream_config := self.config.stream:
64
+ return stream_config
65
+ raise ValueError("Stream config not found in config")
66
+
67
+ @property
68
+ def rtsp_url(self) -> str:
69
+ return self.stream_config.source
70
+
71
+ @property
72
+ def flush_buffer_size(self) -> int:
73
+ return self.stream_config.flush_buffer_size
74
+
75
+ @property
76
+ def fps(self) -> float:
77
+ return self.config.convert.output_fps
78
+
79
+ def __init__(
80
+ self,
81
+ subject: Subject[FlushEvent],
82
+ datetime_provider: DatetimeProvider,
83
+ frame_counter: Counter,
84
+ get_current_config: GetCurrentConfig,
85
+ ) -> None:
86
+ super().__init__(subject)
87
+ self._datetime_provider = datetime_provider
88
+ self._stop_capture = False
89
+ self._frame_counter = frame_counter
90
+ self._get_current_config = get_current_config
91
+ self._current_stream: str | None = None
92
+ self._current_video_capture: VideoCapture | None = None
93
+ self._stream_start_time: datetime = self._datetime_provider.provide()
94
+ self._current_video_start_time = self._stream_start_time
95
+ self._outdated = True
96
+
97
+ @property
98
+ def _video_capture(self) -> VideoCapture:
99
+ # Property is moved below __init__ otherwise mypy is somehow unable to determine
100
+ # types of self._current_stream and self._current_video_capture
101
+ new_source = self.stream_config.source
102
+ if (
103
+ self._current_stream is not None
104
+ and self._current_stream == new_source
105
+ and self._current_video_capture
106
+ ):
107
+ # current source has not changed
108
+ return self._current_video_capture
109
+
110
+ # Stream changed or has not been initialized
111
+ if self._current_video_capture is not None:
112
+ # If the stream changed and there's an existing capture, release it
113
+ self._current_video_capture.release()
114
+
115
+ self._current_stream = new_source
116
+ self._current_video_capture = self._init_video_capture(self._current_stream)
117
+ return self._current_video_capture
118
+
119
+ def produce(self) -> Generator[Frame, None, None]:
120
+ self._stream_start_time = self._datetime_provider.provide()
121
+ self._current_video_start_time = self._stream_start_time
122
+ while not self.should_stop():
123
+ if (frame := self._read_next_frame()) is not None:
124
+ self._frame_counter.increment()
125
+ occurrence = self._datetime_provider.provide()
126
+
127
+ if self._outdated:
128
+ self._current_video_start_time = occurrence
129
+ self._outdated = False
130
+
131
+ yield Frame(
132
+ data=convert_frame_to_rgb(frame), # YOLO expects RGB
133
+ frame=self.current_frame_number,
134
+ source=self.rtsp_url,
135
+ output=self.create_output(),
136
+ occurrence=occurrence,
137
+ )
138
+ if self.flush_condition_met():
139
+ self._notify()
140
+ self._outdated = True
141
+ self._frame_counter.reset()
142
+
143
+ self._notify()
144
+
145
+ def _init_video_capture(self, source: str) -> VideoCapture:
146
+ cap = VideoCapture(source)
147
+ while not self.should_stop() and not cap.isOpened():
148
+ logger().warning(
149
+ f"Couldn't open the RTSP stream: {source}. "
150
+ f"Trying again in {RETRY_SECONDS}s..."
151
+ )
152
+ sleep(RETRY_SECONDS)
153
+ cap.release()
154
+ cap = VideoCapture(source)
155
+ return cap
156
+
157
+ def _read_next_frame(self) -> ndarray | None:
158
+ successful, frame = self._video_capture.read()
159
+ if successful:
160
+ return frame
161
+ logger().debug("Failed to grab frame")
162
+ return None
163
+
164
+ def should_stop(self) -> bool:
165
+ return self._stop_capture
166
+
167
+ def stop(self) -> None:
168
+ self._stop_capture = True
169
+
170
+ def start(self) -> None:
171
+ self._stop_capture = False
172
+
173
+ def flush_condition_met(self) -> bool:
174
+ return self.current_frame_number % self.flush_buffer_size == 0
175
+
176
+ def _notify(self) -> None:
177
+ frame_width = int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
178
+ frame_height = int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
179
+ frames = (
180
+ self.flush_buffer_size
181
+ if self.current_frame_number % self.flush_buffer_size == 0
182
+ else self.current_frame_number % self.flush_buffer_size
183
+ )
184
+ duration = timedelta(seconds=round(frames / self.fps))
185
+ output = self.create_output()
186
+ self._subject.notify(
187
+ FlushEvent.create(
188
+ source=self.rtsp_url,
189
+ output=output,
190
+ duration=duration,
191
+ source_width=frame_width,
192
+ source_height=frame_height,
193
+ source_fps=self.fps,
194
+ start_time=self._current_video_start_time,
195
+ )
196
+ )
197
+
198
+ def create_output(self) -> str:
199
+ output_filename = (
200
+ f"{self.stream_config.name}_FR{round(self.fps)}"
201
+ f"_{self._current_video_start_time.strftime(DATETIME_FORMAT)}.mp4"
202
+ )
203
+ return str(self.stream_config.save_dir / output_filename)
204
+
205
+
206
+ def convert_frame_to_rgb(frame: ndarray) -> ndarray:
207
+ 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
@@ -61,6 +61,7 @@ class VideoTimestamper(Timestamper):
61
61
  data=frame[FrameKeys.data],
62
62
  frame=frame[FrameKeys.frame],
63
63
  source=frame[FrameKeys.source],
64
+ output=frame[FrameKeys.output],
64
65
  occurrence=occurrence,
65
66
  )
66
67
 
@@ -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
@@ -116,6 +116,7 @@ class VideoSource(InputSourceDetect):
116
116
  FrameKeys.data: rotated_image,
117
117
  FrameKeys.frame: frame_number,
118
118
  FrameKeys.source: str(video_file),
119
+ FrameKeys.output: str(video_file),
119
120
  }
120
121
  )
121
122
  else:
@@ -124,6 +125,7 @@ class VideoSource(InputSourceDetect):
124
125
  FrameKeys.data: None,
125
126
  FrameKeys.frame: frame_number,
126
127
  FrameKeys.source: str(video_file),
128
+ FrameKeys.output: str(video_file),
127
129
  }
128
130
  )
129
131
  counter += 1
@@ -181,6 +183,7 @@ class VideoSource(InputSourceDetect):
181
183
  self._subject.notify(
182
184
  FlushEvent.create(
183
185
  source=str(current_video_file),
186
+ output=str(current_video_file),
184
187
  duration=duration,
185
188
  source_height=height,
186
189
  source_width=width,
@@ -205,8 +208,9 @@ class VideoSource(InputSourceDetect):
205
208
  def __add_occurrence(self, timestamper: Timestamper, frame: dict) -> Frame:
206
209
  updated = timestamper.stamp(frame)
207
210
  return Frame(
208
- data=updated["data"],
209
- frame=updated["frame"],
210
- source=updated["source"],
211
- occurrence=updated["occurrence"],
211
+ data=updated[FrameKeys.data],
212
+ frame=updated[FrameKeys.frame],
213
+ source=updated[FrameKeys.source],
214
+ output=updated[FrameKeys.output],
215
+ occurrence=updated[FrameKeys.occurrence],
212
216
  )
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: