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.
- OTVision/abstraction/pipes_and_filter.py +4 -4
- OTVision/application/buffer.py +5 -12
- OTVision/application/config.py +18 -0
- OTVision/application/config_parser.py +68 -11
- OTVision/application/detect/current_object_detector.py +2 -4
- OTVision/application/detect/update_detect_config_with_cli_args.py +16 -0
- OTVision/application/event/__init__.py +0 -0
- OTVision/application/event/new_otvision_config.py +6 -0
- OTVision/application/event/new_video_start.py +9 -0
- OTVision/application/{detect/detection_file_save_path_provider.py → otvision_save_path_provider.py} +8 -7
- OTVision/application/track/ottrk.py +203 -0
- OTVision/application/track/tracking_run_id.py +35 -0
- OTVision/application/video/__init__.py +0 -0
- OTVision/application/video/generate_video.py +15 -0
- OTVision/config.py +2 -0
- OTVision/detect/builder.py +35 -22
- OTVision/detect/cli.py +44 -1
- OTVision/detect/detected_frame_buffer.py +13 -1
- OTVision/detect/detected_frame_producer.py +14 -0
- OTVision/detect/detected_frame_producer_factory.py +39 -0
- OTVision/detect/file_based_detect_builder.py +39 -3
- OTVision/detect/otdet.py +109 -41
- OTVision/detect/otdet_file_writer.py +52 -29
- OTVision/detect/rtsp_based_detect_builder.py +35 -3
- OTVision/detect/rtsp_input_source.py +134 -37
- OTVision/detect/video_input_source.py +46 -14
- OTVision/detect/yolo.py +12 -8
- OTVision/domain/cli.py +10 -0
- OTVision/domain/detect_producer_consumer.py +3 -3
- OTVision/domain/frame.py +12 -0
- OTVision/domain/input_source_detect.py +4 -6
- OTVision/domain/object_detection.py +4 -6
- OTVision/domain/time.py +2 -2
- OTVision/domain/video_writer.py +30 -0
- OTVision/helpers/date.py +16 -0
- OTVision/plugin/ffmpeg_video_writer.py +298 -0
- OTVision/plugin/generate_video.py +24 -0
- OTVision/track/builder.py +2 -5
- OTVision/track/id_generator.py +1 -3
- OTVision/track/parser/chunk_parser_plugins.py +1 -19
- OTVision/track/parser/frame_group_parser_plugins.py +19 -74
- OTVision/track/stream_ottrk_file_writer.py +116 -0
- OTVision/track/track.py +2 -1
- OTVision/version.py +1 -1
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/METADATA +6 -5
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/RECORD +48 -36
- OTVision/application/detect/detected_frame_producer.py +0 -24
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/WHEEL +0 -0
- {otvision-0.6.7.dist-info → otvision-0.6.9.dist-info}/licenses/LICENSE +0 -0
OTVision/detect/builder.py
CHANGED
|
@@ -3,8 +3,7 @@ from argparse import ArgumentParser
|
|
|
3
3
|
from functools import cached_property
|
|
4
4
|
|
|
5
5
|
from OTVision.abstraction.observer import Subject
|
|
6
|
-
from OTVision.application.
|
|
7
|
-
from OTVision.application.config import Config
|
|
6
|
+
from OTVision.application.config import Config, DetectConfig
|
|
8
7
|
from OTVision.application.config_parser import ConfigParser
|
|
9
8
|
from OTVision.application.configure_logger import ConfigureLogger
|
|
10
9
|
from OTVision.application.detect.current_object_detector import CurrentObjectDetector
|
|
@@ -12,12 +11,6 @@ from OTVision.application.detect.current_object_detector_metadata import (
|
|
|
12
11
|
CurrentObjectDetectorMetadata,
|
|
13
12
|
)
|
|
14
13
|
from OTVision.application.detect.detected_frame_factory import DetectedFrameFactory
|
|
15
|
-
from OTVision.application.detect.detected_frame_producer import (
|
|
16
|
-
SimpleDetectedFrameProducer,
|
|
17
|
-
)
|
|
18
|
-
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
19
|
-
DetectionFileSavePathProvider,
|
|
20
|
-
)
|
|
21
14
|
from OTVision.application.detect.factory import ObjectDetectorCachedFactory
|
|
22
15
|
from OTVision.application.detect.get_detect_cli_args import GetDetectCliArgs
|
|
23
16
|
from OTVision.application.detect.update_detect_config_with_cli_args import (
|
|
@@ -26,16 +19,20 @@ from OTVision.application.detect.update_detect_config_with_cli_args import (
|
|
|
26
19
|
from OTVision.application.frame_count_provider import FrameCountProvider
|
|
27
20
|
from OTVision.application.get_config import GetConfig
|
|
28
21
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
22
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
29
23
|
from OTVision.application.update_current_config import UpdateCurrentConfig
|
|
30
24
|
from OTVision.detect.cli import ArgparseDetectCliParser
|
|
31
25
|
from OTVision.detect.detect import OTVisionVideoDetect
|
|
32
26
|
from OTVision.detect.detected_frame_buffer import (
|
|
33
27
|
DetectedFrameBuffer,
|
|
34
28
|
DetectedFrameBufferEvent,
|
|
35
|
-
FlushEvent,
|
|
36
29
|
)
|
|
37
|
-
from OTVision.detect.
|
|
38
|
-
|
|
30
|
+
from OTVision.detect.detected_frame_producer import (
|
|
31
|
+
DetectedFrameProducerFactory,
|
|
32
|
+
SimpleDetectedFrameProducer,
|
|
33
|
+
)
|
|
34
|
+
from OTVision.detect.otdet import OtdetBuilder, OtdetMetadataBuilder
|
|
35
|
+
from OTVision.detect.otdet_file_writer import OtdetFileWriter, OtdetFileWrittenEvent
|
|
39
36
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
40
37
|
from OTVision.detect.pyav_frame_count_provider import PyAVFrameCountProvider
|
|
41
38
|
from OTVision.detect.timestamper import TimestamperFactory
|
|
@@ -43,10 +40,10 @@ from OTVision.detect.yolo import YoloDetectionConverter, YoloFactory
|
|
|
43
40
|
from OTVision.domain.cli import DetectCliParser
|
|
44
41
|
from OTVision.domain.current_config import CurrentConfig
|
|
45
42
|
from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
|
|
46
|
-
from OTVision.domain.frame import DetectedFrame
|
|
47
43
|
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
48
44
|
from OTVision.domain.object_detection import ObjectDetectorFactory
|
|
49
45
|
from OTVision.domain.serialization import Deserializer
|
|
46
|
+
from OTVision.domain.video_writer import VideoWriter
|
|
50
47
|
from OTVision.plugin.yaml_serialization import YamlDeserializer
|
|
51
48
|
|
|
52
49
|
|
|
@@ -75,7 +72,7 @@ class DetectBuilder(ABC):
|
|
|
75
72
|
|
|
76
73
|
@cached_property
|
|
77
74
|
def otdet_builder(self) -> OtdetBuilder:
|
|
78
|
-
return OtdetBuilder()
|
|
75
|
+
return OtdetBuilder(OtdetMetadataBuilder())
|
|
79
76
|
|
|
80
77
|
@cached_property
|
|
81
78
|
def object_detector_factory(self) -> ObjectDetectorFactory:
|
|
@@ -116,8 +113,8 @@ class DetectBuilder(ABC):
|
|
|
116
113
|
return TimestamperFactory(self.frame_count_provider, self.get_current_config)
|
|
117
114
|
|
|
118
115
|
@cached_property
|
|
119
|
-
def detection_file_save_path_provider(self) ->
|
|
120
|
-
return
|
|
116
|
+
def detection_file_save_path_provider(self) -> OtvisionSavePathProvider:
|
|
117
|
+
return OtvisionSavePathProvider(self.get_current_config)
|
|
121
118
|
|
|
122
119
|
@cached_property
|
|
123
120
|
def frame_count_provider(self) -> FrameCountProvider:
|
|
@@ -126,6 +123,7 @@ class DetectBuilder(ABC):
|
|
|
126
123
|
@cached_property
|
|
127
124
|
def otdet_file_writer(self) -> OtdetFileWriter:
|
|
128
125
|
return OtdetFileWriter(
|
|
126
|
+
subject=Subject[OtdetFileWrittenEvent](),
|
|
129
127
|
builder=self.otdet_builder,
|
|
130
128
|
get_current_config=self.get_current_config,
|
|
131
129
|
current_object_detector_metadata=self.current_object_detector_metadata,
|
|
@@ -144,17 +142,23 @@ class DetectBuilder(ABC):
|
|
|
144
142
|
)
|
|
145
143
|
|
|
146
144
|
@cached_property
|
|
147
|
-
def detected_frame_buffer(
|
|
148
|
-
self,
|
|
149
|
-
) -> Buffer[DetectedFrame, DetectedFrameBufferEvent, FlushEvent]:
|
|
145
|
+
def detected_frame_buffer(self) -> DetectedFrameBuffer:
|
|
150
146
|
return DetectedFrameBuffer(subject=Subject[DetectedFrameBufferEvent]())
|
|
151
147
|
|
|
152
148
|
@cached_property
|
|
153
149
|
def detected_frame_producer(self) -> DetectedFrameProducer:
|
|
154
150
|
return SimpleDetectedFrameProducer(
|
|
151
|
+
producer_factory=self.detected_frame_producer_factory,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
@cached_property
|
|
155
|
+
def detected_frame_producer_factory(self) -> DetectedFrameProducerFactory:
|
|
156
|
+
return DetectedFrameProducerFactory(
|
|
155
157
|
input_source=self.input_source,
|
|
158
|
+
video_writer_filter=self.video_file_writer,
|
|
156
159
|
detection_filter=self.current_object_detector,
|
|
157
160
|
detected_frame_buffer=self.detected_frame_buffer,
|
|
161
|
+
get_current_config=self.get_current_config,
|
|
158
162
|
)
|
|
159
163
|
|
|
160
164
|
@cached_property
|
|
@@ -165,6 +169,10 @@ class DetectBuilder(ABC):
|
|
|
165
169
|
def yaml_deserializer(self) -> Deserializer:
|
|
166
170
|
return YamlDeserializer()
|
|
167
171
|
|
|
172
|
+
@property
|
|
173
|
+
def detect_config(self) -> DetectConfig:
|
|
174
|
+
return self.current_config.get().detect
|
|
175
|
+
|
|
168
176
|
def __init__(self, argv: list[str] | None = None) -> None:
|
|
169
177
|
self.argv = argv
|
|
170
178
|
|
|
@@ -173,6 +181,15 @@ class DetectBuilder(ABC):
|
|
|
173
181
|
def input_source(self) -> InputSourceDetect:
|
|
174
182
|
raise NotImplementedError
|
|
175
183
|
|
|
184
|
+
@property
|
|
185
|
+
@abstractmethod
|
|
186
|
+
def video_file_writer(self) -> VideoWriter:
|
|
187
|
+
raise NotImplementedError
|
|
188
|
+
|
|
189
|
+
@abstractmethod
|
|
190
|
+
def register_observers(self) -> None:
|
|
191
|
+
raise NotImplementedError
|
|
192
|
+
|
|
176
193
|
def build(self) -> OTVisionVideoDetect:
|
|
177
194
|
self.register_observers()
|
|
178
195
|
self._preload_object_detection_model()
|
|
@@ -181,7 +198,3 @@ class DetectBuilder(ABC):
|
|
|
181
198
|
def _preload_object_detection_model(self) -> None:
|
|
182
199
|
model = self.current_object_detector.get()
|
|
183
200
|
model.preload()
|
|
184
|
-
|
|
185
|
-
def register_observers(self) -> None:
|
|
186
|
-
self.input_source.register(self.detected_frame_buffer.on_flush)
|
|
187
|
-
self.detected_frame_buffer.register(self.otdet_file_writer.write)
|
OTVision/detect/cli.py
CHANGED
|
@@ -6,6 +6,11 @@ from OTVision.application.config import DATETIME_FORMAT
|
|
|
6
6
|
from OTVision.domain.cli import CliParseError, DetectCliArgs, DetectCliParser
|
|
7
7
|
from OTVision.helpers.files import check_if_all_paths_exist
|
|
8
8
|
from OTVision.helpers.log import DEFAULT_LOG_FILE, VALID_LOG_LEVELS
|
|
9
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
10
|
+
ConstantRateFactor,
|
|
11
|
+
EncodingSpeed,
|
|
12
|
+
VideoCodec,
|
|
13
|
+
)
|
|
9
14
|
|
|
10
15
|
|
|
11
16
|
class ArgparseDetectCliParser(DetectCliParser):
|
|
@@ -128,6 +133,34 @@ class ArgparseDetectCliParser(DetectCliParser):
|
|
|
128
133
|
help="Specify end of detection in seconds.",
|
|
129
134
|
required=False,
|
|
130
135
|
)
|
|
136
|
+
self._parser.add_argument(
|
|
137
|
+
"--write-video",
|
|
138
|
+
default=None,
|
|
139
|
+
action="store_true",
|
|
140
|
+
help="Write video to output folder. Not supported on Windows.",
|
|
141
|
+
required=False,
|
|
142
|
+
)
|
|
143
|
+
self._parser.add_argument(
|
|
144
|
+
"--video-codec",
|
|
145
|
+
default=None,
|
|
146
|
+
choices=VideoCodec.as_list(),
|
|
147
|
+
help="Video codec for video writer. Default is 'libx264'",
|
|
148
|
+
required=False,
|
|
149
|
+
)
|
|
150
|
+
self._parser.add_argument(
|
|
151
|
+
"--encoding-speed",
|
|
152
|
+
default=None,
|
|
153
|
+
choices=EncodingSpeed.as_list(),
|
|
154
|
+
help="Encoding speed for video writer. Default is 'fast'",
|
|
155
|
+
required=False,
|
|
156
|
+
)
|
|
157
|
+
self._parser.add_argument(
|
|
158
|
+
"--crf",
|
|
159
|
+
default=None,
|
|
160
|
+
choices=ConstantRateFactor.as_list(),
|
|
161
|
+
help="Constant rate factor for video writer. Default is 'DEFAULT'",
|
|
162
|
+
required=False,
|
|
163
|
+
)
|
|
131
164
|
|
|
132
165
|
def parse(self) -> DetectCliArgs:
|
|
133
166
|
args = self._parser.parse_args(self._argv)
|
|
@@ -151,11 +184,21 @@ class ArgparseDetectCliParser(DetectCliParser):
|
|
|
151
184
|
detect_start=(
|
|
152
185
|
int(args.detect_start) if args.detect_start is not None else None
|
|
153
186
|
),
|
|
154
|
-
detect_end=int(args.detect_end) if args.detect_end is not None else None,
|
|
187
|
+
detect_end=(int(args.detect_end) if args.detect_end is not None else None),
|
|
155
188
|
logfile=Path(args.logfile),
|
|
156
189
|
log_level_console=args.log_level_console,
|
|
157
190
|
log_level_file=args.log_level_file,
|
|
158
191
|
logfile_overwrite=args.logfile_overwrite,
|
|
192
|
+
write_video=args.write_video,
|
|
193
|
+
video_codec=(
|
|
194
|
+
VideoCodec(args.video_codec) if args.video_codec is not None else None
|
|
195
|
+
),
|
|
196
|
+
encoding_speed=(
|
|
197
|
+
EncodingSpeed(args.encoding_speed)
|
|
198
|
+
if args.encoding_speed is not None
|
|
199
|
+
else None
|
|
200
|
+
),
|
|
201
|
+
crf=ConstantRateFactor[args.crf] if args.crf is not None else None,
|
|
159
202
|
)
|
|
160
203
|
|
|
161
204
|
def _parse_start_time(self, start_time: str | None) -> datetime | None:
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from dataclasses import dataclass
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
|
|
4
|
+
from OTVision.abstraction.observer import Observable, Subject
|
|
4
5
|
from OTVision.application.buffer import Buffer
|
|
5
6
|
from OTVision.domain.frame import DetectedFrame
|
|
6
7
|
|
|
@@ -49,7 +50,18 @@ class DetectedFrameBufferEvent:
|
|
|
49
50
|
frames: list[DetectedFrame]
|
|
50
51
|
|
|
51
52
|
|
|
52
|
-
class DetectedFrameBuffer(
|
|
53
|
+
class DetectedFrameBuffer(
|
|
54
|
+
Buffer[DetectedFrame, FlushEvent], Observable[DetectedFrameBufferEvent]
|
|
55
|
+
):
|
|
56
|
+
def __init__(self, subject: Subject[DetectedFrameBufferEvent]) -> None:
|
|
57
|
+
Buffer.__init__(self)
|
|
58
|
+
Observable.__init__(self, subject)
|
|
59
|
+
|
|
60
|
+
def on_flush(self, event: FlushEvent) -> None:
|
|
61
|
+
buffered_elements = self._get_buffered_elements()
|
|
62
|
+
self._notify_observers(buffered_elements, event)
|
|
63
|
+
self._reset_buffer()
|
|
64
|
+
|
|
53
65
|
def _notify_observers(
|
|
54
66
|
self, elements: list[DetectedFrame], event: FlushEvent
|
|
55
67
|
) -> None:
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
from OTVision.detect.detected_frame_producer_factory import DetectedFrameProducerFactory
|
|
4
|
+
from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
|
|
5
|
+
from OTVision.domain.frame import DetectedFrame
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class SimpleDetectedFrameProducer(DetectedFrameProducer):
|
|
9
|
+
|
|
10
|
+
def __init__(self, producer_factory: DetectedFrameProducerFactory) -> None:
|
|
11
|
+
self._producer_factory = producer_factory
|
|
12
|
+
|
|
13
|
+
def produce(self) -> Iterator[DetectedFrame]:
|
|
14
|
+
return self._producer_factory.create()
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Iterator
|
|
2
|
+
|
|
3
|
+
from OTVision.abstraction.pipes_and_filter import Filter
|
|
4
|
+
from OTVision.application.get_current_config import GetCurrentConfig
|
|
5
|
+
from OTVision.domain.frame import DetectedFrame, Frame
|
|
6
|
+
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DetectedFrameProducerFactory:
|
|
10
|
+
def __init__(
|
|
11
|
+
self,
|
|
12
|
+
input_source: InputSourceDetect,
|
|
13
|
+
video_writer_filter: Filter[Frame, Frame],
|
|
14
|
+
detection_filter: Filter[Frame, DetectedFrame],
|
|
15
|
+
detected_frame_buffer: Filter[DetectedFrame, DetectedFrame],
|
|
16
|
+
get_current_config: GetCurrentConfig,
|
|
17
|
+
) -> None:
|
|
18
|
+
self._input_source = input_source
|
|
19
|
+
self._video_writer_filter = video_writer_filter
|
|
20
|
+
self._detection_filter = detection_filter
|
|
21
|
+
self._detected_frame_buffer = detected_frame_buffer
|
|
22
|
+
self._get_current_config = get_current_config
|
|
23
|
+
|
|
24
|
+
def create(self) -> Iterator[DetectedFrame]:
|
|
25
|
+
if self._get_current_config.get().detect.write_video:
|
|
26
|
+
return self.__create_with_video_writer()
|
|
27
|
+
return self.__create_without_video_writer()
|
|
28
|
+
|
|
29
|
+
def __create_without_video_writer(self) -> Iterator[DetectedFrame]:
|
|
30
|
+
return self._detected_frame_buffer.filter(
|
|
31
|
+
self._detection_filter.filter(self._input_source.produce())
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
def __create_with_video_writer(self) -> Iterator[DetectedFrame]:
|
|
35
|
+
return self._detected_frame_buffer.filter(
|
|
36
|
+
self._detection_filter.filter(
|
|
37
|
+
self._video_writer_filter.filter(self._input_source.produce())
|
|
38
|
+
)
|
|
39
|
+
)
|
|
@@ -1,19 +1,55 @@
|
|
|
1
1
|
from functools import cached_property
|
|
2
2
|
|
|
3
3
|
from OTVision.abstraction.observer import Subject
|
|
4
|
+
from OTVision.application.event.new_video_start import NewVideoStartEvent
|
|
4
5
|
from OTVision.detect.builder import DetectBuilder
|
|
5
6
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
6
7
|
from OTVision.detect.video_input_source import VideoSource
|
|
7
|
-
from OTVision.domain.
|
|
8
|
+
from OTVision.domain.video_writer import VideoWriter
|
|
9
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
10
|
+
FfmpegVideoWriter,
|
|
11
|
+
PixelFormat,
|
|
12
|
+
VideoFormat,
|
|
13
|
+
append_save_suffix_to_save_location,
|
|
14
|
+
)
|
|
8
15
|
|
|
9
16
|
|
|
10
17
|
class FileBasedDetectBuilder(DetectBuilder):
|
|
18
|
+
|
|
11
19
|
@cached_property
|
|
12
|
-
def input_source(self) ->
|
|
20
|
+
def input_source(self) -> VideoSource:
|
|
13
21
|
return VideoSource(
|
|
14
|
-
|
|
22
|
+
subject_flush=Subject[FlushEvent](),
|
|
23
|
+
subject_new_video_start=Subject[NewVideoStartEvent](),
|
|
15
24
|
get_current_config=self.get_current_config,
|
|
16
25
|
frame_rotator=self.frame_rotator,
|
|
17
26
|
timestamper_factory=self.timestamper_factory,
|
|
18
27
|
save_path_provider=self.detection_file_save_path_provider,
|
|
19
28
|
)
|
|
29
|
+
|
|
30
|
+
@cached_property
|
|
31
|
+
def video_file_writer(self) -> VideoWriter:
|
|
32
|
+
# Using save_location_strategy=keep_original_save_location is not supported for
|
|
33
|
+
# file-based detection. Otherwise, we would be overwriting the input source that
|
|
34
|
+
# we are reading from.
|
|
35
|
+
return FfmpegVideoWriter(
|
|
36
|
+
save_location_strategy=append_save_suffix_to_save_location,
|
|
37
|
+
encoding_speed=self.detect_config.encoding_speed,
|
|
38
|
+
input_format=VideoFormat.RAW,
|
|
39
|
+
output_format=VideoFormat.MP4,
|
|
40
|
+
input_pixel_format=PixelFormat.RGB24,
|
|
41
|
+
output_pixel_format=PixelFormat.YUV420P,
|
|
42
|
+
output_video_codec=self.detect_config.video_codec,
|
|
43
|
+
constant_rate_factor=self.detect_config.crf,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def register_observers(self) -> None:
|
|
47
|
+
if self.detect_config.write_video:
|
|
48
|
+
self.input_source.subject_new_video_start.register(
|
|
49
|
+
self.video_file_writer.notify_on_new_video_start
|
|
50
|
+
)
|
|
51
|
+
self.input_source.subject_flush.register(
|
|
52
|
+
self.video_file_writer.notify_on_flush_event
|
|
53
|
+
)
|
|
54
|
+
self.input_source.subject_flush.register(self.detected_frame_buffer.on_flush)
|
|
55
|
+
self.detected_frame_buffer.register(self.otdet_file_writer.write)
|
OTVision/detect/otdet.py
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import re
|
|
1
2
|
from dataclasses import dataclass
|
|
2
3
|
from datetime import datetime, timedelta
|
|
3
4
|
from pathlib import Path
|
|
@@ -6,6 +7,14 @@ from typing import Self
|
|
|
6
7
|
from OTVision import dataformat, version
|
|
7
8
|
from OTVision.domain.detection import Detection
|
|
8
9
|
from OTVision.domain.frame import DetectedFrame
|
|
10
|
+
from OTVision.helpers.date import parse_datetime
|
|
11
|
+
from OTVision.helpers.files import (
|
|
12
|
+
FULL_FILE_NAME_PATTERN,
|
|
13
|
+
HOSTNAME,
|
|
14
|
+
InproperFormattedFilename,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
MISSING_START_DATE = datetime(1900, 1, 1)
|
|
9
18
|
|
|
10
19
|
|
|
11
20
|
@dataclass
|
|
@@ -35,7 +44,7 @@ class OtdetBuilderError(Exception):
|
|
|
35
44
|
pass
|
|
36
45
|
|
|
37
46
|
|
|
38
|
-
class
|
|
47
|
+
class OtdetMetadataBuilder:
|
|
39
48
|
@property
|
|
40
49
|
def config(self) -> OtdetBuilderConfig:
|
|
41
50
|
if self._config is None:
|
|
@@ -45,51 +54,14 @@ class OtdetBuilder:
|
|
|
45
54
|
def __init__(self) -> None:
|
|
46
55
|
self._config: OtdetBuilderConfig | None = None
|
|
47
56
|
|
|
48
|
-
def
|
|
49
|
-
self._config = config
|
|
50
|
-
return self
|
|
51
|
-
|
|
52
|
-
def reset(self) -> Self:
|
|
53
|
-
self._config = None
|
|
54
|
-
return self
|
|
55
|
-
|
|
56
|
-
def build(self, detections: list[DetectedFrame]) -> dict:
|
|
57
|
-
number_of_frames = len(detections)
|
|
57
|
+
def build(self, number_of_frames: int) -> dict:
|
|
58
58
|
result = {
|
|
59
|
-
dataformat.METADATA: self._build_metadata(number_of_frames),
|
|
60
|
-
dataformat.DATA: self._build_data(detections),
|
|
61
|
-
}
|
|
62
|
-
self.reset()
|
|
63
|
-
return result
|
|
64
|
-
|
|
65
|
-
def _build_metadata(self, number_of_frames: int) -> dict:
|
|
66
|
-
return {
|
|
67
59
|
dataformat.OTDET_VERSION: version.otdet_version(),
|
|
68
60
|
dataformat.VIDEO: self._build_video_config(number_of_frames),
|
|
69
61
|
dataformat.DETECTION: self._build_detection_config(),
|
|
70
62
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
data = {}
|
|
74
|
-
for frame in frames:
|
|
75
|
-
converted_detections = [
|
|
76
|
-
self.__convert_detection(detection) for detection in frame.detections
|
|
77
|
-
]
|
|
78
|
-
data[str(frame.no)] = {
|
|
79
|
-
dataformat.DETECTIONS: converted_detections,
|
|
80
|
-
dataformat.OCCURRENCE: frame.occurrence.timestamp(),
|
|
81
|
-
}
|
|
82
|
-
return data
|
|
83
|
-
|
|
84
|
-
def __convert_detection(self, detection: Detection) -> dict:
|
|
85
|
-
return {
|
|
86
|
-
dataformat.CLASS: detection.label,
|
|
87
|
-
dataformat.CONFIDENCE: detection.conf,
|
|
88
|
-
dataformat.X: detection.x,
|
|
89
|
-
dataformat.Y: detection.y,
|
|
90
|
-
dataformat.W: detection.w,
|
|
91
|
-
dataformat.H: detection.h,
|
|
92
|
-
}
|
|
63
|
+
self.reset()
|
|
64
|
+
return result
|
|
93
65
|
|
|
94
66
|
def _build_video_config(self, number_of_frames: int) -> dict:
|
|
95
67
|
source = Path(self.config.source)
|
|
@@ -128,6 +100,69 @@ class OtdetBuilder:
|
|
|
128
100
|
dataformat.DETECT_END: self.config.detect_end,
|
|
129
101
|
}
|
|
130
102
|
|
|
103
|
+
def add_config(self, config: OtdetBuilderConfig) -> Self:
|
|
104
|
+
self._config = config
|
|
105
|
+
return self
|
|
106
|
+
|
|
107
|
+
def reset(self) -> Self:
|
|
108
|
+
self._config = None
|
|
109
|
+
return self
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
class OtdetBuilder:
|
|
113
|
+
@property
|
|
114
|
+
def config(self) -> OtdetBuilderConfig:
|
|
115
|
+
if self._config is None:
|
|
116
|
+
raise OtdetBuilderError("Otdet builder config is not set")
|
|
117
|
+
return self._config
|
|
118
|
+
|
|
119
|
+
def __init__(self, metadata_builder: OtdetMetadataBuilder) -> None:
|
|
120
|
+
self._config: OtdetBuilderConfig | None = None
|
|
121
|
+
self._metadata_builder = metadata_builder
|
|
122
|
+
|
|
123
|
+
def add_config(self, config: OtdetBuilderConfig) -> Self:
|
|
124
|
+
self._config = config
|
|
125
|
+
self._metadata_builder.add_config(config)
|
|
126
|
+
return self
|
|
127
|
+
|
|
128
|
+
def reset(self) -> Self:
|
|
129
|
+
self._config = None
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
def build(self, detections: list[DetectedFrame]) -> dict:
|
|
133
|
+
number_of_frames = len(detections)
|
|
134
|
+
result = {
|
|
135
|
+
dataformat.METADATA: self._build_metadata(number_of_frames),
|
|
136
|
+
dataformat.DATA: self._build_data(detections),
|
|
137
|
+
}
|
|
138
|
+
self.reset()
|
|
139
|
+
return result
|
|
140
|
+
|
|
141
|
+
def _build_metadata(self, number_of_frames: int) -> dict:
|
|
142
|
+
return self._metadata_builder.build(number_of_frames)
|
|
143
|
+
|
|
144
|
+
def _build_data(self, frames: list[DetectedFrame]) -> dict:
|
|
145
|
+
data = {}
|
|
146
|
+
for frame in frames:
|
|
147
|
+
converted_detections = [
|
|
148
|
+
self.__convert_detection(detection) for detection in frame.detections
|
|
149
|
+
]
|
|
150
|
+
data[str(frame.no)] = {
|
|
151
|
+
dataformat.DETECTIONS: converted_detections,
|
|
152
|
+
dataformat.OCCURRENCE: frame.occurrence.timestamp(),
|
|
153
|
+
}
|
|
154
|
+
return data
|
|
155
|
+
|
|
156
|
+
def __convert_detection(self, detection: Detection) -> dict:
|
|
157
|
+
return {
|
|
158
|
+
dataformat.CLASS: detection.label,
|
|
159
|
+
dataformat.CONFIDENCE: detection.conf,
|
|
160
|
+
dataformat.X: detection.x,
|
|
161
|
+
dataformat.Y: detection.y,
|
|
162
|
+
dataformat.W: detection.w,
|
|
163
|
+
dataformat.H: detection.h,
|
|
164
|
+
}
|
|
165
|
+
|
|
131
166
|
|
|
132
167
|
def serialize_video_length(video_length: timedelta) -> str:
|
|
133
168
|
"""Serialize a timedelta object to a video length string in 'H+:MM:SS' format.
|
|
@@ -180,3 +215,36 @@ def parse_video_length(video_length: str) -> timedelta:
|
|
|
180
215
|
f"Could not parse video length '{video_length}'. "
|
|
181
216
|
"Expected format 'HH:MM:SS'."
|
|
182
217
|
) from cause
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def extract_start_date_from_otdet(metadata: dict) -> datetime:
|
|
221
|
+
if dataformat.RECORDED_START_DATE in metadata[dataformat.VIDEO].keys():
|
|
222
|
+
recorded_start_date = metadata[dataformat.VIDEO][dataformat.RECORDED_START_DATE]
|
|
223
|
+
return parse_datetime(recorded_start_date)
|
|
224
|
+
return MISSING_START_DATE
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def extract_expected_duration_from_otdet(metadata: dict) -> timedelta:
|
|
228
|
+
if dataformat.EXPECTED_DURATION in metadata[dataformat.VIDEO].keys():
|
|
229
|
+
if expected_duration := metadata[dataformat.VIDEO][
|
|
230
|
+
dataformat.EXPECTED_DURATION
|
|
231
|
+
]:
|
|
232
|
+
return timedelta(seconds=int(expected_duration))
|
|
233
|
+
return extract_otdet_video_length(metadata)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def extract_otdet_video_length(metadata: dict) -> timedelta:
|
|
237
|
+
video_length = metadata[dataformat.VIDEO][dataformat.LENGTH]
|
|
238
|
+
return parse_video_length(video_length)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def extract_hostname_from_otdet(metadata: dict) -> str:
|
|
242
|
+
video_name = Path(metadata[dataformat.VIDEO][dataformat.FILENAME]).name
|
|
243
|
+
match = re.search(
|
|
244
|
+
FULL_FILE_NAME_PATTERN,
|
|
245
|
+
video_name,
|
|
246
|
+
)
|
|
247
|
+
if match:
|
|
248
|
+
return match.group(HOSTNAME)
|
|
249
|
+
|
|
250
|
+
raise InproperFormattedFilename(f"Could not parse {video_name}.")
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import logging
|
|
2
|
+
from dataclasses import dataclass
|
|
2
3
|
|
|
4
|
+
from OTVision.abstraction.observer import Observer, Subject
|
|
3
5
|
from OTVision.application.detect.current_object_detector_metadata import (
|
|
4
6
|
CurrentObjectDetectorMetadata,
|
|
5
7
|
)
|
|
6
|
-
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
7
|
-
DetectionFileSavePathProvider,
|
|
8
|
-
)
|
|
9
8
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
9
|
+
from OTVision.application.otvision_save_path_provider import OtvisionSavePathProvider
|
|
10
10
|
from OTVision.detect.detected_frame_buffer import DetectedFrameBufferEvent
|
|
11
11
|
from OTVision.detect.otdet import OtdetBuilder, OtdetBuilderConfig
|
|
12
12
|
from OTVision.helpers.files import write_json
|
|
@@ -15,6 +15,14 @@ from OTVision.helpers.log import LOGGER_NAME
|
|
|
15
15
|
log = logging.getLogger(LOGGER_NAME)
|
|
16
16
|
|
|
17
17
|
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class OtdetFileWrittenEvent:
|
|
20
|
+
"""Event that is emitted when an OTDET file is written."""
|
|
21
|
+
|
|
22
|
+
otdet_builder_config: OtdetBuilderConfig
|
|
23
|
+
number_of_frames: int
|
|
24
|
+
|
|
25
|
+
|
|
18
26
|
class OtdetFileWriter:
|
|
19
27
|
"""Handles writing object detection results to a file in OTDET format.
|
|
20
28
|
|
|
@@ -28,18 +36,20 @@ class OtdetFileWriter:
|
|
|
28
36
|
settings.
|
|
29
37
|
current_object_detector_metadata (CurrentObjectDetectorMetadata): Provides
|
|
30
38
|
metadata about the current object detector.
|
|
31
|
-
save_path_provider (
|
|
39
|
+
save_path_provider (OtvisionSavePathProvider): determines the save path for
|
|
32
40
|
the otdet file to be written.
|
|
33
41
|
|
|
34
42
|
"""
|
|
35
43
|
|
|
36
44
|
def __init__(
|
|
37
45
|
self,
|
|
46
|
+
subject: Subject[OtdetFileWrittenEvent],
|
|
38
47
|
builder: OtdetBuilder,
|
|
39
48
|
get_current_config: GetCurrentConfig,
|
|
40
49
|
current_object_detector_metadata: CurrentObjectDetectorMetadata,
|
|
41
|
-
save_path_provider:
|
|
50
|
+
save_path_provider: OtvisionSavePathProvider,
|
|
42
51
|
):
|
|
52
|
+
self._subject = subject
|
|
43
53
|
self._builder = builder
|
|
44
54
|
self._get_current_config = get_current_config
|
|
45
55
|
self._current_object_detector_metadata = current_object_detector_metadata
|
|
@@ -68,31 +78,32 @@ class OtdetFileWriter:
|
|
|
68
78
|
actual_fps = actual_frames / source_metadata.duration.total_seconds()
|
|
69
79
|
|
|
70
80
|
class_mapping = self._current_object_detector_metadata.get().classifications
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
).build(event.frames)
|
|
81
|
+
builder_config = OtdetBuilderConfig(
|
|
82
|
+
conf=detect_config.confidence,
|
|
83
|
+
iou=detect_config.iou,
|
|
84
|
+
source=source_metadata.output,
|
|
85
|
+
video_width=source_metadata.width,
|
|
86
|
+
video_height=source_metadata.height,
|
|
87
|
+
expected_duration=expected_duration,
|
|
88
|
+
actual_duration=source_metadata.duration,
|
|
89
|
+
recorded_fps=source_metadata.fps,
|
|
90
|
+
recorded_start_date=source_metadata.start_time,
|
|
91
|
+
actual_fps=actual_fps,
|
|
92
|
+
actual_frames=actual_frames,
|
|
93
|
+
detection_img_size=detect_config.img_size,
|
|
94
|
+
normalized=detect_config.normalized,
|
|
95
|
+
detection_model=detect_config.weights,
|
|
96
|
+
half_precision=detect_config.half_precision,
|
|
97
|
+
chunksize=1,
|
|
98
|
+
classifications=class_mapping,
|
|
99
|
+
detect_start=detect_config.detect_start,
|
|
100
|
+
detect_end=detect_config.detect_end,
|
|
101
|
+
)
|
|
102
|
+
otdet = self._builder.add_config(builder_config).build(event.frames)
|
|
94
103
|
|
|
95
|
-
detections_file = self._save_path_provider.provide(
|
|
104
|
+
detections_file = self._save_path_provider.provide(
|
|
105
|
+
source_metadata.output, config.filetypes.detect
|
|
106
|
+
)
|
|
96
107
|
detections_file.parent.mkdir(parents=True, exist_ok=True)
|
|
97
108
|
write_json(
|
|
98
109
|
otdet,
|
|
@@ -105,3 +116,15 @@ class OtdetFileWriter:
|
|
|
105
116
|
|
|
106
117
|
finished_msg = "Finished detection"
|
|
107
118
|
log.info(finished_msg)
|
|
119
|
+
self.__notify(num_frames=actual_frames, builder_config=builder_config)
|
|
120
|
+
|
|
121
|
+
def __notify(self, num_frames: int, builder_config: OtdetBuilderConfig) -> None:
|
|
122
|
+
self._subject.notify(
|
|
123
|
+
OtdetFileWrittenEvent(
|
|
124
|
+
number_of_frames=num_frames, otdet_builder_config=builder_config
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
def register_observer(self, observer: Observer[OtdetFileWrittenEvent]) -> None:
|
|
129
|
+
"""Register an observer to receive notifications about otdet file writes.."""
|
|
130
|
+
self._subject.register(observer)
|