OTVision 0.5.5__py3-none-any.whl → 0.6.0__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 (42) hide show
  1. OTVision/abstraction/__init__.py +0 -0
  2. OTVision/abstraction/observer.py +43 -0
  3. OTVision/abstraction/pipes_and_filter.py +28 -0
  4. OTVision/application/buffer.py +34 -0
  5. OTVision/application/detect/current_object_detector.py +40 -0
  6. OTVision/application/detect/current_object_detector_metadata.py +27 -0
  7. OTVision/application/detect/detected_frame_factory.py +26 -0
  8. OTVision/application/detect/detected_frame_producer.py +25 -0
  9. OTVision/application/detect/detection_file_save_path_provider.py +59 -0
  10. OTVision/application/detect/factory.py +25 -0
  11. OTVision/application/detect/timestamper.py +25 -0
  12. OTVision/application/detect/update_detect_config_with_cli_args.py +5 -0
  13. OTVision/application/frame_count_provider.py +23 -0
  14. OTVision/application/get_current_config.py +10 -0
  15. OTVision/application/update_current_config.py +10 -0
  16. OTVision/config.py +70 -1
  17. OTVision/detect/builder.py +133 -1
  18. OTVision/detect/cli.py +16 -1
  19. OTVision/detect/detect.py +11 -280
  20. OTVision/detect/detected_frame_buffer.py +57 -0
  21. OTVision/detect/otdet.py +36 -15
  22. OTVision/detect/otdet_file_writer.py +106 -0
  23. OTVision/detect/pyav_frame_count_provider.py +15 -0
  24. OTVision/detect/timestamper.py +152 -0
  25. OTVision/detect/video_input_source.py +213 -0
  26. OTVision/detect/yolo.py +198 -205
  27. OTVision/domain/cli.py +2 -1
  28. OTVision/domain/current_config.py +12 -0
  29. OTVision/domain/detect_producer_consumer.py +30 -0
  30. OTVision/domain/detection.py +20 -0
  31. OTVision/domain/frame.py +29 -0
  32. OTVision/domain/input_source_detect.py +31 -0
  33. OTVision/domain/object_detection.py +47 -0
  34. OTVision/helpers/files.py +1 -1
  35. OTVision/helpers/video.py +6 -0
  36. OTVision/track/preprocess.py +4 -2
  37. OTVision/version.py +1 -1
  38. {otvision-0.5.5.dist-info → otvision-0.6.0.dist-info}/METADATA +1 -1
  39. otvision-0.6.0.dist-info/RECORD +75 -0
  40. otvision-0.5.5.dist-info/RECORD +0 -50
  41. {otvision-0.5.5.dist-info → otvision-0.6.0.dist-info}/WHEEL +0 -0
  42. {otvision-0.5.5.dist-info → otvision-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,16 +1,50 @@
1
1
  from argparse import ArgumentParser
2
2
  from functools import cached_property
3
3
 
4
+ from OTVision.abstraction.observer import Subject
5
+ from OTVision.application.buffer import Buffer
4
6
  from OTVision.application.configure_logger import ConfigureLogger
7
+ from OTVision.application.detect.current_object_detector import CurrentObjectDetector
8
+ from OTVision.application.detect.current_object_detector_metadata import (
9
+ CurrentObjectDetectorMetadata,
10
+ )
11
+ from OTVision.application.detect.detected_frame_factory import DetectedFrameFactory
12
+ from OTVision.application.detect.detected_frame_producer import (
13
+ SimpleDetectedFrameProducer,
14
+ )
15
+ from OTVision.application.detect.detection_file_save_path_provider import (
16
+ DetectionFileSavePathProvider,
17
+ )
18
+ from OTVision.application.detect.factory import ObjectDetectorCachedFactory
5
19
  from OTVision.application.detect.get_detect_cli_args import GetDetectCliArgs
6
20
  from OTVision.application.detect.update_detect_config_with_cli_args import (
7
21
  UpdateDetectConfigWithCliArgs,
8
22
  )
23
+ from OTVision.application.frame_count_provider import FrameCountProvider
9
24
  from OTVision.application.get_config import GetConfig
10
- from OTVision.config import ConfigParser
25
+ from OTVision.application.get_current_config import GetCurrentConfig
26
+ from OTVision.application.update_current_config import UpdateCurrentConfig
27
+ from OTVision.config import Config, ConfigParser
11
28
  from OTVision.detect.cli import ArgparseDetectCliParser
29
+ from OTVision.detect.detect import OTVisionVideoDetect
30
+ from OTVision.detect.detected_frame_buffer import (
31
+ DetectedFrameBuffer,
32
+ DetectedFrameBufferEvent,
33
+ FlushEvent,
34
+ )
12
35
  from OTVision.detect.otdet import OtdetBuilder
36
+ from OTVision.detect.otdet_file_writer import OtdetFileWriter
37
+ from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
38
+ from OTVision.detect.pyav_frame_count_provider import PyAVFrameCountProvider
39
+ from OTVision.detect.timestamper import TimestamperFactory
40
+ from OTVision.detect.video_input_source import VideoSource
41
+ from OTVision.detect.yolo import YoloDetectionConverter, YoloFactory
13
42
  from OTVision.domain.cli import DetectCliParser
43
+ from OTVision.domain.current_config import CurrentConfig
44
+ from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
45
+ from OTVision.domain.detection import DetectedFrame
46
+ from OTVision.domain.input_source_detect import InputSourceDetect
47
+ from OTVision.domain.object_detection import ObjectDetectorFactory
14
48
 
15
49
 
16
50
  class DetectBuilder:
@@ -44,5 +78,103 @@ class DetectBuilder:
44
78
  def otdet_builder(self) -> OtdetBuilder:
45
79
  return OtdetBuilder()
46
80
 
81
+ @cached_property
82
+ def object_detector_factory(self) -> ObjectDetectorFactory:
83
+ return ObjectDetectorCachedFactory(
84
+ YoloFactory(
85
+ get_current_config=self.get_current_config,
86
+ detection_converter=self.detection_converter,
87
+ detected_frame_factory=self.frame_converter,
88
+ )
89
+ )
90
+
91
+ @cached_property
92
+ def detection_converter(self) -> YoloDetectionConverter:
93
+ return YoloDetectionConverter()
94
+
95
+ @cached_property
96
+ def frame_converter(self) -> DetectedFrameFactory:
97
+ return DetectedFrameFactory()
98
+
99
+ @cached_property
100
+ def current_config(self) -> CurrentConfig:
101
+ return CurrentConfig(Config())
102
+
103
+ @cached_property
104
+ def get_current_config(self) -> GetCurrentConfig:
105
+ return GetCurrentConfig(self.current_config)
106
+
107
+ @cached_property
108
+ def update_current_config(self) -> UpdateCurrentConfig:
109
+ return UpdateCurrentConfig(self.current_config)
110
+
47
111
  def __init__(self, argv: list[str] | None = None) -> None:
48
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
+ @cached_property
125
+ def frame_rotator(self) -> AvVideoFrameRotator:
126
+ return AvVideoFrameRotator()
127
+
128
+ @cached_property
129
+ def timestamper_factory(self) -> TimestamperFactory:
130
+ return TimestamperFactory(self.frame_count_provider, self.get_current_config)
131
+
132
+ @cached_property
133
+ def detection_file_save_path_provider(self) -> DetectionFileSavePathProvider:
134
+ return DetectionFileSavePathProvider(self.get_current_config)
135
+
136
+ @cached_property
137
+ def frame_count_provider(self) -> FrameCountProvider:
138
+ return PyAVFrameCountProvider()
139
+
140
+ @cached_property
141
+ def otdet_file_writer(self) -> OtdetFileWriter:
142
+ return OtdetFileWriter(
143
+ builder=self.otdet_builder,
144
+ get_current_config=self.get_current_config,
145
+ current_object_detector_metadata=self.current_object_detector_metadata,
146
+ save_path_provider=self.detection_file_save_path_provider,
147
+ )
148
+
149
+ @cached_property
150
+ def current_object_detector_metadata(self) -> CurrentObjectDetectorMetadata:
151
+ return CurrentObjectDetectorMetadata(self.current_object_detector)
152
+
153
+ @cached_property
154
+ def current_object_detector(self) -> CurrentObjectDetector:
155
+ return CurrentObjectDetector(
156
+ get_current_config=self.get_current_config,
157
+ factory=self.object_detector_factory,
158
+ )
159
+
160
+ @cached_property
161
+ def detected_frame_buffer(
162
+ self,
163
+ ) -> Buffer[DetectedFrame, DetectedFrameBufferEvent, FlushEvent]:
164
+ return DetectedFrameBuffer(subject=Subject[DetectedFrameBufferEvent]())
165
+
166
+ @cached_property
167
+ def detected_frame_producer(self) -> DetectedFrameProducer:
168
+ return SimpleDetectedFrameProducer(
169
+ input_source=self.input_source,
170
+ detection_filter=self.current_object_detector,
171
+ detected_frame_buffer=self.detected_frame_buffer,
172
+ )
173
+
174
+ def build(self) -> OTVisionVideoDetect:
175
+ self.register_observers()
176
+ return OTVisionVideoDetect(self.detected_frame_producer)
177
+
178
+ def register_observers(self) -> None:
179
+ self.input_source.register(self.detected_frame_buffer.on_flush)
180
+ self.detected_frame_buffer.register(self.otdet_file_writer.write)
OTVision/detect/cli.py CHANGED
@@ -1,7 +1,8 @@
1
1
  from argparse import ArgumentParser, BooleanOptionalAction, Namespace
2
- from datetime import timedelta
2
+ from datetime import datetime, timedelta
3
3
  from pathlib import Path
4
4
 
5
+ from OTVision.config import DATETIME_FORMAT
5
6
  from OTVision.domain.cli import CliParseError, DetectCliArgs, DetectCliParser
6
7
  from OTVision.helpers.files import check_if_all_paths_exist
7
8
  from OTVision.helpers.log import DEFAULT_LOG_FILE, VALID_LOG_LEVELS
@@ -105,6 +106,14 @@ class ArgparseDetectCliParser(DetectCliParser):
105
106
  help="Overwrite log file if it already exists.",
106
107
  required=False,
107
108
  )
109
+ self._parser.add_argument(
110
+ "--start-time",
111
+ default=None,
112
+ type=str,
113
+ help=f"Specify start date and time of the recording in format "
114
+ f"{DATETIME_FORMAT}.",
115
+ required=False,
116
+ )
108
117
  self._parser.add_argument(
109
118
  "--detect-start",
110
119
  default=None,
@@ -138,6 +147,7 @@ class ArgparseDetectCliParser(DetectCliParser):
138
147
  ),
139
148
  half=bool(args.half) if args.half else None,
140
149
  overwrite=args.overwrite,
150
+ start_time=self._parse_start_time(args.start_time),
141
151
  detect_start=(
142
152
  int(args.detect_start) if args.detect_start is not None else None
143
153
  ),
@@ -148,6 +158,11 @@ class ArgparseDetectCliParser(DetectCliParser):
148
158
  logfile_overwrite=args.logfile_overwrite,
149
159
  )
150
160
 
161
+ def _parse_start_time(self, start_time: str | None) -> datetime | None:
162
+ if start_time is None:
163
+ return None
164
+ return datetime.strptime(start_time, DATETIME_FORMAT)
165
+
151
166
  def __assert_cli_args_valid(self, args: Namespace) -> None:
152
167
  if args.paths is None and args.config is None:
153
168
  raise CliParseError(
OTVision/detect/detect.py CHANGED
@@ -19,290 +19,21 @@ OTVision main module to detect objects in single or multiple images or videos.
19
19
  # You should have received a copy of the GNU General Public License
20
20
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
21
21
 
22
- import logging
23
- import re
24
- from datetime import datetime, timedelta, timezone
25
- from pathlib import Path
26
22
 
27
- from tqdm import tqdm
28
-
29
- from OTVision.config import Config
30
- from OTVision.dataformat import DATA, LENGTH, METADATA, RECORDED_START_DATE, VIDEO
31
- from OTVision.detect.otdet import OtdetBuilder, OtdetBuilderConfig
32
- from OTVision.detect.yolo import create_model
33
- from OTVision.helpers.date import parse_date_string_to_utc_datime
34
- from OTVision.helpers.files import (
35
- FILE_NAME_PATTERN,
36
- START_DATE,
37
- InproperFormattedFilename,
38
- get_files,
39
- write_json,
23
+ from OTVision.domain.detect_producer_consumer import (
24
+ DetectedFrameConsumer,
25
+ DetectedFrameProducer,
40
26
  )
41
- from OTVision.helpers.log import LOGGER_NAME
42
- from OTVision.helpers.video import get_duration, get_fps, get_video_dimensions
43
- from OTVision.track.preprocess import OCCURRENCE
44
-
45
- log = logging.getLogger(LOGGER_NAME)
46
- DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S"
47
-
48
27
 
49
- class OTVisionDetect:
50
- @property
51
- def config(self) -> Config:
52
- if self._config is None:
53
- raise ValueError("Config is missing!")
54
- return self._config
55
28
 
56
- def __init__(self, otdet_builder: OtdetBuilder) -> None:
57
- self._config: Config | None = None
58
- self._otdet_builder = otdet_builder
59
-
60
- def update_config(self, config: Config) -> None:
61
- self._config = config
29
+ class OTVisionVideoDetect(DetectedFrameConsumer):
30
+ def __init__(self, producer: DetectedFrameProducer) -> None:
31
+ self._producer = producer
62
32
 
63
33
  def start(self) -> None:
64
- """Starts the detection of objects in multiple videos and/or images.
65
-
66
- Writes detections to one file per video/object.
67
-
68
- """
69
- filetypes = self.config.filetypes.video_filetypes.to_list()
70
- video_files = get_files(paths=self.config.detect.paths, filetypes=filetypes)
71
-
72
- start_msg = f"Start detection of {len(video_files)} video files"
73
- log.info(start_msg)
74
- print(start_msg)
75
-
76
- if not video_files:
77
- log.warning(f"No videos of type '{filetypes}' found to detect!")
78
- return
79
-
80
- model = create_model(
81
- weights=self.config.detect.yolo_config.weights,
82
- confidence=self.config.detect.yolo_config.conf,
83
- iou=self.config.detect.yolo_config.iou,
84
- img_size=self.config.detect.yolo_config.img_size,
85
- half_precision=self.config.detect.half_precision,
86
- normalized=self.config.detect.yolo_config.normalized,
87
- )
88
- for video_file in tqdm(video_files, desc="Detected video files", unit=" files"):
89
- detections_file = derive_filename(
90
- video_file=video_file,
91
- detect_start=self.config.detect.detect_start,
92
- detect_end=self.config.detect.detect_end,
93
- detect_suffix=self.config.filetypes.detect,
94
- )
95
-
96
- try:
97
- parse_start_time_from(video_file)
98
- except InproperFormattedFilename:
99
- log.warning(
100
- f"Video file name of '{video_file}' must include date "
101
- f"and time in format: {DATETIME_FORMAT}"
102
- )
103
- continue
104
-
105
- if not self.config.detect.overwrite and detections_file.is_file():
106
- log.warning(
107
- f"{detections_file} already exists. To overwrite, set overwrite "
108
- "to True"
109
- )
110
- continue
111
-
112
- log.info(f"Detect {video_file}")
113
-
114
- video_fps = get_fps(video_file)
115
- detect_start_in_frames = convert_seconds_to_frames(
116
- self.config.detect.detect_start, video_fps
117
- )
118
- detect_end_in_frames = convert_seconds_to_frames(
119
- self.config.detect.detect_end, video_fps
120
- )
121
- detections = model.detect(
122
- file=video_file,
123
- detect_start=detect_start_in_frames,
124
- detect_end=detect_end_in_frames,
125
- )
126
-
127
- video_width, video_height = get_video_dimensions(video_file)
128
- actual_duration = get_duration(video_file)
129
- actual_frames = len(detections)
130
- if (expected_duration := self.config.detect.expected_duration) is not None:
131
- actual_fps = actual_frames / expected_duration.total_seconds()
132
- else:
133
- actual_fps = actual_frames / actual_duration.total_seconds()
134
- otdet = self._otdet_builder.add_config(
135
- OtdetBuilderConfig(
136
- conf=model.confidence,
137
- iou=model.iou,
138
- video=video_file,
139
- video_width=video_width,
140
- video_height=video_height,
141
- expected_duration=expected_duration,
142
- recorded_fps=video_fps,
143
- actual_fps=actual_fps,
144
- actual_frames=actual_frames,
145
- detection_img_size=model.img_size,
146
- normalized=model.normalized,
147
- detection_model=model.weights,
148
- half_precision=model.half_precision,
149
- chunksize=1,
150
- classifications=model.classifications,
151
- detect_start=self.config.detect.detect_start,
152
- detect_end=self.config.detect.detect_end,
153
- )
154
- ).build(detections)
155
-
156
- stamped_detections = add_timestamps(otdet, video_file, expected_duration)
157
- write_json(
158
- stamped_detections,
159
- file=detections_file,
160
- filetype=self.config.filetypes.detect,
161
- overwrite=self.config.detect.overwrite,
162
- )
163
-
164
- log.info(f"Successfully detected and wrote {detections_file}")
165
-
166
- finished_msg = "Finished detection"
167
- log.info(finished_msg)
168
- print(finished_msg)
169
-
170
-
171
- def derive_filename(
172
- video_file: Path,
173
- detect_suffix: str,
174
- detect_start: int | None = None,
175
- detect_end: int | None = None,
176
- ) -> Path:
177
- """
178
- Generates a filename for detection files by appending specified start and end
179
- markers and a suffix to the stem of the input video file.
180
-
181
- Args:
182
- video_file (Path): The input video file whose filename is to be modified.
183
- detect_start (int | None): The starting marker to append to the filename.
184
- If None, no starting marker will be appended.
185
- detect_end (int | None): The ending marker to append to the filename. If None,
186
- no ending marker will be appended.
187
- detect_suffix (str): The file suffix to apply to the derived filename.
188
-
189
- Returns:
190
- Path: The modified video file path with the updated stem and suffix applied.
191
- """
192
- cutout = ""
193
- if detect_start is not None:
194
- cutout += f"_start_{detect_start}"
195
- if detect_end is not None:
196
- cutout += f"_end_{detect_end}"
197
- new_stem = f"{video_file.stem}{cutout}"
198
- return video_file.with_stem(new_stem).with_suffix(detect_suffix)
199
-
200
-
201
- def convert_seconds_to_frames(seconds: int | None, fps: float) -> int | None:
202
- if seconds is None:
203
- return None
204
- return round(seconds * fps)
205
-
206
-
207
- class FormatNotSupportedError(Exception):
208
- pass
209
-
210
-
211
- def add_timestamps(
212
- detections: dict, video_file: Path, expected_duration: timedelta | None
213
- ) -> dict:
214
- return Timestamper().stamp(detections, video_file, expected_duration)
215
-
216
-
217
- class Timestamper:
218
- def stamp(
219
- self, detections: dict, video_file: Path, expected_duration: timedelta | None
220
- ) -> dict:
221
- """This method adds timestamps when the frame occurred in real time to each
222
- frame.
223
-
224
- Args:
225
- detections (dict): dictionary containing all frames
226
- video_file (Path): path to video file
227
- expected_duration (timedelta | None): expected duration of the video used to
228
- calculate the number of actual frames per second
229
-
230
- Returns:
231
- dict: input dictionary with additional occurrence per frame
232
- """
233
- start_time = parse_start_time_from(video_file)
234
- actual_duration = get_duration(video_file)
235
- if expected_duration:
236
- time_per_frame = self._get_time_per_frame(detections, expected_duration)
237
- else:
238
- time_per_frame = self._get_time_per_frame(detections, actual_duration)
239
- self._update_metadata(detections, start_time, actual_duration)
240
- return self._stamp(detections, start_time, time_per_frame)
241
-
242
- @staticmethod
243
- def _get_time_per_frame(detections: dict, duration: timedelta) -> timedelta:
244
- """Calculates the duration for each frame. This is done using the total
245
- duration of the video and the number of frames.
246
-
247
- Args:
248
- detections (dict): dictionary containing all frames
249
- video_file (Path): path to video file
250
-
251
- Returns:
252
- timedelta: duration per frame
253
- """
254
- number_of_frames = len(detections[DATA].keys())
255
- return duration / number_of_frames
256
-
257
- @staticmethod
258
- def _update_metadata(
259
- detections: dict, start_time: datetime, duration: timedelta
260
- ) -> dict:
261
- detections[METADATA][VIDEO][RECORDED_START_DATE] = start_time.timestamp()
262
- detections[METADATA][VIDEO][LENGTH] = str(duration)
263
- return detections
264
-
265
- def _stamp(
266
- self, detections: dict, start_date: datetime, time_per_frame: timedelta
267
- ) -> dict:
268
- """Add a timestamp (occurrence in real time) to each frame.
269
-
270
- Args:
271
- detections (dict): dictionary containing all frames
272
- start_date (datetime): start date of the video recording
273
- time_per_frame (timedelta): duration per frame
274
-
275
- Returns:
276
- dict: dictionary containing all frames with their occurrence in real time
277
- """
278
- data: dict = detections[DATA]
279
- for key, value in data.items():
280
- occurrence = start_date + (int(key) - 1) * time_per_frame
281
- value[OCCURRENCE] = occurrence.timestamp()
282
- return detections
283
-
284
-
285
- def parse_start_time_from(video_file: Path) -> datetime:
286
- """Parse the given filename and retrieve the start date of the video.
287
-
288
- Args:
289
- video_file (Path): path to video file
290
-
291
- Raises:
292
- InproperFormattedFilename: if the filename is not formatted as expected, an
293
- exception will be raised
294
-
295
- Returns:
296
- datetime: start date of the video
297
- """
298
- match = re.search(
299
- FILE_NAME_PATTERN,
300
- video_file.name,
301
- )
302
- if match:
303
- start_date: str = match.group(START_DATE)
304
- return parse_date_string_to_utc_datime(start_date, "%Y-%m-%d_%H-%M-%S").replace(
305
- tzinfo=timezone.utc
306
- )
34
+ """Starts the detection of objects in multiple videos and/or images."""
35
+ self.consume()
307
36
 
308
- raise InproperFormattedFilename(f"Could not parse {video_file.name}.")
37
+ def consume(self) -> None:
38
+ for _ in self._producer.produce():
39
+ pass
@@ -0,0 +1,57 @@
1
+ from dataclasses import dataclass
2
+ from datetime import datetime, timedelta
3
+
4
+ from OTVision.application.buffer import Buffer
5
+ from OTVision.domain.detection import DetectedFrame
6
+
7
+
8
+ @dataclass
9
+ class SourceMetadata:
10
+ source: str
11
+ duration: timedelta
12
+ height: int
13
+ width: int
14
+ fps: float
15
+ start_time: datetime
16
+
17
+
18
+ @dataclass
19
+ class FlushEvent:
20
+ source_metadata: SourceMetadata
21
+
22
+ @staticmethod
23
+ def create(
24
+ source: str,
25
+ duration: timedelta,
26
+ source_height: int,
27
+ source_width: int,
28
+ source_fps: float,
29
+ start_time: datetime,
30
+ ) -> "FlushEvent":
31
+ return FlushEvent(
32
+ SourceMetadata(
33
+ source,
34
+ duration,
35
+ source_height,
36
+ source_width,
37
+ source_fps,
38
+ start_time=start_time,
39
+ )
40
+ )
41
+
42
+
43
+ @dataclass
44
+ class DetectedFrameBufferEvent:
45
+ source_metadata: SourceMetadata
46
+ frames: list[DetectedFrame]
47
+
48
+
49
+ class DetectedFrameBuffer(Buffer[DetectedFrame, DetectedFrameBufferEvent, FlushEvent]):
50
+ def _notify_observers(
51
+ self, elements: list[DetectedFrame], event: FlushEvent
52
+ ) -> None:
53
+ self._subject.notify(
54
+ DetectedFrameBufferEvent(
55
+ source_metadata=event.source_metadata, frames=elements
56
+ )
57
+ )
OTVision/detect/otdet.py CHANGED
@@ -1,21 +1,23 @@
1
1
  from dataclasses import dataclass
2
- from datetime import timedelta
2
+ from datetime import datetime, timedelta
3
3
  from pathlib import Path
4
4
  from typing import Self
5
5
 
6
6
  from OTVision import dataformat, version
7
- from OTVision.track.preprocess import Detection
7
+ from OTVision.domain.detection import DetectedFrame, Detection
8
8
 
9
9
 
10
10
  @dataclass
11
11
  class OtdetBuilderConfig:
12
12
  conf: float
13
13
  iou: float
14
- video: Path
14
+ source: str
15
15
  video_width: int
16
16
  video_height: int
17
17
  expected_duration: timedelta | None
18
+ actual_duration: timedelta
18
19
  recorded_fps: float
20
+ recorded_start_date: datetime
19
21
  actual_fps: float
20
22
  actual_frames: int
21
23
  detection_img_size: int
@@ -50,37 +52,56 @@ class OtdetBuilder:
50
52
  self._config = None
51
53
  return self
52
54
 
53
- def build(self, detections: list[list[Detection]]) -> dict:
55
+ def build(self, detections: list[DetectedFrame]) -> dict:
56
+ number_of_frames = len(detections)
54
57
  result = {
55
- dataformat.METADATA: self._build_metadata(),
58
+ dataformat.METADATA: self._build_metadata(number_of_frames),
56
59
  dataformat.DATA: self._build_data(detections),
57
60
  }
58
61
  self.reset()
59
62
  return result
60
63
 
61
- def _build_metadata(self) -> dict:
64
+ def _build_metadata(self, number_of_frames: int) -> dict:
62
65
  return {
63
66
  dataformat.OTDET_VERSION: version.otdet_version(),
64
- dataformat.VIDEO: self._build_video_config(),
67
+ dataformat.VIDEO: self._build_video_config(number_of_frames),
65
68
  dataformat.DETECTION: self._build_detection_config(),
66
69
  }
67
70
 
68
- def _build_data(self, frames: list[list[Detection]]) -> dict:
71
+ def _build_data(self, frames: list[DetectedFrame]) -> dict:
69
72
  data = {}
70
- for frame, detections in enumerate(frames, start=1):
71
- converted_detections = [detection.to_otdet() for detection in detections]
72
- data[str(frame)] = {dataformat.DETECTIONS: converted_detections}
73
+ for frame in frames:
74
+ converted_detections = [
75
+ self.__convert_detection(detection) for detection in frame.detections
76
+ ]
77
+ data[str(frame.frame_number)] = {
78
+ dataformat.DETECTIONS: converted_detections,
79
+ dataformat.OCCURRENCE: frame.occurrence.timestamp(),
80
+ }
73
81
  return data
74
82
 
75
- def _build_video_config(self) -> dict:
83
+ def __convert_detection(self, detection: Detection) -> dict:
84
+ return {
85
+ dataformat.CLASS: detection.label,
86
+ dataformat.CONFIDENCE: detection.conf,
87
+ dataformat.X: detection.x,
88
+ dataformat.Y: detection.y,
89
+ dataformat.W: detection.w,
90
+ dataformat.H: detection.h,
91
+ }
92
+
93
+ def _build_video_config(self, number_of_frames: int) -> dict:
94
+ source = Path(self.config.source)
76
95
  video_config = {
77
- dataformat.FILENAME: str(self.config.video.stem),
78
- dataformat.FILETYPE: str(self.config.video.suffix),
96
+ dataformat.FILENAME: str(source.stem),
97
+ dataformat.FILETYPE: str(source.suffix),
79
98
  dataformat.WIDTH: self.config.video_width,
80
99
  dataformat.HEIGHT: self.config.video_height,
81
100
  dataformat.RECORDED_FPS: self.config.recorded_fps,
82
101
  dataformat.ACTUAL_FPS: self.config.actual_fps,
83
- dataformat.NUMBER_OF_FRAMES: self.config.actual_frames,
102
+ dataformat.NUMBER_OF_FRAMES: number_of_frames,
103
+ dataformat.RECORDED_START_DATE: self.config.recorded_start_date.timestamp(),
104
+ dataformat.LENGTH: str(self.config.actual_duration),
84
105
  }
85
106
  if self.config.expected_duration is not None:
86
107
  video_config[dataformat.EXPECTED_DURATION] = int(