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
OTVision/helpers/date.py CHANGED
@@ -1,5 +1,7 @@
1
1
  from datetime import datetime, timezone
2
2
 
3
+ from OTVision.dataformat import DATE_FORMAT
4
+
3
5
 
4
6
  def parse_date_string_to_utc_datime(date_string: str, date_format: str) -> datetime:
5
7
  """Parse a date string to a datetime object with UTC set as timezone.
@@ -24,3 +26,17 @@ def parse_timestamp_string_to_utc_datetime(timestamp: str | float) -> datetime:
24
26
  datetime: the datetime object with UTC as set timezone
25
27
  """
26
28
  return datetime.fromtimestamp(float(timestamp), timezone.utc)
29
+
30
+
31
+ def parse_datetime(date: str | float) -> datetime:
32
+ """Parse a date string or timestamp to a datetime with UTC as timezone.
33
+
34
+ Args:
35
+ date (str | float): the date to parse
36
+
37
+ Returns:
38
+ datetime: the parsed datetime object with UTC set as timezone
39
+ """
40
+ if isinstance(date, str) and ("-" in date):
41
+ return parse_date_string_to_utc_datime(date, DATE_FORMAT)
42
+ return parse_timestamp_string_to_utc_datetime(date)
@@ -0,0 +1,298 @@
1
+ import logging
2
+ from enum import IntEnum, StrEnum
3
+ from pathlib import Path
4
+ from subprocess import PIPE, Popen, TimeoutExpired
5
+ from threading import Thread
6
+ from typing import Callable, Iterator
7
+
8
+ import ffmpeg
9
+ from numpy import ndarray
10
+
11
+ from OTVision.application.event.new_video_start import NewVideoStartEvent
12
+ from OTVision.detect.detected_frame_buffer import FlushEvent
13
+ from OTVision.domain.frame import Frame, FrameKeys
14
+ from OTVision.domain.video_writer import VideoWriter
15
+ from OTVision.helpers.log import LOGGER_NAME
16
+ from OTVision.helpers.machine import ON_WINDOWS
17
+
18
+ VideoSaveLocationStrategy = Callable[[str], str]
19
+
20
+ BUFFER_SIZE_100MB = 10**8
21
+ DEFAULT_CRF = 23
22
+ VIDEO_SAVE_FILE_POSTFIX = "_processed"
23
+
24
+ log = logging.getLogger(LOGGER_NAME)
25
+
26
+
27
+ class VideoCodec(StrEnum):
28
+ """Enum of possible video codecs to be used with ffmpeg
29
+
30
+ Attributes:
31
+ H264_SOFTWARE: Software-based H.264 encoder.
32
+ H264_NVENC: NVIDIA GPU-based H.264 encoder.
33
+ H264_QSV: Intel Quick Sync Video encoder.
34
+ H264_VAAPI: Intel/AMD GPU-based H.264 encoder.
35
+ H264_VIDEOTOOLBOX: macOS hardware encoder.
36
+ """
37
+
38
+ H264_SOFTWARE = "libx264"
39
+ H264_NVENC = "h264_nvenc"
40
+ H264_QSV = "h264_qsv"
41
+ H264_VAAPI = "h264_vaapi"
42
+ H264_VIDEOTOOLBOX = "h264_videotoolbox"
43
+
44
+ @staticmethod
45
+ def as_list() -> list[str]:
46
+ return list(VideoCodec.__members__.values())
47
+
48
+
49
+ class VideoFormat(StrEnum):
50
+ RAW = "rawvideo"
51
+ MP4 = "mp4"
52
+
53
+ @staticmethod
54
+ def as_list() -> list[str]:
55
+ return list(VideoFormat.__members__.values())
56
+
57
+
58
+ class PixelFormat(StrEnum):
59
+ YUV420P = "yuv420p" # compatible with most players for H.264
60
+ RGB24 = "rgb24"
61
+ BGR24 = "bgr24"
62
+
63
+ @staticmethod
64
+ def as_list() -> list[str]:
65
+ return list(PixelFormat.__members__.values())
66
+
67
+
68
+ class EncodingSpeed(StrEnum):
69
+ ULTRA_FAST = "ultrafast"
70
+ SUPER_FAST = "superfast"
71
+ VERY_FAST = "veryfast"
72
+ FASTER = "faster"
73
+ FAST = "fast"
74
+ MEDIUM = "medium"
75
+ SLOW = "slow"
76
+ SLOWER = "slower"
77
+ VERY_SLOW = "veryslow"
78
+
79
+ @staticmethod
80
+ def as_list() -> list[str]:
81
+ return list(EncodingSpeed.__members__.values())
82
+
83
+
84
+ class ConstantRateFactor(IntEnum):
85
+ """Adjust quality/size (lower means better quality/larger file).
86
+ Attributes:
87
+ LOSSLESS: Perfect quality, massive file size.
88
+ HIGH_QUALITY: Visually lossless for most eyes.
89
+ GOOD: High quality, slightly compressed.
90
+ DEFAULT: x264 default; good balance of size and quality.
91
+ COMPACT: Acceptable quality for small screens or streaming.
92
+ LOW_QUALITY: Noticeable compression artifacts; smaller file.
93
+ WORST_ACCEPTABLE: Very low quality; only for previews or constrained storage.
94
+ """
95
+
96
+ LOSSLESS = 0
97
+ HIGH_QUALITY = 18
98
+ GOOD = 20
99
+ DEFAULT = 23
100
+ COMPACT = 26
101
+ LOW_QUALITY = 28
102
+ WORST_ACCEPTABLE = 35
103
+
104
+ @staticmethod
105
+ def as_list() -> list[str]:
106
+ return list(ConstantRateFactor.__members__.keys())
107
+
108
+
109
+ class FfmpegVideoWriter(VideoWriter):
110
+
111
+ @property
112
+ def _current_video_metadata(self) -> NewVideoStartEvent:
113
+ if self.__current_video_metadata is None:
114
+ raise ValueError("FfmpegVideoWriter is not configured yet.")
115
+ return self.__current_video_metadata
116
+
117
+ @property
118
+ def _ffmpeg_process(self) -> Popen:
119
+ if self.__ffmpeg_process is None:
120
+ raise ValueError("FfmpegVideoWriter is not initialized yet.")
121
+ return self.__ffmpeg_process
122
+
123
+ @property
124
+ def is_open(self) -> bool:
125
+ return self.__ffmpeg_process is not None
126
+
127
+ @property
128
+ def is_closed(self) -> bool:
129
+ return self.is_open is False
130
+
131
+ def __init__(
132
+ self,
133
+ save_location_strategy: VideoSaveLocationStrategy,
134
+ encoding_speed: EncodingSpeed = EncodingSpeed.FAST,
135
+ input_format: VideoFormat = VideoFormat.RAW,
136
+ output_format: VideoFormat = VideoFormat.MP4,
137
+ input_pixel_format: PixelFormat = PixelFormat.RGB24,
138
+ output_pixel_format: PixelFormat = PixelFormat.YUV420P,
139
+ output_video_codec: VideoCodec = VideoCodec.H264_SOFTWARE,
140
+ constant_rate_factor: ConstantRateFactor = ConstantRateFactor.LOSSLESS,
141
+ ) -> None:
142
+ if ON_WINDOWS:
143
+ log.warning(
144
+ "Writing every frame into a new video is not supported on Windows."
145
+ )
146
+ self._save_location_strategy = save_location_strategy
147
+ self._encoding_speed = encoding_speed
148
+ self._input_format = input_format
149
+ self._output_format = output_format
150
+ self._input_pixel_format = input_pixel_format
151
+ self._output_pixel_format = output_pixel_format
152
+ self._output_video_codec = output_video_codec
153
+ self.__ffmpeg_process: Popen | None = None
154
+ self.__current_video_metadata: NewVideoStartEvent | None = None
155
+ self._constant_rate_factor = constant_rate_factor
156
+ log.info(
157
+ "FFmpeg video writer settings: "
158
+ f"video_codec='{self._output_video_codec.value}', "
159
+ f"encoding_speed='{self._encoding_speed.value}', "
160
+ f"crf='{self._constant_rate_factor.value}'"
161
+ )
162
+
163
+ def open(self, output: str, width: int, height: int, fps: float) -> None:
164
+ self.__ffmpeg_process = self.__create_ffmpeg_process(
165
+ output_file=output,
166
+ width=width,
167
+ height=height,
168
+ fps=fps,
169
+ )
170
+
171
+ def write(self, image: ndarray) -> None:
172
+ try:
173
+ if self._ffmpeg_process.stdin:
174
+ self._ffmpeg_process.stdin.write(image.tobytes())
175
+ self._ffmpeg_process.stdin.flush()
176
+ except BrokenPipeError:
177
+ # Check if the process is still running
178
+ if self._ffmpeg_process.poll() is not None:
179
+ # Process has terminated, get the error message
180
+ stderr = (
181
+ self._ffmpeg_process.stderr.read()
182
+ if self._ffmpeg_process.stderr
183
+ else b""
184
+ )
185
+ log.info(stderr.decode("utf-8", errors="ignore"))
186
+ raise RuntimeError(
187
+ "ffmpeg process terminated unexpectedly: "
188
+ f"{stderr.decode('utf-8', errors='ignore')}"
189
+ )
190
+ raise # Re-raise the original exception if the process is still running
191
+
192
+ def close(self) -> None:
193
+ if self.__ffmpeg_process is not None:
194
+ process_to_cleanup = self._ffmpeg_process
195
+ self.__ffmpeg_process = None # Immediately mark as closed
196
+
197
+ # Close stdin synchronously (fast operation)
198
+ try:
199
+ if process_to_cleanup.stdin:
200
+ process_to_cleanup.stdin.flush()
201
+ process_to_cleanup.stdin.close()
202
+ except Exception as cause:
203
+ log.debug(f"Error closing stdin: {cause}")
204
+
205
+ # Handle cleanup in background thread
206
+ def cleanup_process() -> None:
207
+ try:
208
+ # Just check if it's still running and wait for it
209
+ if process_to_cleanup.poll() is None:
210
+ # Still running, close stdin and wait
211
+ try:
212
+ if (
213
+ process_to_cleanup.stdin
214
+ and not process_to_cleanup.stdin.closed
215
+ ):
216
+ process_to_cleanup.stdin.close()
217
+ except Exception as cause:
218
+ log.error(f"Error closing stdin: {cause}")
219
+
220
+ # Simple wait with timeout
221
+ try:
222
+ process_to_cleanup.wait(timeout=5.0)
223
+ except TimeoutExpired:
224
+ process_to_cleanup.kill()
225
+ try:
226
+ process_to_cleanup.wait(timeout=1.0)
227
+ except TimeoutExpired:
228
+ log.error("Could not kill FFmpeg process")
229
+
230
+ # Log return code if we care
231
+ if process_to_cleanup.returncode != 0:
232
+ log.debug(
233
+ f"FFmpeg ended with code: {process_to_cleanup.returncode}"
234
+ )
235
+
236
+ except Exception as e:
237
+ log.debug(f"Cleanup completed with minor issues: {e}")
238
+
239
+ # Start cleanup in daemon thread (won't prevent program exit)
240
+ cleanup_thread = Thread(target=cleanup_process, daemon=True)
241
+ cleanup_thread.start()
242
+
243
+ self.__current_video_metadata = None
244
+
245
+ def notify_on_flush_event(self, event: FlushEvent) -> None:
246
+ self.close()
247
+
248
+ def notify_on_new_video_start(self, event: NewVideoStartEvent) -> None:
249
+ self.__current_video_metadata = event
250
+ self.open(event.output, event.width, event.height, event.fps)
251
+
252
+ def __create_ffmpeg_process(
253
+ self, output_file: str, width: int, height: int, fps: float
254
+ ) -> Popen:
255
+ save_file = self._save_location_strategy(output_file)
256
+ cmd = (
257
+ ffmpeg.input(
258
+ "pipe:0",
259
+ format=self._input_format.value,
260
+ framerate=fps,
261
+ pix_fmt=self._input_pixel_format.value,
262
+ s=f"{width}x{height}",
263
+ )
264
+ .output(
265
+ save_file,
266
+ pix_fmt=self._output_pixel_format.value,
267
+ vcodec=self._output_video_codec.value,
268
+ preset=self._encoding_speed.value,
269
+ crf=self._constant_rate_factor.value,
270
+ format=self._output_format.value,
271
+ )
272
+ .overwrite_output()
273
+ .compile()
274
+ )
275
+
276
+ process = Popen(
277
+ cmd,
278
+ stdin=PIPE,
279
+ stderr=PIPE,
280
+ bufsize=BUFFER_SIZE_100MB,
281
+ )
282
+ log.info(f"Writing new video file to '{save_file}'.")
283
+ return process
284
+
285
+ def filter(self, pipe: Iterator[Frame]) -> Iterator[Frame]:
286
+ for frame in pipe:
287
+ if (image := frame.get(FrameKeys.data)) is not None:
288
+ self.write(image)
289
+ yield frame
290
+
291
+
292
+ def append_save_suffix_to_save_location(given: str) -> str:
293
+ filepath = Path(given)
294
+ return str(Path(filepath).with_stem(f"{filepath.stem}{VIDEO_SAVE_FILE_POSTFIX}"))
295
+
296
+
297
+ def keep_original_save_location(given: str) -> str:
298
+ return given
@@ -0,0 +1,24 @@
1
+ from functools import cached_property
2
+
3
+ from OTVision.application.video.generate_video import GenerateVideo
4
+ from OTVision.detect.file_based_detect_builder import FileBasedDetectBuilder
5
+
6
+
7
+ class GenerateVideoBuilder(FileBasedDetectBuilder):
8
+ @cached_property
9
+ def generate_video(self) -> GenerateVideo:
10
+ return GenerateVideo(
11
+ input_source=self.input_source, video_writer=self.video_file_writer
12
+ )
13
+
14
+ def build_generate_video(self) -> GenerateVideo:
15
+ self.register_observers()
16
+ return self.generate_video
17
+
18
+ def register_observers(self) -> None:
19
+ self.input_source.subject_new_video_start.register(
20
+ self.video_file_writer.notify_on_new_video_start
21
+ )
22
+ self.input_source.subject_flush.register(
23
+ self.video_file_writer.notify_on_flush_event
24
+ )
OTVision/track/builder.py CHANGED
@@ -7,6 +7,7 @@ from OTVision.application.configure_logger import ConfigureLogger
7
7
  from OTVision.application.get_config import GetConfig
8
8
  from OTVision.application.get_current_config import GetCurrentConfig
9
9
  from OTVision.application.track.get_track_cli_args import GetTrackCliArgs
10
+ from OTVision.application.track.tracking_run_id import StrIdGenerator
10
11
  from OTVision.application.track.update_current_track_config import (
11
12
  UpdateCurrentTrackConfig,
12
13
  )
@@ -20,11 +21,7 @@ from OTVision.domain.serialization import Deserializer
20
21
  from OTVision.plugin.yaml_serialization import YamlDeserializer
21
22
  from OTVision.track.cli import ArgparseTrackCliParser
22
23
  from OTVision.track.exporter.filebased_exporter import FinishedChunkTrackExporter
23
- from OTVision.track.id_generator import (
24
- StrIdGenerator,
25
- track_id_generator,
26
- tracking_run_uuid_generator,
27
- )
24
+ from OTVision.track.id_generator import track_id_generator, tracking_run_uuid_generator
28
25
  from OTVision.track.model.filebased.frame_chunk import ChunkParser
29
26
  from OTVision.track.model.filebased.frame_group import FrameGroupParser
30
27
  from OTVision.track.model.track_exporter import FinishedTracksExporter
@@ -1,7 +1,5 @@
1
1
  import uuid
2
- from typing import Callable, Iterator
3
-
4
- StrIdGenerator = Callable[[], str]
2
+ from typing import Iterator
5
3
 
6
4
 
7
5
  def tracking_run_uuid_generator() -> str:
@@ -8,7 +8,6 @@ from OTVision.dataformat import (
8
8
  CLASS,
9
9
  CONFIDENCE,
10
10
  DATA,
11
- DATE_FORMAT,
12
11
  DETECTIONS,
13
12
  OCCURRENCE,
14
13
  H,
@@ -18,10 +17,7 @@ from OTVision.dataformat import (
18
17
  )
19
18
  from OTVision.domain.detection import Detection
20
19
  from OTVision.domain.frame import DetectedFrame
21
- from OTVision.helpers.date import (
22
- parse_date_string_to_utc_datime,
23
- parse_timestamp_string_to_utc_datetime,
24
- )
20
+ from OTVision.helpers.date import parse_datetime
25
21
  from OTVision.helpers.files import denormalize_bbox, read_json
26
22
  from OTVision.track.model.filebased.frame_chunk import ChunkParser, FrameChunk
27
23
  from OTVision.track.model.filebased.frame_group import FrameGroup
@@ -84,17 +80,3 @@ class DetectionParser:
84
80
  )
85
81
  detections.append(detected_item)
86
82
  return detections
87
-
88
-
89
- def parse_datetime(date: str | float) -> datetime:
90
- """Parse a date string or timestamp to a datetime with UTC as timezone.
91
-
92
- Args:
93
- date (str | float): the date to parse
94
-
95
- Returns:
96
- datetime: the parsed datetime object with UTC set as timezone
97
- """
98
- if isinstance(date, str) and ("-" in date):
99
- return parse_date_string_to_utc_datime(date, DATE_FORMAT)
100
- return parse_timestamp_string_to_utc_datetime(date)
@@ -1,34 +1,17 @@
1
- import re
2
1
  from datetime import datetime, timedelta
3
2
  from pathlib import Path
4
3
 
5
- from OTVision import dataformat, version
6
4
  from OTVision.application.config import TrackConfig
7
5
  from OTVision.application.get_current_config import GetCurrentConfig
8
- from OTVision.dataformat import (
9
- EXPECTED_DURATION,
10
- FILENAME,
11
- FIRST_TRACKED_VIDEO_START,
12
- LAST_TRACKED_VIDEO_END,
13
- LENGTH,
14
- OTTRACK_VERSION,
15
- OTVISION_VERSION,
16
- RECORDED_START_DATE,
17
- TRACKER,
18
- TRACKING,
19
- VIDEO,
20
- )
21
- from OTVision.detect.otdet import parse_video_length
22
- from OTVision.helpers.files import (
23
- FULL_FILE_NAME_PATTERN,
24
- HOSTNAME,
25
- InproperFormattedFilename,
26
- read_json_bz2_metadata,
6
+ from OTVision.application.track.ottrk import create_ottrk_metadata_entry
7
+ from OTVision.detect.otdet import (
8
+ extract_expected_duration_from_otdet,
9
+ extract_hostname_from_otdet,
10
+ extract_start_date_from_otdet,
27
11
  )
12
+ from OTVision.helpers.files import read_json_bz2_metadata
28
13
  from OTVision.track.model.filebased.frame_group import FrameGroup, FrameGroupParser
29
- from OTVision.track.parser.chunk_parser_plugins import parse_datetime
30
14
 
31
- MISSING_START_DATE = datetime(1900, 1, 1)
32
15
  MISSING_EXPECTED_DURATION = timedelta(minutes=15)
33
16
 
34
17
 
@@ -37,16 +20,6 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
37
20
  def config(self) -> TrackConfig:
38
21
  return self._get_current_config.get().track
39
22
 
40
- @property
41
- def _tracker_data(self) -> dict:
42
- return tracker_metadata(
43
- sigma_l=self.config.sigma_l,
44
- sigma_h=self.config.sigma_h,
45
- sigma_iou=self.config.sigma_iou,
46
- t_min=self.config.t_min,
47
- t_miss_max=self.config.t_miss_max,
48
- )
49
-
50
23
  def __init__(
51
24
  self,
52
25
  get_current_config: GetCurrentConfig,
@@ -80,43 +53,28 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
80
53
  )
81
54
 
82
55
  def get_hostname(self, file_metadata: dict) -> str:
83
- video_name = Path(file_metadata[VIDEO][FILENAME]).name
84
- match = re.search(
85
- FULL_FILE_NAME_PATTERN,
86
- video_name,
87
- )
88
- if match:
89
- return match.group(HOSTNAME)
90
-
91
- raise InproperFormattedFilename(f"Could not parse {video_name}.")
56
+ return extract_hostname_from_otdet(file_metadata)
92
57
 
93
58
  def extract_start_date_from(self, metadata: dict) -> datetime:
94
- if RECORDED_START_DATE in metadata[VIDEO].keys():
95
- recorded_start_date = metadata[VIDEO][RECORDED_START_DATE]
96
- return parse_datetime(recorded_start_date)
97
- return MISSING_START_DATE
59
+ return extract_start_date_from_otdet(metadata)
98
60
 
99
61
  def extract_expected_duration_from(self, metadata: dict) -> timedelta:
100
- if EXPECTED_DURATION in metadata[VIDEO].keys():
101
- if expected_duration := metadata[VIDEO][EXPECTED_DURATION]:
102
- return timedelta(seconds=int(expected_duration))
103
- return self.parse_video_length(metadata)
104
-
105
- def parse_video_length(self, metadata: dict) -> timedelta:
106
- video_length = metadata[VIDEO][LENGTH]
107
- return parse_video_length(video_length)
62
+ return extract_expected_duration_from_otdet(metadata)
108
63
 
109
64
  def update_metadata(self, frame_group: FrameGroup) -> dict[Path, dict]:
110
65
  metadata_by_file = dict(frame_group.metadata_by_file)
111
66
  for filepath in frame_group.files:
112
67
  metadata = metadata_by_file[filepath]
113
- metadata[OTTRACK_VERSION] = version.ottrack_version()
114
- metadata[TRACKING] = {
115
- OTVISION_VERSION: version.otvision_version(),
116
- FIRST_TRACKED_VIDEO_START: frame_group.start_date.timestamp(),
117
- LAST_TRACKED_VIDEO_END: frame_group.end_date.timestamp(),
118
- TRACKER: self._tracker_data,
119
- }
68
+ ottrk_metadata = create_ottrk_metadata_entry(
69
+ start_date=frame_group.start_date,
70
+ end_date=frame_group.end_date,
71
+ sigma_l=self.config.sigma_l,
72
+ sigma_h=self.config.sigma_h,
73
+ sigma_iou=self.config.sigma_iou,
74
+ t_min=self.config.t_min,
75
+ t_miss_max=self.config.t_miss_max,
76
+ )
77
+ metadata.update(ottrk_metadata)
120
78
 
121
79
  return metadata_by_file
122
80
 
@@ -142,16 +100,3 @@ class TimeThresholdFrameGroupParser(FrameGroupParser):
142
100
  last_group = current_group
143
101
  merged_groups.append(last_group)
144
102
  return merged_groups
145
-
146
-
147
- def tracker_metadata(
148
- sigma_l: float, sigma_h: float, sigma_iou: float, t_min: float, t_miss_max: float
149
- ) -> dict:
150
- return {
151
- dataformat.NAME: "IOU",
152
- dataformat.SIGMA_L: sigma_l,
153
- dataformat.SIGMA_H: sigma_h,
154
- dataformat.SIGMA_IOU: sigma_iou,
155
- dataformat.T_MIN: t_min,
156
- dataformat.T_MISS_MAX: t_miss_max,
157
- }
@@ -0,0 +1,116 @@
1
+ from pathlib import Path
2
+
3
+ from OTVision.application.buffer import Buffer
4
+ from OTVision.application.config import Config, TrackConfig
5
+ from OTVision.application.configure_logger import logger
6
+ from OTVision.application.get_current_config import GetCurrentConfig
7
+ from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
8
+ from OTVision.application.track.ottrk import OttrkBuilder, OttrkBuilderConfig
9
+ from OTVision.application.track.tracking_run_id import GetCurrentTrackingRunId
10
+ from OTVision.detect.otdet import OtdetBuilderConfig
11
+ from OTVision.detect.otdet_file_writer import OtdetFileWrittenEvent
12
+ from OTVision.domain.detection import TrackId
13
+ from OTVision.domain.frame import TrackedFrame
14
+ from OTVision.helpers.files import write_json
15
+
16
+ STREAMING_FRAME_GROUP_ID = 0
17
+
18
+
19
+ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
20
+ @property
21
+ def config(self) -> Config:
22
+ return self._get_current_config.get()
23
+
24
+ @property
25
+ def track_config(self) -> TrackConfig:
26
+ return self.config.track
27
+
28
+ @property
29
+ def build_condition_fulfilled(self) -> bool:
30
+ return len(self._ottrk_unfinished_tracks) == 0
31
+
32
+ @property
33
+ def current_output_file(self) -> Path:
34
+ if self._current_output_file is None:
35
+ raise ValueError("Output file has not been set yet.")
36
+ return self._current_output_file
37
+
38
+ def __init__(
39
+ self,
40
+ builder: OttrkBuilder,
41
+ get_current_config: GetCurrentConfig,
42
+ get_current_tracking_run_id: GetCurrentTrackingRunId,
43
+ save_path_provider: OtvisionSavePathProvider,
44
+ ) -> None:
45
+ Buffer.__init__(self)
46
+ self._builder = builder
47
+ self._get_current_config = get_current_config
48
+ self._current_tracking_run_id = get_current_tracking_run_id
49
+ self._save_path_provider = save_path_provider
50
+
51
+ self._in_writing_state: bool = False
52
+ self._ottrk_unfinished_tracks: set[TrackId] = set()
53
+ self._current_output_file: Path | None = None
54
+
55
+ def on_flush(self, event: OtdetFileWrittenEvent) -> None:
56
+ tracked_frames = self._get_buffered_elements()
57
+ if not tracked_frames:
58
+ return
59
+
60
+ self._in_writing_state = True
61
+ self._current_output_file = self._save_path_provider.provide(
62
+ event.otdet_builder_config.source, self.config.filetypes.track
63
+ )
64
+ builder_config = self._create_ottrk_builder_config(
65
+ event.otdet_builder_config, event.number_of_frames
66
+ )
67
+ self._builder.set_config(builder_config)
68
+ last_frame = tracked_frames[-1]
69
+ self._builder.add_tracked_frames(self._get_buffered_elements())
70
+ self._ottrk_unfinished_tracks = last_frame.unfinished_tracks
71
+ self.reset()
72
+
73
+ def _create_ottrk_builder_config(
74
+ self,
75
+ otdet_builder_config: OtdetBuilderConfig,
76
+ number_of_frames: int,
77
+ ) -> OttrkBuilderConfig:
78
+ return OttrkBuilderConfig(
79
+ otdet_builder_config=otdet_builder_config,
80
+ number_of_frames=number_of_frames,
81
+ sigma_l=self.track_config.sigma_l,
82
+ sigma_h=self.track_config.sigma_h,
83
+ sigma_iou=self.track_config.sigma_iou,
84
+ t_min=self.track_config.t_min,
85
+ t_miss_max=self.track_config.t_miss_max,
86
+ tracking_run_id=self._current_tracking_run_id.get(),
87
+ frame_group=STREAMING_FRAME_GROUP_ID,
88
+ )
89
+
90
+ def reset(self) -> None:
91
+ self._reset_buffer()
92
+
93
+ def buffer(self, to_buffer: TrackedFrame) -> None:
94
+ self._buffer.append(to_buffer.without_image())
95
+
96
+ if self._in_writing_state:
97
+ self._builder.finish_tracks(to_buffer.finished_tracks)
98
+ self._builder.discard_tracks(to_buffer.discarded_tracks)
99
+ self._ottrk_unfinished_tracks = (
100
+ self._ottrk_unfinished_tracks.difference(to_buffer.unfinished_tracks)
101
+ .difference(to_buffer.finished_tracks)
102
+ .difference(to_buffer.discarded_tracks)
103
+ )
104
+ logger().warning(f"Unfinished tracks: {self._ottrk_unfinished_tracks}")
105
+ if self.build_condition_fulfilled:
106
+ ottrk_data = self._builder.build()
107
+ self.write(ottrk_data)
108
+ self._in_writing_state = False
109
+
110
+ def write(self, ottrk: dict) -> None:
111
+ write_json(
112
+ dict_to_write=ottrk,
113
+ file=self.current_output_file,
114
+ filetype=self.config.filetypes.track,
115
+ overwrite=True,
116
+ )
OTVision/track/track.py CHANGED
@@ -4,10 +4,11 @@ from tqdm import tqdm
4
4
 
5
5
  from OTVision.application.config import Config
6
6
  from OTVision.application.get_current_config import GetCurrentConfig
7
+ from OTVision.application.track.tracking_run_id import StrIdGenerator
7
8
  from OTVision.helpers.files import get_files
8
9
  from OTVision.helpers.input_types import check_types
9
10
  from OTVision.helpers.log import LOGGER_NAME
10
- from OTVision.track.id_generator import StrIdGenerator, tracking_run_uuid_generator
11
+ from OTVision.track.id_generator import tracking_run_uuid_generator
11
12
  from OTVision.track.model.track_exporter import FinishedTracksExporter
12
13
  from OTVision.track.tracker.filebased_tracking import UnfinishedChunksBuffer
13
14
 
OTVision/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "v0.6.7"
1
+ __version__ = "v0.6.9"
2
2
 
3
3
 
4
4
  def otdet_version() -> str: