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