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
@@ -3,8 +3,7 @@ from argparse import ArgumentParser
3
3
  from functools import cached_property
4
4
 
5
5
  from OTVision.abstraction.observer import Subject
6
- from OTVision.application.buffer import Buffer
7
- from OTVision.application.config import Config
6
+ from OTVision.application.config import Config, DetectConfig
8
7
  from OTVision.application.config_parser import ConfigParser
9
8
  from OTVision.application.configure_logger import ConfigureLogger
10
9
  from OTVision.application.detect.current_object_detector import CurrentObjectDetector
@@ -12,12 +11,6 @@ from OTVision.application.detect.current_object_detector_metadata import (
12
11
  CurrentObjectDetectorMetadata,
13
12
  )
14
13
  from OTVision.application.detect.detected_frame_factory import DetectedFrameFactory
15
- from OTVision.application.detect.detected_frame_producer import (
16
- SimpleDetectedFrameProducer,
17
- )
18
- from OTVision.application.detect.detection_file_save_path_provider import (
19
- DetectionFileSavePathProvider,
20
- )
21
14
  from OTVision.application.detect.factory import ObjectDetectorCachedFactory
22
15
  from OTVision.application.detect.get_detect_cli_args import GetDetectCliArgs
23
16
  from OTVision.application.detect.update_detect_config_with_cli_args import (
@@ -26,16 +19,20 @@ from OTVision.application.detect.update_detect_config_with_cli_args import (
26
19
  from OTVision.application.frame_count_provider import FrameCountProvider
27
20
  from OTVision.application.get_config import GetConfig
28
21
  from OTVision.application.get_current_config import GetCurrentConfig
22
+ from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
29
23
  from OTVision.application.update_current_config import UpdateCurrentConfig
30
24
  from OTVision.detect.cli import ArgparseDetectCliParser
31
25
  from OTVision.detect.detect import OTVisionVideoDetect
32
26
  from OTVision.detect.detected_frame_buffer import (
33
27
  DetectedFrameBuffer,
34
28
  DetectedFrameBufferEvent,
35
- FlushEvent,
36
29
  )
37
- from OTVision.detect.otdet import OtdetBuilder
38
- from OTVision.detect.otdet_file_writer import OtdetFileWriter
30
+ from OTVision.detect.detected_frame_producer import (
31
+ DetectedFrameProducerFactory,
32
+ SimpleDetectedFrameProducer,
33
+ )
34
+ from OTVision.detect.otdet import OtdetBuilder, OtdetMetadataBuilder
35
+ from OTVision.detect.otdet_file_writer import OtdetFileWriter, OtdetFileWrittenEvent
39
36
  from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
40
37
  from OTVision.detect.pyav_frame_count_provider import PyAVFrameCountProvider
41
38
  from OTVision.detect.timestamper import TimestamperFactory
@@ -43,10 +40,10 @@ from OTVision.detect.yolo import YoloDetectionConverter, YoloFactory
43
40
  from OTVision.domain.cli import DetectCliParser
44
41
  from OTVision.domain.current_config import CurrentConfig
45
42
  from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
46
- from OTVision.domain.frame import DetectedFrame
47
43
  from OTVision.domain.input_source_detect import InputSourceDetect
48
44
  from OTVision.domain.object_detection import ObjectDetectorFactory
49
45
  from OTVision.domain.serialization import Deserializer
46
+ from OTVision.domain.video_writer import VideoWriter
50
47
  from OTVision.plugin.yaml_serialization import YamlDeserializer
51
48
 
52
49
 
@@ -75,7 +72,7 @@ class DetectBuilder(ABC):
75
72
 
76
73
  @cached_property
77
74
  def otdet_builder(self) -> OtdetBuilder:
78
- return OtdetBuilder()
75
+ return OtdetBuilder(OtdetMetadataBuilder())
79
76
 
80
77
  @cached_property
81
78
  def object_detector_factory(self) -> ObjectDetectorFactory:
@@ -116,8 +113,8 @@ class DetectBuilder(ABC):
116
113
  return TimestamperFactory(self.frame_count_provider, self.get_current_config)
117
114
 
118
115
  @cached_property
119
- def detection_file_save_path_provider(self) -> DetectionFileSavePathProvider:
120
- return DetectionFileSavePathProvider(self.get_current_config)
116
+ def detection_file_save_path_provider(self) -> OtvisionSavePathProvider:
117
+ return OtvisionSavePathProvider(self.get_current_config)
121
118
 
122
119
  @cached_property
123
120
  def frame_count_provider(self) -> FrameCountProvider:
@@ -126,6 +123,7 @@ class DetectBuilder(ABC):
126
123
  @cached_property
127
124
  def otdet_file_writer(self) -> OtdetFileWriter:
128
125
  return OtdetFileWriter(
126
+ subject=Subject[OtdetFileWrittenEvent](),
129
127
  builder=self.otdet_builder,
130
128
  get_current_config=self.get_current_config,
131
129
  current_object_detector_metadata=self.current_object_detector_metadata,
@@ -144,17 +142,23 @@ class DetectBuilder(ABC):
144
142
  )
145
143
 
146
144
  @cached_property
147
- def detected_frame_buffer(
148
- self,
149
- ) -> Buffer[DetectedFrame, DetectedFrameBufferEvent, FlushEvent]:
145
+ def detected_frame_buffer(self) -> DetectedFrameBuffer:
150
146
  return DetectedFrameBuffer(subject=Subject[DetectedFrameBufferEvent]())
151
147
 
152
148
  @cached_property
153
149
  def detected_frame_producer(self) -> DetectedFrameProducer:
154
150
  return SimpleDetectedFrameProducer(
151
+ producer_factory=self.detected_frame_producer_factory,
152
+ )
153
+
154
+ @cached_property
155
+ def detected_frame_producer_factory(self) -> DetectedFrameProducerFactory:
156
+ return DetectedFrameProducerFactory(
155
157
  input_source=self.input_source,
158
+ video_writer_filter=self.video_file_writer,
156
159
  detection_filter=self.current_object_detector,
157
160
  detected_frame_buffer=self.detected_frame_buffer,
161
+ get_current_config=self.get_current_config,
158
162
  )
159
163
 
160
164
  @cached_property
@@ -165,6 +169,10 @@ class DetectBuilder(ABC):
165
169
  def yaml_deserializer(self) -> Deserializer:
166
170
  return YamlDeserializer()
167
171
 
172
+ @property
173
+ def detect_config(self) -> DetectConfig:
174
+ return self.current_config.get().detect
175
+
168
176
  def __init__(self, argv: list[str] | None = None) -> None:
169
177
  self.argv = argv
170
178
 
@@ -173,6 +181,15 @@ class DetectBuilder(ABC):
173
181
  def input_source(self) -> InputSourceDetect:
174
182
  raise NotImplementedError
175
183
 
184
+ @property
185
+ @abstractmethod
186
+ def video_file_writer(self) -> VideoWriter:
187
+ raise NotImplementedError
188
+
189
+ @abstractmethod
190
+ def register_observers(self) -> None:
191
+ raise NotImplementedError
192
+
176
193
  def build(self) -> OTVisionVideoDetect:
177
194
  self.register_observers()
178
195
  self._preload_object_detection_model()
@@ -181,7 +198,3 @@ class DetectBuilder(ABC):
181
198
  def _preload_object_detection_model(self) -> None:
182
199
  model = self.current_object_detector.get()
183
200
  model.preload()
184
-
185
- def register_observers(self) -> None:
186
- self.input_source.register(self.detected_frame_buffer.on_flush)
187
- self.detected_frame_buffer.register(self.otdet_file_writer.write)
OTVision/detect/cli.py CHANGED
@@ -6,6 +6,11 @@ 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
9
+ from OTVision.plugin.ffmpeg_video_writer import (
10
+ ConstantRateFactor,
11
+ EncodingSpeed,
12
+ VideoCodec,
13
+ )
9
14
 
10
15
 
11
16
  class ArgparseDetectCliParser(DetectCliParser):
@@ -128,6 +133,34 @@ class ArgparseDetectCliParser(DetectCliParser):
128
133
  help="Specify end of detection in seconds.",
129
134
  required=False,
130
135
  )
136
+ self._parser.add_argument(
137
+ "--write-video",
138
+ default=None,
139
+ action="store_true",
140
+ help="Write video to output folder. Not supported on Windows.",
141
+ required=False,
142
+ )
143
+ self._parser.add_argument(
144
+ "--video-codec",
145
+ default=None,
146
+ choices=VideoCodec.as_list(),
147
+ help="Video codec for video writer. Default is 'libx264'",
148
+ required=False,
149
+ )
150
+ self._parser.add_argument(
151
+ "--encoding-speed",
152
+ default=None,
153
+ choices=EncodingSpeed.as_list(),
154
+ help="Encoding speed for video writer. Default is 'fast'",
155
+ required=False,
156
+ )
157
+ self._parser.add_argument(
158
+ "--crf",
159
+ default=None,
160
+ choices=ConstantRateFactor.as_list(),
161
+ help="Constant rate factor for video writer. Default is 'DEFAULT'",
162
+ required=False,
163
+ )
131
164
 
132
165
  def parse(self) -> DetectCliArgs:
133
166
  args = self._parser.parse_args(self._argv)
@@ -151,11 +184,21 @@ class ArgparseDetectCliParser(DetectCliParser):
151
184
  detect_start=(
152
185
  int(args.detect_start) if args.detect_start is not None else None
153
186
  ),
154
- detect_end=int(args.detect_end) if args.detect_end is not None else None,
187
+ detect_end=(int(args.detect_end) if args.detect_end is not None else None),
155
188
  logfile=Path(args.logfile),
156
189
  log_level_console=args.log_level_console,
157
190
  log_level_file=args.log_level_file,
158
191
  logfile_overwrite=args.logfile_overwrite,
192
+ write_video=args.write_video,
193
+ video_codec=(
194
+ VideoCodec(args.video_codec) if args.video_codec is not None else None
195
+ ),
196
+ encoding_speed=(
197
+ EncodingSpeed(args.encoding_speed)
198
+ if args.encoding_speed is not None
199
+ else None
200
+ ),
201
+ crf=ConstantRateFactor[args.crf] if args.crf is not None else None,
159
202
  )
160
203
 
161
204
  def _parse_start_time(self, start_time: str | None) -> datetime | None:
@@ -1,6 +1,7 @@
1
1
  from dataclasses import dataclass
2
2
  from datetime import datetime, timedelta
3
3
 
4
+ from OTVision.abstraction.observer import Observable, Subject
4
5
  from OTVision.application.buffer import Buffer
5
6
  from OTVision.domain.frame import DetectedFrame
6
7
 
@@ -49,7 +50,18 @@ class DetectedFrameBufferEvent:
49
50
  frames: list[DetectedFrame]
50
51
 
51
52
 
52
- class DetectedFrameBuffer(Buffer[DetectedFrame, DetectedFrameBufferEvent, FlushEvent]):
53
+ class DetectedFrameBuffer(
54
+ Buffer[DetectedFrame, FlushEvent], Observable[DetectedFrameBufferEvent]
55
+ ):
56
+ def __init__(self, subject: Subject[DetectedFrameBufferEvent]) -> None:
57
+ Buffer.__init__(self)
58
+ Observable.__init__(self, subject)
59
+
60
+ def on_flush(self, event: FlushEvent) -> None:
61
+ buffered_elements = self._get_buffered_elements()
62
+ self._notify_observers(buffered_elements, event)
63
+ self._reset_buffer()
64
+
53
65
  def _notify_observers(
54
66
  self, elements: list[DetectedFrame], event: FlushEvent
55
67
  ) -> None:
@@ -0,0 +1,14 @@
1
+ from typing import Iterator
2
+
3
+ from OTVision.detect.detected_frame_producer_factory import DetectedFrameProducerFactory
4
+ from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
5
+ from OTVision.domain.frame import DetectedFrame
6
+
7
+
8
+ class SimpleDetectedFrameProducer(DetectedFrameProducer):
9
+
10
+ def __init__(self, producer_factory: DetectedFrameProducerFactory) -> None:
11
+ self._producer_factory = producer_factory
12
+
13
+ def produce(self) -> Iterator[DetectedFrame]:
14
+ return self._producer_factory.create()
@@ -0,0 +1,39 @@
1
+ from typing import Iterator
2
+
3
+ from OTVision.abstraction.pipes_and_filter import Filter
4
+ from OTVision.application.get_current_config import GetCurrentConfig
5
+ from OTVision.domain.frame import DetectedFrame, Frame
6
+ from OTVision.domain.input_source_detect import InputSourceDetect
7
+
8
+
9
+ class DetectedFrameProducerFactory:
10
+ def __init__(
11
+ self,
12
+ input_source: InputSourceDetect,
13
+ video_writer_filter: Filter[Frame, Frame],
14
+ detection_filter: Filter[Frame, DetectedFrame],
15
+ detected_frame_buffer: Filter[DetectedFrame, DetectedFrame],
16
+ get_current_config: GetCurrentConfig,
17
+ ) -> None:
18
+ self._input_source = input_source
19
+ self._video_writer_filter = video_writer_filter
20
+ self._detection_filter = detection_filter
21
+ self._detected_frame_buffer = detected_frame_buffer
22
+ self._get_current_config = get_current_config
23
+
24
+ def create(self) -> Iterator[DetectedFrame]:
25
+ if self._get_current_config.get().detect.write_video:
26
+ return self.__create_with_video_writer()
27
+ return self.__create_without_video_writer()
28
+
29
+ def __create_without_video_writer(self) -> Iterator[DetectedFrame]:
30
+ return self._detected_frame_buffer.filter(
31
+ self._detection_filter.filter(self._input_source.produce())
32
+ )
33
+
34
+ def __create_with_video_writer(self) -> Iterator[DetectedFrame]:
35
+ return self._detected_frame_buffer.filter(
36
+ self._detection_filter.filter(
37
+ self._video_writer_filter.filter(self._input_source.produce())
38
+ )
39
+ )
@@ -1,19 +1,55 @@
1
1
  from functools import cached_property
2
2
 
3
3
  from OTVision.abstraction.observer import Subject
4
+ from OTVision.application.event.new_video_start import NewVideoStartEvent
4
5
  from OTVision.detect.builder import DetectBuilder
5
6
  from OTVision.detect.detected_frame_buffer import FlushEvent
6
7
  from OTVision.detect.video_input_source import VideoSource
7
- from OTVision.domain.input_source_detect import InputSourceDetect
8
+ from OTVision.domain.video_writer import VideoWriter
9
+ from OTVision.plugin.ffmpeg_video_writer import (
10
+ FfmpegVideoWriter,
11
+ PixelFormat,
12
+ VideoFormat,
13
+ append_save_suffix_to_save_location,
14
+ )
8
15
 
9
16
 
10
17
  class FileBasedDetectBuilder(DetectBuilder):
18
+
11
19
  @cached_property
12
- def input_source(self) -> InputSourceDetect:
20
+ def input_source(self) -> VideoSource:
13
21
  return VideoSource(
14
- subject=Subject[FlushEvent](),
22
+ subject_flush=Subject[FlushEvent](),
23
+ subject_new_video_start=Subject[NewVideoStartEvent](),
15
24
  get_current_config=self.get_current_config,
16
25
  frame_rotator=self.frame_rotator,
17
26
  timestamper_factory=self.timestamper_factory,
18
27
  save_path_provider=self.detection_file_save_path_provider,
19
28
  )
29
+
30
+ @cached_property
31
+ def video_file_writer(self) -> VideoWriter:
32
+ # Using save_location_strategy=keep_original_save_location is not supported for
33
+ # file-based detection. Otherwise, we would be overwriting the input source that
34
+ # we are reading from.
35
+ return FfmpegVideoWriter(
36
+ save_location_strategy=append_save_suffix_to_save_location,
37
+ encoding_speed=self.detect_config.encoding_speed,
38
+ input_format=VideoFormat.RAW,
39
+ output_format=VideoFormat.MP4,
40
+ input_pixel_format=PixelFormat.RGB24,
41
+ output_pixel_format=PixelFormat.YUV420P,
42
+ output_video_codec=self.detect_config.video_codec,
43
+ constant_rate_factor=self.detect_config.crf,
44
+ )
45
+
46
+ def register_observers(self) -> None:
47
+ if self.detect_config.write_video:
48
+ self.input_source.subject_new_video_start.register(
49
+ self.video_file_writer.notify_on_new_video_start
50
+ )
51
+ self.input_source.subject_flush.register(
52
+ self.video_file_writer.notify_on_flush_event
53
+ )
54
+ self.input_source.subject_flush.register(self.detected_frame_buffer.on_flush)
55
+ self.detected_frame_buffer.register(self.otdet_file_writer.write)
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)