OTVision 0.6.17__py3-none-any.whl → 0.7.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 (31) hide show
  1. OTVision/abstraction/observer.py +90 -1
  2. OTVision/abstraction/pipes_and_filter.py +4 -4
  3. OTVision/application/buffer.py +6 -6
  4. OTVision/application/detect/current_object_detector.py +4 -3
  5. OTVision/application/video/generate_video.py +2 -2
  6. OTVision/detect/builder.py +3 -3
  7. OTVision/detect/detect.py +4 -4
  8. OTVision/detect/detected_frame_buffer.py +9 -9
  9. OTVision/detect/detected_frame_producer.py +2 -2
  10. OTVision/detect/detected_frame_producer_factory.py +4 -4
  11. OTVision/detect/file_based_detect_builder.py +2 -2
  12. OTVision/detect/otdet_file_writer.py +7 -7
  13. OTVision/detect/rtsp_based_detect_builder.py +2 -2
  14. OTVision/detect/rtsp_input_source.py +12 -9
  15. OTVision/detect/video_input_source.py +17 -11
  16. OTVision/detect/yolo.py +9 -6
  17. OTVision/domain/detect_producer_consumer.py +5 -5
  18. OTVision/domain/input_source_detect.py +3 -3
  19. OTVision/domain/object_detection.py +5 -5
  20. OTVision/domain/video_writer.py +1 -1
  21. OTVision/plugin/ffmpeg_video_writer.py +4 -4
  22. OTVision/track/model/track_exporter.py +4 -4
  23. OTVision/track/model/tracking_interfaces.py +18 -15
  24. OTVision/track/stream_ottrk_file_writer.py +16 -14
  25. OTVision/track/track.py +4 -4
  26. OTVision/track/tracker/filebased_tracking.py +23 -16
  27. OTVision/version.py +1 -1
  28. {otvision-0.6.17.dist-info → otvision-0.7.0.dist-info}/METADATA +1 -1
  29. {otvision-0.6.17.dist-info → otvision-0.7.0.dist-info}/RECORD +31 -31
  30. {otvision-0.6.17.dist-info → otvision-0.7.0.dist-info}/WHEEL +1 -1
  31. {otvision-0.6.17.dist-info → otvision-0.7.0.dist-info}/licenses/LICENSE +0 -0
@@ -1,7 +1,12 @@
1
- from typing import Callable, TypeVar
1
+ import asyncio
2
+ import logging
3
+ from typing import Awaitable, Callable, TypeVar
4
+
5
+ from OTVision.helpers.log import LOGGER_NAME
2
6
 
3
7
  VALUE = TypeVar("VALUE")
4
8
  type Observer[T] = Callable[[T], None]
9
+ type AsyncObserver[T] = Callable[[T], Awaitable[None]]
5
10
 
6
11
 
7
12
  class Subject[T]:
@@ -41,3 +46,87 @@ class Observable[T]:
41
46
 
42
47
  def register(self, observer: Observer[T]) -> None:
43
48
  self._subject.register(observer)
49
+
50
+
51
+ class AsyncSubject[T]:
52
+ """Generic async subject class to handle and notify async observers.
53
+
54
+ This class ensures that no duplicate observers can be registered.
55
+ The order that registered observers are notified is dictated by the order
56
+ they have been registered. Meaning, first to be registered is first to be
57
+ notified. Observers are executed as fire-and-forget background tasks for
58
+ non-blocking notifications.
59
+ """
60
+
61
+ def __init__(self) -> None:
62
+ self._observers: list[AsyncObserver[T]] = []
63
+ self._pending_tasks: set[asyncio.Task[None]] = set()
64
+
65
+ def register(self, observer: AsyncObserver[T]) -> None:
66
+ """Listen to changes of subject.
67
+
68
+ Args:
69
+ observer (AsyncObserver[T]): the observer to be registered. This must be an
70
+ async `Callable` that returns an `Awaitable`.
71
+ """
72
+ new_observers = self._observers.copy()
73
+ new_observers.append(observer)
74
+ self._observers = list(dict.fromkeys(new_observers))
75
+
76
+ async def notify(self, value: T) -> None:
77
+ """Notifies observers about the value asynchronously.
78
+
79
+ All observers are notified as fire-and-forget background tasks,
80
+ allowing the event loop to continue processing while observers execute.
81
+ Each observer runs independently; exceptions are caught and logged
82
+ without affecting other observers or blocking the caller.
83
+
84
+ Args:
85
+ value (T): value to notify the observer with.
86
+ """
87
+ log = logging.getLogger(LOGGER_NAME)
88
+
89
+ async def _safe_observer_call(observer: AsyncObserver[T], value: T) -> None:
90
+ """Wrapper to safely call an observer and log any exceptions."""
91
+ try:
92
+ await observer(value)
93
+ except Exception as e:
94
+ observer_name = (
95
+ observer.__name__
96
+ if hasattr(observer, "__name__")
97
+ else repr(observer)
98
+ )
99
+ log.error(
100
+ f"Exception in async observer {observer_name}: {e}",
101
+ exc_info=True,
102
+ )
103
+
104
+ for observer in self._observers:
105
+ task = asyncio.create_task(_safe_observer_call(observer, value))
106
+ self._pending_tasks.add(task)
107
+ task.add_done_callback(self._pending_tasks.discard)
108
+
109
+ async def wait_for_all_observers(self) -> None:
110
+ """Wait for all pending observer tasks to complete.
111
+
112
+ This method can be used when you need to ensure all observers have
113
+ finished processing before proceeding. Useful in tests or when
114
+ synchronization is required.
115
+ """
116
+ if self._pending_tasks:
117
+ await asyncio.gather(*self._pending_tasks, return_exceptions=True)
118
+
119
+
120
+ class AsyncObservable[T]:
121
+ def __init__(self, subject: AsyncSubject[T]) -> None:
122
+ self._subject = subject
123
+
124
+ def register(self, observer: AsyncObserver[T]) -> None:
125
+ self._subject.register(observer)
126
+
127
+ async def wait_for_all_observers(self) -> None:
128
+ """Wait for all pending observer tasks to complete.
129
+
130
+ This method delegates to the subject's wait_for_all_observers method.
131
+ """
132
+ await self._subject.wait_for_all_observers()
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Iterator
2
+ from typing import AsyncIterator
3
3
 
4
4
 
5
5
  class Filter[IN, OUT](ABC):
@@ -14,14 +14,14 @@ class Filter[IN, OUT](ABC):
14
14
  """
15
15
 
16
16
  @abstractmethod
17
- def filter(self, pipe: Iterator[IN]) -> Iterator[OUT]:
17
+ def filter(self, pipe: AsyncIterator[IN]) -> AsyncIterator[OUT]:
18
18
  """Process elements from the input pipe and produce output elements.
19
19
 
20
20
  Args:
21
- pipe (Iterator[IN]): Input stream of elements to be processed.
21
+ pipe (AsyncIterator[IN]): Input stream of elements to be processed.
22
22
 
23
23
  Returns:
24
- Iterator[OUT]: Output stream of processed elements.
24
+ AsyncIterator[OUT]: Output stream of processed elements.
25
25
 
26
26
  """
27
27
 
@@ -1,5 +1,5 @@
1
1
  from abc import abstractmethod
2
- from typing import Iterator
2
+ from typing import AsyncIterator
3
3
 
4
4
  from OTVision.abstraction.pipes_and_filter import Filter
5
5
 
@@ -8,12 +8,12 @@ class Buffer[T, OBSERVING_TYPE](Filter[T, T]):
8
8
  def __init__(self) -> None:
9
9
  self._buffer: list[T] = []
10
10
 
11
- def filter(self, pipe: Iterator[T]) -> Iterator[T]:
12
- for element in pipe:
13
- self.buffer(element)
11
+ async def filter(self, pipe: AsyncIterator[T]) -> AsyncIterator[T]:
12
+ async for element in pipe:
13
+ await self.buffer(element)
14
14
  yield element
15
15
 
16
- def buffer(self, to_buffer: T) -> None:
16
+ async def buffer(self, to_buffer: T) -> None:
17
17
  self._buffer.append(to_buffer)
18
18
 
19
19
  def _get_buffered_elements(self) -> list[T]:
@@ -24,5 +24,5 @@ class Buffer[T, OBSERVING_TYPE](Filter[T, T]):
24
24
  self._buffer = list()
25
25
 
26
26
  @abstractmethod
27
- def on_flush(self, event: OBSERVING_TYPE) -> None:
27
+ async def on_flush(self, event: OBSERVING_TYPE) -> None:
28
28
  raise NotImplementedError
@@ -1,4 +1,4 @@
1
- from typing import Iterator
1
+ from typing import AsyncIterator
2
2
 
3
3
  from OTVision.abstraction.pipes_and_filter import Filter
4
4
  from OTVision.application.get_current_config import GetCurrentConfig
@@ -33,5 +33,6 @@ class CurrentObjectDetector(Filter[Frame, DetectedFrame]):
33
33
  detect_config = self._get_current_config.get().detect
34
34
  return self._factory.create(detect_config)
35
35
 
36
- def filter(self, pipe: Iterator[Frame]) -> Iterator[DetectedFrame]:
37
- return self.get().detect(pipe)
36
+ async def filter(self, pipe: AsyncIterator[Frame]) -> AsyncIterator[DetectedFrame]:
37
+ async for detected_frame in self.get().detect(pipe):
38
+ yield detected_frame
@@ -10,6 +10,6 @@ class GenerateVideo:
10
10
  self._input_source = input_source
11
11
  self._video_writer = video_writer
12
12
 
13
- def generate(self) -> None:
14
- for frame in self._video_writer.filter(self._input_source.produce()):
13
+ async def generate(self) -> None:
14
+ async for frame in self._video_writer.filter(self._input_source.produce()):
15
15
  pass
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
2
2
  from argparse import ArgumentParser
3
3
  from functools import cached_property
4
4
 
5
- from OTVision.abstraction.observer import Subject
5
+ from OTVision.abstraction.observer import AsyncSubject
6
6
  from OTVision.application.config import Config, DetectConfig
7
7
  from OTVision.application.config_parser import ConfigParser
8
8
  from OTVision.application.configure_logger import ConfigureLogger
@@ -127,7 +127,7 @@ class DetectBuilder(ABC):
127
127
  @cached_property
128
128
  def otdet_file_writer(self) -> OtdetFileWriter:
129
129
  return OtdetFileWriter(
130
- subject=Subject[OtdetFileWrittenEvent](),
130
+ subject=AsyncSubject[OtdetFileWrittenEvent](),
131
131
  builder=self.otdet_builder,
132
132
  get_current_config=self.get_current_config,
133
133
  current_object_detector_metadata=self.current_object_detector_metadata,
@@ -147,7 +147,7 @@ class DetectBuilder(ABC):
147
147
 
148
148
  @cached_property
149
149
  def detected_frame_buffer(self) -> DetectedFrameBuffer:
150
- return DetectedFrameBuffer(subject=Subject[DetectedFrameBufferEvent]())
150
+ return DetectedFrameBuffer(subject=AsyncSubject[DetectedFrameBufferEvent]())
151
151
 
152
152
  @cached_property
153
153
  def detected_frame_producer(self) -> DetectedFrameProducer:
OTVision/detect/detect.py CHANGED
@@ -30,10 +30,10 @@ class OTVisionVideoDetect(DetectedFrameConsumer):
30
30
  def __init__(self, producer: DetectedFrameProducer) -> None:
31
31
  self._producer = producer
32
32
 
33
- def start(self) -> None:
33
+ async def start(self) -> None:
34
34
  """Starts the detection of objects in multiple videos and/or images."""
35
- self.consume()
35
+ await self.consume()
36
36
 
37
- def consume(self) -> None:
38
- for _ in self._producer.produce():
37
+ async def consume(self) -> None:
38
+ async for _ in self._producer.produce():
39
39
  pass
@@ -1,7 +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
+ from OTVision.abstraction.observer import AsyncObservable, AsyncSubject
5
5
  from OTVision.application.buffer import Buffer
6
6
  from OTVision.domain.frame import DetectedFrame
7
7
 
@@ -51,25 +51,25 @@ class DetectedFrameBufferEvent:
51
51
 
52
52
 
53
53
  class DetectedFrameBuffer(
54
- Buffer[DetectedFrame, FlushEvent], Observable[DetectedFrameBufferEvent]
54
+ Buffer[DetectedFrame, FlushEvent], AsyncObservable[DetectedFrameBufferEvent]
55
55
  ):
56
- def __init__(self, subject: Subject[DetectedFrameBufferEvent]) -> None:
56
+ def __init__(self, subject: AsyncSubject[DetectedFrameBufferEvent]) -> None:
57
57
  Buffer.__init__(self)
58
- Observable.__init__(self, subject)
58
+ AsyncObservable.__init__(self, subject)
59
59
 
60
- def on_flush(self, event: FlushEvent) -> None:
60
+ async def on_flush(self, event: FlushEvent) -> None:
61
61
  buffered_elements = self._get_buffered_elements()
62
- self._notify_observers(buffered_elements, event)
62
+ await self._notify_observers(buffered_elements, event)
63
63
  self._reset_buffer()
64
64
 
65
- def _notify_observers(
65
+ async def _notify_observers(
66
66
  self, elements: list[DetectedFrame], event: FlushEvent
67
67
  ) -> None:
68
- self._subject.notify(
68
+ await self._subject.notify(
69
69
  DetectedFrameBufferEvent(
70
70
  source_metadata=event.source_metadata, frames=elements
71
71
  )
72
72
  )
73
73
 
74
- def buffer(self, to_buffer: DetectedFrame) -> None:
74
+ async def buffer(self, to_buffer: DetectedFrame) -> None:
75
75
  self._buffer.append(to_buffer.without_image())
@@ -1,4 +1,4 @@
1
- from typing import Iterator
1
+ from typing import AsyncIterator
2
2
 
3
3
  from OTVision.detect.detected_frame_producer_factory import DetectedFrameProducerFactory
4
4
  from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
@@ -10,5 +10,5 @@ class SimpleDetectedFrameProducer(DetectedFrameProducer):
10
10
  def __init__(self, producer_factory: DetectedFrameProducerFactory) -> None:
11
11
  self._producer_factory = producer_factory
12
12
 
13
- def produce(self) -> Iterator[DetectedFrame]:
13
+ def produce(self) -> AsyncIterator[DetectedFrame]:
14
14
  return self._producer_factory.create()
@@ -1,4 +1,4 @@
1
- from typing import Iterator
1
+ from typing import AsyncIterator
2
2
 
3
3
  from OTVision.abstraction.pipes_and_filter import Filter
4
4
  from OTVision.application.get_current_config import GetCurrentConfig
@@ -21,17 +21,17 @@ class DetectedFrameProducerFactory:
21
21
  self._detected_frame_buffer = detected_frame_buffer
22
22
  self._get_current_config = get_current_config
23
23
 
24
- def create(self) -> Iterator[DetectedFrame]:
24
+ def create(self) -> AsyncIterator[DetectedFrame]:
25
25
  if self._get_current_config.get().detect.write_video:
26
26
  return self.__create_with_video_writer()
27
27
  return self.__create_without_video_writer()
28
28
 
29
- def __create_without_video_writer(self) -> Iterator[DetectedFrame]:
29
+ def __create_without_video_writer(self) -> AsyncIterator[DetectedFrame]:
30
30
  return self._detected_frame_buffer.filter(
31
31
  self._detection_filter.filter(self._input_source.produce())
32
32
  )
33
33
 
34
- def __create_with_video_writer(self) -> Iterator[DetectedFrame]:
34
+ def __create_with_video_writer(self) -> AsyncIterator[DetectedFrame]:
35
35
  return self._detected_frame_buffer.filter(
36
36
  self._detection_filter.filter(
37
37
  self._video_writer_filter.filter(self._input_source.produce())
@@ -1,6 +1,6 @@
1
1
  from functools import cached_property
2
2
 
3
- from OTVision.abstraction.observer import Subject
3
+ from OTVision.abstraction.observer import AsyncSubject, Subject
4
4
  from OTVision.application.event.new_video_start import NewVideoStartEvent
5
5
  from OTVision.detect.builder import DetectBuilder
6
6
  from OTVision.detect.detected_frame_buffer import FlushEvent
@@ -19,7 +19,7 @@ class FileBasedDetectBuilder(DetectBuilder):
19
19
  @cached_property
20
20
  def input_source(self) -> VideoSource:
21
21
  return VideoSource(
22
- subject_flush=Subject[FlushEvent](),
22
+ subject_flush=AsyncSubject[FlushEvent](),
23
23
  subject_new_video_start=Subject[NewVideoStartEvent](),
24
24
  get_current_config=self.get_current_config,
25
25
  frame_rotator=self.frame_rotator,
@@ -2,7 +2,7 @@ import logging
2
2
  from dataclasses import dataclass
3
3
  from pathlib import Path
4
4
 
5
- from OTVision.abstraction.observer import Observer, Subject
5
+ from OTVision.abstraction.observer import AsyncObserver, AsyncSubject
6
6
  from OTVision.application.detect.current_object_detector_metadata import (
7
7
  CurrentObjectDetectorMetadata,
8
8
  )
@@ -45,7 +45,7 @@ class OtdetFileWriter:
45
45
 
46
46
  def __init__(
47
47
  self,
48
- subject: Subject[OtdetFileWrittenEvent],
48
+ subject: AsyncSubject[OtdetFileWrittenEvent],
49
49
  builder: OtdetBuilder,
50
50
  get_current_config: GetCurrentConfig,
51
51
  current_object_detector_metadata: CurrentObjectDetectorMetadata,
@@ -57,7 +57,7 @@ class OtdetFileWriter:
57
57
  self._current_object_detector_metadata = current_object_detector_metadata
58
58
  self._save_path_provider = save_path_provider
59
59
 
60
- def write(self, event: DetectedFrameBufferEvent) -> None:
60
+ async def write(self, event: DetectedFrameBufferEvent) -> None:
61
61
  """Writes detection results to a file in OTDET format.
62
62
 
63
63
  Processes the detected frames and associated metadata, builds the OTDET
@@ -118,16 +118,16 @@ class OtdetFileWriter:
118
118
 
119
119
  finished_msg = "Finished detection"
120
120
  log.info(finished_msg)
121
- self.__notify(
121
+ await self.__notify(
122
122
  num_frames=actual_frames,
123
123
  builder_config=builder_config,
124
124
  save_location=detections_file,
125
125
  )
126
126
 
127
- def __notify(
127
+ async def __notify(
128
128
  self, num_frames: int, builder_config: OtdetBuilderConfig, save_location: Path
129
129
  ) -> None:
130
- self._subject.notify(
130
+ await self._subject.notify(
131
131
  OtdetFileWrittenEvent(
132
132
  number_of_frames=num_frames,
133
133
  otdet_builder_config=builder_config,
@@ -135,6 +135,6 @@ class OtdetFileWriter:
135
135
  )
136
136
  )
137
137
 
138
- def register_observer(self, observer: Observer[OtdetFileWrittenEvent]) -> None:
138
+ def register_observer(self, observer: AsyncObserver[OtdetFileWrittenEvent]) -> None:
139
139
  """Register an observer to receive notifications about otdet file writes.."""
140
140
  self._subject.register(observer)
@@ -1,6 +1,6 @@
1
1
  from functools import cached_property
2
2
 
3
- from OTVision.abstraction.observer import Subject
3
+ from OTVision.abstraction.observer import AsyncSubject, Subject
4
4
  from OTVision.application.config import StreamConfig
5
5
  from OTVision.application.event.new_video_start import NewVideoStartEvent
6
6
  from OTVision.detect.builder import DetectBuilder
@@ -33,7 +33,7 @@ class RtspBasedDetectBuilder(DetectBuilder):
33
33
  @cached_property
34
34
  def input_source(self) -> RtspInputSource:
35
35
  return RtspInputSource(
36
- subject_flush=Subject[FlushEvent](),
36
+ subject_flush=AsyncSubject[FlushEvent](),
37
37
  subject_new_video_start=Subject[NewVideoStartEvent](),
38
38
  datetime_provider=self.datetime_provider,
39
39
  frame_counter=Counter(),
@@ -1,7 +1,8 @@
1
+ import asyncio
1
2
  import socket
2
3
  from datetime import datetime, timedelta
3
4
  from time import sleep
4
- from typing import Iterator
5
+ from typing import AsyncIterator
5
6
  from urllib.parse import urlparse
6
7
 
7
8
  from cv2 import (
@@ -13,7 +14,7 @@ from cv2 import (
13
14
  )
14
15
  from numpy import ndarray
15
16
 
16
- from OTVision.abstraction.observer import Subject
17
+ from OTVision.abstraction.observer import AsyncSubject, Subject
17
18
  from OTVision.application.config import (
18
19
  DATETIME_FORMAT,
19
20
  Config,
@@ -87,7 +88,7 @@ class RtspInputSource(InputSourceDetect):
87
88
 
88
89
  def __init__(
89
90
  self,
90
- subject_flush: Subject[FlushEvent],
91
+ subject_flush: AsyncSubject[FlushEvent],
91
92
  subject_new_video_start: Subject[NewVideoStartEvent],
92
93
  datetime_provider: DatetimeProvider,
93
94
  frame_counter: Counter,
@@ -131,7 +132,7 @@ class RtspInputSource(InputSourceDetect):
131
132
  self._current_video_capture = self._init_video_capture(self._current_stream)
132
133
  return self._current_video_capture
133
134
 
134
- def produce(self) -> Iterator[Frame]:
135
+ async def produce(self) -> AsyncIterator[Frame]:
135
136
  self._stream_start_time = self._datetime_provider.provide()
136
137
  self._current_video_start_time = self._stream_start_time
137
138
  try:
@@ -153,10 +154,10 @@ class RtspInputSource(InputSourceDetect):
153
154
  occurrence=occurrence,
154
155
  )
155
156
  if self.flush_condition_met():
156
- self._notify_flush_observers()
157
+ await self._notify_flush_observers()
157
158
  self._outdated = True
158
159
  self._frame_counter.reset()
159
- self._notify_flush_observers()
160
+ await self._notify_flush_observers()
160
161
  except InvalidRtspUrlError as cause:
161
162
  logger().error(cause)
162
163
 
@@ -209,7 +210,7 @@ class RtspInputSource(InputSourceDetect):
209
210
  def flush_condition_met(self) -> bool:
210
211
  return self.current_frame_number % self.flush_buffer_size == 0
211
212
 
212
- def _notify_flush_observers(self) -> None:
213
+ async def _notify_flush_observers(self) -> None:
213
214
  frame_width = self._get_width()
214
215
  frame_height = self._get_height()
215
216
  frames = (
@@ -219,7 +220,7 @@ class RtspInputSource(InputSourceDetect):
219
220
  )
220
221
  duration = timedelta(seconds=round(frames / self.fps))
221
222
  output = self.create_output()
222
- self.subject_flush.notify(
223
+ await self.subject_flush.notify(
223
224
  FlushEvent.create(
224
225
  source=self.rtsp_url,
225
226
  output=output,
@@ -256,7 +257,9 @@ class RtspInputSource(InputSourceDetect):
256
257
  def notify_new_config(self, config: NewOtvisionConfigEvent) -> None:
257
258
  try:
258
259
  logger().debug("New OTVision config detected. Flushing buffers...")
259
- self._notify_flush_observers()
260
+
261
+ # Create task to handle async flush notification
262
+ asyncio.create_task(self._notify_flush_observers())
260
263
  except NoConfigurationFoundError:
261
264
  logger().info("No configuration found for RTSP stream. Skipping flushing.")
262
265
 
@@ -1,13 +1,13 @@
1
1
  import logging
2
2
  from datetime import datetime
3
3
  from pathlib import Path
4
- from typing import Iterable, Iterator
4
+ from typing import AsyncIterator, Iterable
5
5
 
6
6
  import av
7
7
  from av.container.input import InputContainer
8
- from tqdm import tqdm
8
+ from tqdm.asyncio import tqdm
9
9
 
10
- from OTVision.abstraction.observer import Subject
10
+ from OTVision.abstraction.observer import AsyncSubject, Subject
11
11
  from OTVision.application.config import DATETIME_FORMAT, Config
12
12
  from OTVision.application.detect.timestamper import Timestamper
13
13
  from OTVision.application.event.new_video_start import NewVideoStartEvent
@@ -59,7 +59,7 @@ class VideoSource(InputSourceDetect):
59
59
 
60
60
  def __init__(
61
61
  self,
62
- subject_flush: Subject[FlushEvent],
62
+ subject_flush: AsyncSubject[FlushEvent],
63
63
  subject_new_video_start: Subject[NewVideoStartEvent],
64
64
  get_current_config: GetCurrentConfig,
65
65
  frame_rotator: AvVideoFrameRotator,
@@ -74,7 +74,7 @@ class VideoSource(InputSourceDetect):
74
74
  self._save_path_provider = save_path_provider
75
75
  self.__should_flush = False
76
76
 
77
- def produce(self) -> Iterator[Frame]:
77
+ async def produce(self) -> AsyncIterator[Frame]:
78
78
  """Generate frames from video files that meet detection requirements.
79
79
 
80
80
  Yields frames from valid video files while managing rotation, timestamping,
@@ -84,11 +84,13 @@ class VideoSource(InputSourceDetect):
84
84
  Frame: Processed video frames ready for detection.
85
85
  """
86
86
 
87
- video_files = self._collect_files_to_detect()
87
+ video_files = await self._collect_files_to_detect()
88
88
 
89
89
  log.info("Start detection of video files")
90
90
 
91
- for video_file in tqdm(video_files, desc="Detected video files", unit=" files"):
91
+ async for video_file in tqdm(
92
+ video_files, desc="Detected video files", unit=" files"
93
+ ):
92
94
  detections_file = self._save_path_provider.provide(
93
95
  str(video_file), self._current_config.filetypes.detect
94
96
  )
@@ -135,11 +137,15 @@ class VideoSource(InputSourceDetect):
135
137
  }
136
138
  )
137
139
  counter += 1
138
- self.notify_flush_event_observers(video_file, video_fps)
140
+ await self.notify_flush_event_observers(video_file, video_fps)
139
141
  self._on_video_finished(video_file)
140
142
  except Exception as e:
141
143
  log.error(f"Error processing {video_file}", exc_info=e)
142
144
 
145
+ # Wait for all flush event observers to complete their work
146
+ # (e.g., file writing) before this method returns
147
+ await self.subject_flush.wait_for_all_observers()
148
+
143
149
  def _on_video_finished(self, video_file: Path) -> None:
144
150
  """Hook for handling video processing completion."""
145
151
  pass
@@ -154,7 +160,7 @@ class VideoSource(InputSourceDetect):
154
160
  )
155
161
  return {}
156
162
 
157
- def _collect_files_to_detect(self) -> Iterable[Path]:
163
+ async def _collect_files_to_detect(self) -> Iterable[Path]:
158
164
  filetypes = self._current_config.filetypes.video_filetypes.to_list()
159
165
  video_files = get_files(
160
166
  paths=self._current_config.detect.paths, filetypes=filetypes
@@ -190,7 +196,7 @@ class VideoSource(InputSourceDetect):
190
196
  return False
191
197
  return True
192
198
 
193
- def notify_flush_event_observers(
199
+ async def notify_flush_event_observers(
194
200
  self, current_video_file: Path, video_fps: float
195
201
  ) -> None:
196
202
  if expected_duration := self._current_config.detect.expected_duration:
@@ -203,7 +209,7 @@ class VideoSource(InputSourceDetect):
203
209
  current_video_file, start_time=self._start_time
204
210
  )
205
211
 
206
- self.subject_flush.notify(
212
+ await self.subject_flush.notify(
207
213
  FlushEvent.create(
208
214
  source=str(current_video_file),
209
215
  output=str(current_video_file),
OTVision/detect/yolo.py CHANGED
@@ -22,10 +22,10 @@ OTVision module to detect objects using yolov5
22
22
  import logging
23
23
  from pathlib import Path
24
24
  from time import perf_counter
25
- from typing import Iterator
25
+ from typing import AsyncIterator
26
26
 
27
27
  import torch
28
- from tqdm import tqdm
28
+ from tqdm.asyncio import tqdm
29
29
  from ultralytics import YOLO
30
30
  from ultralytics.engine.results import Boxes
31
31
 
@@ -134,11 +134,14 @@ class YoloDetector(ObjectDetector, Filter[Frame, DetectedFrame]):
134
134
  self._detection_converter = detection_converter
135
135
  self._detected_frame_factory = detected_frame_factory
136
136
 
137
- def filter(self, pipe: Iterator[Frame]) -> Iterator[DetectedFrame]:
138
- return self.detect(pipe)
137
+ async def filter(self, pipe: AsyncIterator[Frame]) -> AsyncIterator[DetectedFrame]:
138
+ async for detected_frame in self.detect(pipe):
139
+ yield detected_frame
139
140
 
140
- def detect(self, frames: Iterator[Frame]) -> Iterator[DetectedFrame]:
141
- for frame in tqdm(
141
+ async def detect(
142
+ self, frames: AsyncIterator[Frame]
143
+ ) -> AsyncIterator[DetectedFrame]:
144
+ async for frame in tqdm(
142
145
  frames,
143
146
  desc="Detected frames",
144
147
  unit=" frames",
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Iterator
2
+ from typing import AsyncIterator
3
3
 
4
4
  from OTVision.domain.frame import DetectedFrame
5
5
 
@@ -8,7 +8,7 @@ class DetectedFrameConsumer(ABC):
8
8
  """Interface for components that consume detected frames."""
9
9
 
10
10
  @abstractmethod
11
- def consume(self) -> None:
11
+ async def consume(self) -> None:
12
12
  """Consume detected frames."""
13
13
  raise NotImplementedError
14
14
 
@@ -21,10 +21,10 @@ class DetectedFrameProducer(ABC):
21
21
  """
22
22
 
23
23
  @abstractmethod
24
- def produce(self) -> Iterator[DetectedFrame]:
24
+ def produce(self) -> AsyncIterator[DetectedFrame]:
25
25
  """Generate a stream of detected frames.
26
26
 
27
27
  Returns:
28
- Iterator[DetectedFrame, None, None]: A stream of detected frames.
28
+ AsyncIterator[DetectedFrame ]: A stream of detected frames.
29
29
  """
30
- raise NotImplementedError
30
+ ...
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Iterator
2
+ from typing import AsyncIterator
3
3
 
4
4
  from OTVision.domain.frame import Frame
5
5
 
@@ -14,7 +14,7 @@ class InputSourceDetect(ABC):
14
14
  """
15
15
 
16
16
  @abstractmethod
17
- def produce(self) -> Iterator[Frame]:
17
+ def produce(self) -> AsyncIterator[Frame]:
18
18
  """Generate a stream of frames from the input source.
19
19
 
20
20
  Implementations should yield Frame objects one at a time from the source,
@@ -22,7 +22,7 @@ class InputSourceDetect(ABC):
22
22
  at appropriate points (e.g., end of video segments or buffer boundaries).
23
23
 
24
24
  Returns:
25
- Iterator [Frame]: A generator yielding Frame objects
25
+ AsyncIterator[Frame]: A generator yielding Frame objects
26
26
  sequentially from the input source.
27
27
  """
28
28
 
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Iterator
2
+ from typing import AsyncIterator
3
3
 
4
4
  from OTVision.application.config import DetectConfig
5
5
  from OTVision.domain.frame import DetectedFrame, Frame
@@ -25,17 +25,17 @@ class ObjectDetectorMetadata(ABC):
25
25
  class ObjectDetector(ObjectDetectorMetadata):
26
26
 
27
27
  @abstractmethod
28
- def detect(self, frames: Iterator[Frame]) -> Iterator[DetectedFrame]:
28
+ def detect(self, frames: AsyncIterator[Frame]) -> AsyncIterator[DetectedFrame]:
29
29
  """Runs object detection on a video.
30
30
 
31
31
  Args:
32
- frames (Iterator[Frame]): the source to read frames from.
32
+ frames (AsyncIterator[Frame]): the source to read frames from.
33
33
 
34
34
  Returns:
35
- Iterator[DetectedFrame]: nested list of detections.
35
+ AsyncIterator[DetectedFrame]: nested list of detections.
36
36
  First level is frames, second level is detections within frame.
37
37
  """
38
- raise NotImplementedError
38
+ ...
39
39
 
40
40
  @abstractmethod
41
41
  def preload(self) -> None:
@@ -22,7 +22,7 @@ class VideoWriter(Filter[Frame, Frame], ABC):
22
22
  raise NotImplementedError
23
23
 
24
24
  @abstractmethod
25
- def notify_on_flush_event(self, event: FlushEvent) -> None:
25
+ async def notify_on_flush_event(self, event: FlushEvent) -> None:
26
26
  raise NotImplementedError
27
27
 
28
28
  @abstractmethod
@@ -3,7 +3,7 @@ from enum import IntEnum, StrEnum
3
3
  from pathlib import Path
4
4
  from subprocess import PIPE, Popen, TimeoutExpired
5
5
  from threading import Thread
6
- from typing import Callable, Iterator
6
+ from typing import AsyncIterator, Callable
7
7
 
8
8
  import ffmpeg
9
9
  from numpy import ndarray
@@ -242,7 +242,7 @@ class FfmpegVideoWriter(VideoWriter):
242
242
 
243
243
  self.__current_video_metadata = None
244
244
 
245
- def notify_on_flush_event(self, event: FlushEvent) -> None:
245
+ async def notify_on_flush_event(self, event: FlushEvent) -> None:
246
246
  self.close()
247
247
 
248
248
  def notify_on_new_video_start(self, event: NewVideoStartEvent) -> None:
@@ -282,8 +282,8 @@ class FfmpegVideoWriter(VideoWriter):
282
282
  log.info(f"Writing new video file to '{save_file}'.")
283
283
  return process
284
284
 
285
- def filter(self, pipe: Iterator[Frame]) -> Iterator[Frame]:
286
- for frame in pipe:
285
+ async def filter(self, pipe: AsyncIterator[Frame]) -> AsyncIterator[Frame]:
286
+ async for frame in pipe:
287
287
  if (image := frame.get(FrameKeys.data)) is not None:
288
288
  self.write(image)
289
289
  yield frame
@@ -1,7 +1,7 @@
1
1
  import logging
2
2
  from abc import ABC, abstractmethod
3
3
  from pathlib import Path
4
- from typing import Generic, Iterator, TypeVar
4
+ from typing import AsyncIterator, Generic, TypeVar
5
5
 
6
6
  from tqdm import tqdm
7
7
 
@@ -48,10 +48,10 @@ class FinishedTracksExporter(ABC, Generic[F]):
48
48
  def get_frame_group_id(self, container: F) -> int:
49
49
  pass
50
50
 
51
- def export(
52
- self, tracking_run_id: str, stream: Iterator[F], overwrite: bool
51
+ async def export(
52
+ self, tracking_run_id: str, stream: AsyncIterator[F], overwrite: bool
53
53
  ) -> None:
54
- for container in stream:
54
+ async for container in stream:
55
55
  self.export_frames(container, tracking_run_id, overwrite)
56
56
 
57
57
  def export_frames(
@@ -1,5 +1,5 @@
1
1
  from abc import ABC, abstractmethod
2
- from typing import Generic, Iterator, TypeVar
2
+ from typing import AsyncIterator, Generic, Iterator, TypeVar
3
3
 
4
4
  from OTVision.domain.detection import TrackId
5
5
  from OTVision.domain.frame import (
@@ -22,22 +22,22 @@ class Tracker(ABC):
22
22
  track_frame for processing a single frame.
23
23
  """
24
24
 
25
- def track(
26
- self, frames: Iterator[DetectedFrame], id_generator: IdGenerator
27
- ) -> Iterator[TrackedFrame]:
25
+ async def track(
26
+ self, frames: AsyncIterator[DetectedFrame], id_generator: IdGenerator
27
+ ) -> AsyncIterator[TrackedFrame]:
28
28
  """Process the given stream of Frames,
29
29
  yielding TrackedFrames one by one as a lazy stream of TrackedFrames.
30
30
 
31
31
  Args:
32
- frames (Iterator[DetectedFrame]): (lazy) stream of Frames
32
+ frames (AsyncIterator[DetectedFrame]): (lazy) stream of Frames
33
33
  with untracked Detections.
34
34
  id_generator (IdGenerator): provider of new (unique) track ids.
35
35
 
36
36
  Yields:
37
- Iterator[TrackedFrame]: (lazy) stream of TrackedFrames with
37
+ AsyncIterator[TrackedFrame]: (lazy) stream of TrackedFrames with
38
38
  TrackedDetections
39
39
  """
40
- for frame in frames:
40
+ async for frame in frames:
41
41
  yield self.track_frame(frame, id_generator)
42
42
 
43
43
  @abstractmethod
@@ -182,10 +182,10 @@ class UnfinishedTracksBuffer(ABC, Generic[C, F]):
182
182
  """
183
183
  pass
184
184
 
185
- def track_and_finish(self, containers: Iterator[C]) -> Iterator[F]:
185
+ async def track_and_finish(self, containers: AsyncIterator[C]) -> AsyncIterator[F]:
186
186
  # TODO template method to obtain containers?
187
187
 
188
- for container in containers:
188
+ async for container in containers:
189
189
 
190
190
  # if track is observed in current iteration, update its last observed frame
191
191
  new_last_track_frames = self._get_last_track_frames(container)
@@ -215,7 +215,8 @@ class UnfinishedTracksBuffer(ABC, Generic[C, F]):
215
215
  ]
216
216
 
217
217
  finished_containers: list[F] = self._finish_containers(ready_containers)
218
- yield from finished_containers
218
+ for finished_container in finished_containers:
219
+ yield finished_container
219
220
 
220
221
  # finish remaining containers with pending tracks
221
222
  remaining_containers = [c for c, _ in self._unfinished_containers]
@@ -223,7 +224,8 @@ class UnfinishedTracksBuffer(ABC, Generic[C, F]):
223
224
 
224
225
  finished_containers = self._finish_containers(remaining_containers)
225
226
  self._merged_last_track_frame = dict()
226
- yield from finished_containers
227
+ for finished_container in finished_containers:
228
+ yield finished_container
227
229
 
228
230
  def _finish_containers(self, containers: list[C]) -> list[F]:
229
231
  if len(containers) == 0:
@@ -269,11 +271,12 @@ class UnfinishedFramesBuffer(UnfinishedTracksBuffer[TrackedFrame, FinishedFrame]
269
271
  super().__init__(keep_discarded)
270
272
  self._tracker = tracker
271
273
 
272
- def track(
273
- self, frames: Iterator[DetectedFrame], id_generator: IdGenerator
274
- ) -> Iterator[FinishedFrame]:
274
+ async def track(
275
+ self, frames: AsyncIterator[DetectedFrame], id_generator: IdGenerator
276
+ ) -> AsyncIterator[FinishedFrame]:
275
277
  tracked_frame_stream = self._tracker.track(frames, id_generator)
276
- return self.track_and_finish(tracked_frame_stream)
278
+ async for finished_frame in self.track_and_finish(tracked_frame_stream):
279
+ yield finished_frame
277
280
 
278
281
  def _get_last_track_frames(self, container: TrackedFrame) -> dict[TrackId, int]:
279
282
  return {o: container.no for o in container.observed_tracks}
@@ -2,7 +2,7 @@ from dataclasses import dataclass
2
2
  from pathlib import Path
3
3
  from typing import Any
4
4
 
5
- from OTVision.abstraction.observer import Observer, Subject
5
+ from OTVision.abstraction.observer import AsyncObserver, AsyncSubject
6
6
  from OTVision.application.buffer import Buffer
7
7
  from OTVision.application.config import Config, TrackConfig
8
8
  from OTVision.application.configure_logger import logger
@@ -45,7 +45,7 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
45
45
 
46
46
  def __init__(
47
47
  self,
48
- subject: Subject[OttrkFileWrittenEvent],
48
+ subject: AsyncSubject[OttrkFileWrittenEvent],
49
49
  builder: OttrkBuilder,
50
50
  get_current_config: GetCurrentConfig,
51
51
  get_current_tracking_run_id: GetCurrentTrackingRunId,
@@ -62,7 +62,7 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
62
62
  self._ottrk_unfinished_tracks: set[TrackId] = set()
63
63
  self._current_output_file: Path | None = None
64
64
 
65
- def on_flush(self, event: OtdetFileWrittenEvent) -> None:
65
+ async def on_flush(self, event: OtdetFileWrittenEvent) -> None:
66
66
  tracked_frames = self._get_buffered_elements()
67
67
  if not tracked_frames:
68
68
  return
@@ -100,7 +100,7 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
100
100
  def reset(self) -> None:
101
101
  self._reset_buffer()
102
102
 
103
- def buffer(self, to_buffer: TrackedFrame) -> None:
103
+ async def buffer(self, to_buffer: TrackedFrame) -> None:
104
104
  self._buffer.append(to_buffer.without_image())
105
105
 
106
106
  if self._in_writing_state:
@@ -113,11 +113,11 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
113
113
  )
114
114
  logger().warning(f"Unfinished tracks: {self._ottrk_unfinished_tracks}")
115
115
  if self.build_condition_fulfilled:
116
- self._create_ottrk()
116
+ await self._create_ottrk()
117
117
 
118
- def _create_ottrk(self) -> None:
118
+ async def _create_ottrk(self) -> None:
119
119
  ottrk_data = self._builder.build()
120
- self.write(ottrk_data)
120
+ await self.write(ottrk_data)
121
121
  self.full_reset()
122
122
 
123
123
  def full_reset(self) -> None:
@@ -126,7 +126,7 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
126
126
  self._ottrk_unfinished_tracks = set()
127
127
  self._current_output_file = None
128
128
 
129
- def write(self, ottrk: dict) -> None:
129
+ async def write(self, ottrk: dict) -> None:
130
130
  current_output_file = self.current_output_file
131
131
  write_json(
132
132
  dict_to_write=ottrk,
@@ -134,13 +134,15 @@ class StreamOttrkFileWriter(Buffer[TrackedFrame, OtdetFileWrittenEvent]):
134
134
  filetype=self.config.filetypes.track,
135
135
  overwrite=True,
136
136
  )
137
- self._notify_ottrk_file_written(save_location=current_output_file)
137
+ await self._notify_ottrk_file_written(save_location=current_output_file)
138
138
 
139
- def force_flush(self, _: Any) -> None:
140
- self._create_ottrk()
139
+ async def force_flush(self, _: Any) -> None:
140
+ await self._create_ottrk()
141
141
 
142
- def register_observers(self, observer: Observer[OttrkFileWrittenEvent]) -> None:
142
+ def register_observers(
143
+ self, observer: AsyncObserver[OttrkFileWrittenEvent]
144
+ ) -> None:
143
145
  self._subject.register(observer)
144
146
 
145
- def _notify_ottrk_file_written(self, save_location: Path) -> None:
146
- self._subject.notify(OttrkFileWrittenEvent(save_location=save_location))
147
+ async def _notify_ottrk_file_written(self, save_location: Path) -> None:
148
+ await self._subject.notify(OttrkFileWrittenEvent(save_location=save_location))
OTVision/track/track.py CHANGED
@@ -1,6 +1,6 @@
1
1
  import logging
2
2
 
3
- from tqdm import tqdm
3
+ from tqdm.asyncio import tqdm
4
4
 
5
5
  from OTVision.application.config import Config
6
6
  from OTVision.application.get_current_config import GetCurrentConfig
@@ -32,7 +32,7 @@ class OtvisionTrack:
32
32
  self._buffer = unfinished_chunks_buffer
33
33
  self._tracking_run_id_generator = tracking_run_id_generator
34
34
 
35
- def start(self) -> None:
35
+ async def start(self) -> None:
36
36
  check_types(
37
37
  self.config.track.sigma_l,
38
38
  self.config.track.sigma_h,
@@ -61,6 +61,6 @@ class OtvisionTrack:
61
61
  finished_chunk_progress = tqdm(
62
62
  finished_chunk_stream, desc="export FrameChunk", total=len(detections_files)
63
63
  )
64
- self._track_exporter.export(
65
- tracking_run_id, iter(finished_chunk_progress), self.config.track.overwrite
64
+ await self._track_exporter.export(
65
+ tracking_run_id, finished_chunk_progress, self.config.track.overwrite
66
66
  )
@@ -1,9 +1,9 @@
1
1
  import logging
2
2
  from pathlib import Path
3
- from typing import Callable, Iterator
3
+ from typing import AsyncIterator, Callable
4
4
 
5
5
  from more_itertools import peekable
6
- from tqdm import tqdm
6
+ from tqdm.asyncio import tqdm
7
7
 
8
8
  from OTVision.application.config import DEFAULT_FILETYPE, OVERWRITE, TRACK
9
9
  from OTVision.config import CONFIG
@@ -40,7 +40,7 @@ class ChunkBasedTracker(Tracker):
40
40
  ) -> TrackedFrame:
41
41
  return self._tracker.track_frame(frames, id_generator)
42
42
 
43
- def track_chunk(
43
+ async def track_chunk(
44
44
  self,
45
45
  chunk: FrameChunk,
46
46
  is_last_chunk: bool,
@@ -50,16 +50,16 @@ class ChunkBasedTracker(Tracker):
50
50
  chunk.frames, desc="track Frame", total=len(chunk.frames), leave=False
51
51
  )
52
52
 
53
- tracked_frames = self.track(iter(frames_progress), id_generator)
53
+ tracked_frames = self.track(frames_progress, id_generator)
54
54
  return TrackedChunk(
55
55
  file=chunk.file,
56
- frames=list(tracked_frames),
56
+ frames=[frame async for frame in tracked_frames],
57
57
  metadata=chunk.metadata,
58
58
  is_last_chunk=is_last_chunk,
59
59
  frame_group_id=chunk.frame_group_id,
60
60
  )
61
61
 
62
- def track_file(
62
+ async def track_file(
63
63
  self,
64
64
  file: Path,
65
65
  frame_group: FrameGroup,
@@ -68,7 +68,7 @@ class ChunkBasedTracker(Tracker):
68
68
  frame_offset: int = 0,
69
69
  ) -> TrackedChunk:
70
70
  chunk = self._chunk_parser.parse(file, frame_group, frame_offset)
71
- return self.track_chunk(chunk, is_last_file, id_generator)
71
+ return await self.track_chunk(chunk, is_last_file, id_generator)
72
72
 
73
73
 
74
74
  IdGeneratorFactory = Callable[[FrameGroup], IdGenerator]
@@ -91,10 +91,12 @@ class GroupedFilesTracker(ChunkBasedTracker):
91
91
  self._overwrite = overwrite
92
92
  self._file_type = file_type
93
93
 
94
- def track_group(self, group: FrameGroup) -> Iterator[TrackedChunk]:
94
+ async def track_group(self, group: FrameGroup) -> AsyncIterator[TrackedChunk]:
95
95
  if self.check_skip_due_to_existing_output_files(group):
96
96
  log.warning(f"Skip FrameGroup {group.id}")
97
- yield from [] # TODO how to create empty generator stream?
97
+ empty: list[TrackedChunk] = []
98
+ for item in empty:
99
+ yield item
98
100
 
99
101
  frame_offset = 0 # frame no starts a 0 for each frame group
100
102
  id_generator = self._id_generator_of(group) # new id generator per group
@@ -113,17 +115,20 @@ class GroupedFilesTracker(ChunkBasedTracker):
113
115
  chunk = self._chunk_parser.parse(file, group, frame_offset)
114
116
  frame_offset = chunk.frames[-1].no + 1 # assuming frames are sorted by no
115
117
 
116
- tracked_chunk = self.track_chunk(chunk, is_last, id_generator)
118
+ tracked_chunk = await self.track_chunk(chunk, is_last, id_generator)
117
119
  yield tracked_chunk
118
120
 
119
- def group_and_track_files(self, files: list[Path]) -> Iterator[TrackedChunk]:
121
+ async def group_and_track_files(
122
+ self, files: list[Path]
123
+ ) -> AsyncIterator[TrackedChunk]:
120
124
  processed = self._group_parser.process_all(files)
121
125
 
122
126
  processed_progress = tqdm(
123
127
  processed, desc="track FrameGroup", total=len(processed), leave=False
124
128
  )
125
129
  for group in processed_progress:
126
- yield from self.track_group(group)
130
+ async for tracked_chunk in self.track_group(group):
131
+ yield tracked_chunk
127
132
 
128
133
  def check_skip_due_to_existing_output_files(self, group: FrameGroup) -> bool:
129
134
  if not self._overwrite and group.check_any_output_file_exists(self._file_type):
@@ -151,18 +156,20 @@ class UnfinishedChunksBuffer(UnfinishedTracksBuffer[TrackedChunk, FinishedChunk]
151
156
  super().__init__(keep_discarded)
152
157
  self.tracker = tracker
153
158
 
154
- def group_and_track(self, files: list[Path]) -> Iterator[FinishedChunk]:
159
+ async def group_and_track(self, files: list[Path]) -> AsyncIterator[FinishedChunk]:
155
160
  processed = self.tracker._group_parser.process_all(files)
156
161
 
157
162
  processed_progress = tqdm(
158
163
  processed, desc="track FrameGroup", total=len(processed), leave=False
159
164
  )
160
165
  for group in processed_progress:
161
- yield from self.track_group(group)
166
+ async for finished_chunk in self.track_group(group):
167
+ yield finished_chunk
162
168
 
163
- def track_group(self, group: FrameGroup) -> Iterator[FinishedChunk]:
169
+ async def track_group(self, group: FrameGroup) -> AsyncIterator[FinishedChunk]:
164
170
  tracked_chunk_stream = self.tracker.track_group(group)
165
- return self.track_and_finish(tracked_chunk_stream)
171
+ async for finished_chunk in self.track_and_finish(tracked_chunk_stream):
172
+ yield finished_chunk
166
173
 
167
174
  def _get_last_track_frames(self, container: TrackedChunk) -> dict[TrackId, FrameNo]:
168
175
  return container.last_track_frame
OTVision/version.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "v0.6.17"
1
+ __version__ = "v0.7.0"
2
2
 
3
3
 
4
4
  def otdet_version() -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: OTVision
3
- Version: 0.6.17
3
+ Version: 0.7.0
4
4
  Summary: OTVision is a core module of the OpenTrafficCam framework to perform object detection and tracking.
5
5
  Project-URL: Homepage, https://opentrafficcam.org/
6
6
  Project-URL: Documentation, https://opentrafficcam.org/overview/
@@ -1,13 +1,13 @@
1
1
  OTVision/__init__.py,sha256=CLnfgTlVHM4_nzDacvy06Z_Crc3hU6usd0mUyEvBf24,781
2
2
  OTVision/config.py,sha256=D4NIio27JG9hZk7yHI6kNKiMxKeKa_MGfrKNDdEH370,5389
3
3
  OTVision/dataformat.py,sha256=BHF7qHzyNb80hI1EKfwcdJ9bgG_X4bp_hCXzdg7_MSA,1941
4
- OTVision/version.py,sha256=Ay8_qdo-wXxrm9e-GbKoT9-hDKLEXS2tzcLHFDgMULk,176
4
+ OTVision/version.py,sha256=5PMTXzldADFnUA2b0NQMVkS5UT5BpP9hvh9U611A_Uk,175
5
5
  OTVision/abstraction/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
6
  OTVision/abstraction/defaults.py,sha256=ftETDe25gmr563RPSbG6flcEiNiHnRb0iXK1Zj_zdNg,442
7
- OTVision/abstraction/observer.py,sha256=ZFGxUUjI3wUpf5ogXg2yDe-QjCcXre6SxH5zOogOx2U,1350
8
- OTVision/abstraction/pipes_and_filter.py,sha256=pzK80ZV03_n6V-bZ9_Etu-aLqZACHgBbhTBZRnhz71M,887
7
+ OTVision/abstraction/observer.py,sha256=jCNtp1_b9VzEDtdjbZQmOX6yXuQQxzYB-NvTgwzjRNk,4751
8
+ OTVision/abstraction/pipes_and_filter.py,sha256=aU28pDf4zJBOcghinPhywBQidhPf0lgpyqaAG_dQ9eo,912
9
9
  OTVision/application/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- OTVision/application/buffer.py,sha256=qJQlNBQtriN2pqC1kfpOKk222_YHG0HNRzwC2DKBJQ0,743
10
+ OTVision/application/buffer.py,sha256=LVRimNWZupcvncZ-UecrnAUtTsaEhVsBPUPjMeZQCKQ,788
11
11
  OTVision/application/config.py,sha256=tTFVo2iJt3zCJ_p06FCeQOJ_NbuwmPd60ttrh-6fANI,13670
12
12
  OTVision/application/config_parser.py,sha256=EVFEnrNs5xd1bWa023s_PPoxAxpLEaWbFoGjsS8PM8U,11920
13
13
  OTVision/application/configure_logger.py,sha256=1TzHB-zm7vGTPtUp7m28ne4WxOyiUYeChLZU-ZPyOVQ,623
@@ -17,7 +17,7 @@ OTVision/application/get_current_config.py,sha256=iqtY10FRpn2FgLsasejjlyPFP3NrQJ
17
17
  OTVision/application/otvision_save_path_provider.py,sha256=lUzjQ92Qjvxf9sbaZX591FuEaUW5gFAq4GpgmN1k_tM,2179
18
18
  OTVision/application/update_current_config.py,sha256=iW1rpCClTHn8tnmVSpLVxdEB0nh1O_JCyxEqog0r0NU,333
19
19
  OTVision/application/detect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
- OTVision/application/detect/current_object_detector.py,sha256=P8BqThQGVBUMDzJ3WdNrOCEkB9h-wXqannZ9g_PYZXM,1302
20
+ OTVision/application/detect/current_object_detector.py,sha256=jow7-h0D6FziHduBiihzcML4rt3bjxk7SOmPGwLyD6Y,1378
21
21
  OTVision/application/detect/current_object_detector_metadata.py,sha256=xai0UBEzxr-rxXCc8mTmNDECds7mdsw2sem5HZxvQ4Q,1017
22
22
  OTVision/application/detect/detected_frame_factory.py,sha256=sW_l0xaPz44_lPEmCPKz4Xg1Mv4ZGKN9CyBCG_iN8dQ,1007
23
23
  OTVision/application/detect/factory.py,sha256=UCnLtgpWdNqwwjW0v2yzKF9Gacx6gewjTyy43wXs2Jg,938
@@ -34,38 +34,38 @@ OTVision/application/track/tracking_run_id.py,sha256=9_RgQSHR-IMgAM4LawtJWxNcVyw
34
34
  OTVision/application/track/update_current_track_config.py,sha256=pCFNuGl6rqpsUUGm2i1kjLSuuITVXv2R79Xf81n45as,1667
35
35
  OTVision/application/track/update_track_config_with_cli_args.py,sha256=5MDr2ih4xHzZ0kI6TSQ2C4mcWsPXW2PB80kA22-M_5I,2273
36
36
  OTVision/application/video/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
37
- OTVision/application/video/generate_video.py,sha256=4zazQoRdwefF_R-sUcnbP-23NTWgn_aYdQJrhtJ5Ni4,513
37
+ OTVision/application/video/generate_video.py,sha256=uv7cJPhWLZV53plkP4rCr6VqsGBQixSQ9NHViq3SiQM,525
38
38
  OTVision/convert/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
39
39
  OTVision/convert/convert.py,sha256=UUfzpbtMlxlJgKE6XeT5nyNqK26-K02bQDhq3o7KrXE,11050
40
40
  OTVision/detect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
- OTVision/detect/builder.py,sha256=uHiAfPjQLtedkSdBe7J3mxjcefr3OIItUNq-TKCeWX8,8047
41
+ OTVision/detect/builder.py,sha256=oVVGj9iPO8IqZOB98E3759WdJS2rtn34Ye64Krvu_tE,8062
42
42
  OTVision/detect/cli.py,sha256=3yf0G_B9cVm2CAz7AFzG10Cctv4ogMOxRHWEg8SZHss,7562
43
- OTVision/detect/detect.py,sha256=YaVS-DJXdEmh-OzwE31UPNl2uk7mcFyO_CKKTgMeiuM,1328
44
- OTVision/detect/detected_frame_buffer.py,sha256=zZZZBcpNcKApPMX4xoLeq7xszDCvxA3eOF_umrwo0N4,1931
45
- OTVision/detect/detected_frame_producer.py,sha256=thMg5nZNiArUpQI7ctGTQy8A2jxsZs5MJFJpkBPAzNk,530
46
- OTVision/detect/detected_frame_producer_factory.py,sha256=HiFAHWTvilwyibDrzMv9skwu901d9QW3zvjc3q4Pp80,1576
47
- OTVision/detect/file_based_detect_builder.py,sha256=G72GFhF2BCXO3fwfC9pXkbJPhRFfU6RphLRQs3ugCUQ,2315
43
+ OTVision/detect/detect.py,sha256=cBMw-sFCyXfd6vzl3zEa-5kVHZ5uW1FpOmtf1GZrw8o,1352
44
+ OTVision/detect/detected_frame_buffer.py,sha256=D_W4ZNyQxWd8ncm7tTvDmQMmRJLCW_EBuKBYfIcAz1k,1986
45
+ OTVision/detect/detected_frame_producer.py,sha256=RkAiw3RRUJmk05XQWR3Z-gEYkz7tbwhh8hEnRJpeSHI,540
46
+ OTVision/detect/detected_frame_producer_factory.py,sha256=eS9ekU2mI8MGjGq_UT-ejxuZDBp6-tauxLAd5i0lSBM,1596
47
+ OTVision/detect/file_based_detect_builder.py,sha256=jz8if-1_vUJSsqHRcyC_7ed_iuXqC9cJxysTFYWkGDw,2334
48
48
  OTVision/detect/otdet.py,sha256=cIwCBVFYWma7oJynoaT0eyIAZw-M2iH1xkvjy6-TwEM,8475
49
- OTVision/detect/otdet_file_writer.py,sha256=0FezBon_R-ve0oYMWMCDytNHIMuOEQqFOWVU7Dr9iTo,5388
49
+ OTVision/detect/otdet_file_writer.py,sha256=N9oQ9ddFpzVjH9mNX_EIHwcH6izJIvGlqPO2D4lrQMA,5432
50
50
  OTVision/detect/pyav_frame_count_provider.py,sha256=w7p9iM3F2fljV8SD7q491gQhIHANbVczqtalcUiKj-E,453
51
- OTVision/detect/rtsp_based_detect_builder.py,sha256=P47nKU2C1PJQmokgacv04DzJ-7eG6CUbqqg3bdQhXTQ,2662
52
- OTVision/detect/rtsp_input_source.py,sha256=nF7AyRcbZlvYwPghvi3QbRPCHU9KTtcgtWiUdAAR4Jg,10806
51
+ OTVision/detect/rtsp_based_detect_builder.py,sha256=CiMAKCtNojvm7DLuAdrrDMOUPlrZXIwWiW8CoJ6fa9M,2681
52
+ OTVision/detect/rtsp_input_source.py,sha256=8lErJm18wFWCU0yyF2VK6VaDPaTPOdeblAOTlF-Y8d4,10963
53
53
  OTVision/detect/timestamper.py,sha256=VvDTzHu9fTI7qQL9x775Gc27r47R8D5Pb040ffwO04k,5288
54
- OTVision/detect/video_input_source.py,sha256=yosHCK5e737AmBk-AgF3sL9rs45w0AIwup0-hU3mf1A,10248
55
- OTVision/detect/yolo.py,sha256=JDnnPO-YO30tu61sw80K9qJS6_aYqdR2dQHAhtJ3tl0,10551
54
+ OTVision/detect/video_input_source.py,sha256=vjQcy9YWphTOBgCDz73fE-ukN7k1e6noMf6XOIKCRKE,10534
55
+ OTVision/detect/yolo.py,sha256=N68U8N7eiBFruIjWy391W7cvkV_WRvvJYUO0QTW-ZQI,10671
56
56
  OTVision/detect/plugin_av/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  OTVision/detect/plugin_av/rotate_frame.py,sha256=4wJqTYI2HRlfa4p2Ffap33vLmKIzE_EwFvQraEkQ4R8,1055
58
58
  OTVision/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  OTVision/domain/cli.py,sha256=INiw1dxQNO4XI6GGCIMhX2G08ig68FNS04fdtMt3gNs,2061
60
60
  OTVision/domain/current_config.py,sha256=Q38NCktoGNU1Z_miXNoJXLH8-NDbVszwVOMGR1aAwWM,286
61
- OTVision/domain/detect_producer_consumer.py,sha256=z2N4aXv0mlH-EZxoNUxKYyn7o4aB638YZMbExz9NgZI,840
61
+ OTVision/domain/detect_producer_consumer.py,sha256=MHnBuF39YnmUn6blnPqtiWp3kYqCpsCt_rsz6RwFqrw,828
62
62
  OTVision/domain/detection.py,sha256=SZLP-87XE3NcTkeYz7GTqp4oPMiqI1P5gILp1_yHtxY,3761
63
63
  OTVision/domain/frame.py,sha256=1H0Cqg_LvO9BEwzGRkvRyJm6AauR_yS6kq0PHujonZk,6791
64
- OTVision/domain/input_source_detect.py,sha256=pdFnQ2xG1P4-KFWMS7a2-blx0hmR6N8Zza8GSMq4CDo,1081
65
- OTVision/domain/object_detection.py,sha256=oCazkbarKPBMzvQ3VeY2HQpWMUOgLeJBxXT52kl1syM,1327
64
+ OTVision/domain/input_source_detect.py,sha256=zUZZycqlkJpEqCnyOo188P12K2i9Os4QUQm2wVje0s8,1095
65
+ OTVision/domain/object_detection.py,sha256=BKwHo80u9w1Cn9Xpca3kqfBZC9JEj8iDRLYfonbxxRU,1330
66
66
  OTVision/domain/serialization.py,sha256=S7gb648z_W8U3Fb6TSk7hVU4qHlGwOZ7D6FeYSLXQwM,257
67
67
  OTVision/domain/time.py,sha256=_6a4zDbhXU7DmK7PdBYWRrrO2yQ4D68qtSYLTwnwWMQ,302
68
- OTVision/domain/video_writer.py,sha256=iYt-QXu8jaNSIwdbpEjc5hsEtHveUgqbmxme7iZNVLA,910
68
+ OTVision/domain/video_writer.py,sha256=hq67zMahLXAtHv57booORputnexhmUMu3XQk8n1Na-U,916
69
69
  OTVision/helpers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
70
70
  OTVision/helpers/date.py,sha256=L99CFQ7u34RwGDisFlrQoopZnkOOggQGDehWUi8kLiY,1347
71
71
  OTVision/helpers/files.py,sha256=G7zoOHzWIYrMmkjgHJHkZbh2hcGtnwZomuspthG2GsE,18444
@@ -75,20 +75,20 @@ OTVision/helpers/log.py,sha256=fOSMTXQRQ3_3zzYL8pDlx85IXPwyDsI2WGpK-V_R47Q,4985
75
75
  OTVision/helpers/machine.py,sha256=8Bz_Eg7PS0IL4riOVeJcEIi5D9E8Ju8-JomTkW975p8,2166
76
76
  OTVision/helpers/video.py,sha256=xyI35CiWXqoeGd3HeLhZUPxrLz8GccWyzHusxoweJr4,1480
77
77
  OTVision/plugin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
78
- OTVision/plugin/ffmpeg_video_writer.py,sha256=Q-exHN7aRoGEXAMyab-EGOqoKDL8ccwpB9IZhr1tjag,10428
78
+ OTVision/plugin/ffmpeg_video_writer.py,sha256=rtzYjk74Vd0ydO2duY4B_AggM6fZwCvKCmGA3GmUx2g,10461
79
79
  OTVision/plugin/generate_video.py,sha256=Jxk8iQBP8YhobRIRJ535yW3gx0h4d7H8oz0rULRPgcc,840
80
80
  OTVision/plugin/yaml_serialization.py,sha256=LjJ_QLJPClRwsaw7ooagWT7LBW08OvSb527jbex1qIQ,557
81
81
  OTVision/track/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
82
82
  OTVision/track/builder.py,sha256=iDq9-QxU8c3CQQd5HDPzT2qkXWIyFl0fk658KQ7Soh0,4905
83
83
  OTVision/track/cli.py,sha256=Lzmij9vMuaArB8MerDcGlaefwKMgRWNWov1YLGWA6SI,4294
84
84
  OTVision/track/id_generator.py,sha256=2Lkegjhz8T2FXiK1HaiS_FbNZrJjIWzHi407-IoKAHg,241
85
- OTVision/track/stream_ottrk_file_writer.py,sha256=4t8O5-DS2LXdgQHM5yn4s2ypdr0ACAsmzg4U6QZW1hU,5495
86
- OTVision/track/track.py,sha256=iMOaukcHnPhRiU1eEccaeGEKLzBQoilaUPRVmkmt0rk,2357
85
+ OTVision/track/stream_ottrk_file_writer.py,sha256=hncE6msUU4lHmYLP9JSv2VUKCJxnqQcX-DJQ0_fCQBg,5595
86
+ OTVision/track/track.py,sha256=wdb8krQGiSxFtZ0kGZkrUkgMElmTvKkQRpPzD7INeCI,2371
87
87
  OTVision/track/exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
88
88
  OTVision/track/exporter/filebased_exporter.py,sha256=sdaWY4CxjawyeEKG329T92Bodf1E8wjnXuXocv0Ppgo,986
89
89
  OTVision/track/model/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
90
- OTVision/track/model/track_exporter.py,sha256=ZzpQJB6e599tapRB-vTbhCltflnPVdgmLwCELRRV9Z8,3267
91
- OTVision/track/model/tracking_interfaces.py,sha256=wZONk_wHn72RRYBgcetGX7NzN_lfVuoa8BJ7RANF6N4,10779
90
+ OTVision/track/model/track_exporter.py,sha256=D3aU1qX-4Z_YNt21UJWq5PS8T_o1fX4wBJaM4JS93Mo,3289
91
+ OTVision/track/model/tracking_interfaces.py,sha256=Q2b2XUh4R4frJ1kfru9SK_9IsryACxOmaM7TqOIVYdM,11029
92
92
  OTVision/track/model/filebased/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
93
  OTVision/track/model/filebased/frame_chunk.py,sha256=rXhQCHXWGJbePy5ZW3JZCdltGz5mZxFdcrW0mgez-2k,6771
94
94
  OTVision/track/model/filebased/frame_group.py,sha256=f-hXS1Vc5U_qf2cgNbYVeSTZ3dg5NUJhasOEHuuX1HE,2977
@@ -96,7 +96,7 @@ OTVision/track/parser/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3h
96
96
  OTVision/track/parser/chunk_parser_plugins.py,sha256=iHAuxnoBQvwQ2j6RYfLZgk-EmsBgU0q29bYnf4kAJIM,2607
97
97
  OTVision/track/parser/frame_group_parser_plugins.py,sha256=CMl_muqSKl4scIm4URluGZtPbQmMk1JfbJm3d5Yod9g,3815
98
98
  OTVision/track/tracker/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
99
- OTVision/track/tracker/filebased_tracking.py,sha256=H3WYbsSca-67898diBoixpLjBqQDnOSiqnbvvySE6fc,6576
99
+ OTVision/track/tracker/filebased_tracking.py,sha256=HKJouBDCCHZdUoZzongdiipB4IS0I9xwuqur4On7-LE,6878
100
100
  OTVision/track/tracker/tracker_plugin_iou.py,sha256=AecE4CXRf4qUdN3_AvSFcsW4so-zDUGAVXqzfjSb-i0,7517
101
101
  OTVision/transform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
102
102
  OTVision/transform/get_homography.py,sha256=29waW61uzCCB7tlBAS2zck9sliAxZqnjjOa4jOOHHIc,5970
@@ -110,7 +110,7 @@ OTVision/view/view_helpers.py,sha256=a5yV_6ZxO5bxsSymOmxdHqzOEv0VFq4wFBopVRGuVRo
110
110
  OTVision/view/view_track.py,sha256=vmfMqpbUfnzg_EsWiL-IIKNOApVF09dzSojHpUfYY6M,5393
111
111
  OTVision/view/view_transform.py,sha256=HvRd8g8geKRy0OoiZUDn_oC3SJC5nuXhZf3uZelfGKg,5473
112
112
  OTVision/view/helpers/OTC.ico,sha256=G9kwlDtgBXmXO3yxW6Z-xVFV2q4nUGuz9E1VPHSu_I8,21662
113
- otvision-0.6.17.dist-info/METADATA,sha256=jHEqrFDQ7_q4medQTUzqTnm_6p_CvI3KvlK8rgi2WKQ,6944
114
- otvision-0.6.17.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
115
- otvision-0.6.17.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
116
- otvision-0.6.17.dist-info/RECORD,,
113
+ otvision-0.7.0.dist-info/METADATA,sha256=kcjT_0BZjW8ANZMATLEOkFQ9h4fC0ywdIgHsk_rOIOg,6943
114
+ otvision-0.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
115
+ otvision-0.7.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
116
+ otvision-0.7.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.27.0
2
+ Generator: hatchling 1.28.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any