OTVision 0.6.4__py3-none-any.whl → 0.6.6__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/defaults.py +15 -0
- OTVision/application/config.py +475 -0
- OTVision/application/config_parser.py +280 -0
- OTVision/application/configure_logger.py +1 -1
- OTVision/application/detect/detected_frame_factory.py +1 -0
- OTVision/application/detect/factory.py +1 -1
- OTVision/application/detect/update_detect_config_with_cli_args.py +2 -1
- OTVision/application/get_config.py +2 -1
- OTVision/application/get_current_config.py +1 -1
- OTVision/application/track/__init__.py +0 -0
- OTVision/application/track/get_track_cli_args.py +20 -0
- OTVision/application/track/update_current_track_config.py +42 -0
- OTVision/application/track/update_track_config_with_cli_args.py +52 -0
- OTVision/application/update_current_config.py +1 -1
- OTVision/config.py +61 -668
- OTVision/convert/convert.py +3 -3
- OTVision/detect/builder.py +27 -20
- OTVision/detect/cli.py +3 -3
- OTVision/detect/detected_frame_buffer.py +3 -0
- OTVision/detect/file_based_detect_builder.py +19 -0
- OTVision/detect/otdet.py +54 -1
- OTVision/detect/otdet_file_writer.py +4 -3
- OTVision/detect/rtsp_based_detect_builder.py +37 -0
- OTVision/detect/rtsp_input_source.py +207 -0
- OTVision/detect/timestamper.py +2 -1
- OTVision/detect/video_input_source.py +9 -5
- OTVision/detect/yolo.py +17 -1
- OTVision/domain/cli.py +31 -1
- OTVision/domain/current_config.py +1 -1
- OTVision/domain/frame.py +6 -0
- OTVision/domain/object_detection.py +6 -1
- OTVision/domain/serialization.py +12 -0
- OTVision/domain/time.py +13 -0
- OTVision/helpers/files.py +14 -15
- OTVision/plugin/__init__.py +0 -0
- OTVision/plugin/yaml_serialization.py +20 -0
- OTVision/track/builder.py +132 -0
- OTVision/track/cli.py +128 -0
- OTVision/track/exporter/filebased_exporter.py +2 -1
- OTVision/track/id_generator.py +15 -0
- OTVision/track/model/track_exporter.py +2 -1
- OTVision/track/model/tracking_interfaces.py +6 -6
- OTVision/track/parser/chunk_parser_plugins.py +1 -0
- OTVision/track/parser/frame_group_parser_plugins.py +35 -5
- OTVision/track/track.py +54 -133
- OTVision/track/tracker/filebased_tracking.py +8 -7
- OTVision/track/tracker/tracker_plugin_iou.py +15 -9
- OTVision/transform/transform.py +2 -2
- OTVision/version.py +1 -1
- otvision-0.6.6.dist-info/METADATA +182 -0
- {otvision-0.6.4.dist-info → otvision-0.6.6.dist-info}/RECORD +53 -36
- otvision-0.6.4.dist-info/METADATA +0 -49
- {otvision-0.6.4.dist-info → otvision-0.6.6.dist-info}/WHEEL +0 -0
- {otvision-0.6.4.dist-info → otvision-0.6.6.dist-info}/licenses/LICENSE +0 -0
OTVision/convert/convert.py
CHANGED
|
@@ -27,8 +27,7 @@ from typing import Optional
|
|
|
27
27
|
|
|
28
28
|
from tqdm import tqdm
|
|
29
29
|
|
|
30
|
-
from OTVision.config import (
|
|
31
|
-
CONFIG,
|
|
30
|
+
from OTVision.application.config import (
|
|
32
31
|
CONVERT,
|
|
33
32
|
DELETE_INPUT,
|
|
34
33
|
FILETYPES,
|
|
@@ -40,6 +39,7 @@ from OTVision.config import (
|
|
|
40
39
|
VID,
|
|
41
40
|
VID_ROTATABLE,
|
|
42
41
|
)
|
|
42
|
+
from OTVision.config import CONFIG
|
|
43
43
|
from OTVision.helpers.files import get_files
|
|
44
44
|
from OTVision.helpers.formats import _get_fps_from_filename
|
|
45
45
|
from OTVision.helpers.log import LOGGER_NAME
|
|
@@ -140,7 +140,7 @@ def convert(
|
|
|
140
140
|
overwrite (bool, optional): Whether to overwrite existing video files.
|
|
141
141
|
Defaults to CONFIG["CONVERT"]["OVERWRITE"].
|
|
142
142
|
delete_input (bool, optional): Whether to delete the input h264.
|
|
143
|
-
|
|
143
|
+
Defaults to CONFIG["CONVERT"]["DELETE_INPUT"].
|
|
144
144
|
|
|
145
145
|
Raises:
|
|
146
146
|
TypeError: If output video filetype is not supported.
|
OTVision/detect/builder.py
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
1
2
|
from argparse import ArgumentParser
|
|
2
3
|
from functools import cached_property
|
|
3
4
|
|
|
4
5
|
from OTVision.abstraction.observer import Subject
|
|
5
6
|
from OTVision.application.buffer import Buffer
|
|
7
|
+
from OTVision.application.config import Config
|
|
8
|
+
from OTVision.application.config_parser import ConfigParser
|
|
6
9
|
from OTVision.application.configure_logger import ConfigureLogger
|
|
7
10
|
from OTVision.application.detect.current_object_detector import CurrentObjectDetector
|
|
8
11
|
from OTVision.application.detect.current_object_detector_metadata import (
|
|
@@ -24,7 +27,6 @@ from OTVision.application.frame_count_provider import FrameCountProvider
|
|
|
24
27
|
from OTVision.application.get_config import GetConfig
|
|
25
28
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
26
29
|
from OTVision.application.update_current_config import UpdateCurrentConfig
|
|
27
|
-
from OTVision.config import Config, ConfigParser
|
|
28
30
|
from OTVision.detect.cli import ArgparseDetectCliParser
|
|
29
31
|
from OTVision.detect.detect import OTVisionVideoDetect
|
|
30
32
|
from OTVision.detect.detected_frame_buffer import (
|
|
@@ -37,7 +39,6 @@ from OTVision.detect.otdet_file_writer import OtdetFileWriter
|
|
|
37
39
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
38
40
|
from OTVision.detect.pyav_frame_count_provider import PyAVFrameCountProvider
|
|
39
41
|
from OTVision.detect.timestamper import TimestamperFactory
|
|
40
|
-
from OTVision.detect.video_input_source import VideoSource
|
|
41
42
|
from OTVision.detect.yolo import YoloDetectionConverter, YoloFactory
|
|
42
43
|
from OTVision.domain.cli import DetectCliParser
|
|
43
44
|
from OTVision.domain.current_config import CurrentConfig
|
|
@@ -45,17 +46,15 @@ from OTVision.domain.detect_producer_consumer import DetectedFrameProducer
|
|
|
45
46
|
from OTVision.domain.frame import DetectedFrame
|
|
46
47
|
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
47
48
|
from OTVision.domain.object_detection import ObjectDetectorFactory
|
|
49
|
+
from OTVision.domain.serialization import Deserializer
|
|
50
|
+
from OTVision.plugin.yaml_serialization import YamlDeserializer
|
|
48
51
|
|
|
49
52
|
|
|
50
|
-
class DetectBuilder:
|
|
53
|
+
class DetectBuilder(ABC):
|
|
51
54
|
@cached_property
|
|
52
55
|
def get_config(self) -> GetConfig:
|
|
53
56
|
return GetConfig(self.config_parser)
|
|
54
57
|
|
|
55
|
-
@cached_property
|
|
56
|
-
def config_parser(self) -> ConfigParser:
|
|
57
|
-
return ConfigParser()
|
|
58
|
-
|
|
59
58
|
@cached_property
|
|
60
59
|
def get_detect_cli_args(self) -> GetDetectCliArgs:
|
|
61
60
|
return GetDetectCliArgs(self.detect_cli_parser)
|
|
@@ -108,19 +107,6 @@ class DetectBuilder:
|
|
|
108
107
|
def update_current_config(self) -> UpdateCurrentConfig:
|
|
109
108
|
return UpdateCurrentConfig(self.current_config)
|
|
110
109
|
|
|
111
|
-
def __init__(self, argv: list[str] | None = None) -> None:
|
|
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
110
|
@cached_property
|
|
125
111
|
def frame_rotator(self) -> AvVideoFrameRotator:
|
|
126
112
|
return AvVideoFrameRotator()
|
|
@@ -171,10 +157,31 @@ class DetectBuilder:
|
|
|
171
157
|
detected_frame_buffer=self.detected_frame_buffer,
|
|
172
158
|
)
|
|
173
159
|
|
|
160
|
+
@cached_property
|
|
161
|
+
def config_parser(self) -> ConfigParser:
|
|
162
|
+
return ConfigParser(self.yaml_deserializer)
|
|
163
|
+
|
|
164
|
+
@cached_property
|
|
165
|
+
def yaml_deserializer(self) -> Deserializer:
|
|
166
|
+
return YamlDeserializer()
|
|
167
|
+
|
|
168
|
+
def __init__(self, argv: list[str] | None = None) -> None:
|
|
169
|
+
self.argv = argv
|
|
170
|
+
|
|
171
|
+
@property
|
|
172
|
+
@abstractmethod
|
|
173
|
+
def input_source(self) -> InputSourceDetect:
|
|
174
|
+
raise NotImplementedError
|
|
175
|
+
|
|
174
176
|
def build(self) -> OTVisionVideoDetect:
|
|
175
177
|
self.register_observers()
|
|
178
|
+
self._preload_object_detection_model()
|
|
176
179
|
return OTVisionVideoDetect(self.detected_frame_producer)
|
|
177
180
|
|
|
181
|
+
def _preload_object_detection_model(self) -> None:
|
|
182
|
+
model = self.current_object_detector.get()
|
|
183
|
+
model.preload()
|
|
184
|
+
|
|
178
185
|
def register_observers(self) -> None:
|
|
179
186
|
self.input_source.register(self.detected_frame_buffer.on_flush)
|
|
180
187
|
self.detected_frame_buffer.register(self.otdet_file_writer.write)
|
OTVision/detect/cli.py
CHANGED
|
@@ -2,7 +2,7 @@ from argparse import ArgumentParser, BooleanOptionalAction, Namespace
|
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
-
from OTVision.config import DATETIME_FORMAT
|
|
5
|
+
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
|
|
@@ -172,10 +172,10 @@ class ArgparseDetectCliParser(DetectCliParser):
|
|
|
172
172
|
)
|
|
173
173
|
)
|
|
174
174
|
|
|
175
|
-
def _parse_files(self, files: list[str] | None) -> list[
|
|
175
|
+
def _parse_files(self, files: list[str] | None) -> list[str] | None:
|
|
176
176
|
if files is None:
|
|
177
177
|
return None
|
|
178
178
|
|
|
179
179
|
result = [Path(file).expanduser() for file in files]
|
|
180
180
|
check_if_all_paths_exist(result)
|
|
181
|
-
return result
|
|
181
|
+
return list(map(str, result))
|
|
@@ -8,6 +8,7 @@ from OTVision.domain.frame import DetectedFrame
|
|
|
8
8
|
@dataclass
|
|
9
9
|
class SourceMetadata:
|
|
10
10
|
source: str
|
|
11
|
+
output: str
|
|
11
12
|
duration: timedelta
|
|
12
13
|
height: int
|
|
13
14
|
width: int
|
|
@@ -22,6 +23,7 @@ class FlushEvent:
|
|
|
22
23
|
@staticmethod
|
|
23
24
|
def create(
|
|
24
25
|
source: str,
|
|
26
|
+
output: str,
|
|
25
27
|
duration: timedelta,
|
|
26
28
|
source_height: int,
|
|
27
29
|
source_width: int,
|
|
@@ -31,6 +33,7 @@ class FlushEvent:
|
|
|
31
33
|
return FlushEvent(
|
|
32
34
|
SourceMetadata(
|
|
33
35
|
source,
|
|
36
|
+
output,
|
|
34
37
|
duration,
|
|
35
38
|
source_height,
|
|
36
39
|
source_width,
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from OTVision.abstraction.observer import Subject
|
|
4
|
+
from OTVision.detect.builder import DetectBuilder
|
|
5
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
6
|
+
from OTVision.detect.video_input_source import VideoSource
|
|
7
|
+
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class FileBasedDetectBuilder(DetectBuilder):
|
|
11
|
+
@cached_property
|
|
12
|
+
def input_source(self) -> InputSourceDetect:
|
|
13
|
+
return VideoSource(
|
|
14
|
+
subject=Subject[FlushEvent](),
|
|
15
|
+
get_current_config=self.get_current_config,
|
|
16
|
+
frame_rotator=self.frame_rotator,
|
|
17
|
+
timestamper_factory=self.timestamper_factory,
|
|
18
|
+
save_path_provider=self.detection_file_save_path_provider,
|
|
19
|
+
)
|
OTVision/detect/otdet.py
CHANGED
|
@@ -102,7 +102,7 @@ class OtdetBuilder:
|
|
|
102
102
|
dataformat.ACTUAL_FPS: self.config.actual_fps,
|
|
103
103
|
dataformat.NUMBER_OF_FRAMES: number_of_frames,
|
|
104
104
|
dataformat.RECORDED_START_DATE: self.config.recorded_start_date.timestamp(),
|
|
105
|
-
dataformat.LENGTH:
|
|
105
|
+
dataformat.LENGTH: serialize_video_length(self.config.actual_duration),
|
|
106
106
|
}
|
|
107
107
|
if self.config.expected_duration is not None:
|
|
108
108
|
video_config[dataformat.EXPECTED_DURATION] = int(
|
|
@@ -127,3 +127,56 @@ class OtdetBuilder:
|
|
|
127
127
|
dataformat.DETECT_START: self.config.detect_start,
|
|
128
128
|
dataformat.DETECT_END: self.config.detect_end,
|
|
129
129
|
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def serialize_video_length(video_length: timedelta) -> str:
|
|
133
|
+
"""Serialize a timedelta object to a video length string in 'H+:MM:SS' format.
|
|
134
|
+
|
|
135
|
+
Args:
|
|
136
|
+
video_length (timedelta): The video length to serialize.
|
|
137
|
+
|
|
138
|
+
Returns:
|
|
139
|
+
str: The video length represented in 'H+:MM:SS' format.
|
|
140
|
+
"""
|
|
141
|
+
seconds_per_hour = 3600
|
|
142
|
+
seconds_per_minute = 60
|
|
143
|
+
|
|
144
|
+
total_seconds = int(video_length.total_seconds())
|
|
145
|
+
hours = total_seconds // seconds_per_hour
|
|
146
|
+
minutes = (total_seconds % seconds_per_hour) // seconds_per_minute
|
|
147
|
+
seconds = total_seconds % seconds_per_minute
|
|
148
|
+
return f"{hours}:{minutes:02}:{seconds:02}"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
class VideoLengthParseError(Exception):
|
|
152
|
+
"""Exception raised for errors in parsing video length strings."""
|
|
153
|
+
|
|
154
|
+
pass
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_video_length(video_length: str) -> timedelta:
|
|
158
|
+
"""Parse a video length string that is in either 'H+:MM:SS' or 'H+:MM:SS.mmmuuu'
|
|
159
|
+
format into a timedelta object ignoring milliseconds and microseconds.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
video_length (str): A string representing the video length in or
|
|
163
|
+
'H+:MM:SS.mmmuuu' or 'H+:MM:SS.mmmuuu' format.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
timedelta: A timedelta object representing the parsed video length ignoring
|
|
167
|
+
milliseconds and microseconds.
|
|
168
|
+
|
|
169
|
+
Raises:
|
|
170
|
+
VideoLengthParseError: If the input string is not in the expected format.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
try:
|
|
174
|
+
hours, minutes, seconds = video_length.strip().split(":")
|
|
175
|
+
return timedelta(
|
|
176
|
+
hours=int(hours), minutes=int(minutes), seconds=int(float(seconds))
|
|
177
|
+
)
|
|
178
|
+
except ValueError as cause:
|
|
179
|
+
raise VideoLengthParseError(
|
|
180
|
+
f"Could not parse video length '{video_length}'. "
|
|
181
|
+
"Expected format 'HH:MM:SS'."
|
|
182
|
+
) from cause
|
|
@@ -62,7 +62,7 @@ class OtdetFileWriter:
|
|
|
62
62
|
detect_config = config.detect
|
|
63
63
|
|
|
64
64
|
actual_frames = len(event.frames)
|
|
65
|
-
if
|
|
65
|
+
if expected_duration := detect_config.expected_duration:
|
|
66
66
|
actual_fps = actual_frames / expected_duration.total_seconds()
|
|
67
67
|
else:
|
|
68
68
|
actual_fps = actual_frames / source_metadata.duration.total_seconds()
|
|
@@ -72,7 +72,7 @@ class OtdetFileWriter:
|
|
|
72
72
|
OtdetBuilderConfig(
|
|
73
73
|
conf=detect_config.confidence,
|
|
74
74
|
iou=detect_config.iou,
|
|
75
|
-
source=source_metadata.
|
|
75
|
+
source=source_metadata.output,
|
|
76
76
|
video_width=source_metadata.width,
|
|
77
77
|
video_height=source_metadata.height,
|
|
78
78
|
expected_duration=expected_duration,
|
|
@@ -92,7 +92,8 @@ class OtdetFileWriter:
|
|
|
92
92
|
)
|
|
93
93
|
).build(event.frames)
|
|
94
94
|
|
|
95
|
-
detections_file = self._save_path_provider.provide(source_metadata.
|
|
95
|
+
detections_file = self._save_path_provider.provide(source_metadata.output)
|
|
96
|
+
detections_file.parent.mkdir(parents=True, exist_ok=True)
|
|
96
97
|
write_json(
|
|
97
98
|
otdet,
|
|
98
99
|
file=detections_file,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from functools import cached_property
|
|
2
|
+
|
|
3
|
+
from OTVision.abstraction.observer import Subject
|
|
4
|
+
from OTVision.application.config import StreamConfig
|
|
5
|
+
from OTVision.detect.builder import DetectBuilder
|
|
6
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
7
|
+
from OTVision.detect.rtsp_input_source import Counter, RtspInputSource
|
|
8
|
+
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
9
|
+
from OTVision.domain.time import CurrentDatetimeProvider, DatetimeProvider
|
|
10
|
+
|
|
11
|
+
FLUSH_BUFFER_SIZE = 18000
|
|
12
|
+
FLUSH_BUFFER_SIZE = 1200
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class RtspBasedDetectBuilder(DetectBuilder):
|
|
16
|
+
@property
|
|
17
|
+
def stream_config(self) -> StreamConfig:
|
|
18
|
+
config = self.get_current_config.get()
|
|
19
|
+
if config.stream is None:
|
|
20
|
+
raise ValueError(
|
|
21
|
+
"Stream config is not provided. "
|
|
22
|
+
"Running OTVision in streaming mode requires stream config"
|
|
23
|
+
)
|
|
24
|
+
return config.stream
|
|
25
|
+
|
|
26
|
+
@cached_property
|
|
27
|
+
def input_source(self) -> InputSourceDetect:
|
|
28
|
+
return RtspInputSource(
|
|
29
|
+
subject=Subject[FlushEvent](),
|
|
30
|
+
datetime_provider=self.datetime_provider,
|
|
31
|
+
frame_counter=Counter(),
|
|
32
|
+
get_current_config=self.get_current_config,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@cached_property
|
|
36
|
+
def datetime_provider(self) -> DatetimeProvider:
|
|
37
|
+
return CurrentDatetimeProvider()
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
from time import sleep
|
|
3
|
+
from typing import Generator
|
|
4
|
+
|
|
5
|
+
from cv2 import (
|
|
6
|
+
CAP_PROP_FRAME_HEIGHT,
|
|
7
|
+
CAP_PROP_FRAME_WIDTH,
|
|
8
|
+
COLOR_BGR2RGB,
|
|
9
|
+
VideoCapture,
|
|
10
|
+
cvtColor,
|
|
11
|
+
)
|
|
12
|
+
from numpy import ndarray
|
|
13
|
+
|
|
14
|
+
from OTVision.abstraction.observer import Subject
|
|
15
|
+
from OTVision.application.config import (
|
|
16
|
+
DATETIME_FORMAT,
|
|
17
|
+
Config,
|
|
18
|
+
DetectConfig,
|
|
19
|
+
StreamConfig,
|
|
20
|
+
)
|
|
21
|
+
from OTVision.application.configure_logger import logger
|
|
22
|
+
from OTVision.application.get_current_config import GetCurrentConfig
|
|
23
|
+
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
24
|
+
from OTVision.domain.frame import Frame
|
|
25
|
+
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
26
|
+
from OTVision.domain.time import DatetimeProvider
|
|
27
|
+
|
|
28
|
+
RTSP_URL = "rtsp://127.0.0.1:8554/test"
|
|
29
|
+
RETRY_SECONDS = 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class Counter:
|
|
33
|
+
def __init__(self, start_value: int = 0) -> None:
|
|
34
|
+
self._start_value = start_value
|
|
35
|
+
self.__counter = start_value
|
|
36
|
+
|
|
37
|
+
def increment(self) -> None:
|
|
38
|
+
self.__counter += 1
|
|
39
|
+
|
|
40
|
+
def get(self) -> int:
|
|
41
|
+
return self.__counter
|
|
42
|
+
|
|
43
|
+
def reset(self) -> None:
|
|
44
|
+
self.__counter = self._start_value
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class RtspInputSource(InputSourceDetect):
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def current_frame_number(self) -> int:
|
|
51
|
+
return self._frame_counter.get()
|
|
52
|
+
|
|
53
|
+
@property
|
|
54
|
+
def config(self) -> Config:
|
|
55
|
+
return self._get_current_config.get()
|
|
56
|
+
|
|
57
|
+
@property
|
|
58
|
+
def detect_config(self) -> DetectConfig:
|
|
59
|
+
return self.config.detect
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def stream_config(self) -> StreamConfig:
|
|
63
|
+
if stream_config := self.config.stream:
|
|
64
|
+
return stream_config
|
|
65
|
+
raise ValueError("Stream config not found in config")
|
|
66
|
+
|
|
67
|
+
@property
|
|
68
|
+
def rtsp_url(self) -> str:
|
|
69
|
+
return self.stream_config.source
|
|
70
|
+
|
|
71
|
+
@property
|
|
72
|
+
def flush_buffer_size(self) -> int:
|
|
73
|
+
return self.stream_config.flush_buffer_size
|
|
74
|
+
|
|
75
|
+
@property
|
|
76
|
+
def fps(self) -> float:
|
|
77
|
+
return self.config.convert.output_fps
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
subject: Subject[FlushEvent],
|
|
82
|
+
datetime_provider: DatetimeProvider,
|
|
83
|
+
frame_counter: Counter,
|
|
84
|
+
get_current_config: GetCurrentConfig,
|
|
85
|
+
) -> None:
|
|
86
|
+
super().__init__(subject)
|
|
87
|
+
self._datetime_provider = datetime_provider
|
|
88
|
+
self._stop_capture = False
|
|
89
|
+
self._frame_counter = frame_counter
|
|
90
|
+
self._get_current_config = get_current_config
|
|
91
|
+
self._current_stream: str | None = None
|
|
92
|
+
self._current_video_capture: VideoCapture | None = None
|
|
93
|
+
self._stream_start_time: datetime = self._datetime_provider.provide()
|
|
94
|
+
self._current_video_start_time = self._stream_start_time
|
|
95
|
+
self._outdated = True
|
|
96
|
+
|
|
97
|
+
@property
|
|
98
|
+
def _video_capture(self) -> VideoCapture:
|
|
99
|
+
# Property is moved below __init__ otherwise mypy is somehow unable to determine
|
|
100
|
+
# types of self._current_stream and self._current_video_capture
|
|
101
|
+
new_source = self.stream_config.source
|
|
102
|
+
if (
|
|
103
|
+
self._current_stream is not None
|
|
104
|
+
and self._current_stream == new_source
|
|
105
|
+
and self._current_video_capture
|
|
106
|
+
):
|
|
107
|
+
# current source has not changed
|
|
108
|
+
return self._current_video_capture
|
|
109
|
+
|
|
110
|
+
# Stream changed or has not been initialized
|
|
111
|
+
if self._current_video_capture is not None:
|
|
112
|
+
# If the stream changed and there's an existing capture, release it
|
|
113
|
+
self._current_video_capture.release()
|
|
114
|
+
|
|
115
|
+
self._current_stream = new_source
|
|
116
|
+
self._current_video_capture = self._init_video_capture(self._current_stream)
|
|
117
|
+
return self._current_video_capture
|
|
118
|
+
|
|
119
|
+
def produce(self) -> Generator[Frame, None, None]:
|
|
120
|
+
self._stream_start_time = self._datetime_provider.provide()
|
|
121
|
+
self._current_video_start_time = self._stream_start_time
|
|
122
|
+
while not self.should_stop():
|
|
123
|
+
if (frame := self._read_next_frame()) is not None:
|
|
124
|
+
self._frame_counter.increment()
|
|
125
|
+
occurrence = self._datetime_provider.provide()
|
|
126
|
+
|
|
127
|
+
if self._outdated:
|
|
128
|
+
self._current_video_start_time = occurrence
|
|
129
|
+
self._outdated = False
|
|
130
|
+
|
|
131
|
+
yield Frame(
|
|
132
|
+
data=convert_frame_to_rgb(frame), # YOLO expects RGB
|
|
133
|
+
frame=self.current_frame_number,
|
|
134
|
+
source=self.rtsp_url,
|
|
135
|
+
output=self.create_output(),
|
|
136
|
+
occurrence=occurrence,
|
|
137
|
+
)
|
|
138
|
+
if self.flush_condition_met():
|
|
139
|
+
self._notify()
|
|
140
|
+
self._outdated = True
|
|
141
|
+
self._frame_counter.reset()
|
|
142
|
+
|
|
143
|
+
self._notify()
|
|
144
|
+
|
|
145
|
+
def _init_video_capture(self, source: str) -> VideoCapture:
|
|
146
|
+
cap = VideoCapture(source)
|
|
147
|
+
while not self.should_stop() and not cap.isOpened():
|
|
148
|
+
logger().warning(
|
|
149
|
+
f"Couldn't open the RTSP stream: {source}. "
|
|
150
|
+
f"Trying again in {RETRY_SECONDS}s..."
|
|
151
|
+
)
|
|
152
|
+
sleep(RETRY_SECONDS)
|
|
153
|
+
cap.release()
|
|
154
|
+
cap = VideoCapture(source)
|
|
155
|
+
return cap
|
|
156
|
+
|
|
157
|
+
def _read_next_frame(self) -> ndarray | None:
|
|
158
|
+
successful, frame = self._video_capture.read()
|
|
159
|
+
if successful:
|
|
160
|
+
return frame
|
|
161
|
+
logger().debug("Failed to grab frame")
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def should_stop(self) -> bool:
|
|
165
|
+
return self._stop_capture
|
|
166
|
+
|
|
167
|
+
def stop(self) -> None:
|
|
168
|
+
self._stop_capture = True
|
|
169
|
+
|
|
170
|
+
def start(self) -> None:
|
|
171
|
+
self._stop_capture = False
|
|
172
|
+
|
|
173
|
+
def flush_condition_met(self) -> bool:
|
|
174
|
+
return self.current_frame_number % self.flush_buffer_size == 0
|
|
175
|
+
|
|
176
|
+
def _notify(self) -> None:
|
|
177
|
+
frame_width = int(self._video_capture.get(CAP_PROP_FRAME_WIDTH))
|
|
178
|
+
frame_height = int(self._video_capture.get(CAP_PROP_FRAME_HEIGHT))
|
|
179
|
+
frames = (
|
|
180
|
+
self.flush_buffer_size
|
|
181
|
+
if self.current_frame_number % self.flush_buffer_size == 0
|
|
182
|
+
else self.current_frame_number % self.flush_buffer_size
|
|
183
|
+
)
|
|
184
|
+
duration = timedelta(seconds=round(frames / self.fps))
|
|
185
|
+
output = self.create_output()
|
|
186
|
+
self._subject.notify(
|
|
187
|
+
FlushEvent.create(
|
|
188
|
+
source=self.rtsp_url,
|
|
189
|
+
output=output,
|
|
190
|
+
duration=duration,
|
|
191
|
+
source_width=frame_width,
|
|
192
|
+
source_height=frame_height,
|
|
193
|
+
source_fps=self.fps,
|
|
194
|
+
start_time=self._current_video_start_time,
|
|
195
|
+
)
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
def create_output(self) -> str:
|
|
199
|
+
output_filename = (
|
|
200
|
+
f"{self.stream_config.name}_FR{round(self.fps)}"
|
|
201
|
+
f"_{self._current_video_start_time.strftime(DATETIME_FORMAT)}.mp4"
|
|
202
|
+
)
|
|
203
|
+
return str(self.stream_config.save_dir / output_filename)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def convert_frame_to_rgb(frame: ndarray) -> ndarray:
|
|
207
|
+
return cvtColor(frame, COLOR_BGR2RGB)
|
OTVision/detect/timestamper.py
CHANGED
|
@@ -2,10 +2,10 @@ import re
|
|
|
2
2
|
from datetime import datetime, timedelta, timezone
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from OTVision.application.config import DATETIME_FORMAT
|
|
5
6
|
from OTVision.application.detect.timestamper import Timestamper
|
|
6
7
|
from OTVision.application.frame_count_provider import FrameCountProvider
|
|
7
8
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
8
|
-
from OTVision.config import DATETIME_FORMAT
|
|
9
9
|
from OTVision.dataformat import FRAME
|
|
10
10
|
from OTVision.domain.frame import Frame, FrameKeys
|
|
11
11
|
from OTVision.helpers.date import parse_date_string_to_utc_datime
|
|
@@ -61,6 +61,7 @@ class VideoTimestamper(Timestamper):
|
|
|
61
61
|
data=frame[FrameKeys.data],
|
|
62
62
|
frame=frame[FrameKeys.frame],
|
|
63
63
|
source=frame[FrameKeys.source],
|
|
64
|
+
output=frame[FrameKeys.output],
|
|
64
65
|
occurrence=occurrence,
|
|
65
66
|
)
|
|
66
67
|
|
|
@@ -7,12 +7,12 @@ import av
|
|
|
7
7
|
from tqdm import tqdm
|
|
8
8
|
|
|
9
9
|
from OTVision.abstraction.observer import Subject
|
|
10
|
+
from OTVision.application.config import DATETIME_FORMAT, Config
|
|
10
11
|
from OTVision.application.detect.detection_file_save_path_provider import (
|
|
11
12
|
DetectionFileSavePathProvider,
|
|
12
13
|
)
|
|
13
14
|
from OTVision.application.detect.timestamper import Timestamper
|
|
14
15
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
15
|
-
from OTVision.config import DATETIME_FORMAT, Config
|
|
16
16
|
from OTVision.detect.detected_frame_buffer import FlushEvent
|
|
17
17
|
from OTVision.detect.plugin_av.rotate_frame import AvVideoFrameRotator
|
|
18
18
|
from OTVision.detect.timestamper import TimestamperFactory, parse_start_time_from
|
|
@@ -116,6 +116,7 @@ class VideoSource(InputSourceDetect):
|
|
|
116
116
|
FrameKeys.data: rotated_image,
|
|
117
117
|
FrameKeys.frame: frame_number,
|
|
118
118
|
FrameKeys.source: str(video_file),
|
|
119
|
+
FrameKeys.output: str(video_file),
|
|
119
120
|
}
|
|
120
121
|
)
|
|
121
122
|
else:
|
|
@@ -124,6 +125,7 @@ class VideoSource(InputSourceDetect):
|
|
|
124
125
|
FrameKeys.data: None,
|
|
125
126
|
FrameKeys.frame: frame_number,
|
|
126
127
|
FrameKeys.source: str(video_file),
|
|
128
|
+
FrameKeys.output: str(video_file),
|
|
127
129
|
}
|
|
128
130
|
)
|
|
129
131
|
counter += 1
|
|
@@ -181,6 +183,7 @@ class VideoSource(InputSourceDetect):
|
|
|
181
183
|
self._subject.notify(
|
|
182
184
|
FlushEvent.create(
|
|
183
185
|
source=str(current_video_file),
|
|
186
|
+
output=str(current_video_file),
|
|
184
187
|
duration=duration,
|
|
185
188
|
source_height=height,
|
|
186
189
|
source_width=width,
|
|
@@ -205,8 +208,9 @@ class VideoSource(InputSourceDetect):
|
|
|
205
208
|
def __add_occurrence(self, timestamper: Timestamper, frame: dict) -> Frame:
|
|
206
209
|
updated = timestamper.stamp(frame)
|
|
207
210
|
return Frame(
|
|
208
|
-
data=updated[
|
|
209
|
-
frame=updated[
|
|
210
|
-
source=updated[
|
|
211
|
-
|
|
211
|
+
data=updated[FrameKeys.data],
|
|
212
|
+
frame=updated[FrameKeys.frame],
|
|
213
|
+
source=updated[FrameKeys.source],
|
|
214
|
+
output=updated[FrameKeys.output],
|
|
215
|
+
occurrence=updated[FrameKeys.occurrence],
|
|
212
216
|
)
|
OTVision/detect/yolo.py
CHANGED
|
@@ -30,9 +30,9 @@ from ultralytics import YOLO
|
|
|
30
30
|
from ultralytics.engine.results import Boxes
|
|
31
31
|
|
|
32
32
|
from OTVision.abstraction.pipes_and_filter import Filter
|
|
33
|
+
from OTVision.application.config import DetectConfig
|
|
33
34
|
from OTVision.application.detect.detected_frame_factory import DetectedFrameFactory
|
|
34
35
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
35
|
-
from OTVision.config import DetectConfig
|
|
36
36
|
from OTVision.domain.detection import Detection
|
|
37
37
|
from OTVision.domain.frame import DetectedFrame, Frame, FrameKeys
|
|
38
38
|
from OTVision.domain.object_detection import ObjectDetector, ObjectDetectorFactory
|
|
@@ -184,6 +184,22 @@ class YoloDetector(ObjectDetector, Filter[Frame, DetectedFrame]):
|
|
|
184
184
|
"""Create a DetectedFrame with no detections."""
|
|
185
185
|
return self._detected_frame_factory.create(frame, detections=[])
|
|
186
186
|
|
|
187
|
+
def preload(self) -> None:
|
|
188
|
+
model_name = Path(self.config.weights).name
|
|
189
|
+
log.info(f"Preloading YOLO model '{model_name}...'")
|
|
190
|
+
self._model.predict(
|
|
191
|
+
source=None,
|
|
192
|
+
conf=self.config.confidence,
|
|
193
|
+
iou=self.config.iou,
|
|
194
|
+
half=self.config.half_precision,
|
|
195
|
+
imgsz=self.config.img_size,
|
|
196
|
+
device=0 if torch.cuda.is_available() else "cpu",
|
|
197
|
+
stream=False,
|
|
198
|
+
verbose=False,
|
|
199
|
+
agnostic_nms=True,
|
|
200
|
+
)
|
|
201
|
+
log.info(f"YOLO model '{model_name}' loaded and ready for inference.'")
|
|
202
|
+
|
|
187
203
|
|
|
188
204
|
class YoloFactory(ObjectDetectorFactory):
|
|
189
205
|
"""
|
OTVision/domain/cli.py
CHANGED
|
@@ -17,7 +17,7 @@ class CliParseError(Exception):
|
|
|
17
17
|
@dataclass
|
|
18
18
|
class DetectCliArgs(CliArgs):
|
|
19
19
|
expected_duration: timedelta | None
|
|
20
|
-
paths: list[
|
|
20
|
+
paths: list[str] | None
|
|
21
21
|
config_file: Path | None
|
|
22
22
|
logfile: Path
|
|
23
23
|
logfile_overwrite: bool
|
|
@@ -41,3 +41,33 @@ class DetectCliParser(ABC):
|
|
|
41
41
|
@abstractmethod
|
|
42
42
|
def parse(self) -> DetectCliArgs:
|
|
43
43
|
raise NotImplementedError
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class TrackCliArgs(CliArgs):
|
|
48
|
+
paths: list[str] | None
|
|
49
|
+
config_file: Path | None
|
|
50
|
+
logfile: Path
|
|
51
|
+
logfile_overwrite: bool
|
|
52
|
+
log_level_console: str | None
|
|
53
|
+
log_level_file: str | None
|
|
54
|
+
overwrite: bool | None = None
|
|
55
|
+
sigma_l: float | None = None
|
|
56
|
+
sigma_h: float | None = None
|
|
57
|
+
sigma_iou: float | None = None
|
|
58
|
+
t_min: int | None = None
|
|
59
|
+
t_miss_max: int | None = None
|
|
60
|
+
|
|
61
|
+
def get_config_file(self) -> Path | None:
|
|
62
|
+
return self.config_file
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class TrackCliParser(ABC):
|
|
66
|
+
@abstractmethod
|
|
67
|
+
def parse(self) -> TrackCliArgs:
|
|
68
|
+
"""Parse track CLI arguments.
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
TrackCliArgs: the parsed track CLI arguments.
|
|
72
|
+
"""
|
|
73
|
+
raise NotImplementedError
|