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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
4
|
|
|
5
5
|
class Filter[IN, OUT](ABC):
|
|
@@ -14,14 +14,14 @@ class Filter[IN, OUT](ABC):
|
|
|
14
14
|
"""
|
|
15
15
|
|
|
16
16
|
@abstractmethod
|
|
17
|
-
def filter(self, pipe:
|
|
17
|
+
def filter(self, pipe: Iterator[IN]) -> Iterator[OUT]:
|
|
18
18
|
"""Process elements from the input pipe and produce output elements.
|
|
19
19
|
|
|
20
20
|
Args:
|
|
21
|
-
pipe (
|
|
21
|
+
pipe (Iterator[IN]): Input stream of elements to be processed.
|
|
22
22
|
|
|
23
23
|
Returns:
|
|
24
|
-
|
|
24
|
+
Iterator[OUT]: Output stream of processed elements.
|
|
25
25
|
|
|
26
26
|
"""
|
|
27
27
|
|
OTVision/application/buffer.py
CHANGED
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
from abc import abstractmethod
|
|
2
|
-
from typing import
|
|
2
|
+
from typing import Iterator
|
|
3
3
|
|
|
4
|
-
from OTVision.abstraction.observer import Observable, Subject
|
|
5
4
|
from OTVision.abstraction.pipes_and_filter import Filter
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
class Buffer[T,
|
|
9
|
-
def __init__(self
|
|
10
|
-
super().__init__(subject)
|
|
7
|
+
class Buffer[T, OBSERVING_TYPE](Filter[T, T]):
|
|
8
|
+
def __init__(self) -> None:
|
|
11
9
|
self._buffer: list[T] = []
|
|
12
10
|
|
|
13
|
-
def filter(self, pipe:
|
|
11
|
+
def filter(self, pipe: Iterator[T]) -> Iterator[T]:
|
|
14
12
|
for element in pipe:
|
|
15
13
|
self.buffer(element)
|
|
16
14
|
yield element
|
|
@@ -25,11 +23,6 @@ class Buffer[T, SUBJECT_TYPE, OBSERVING_TYPE](Observable[SUBJECT_TYPE], Filter[T
|
|
|
25
23
|
del self._buffer
|
|
26
24
|
self._buffer = list()
|
|
27
25
|
|
|
28
|
-
def on_flush(self, event: OBSERVING_TYPE) -> None:
|
|
29
|
-
buffered_elements = self._get_buffered_elements()
|
|
30
|
-
self._notify_observers(buffered_elements, event)
|
|
31
|
-
self._reset_buffer()
|
|
32
|
-
|
|
33
26
|
@abstractmethod
|
|
34
|
-
def
|
|
27
|
+
def on_flush(self, event: OBSERVING_TYPE) -> None:
|
|
35
28
|
raise NotImplementedError
|
OTVision/application/config.py
CHANGED
|
@@ -2,6 +2,12 @@ from dataclasses import dataclass, field
|
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
from pathlib import Path
|
|
4
4
|
|
|
5
|
+
from OTVision.plugin.ffmpeg_video_writer import (
|
|
6
|
+
ConstantRateFactor,
|
|
7
|
+
EncodingSpeed,
|
|
8
|
+
VideoCodec,
|
|
9
|
+
)
|
|
10
|
+
|
|
5
11
|
AVAILABLE_WEIGHTS = "AVAILABLEWEIGHTS"
|
|
6
12
|
CALIBRATIONS = "CALIBRATIONS"
|
|
7
13
|
COL_WIDTH = "COLWIDTH"
|
|
@@ -58,6 +64,10 @@ LOG_DIR = "LOG_DIR"
|
|
|
58
64
|
START_TIME = "START_TIME"
|
|
59
65
|
DETECT_END = "DETECT_END"
|
|
60
66
|
DETECT_START = "DETECT_START"
|
|
67
|
+
WRITE_VIDEO = "WRITE_VIDEO"
|
|
68
|
+
VIDEO_CODEC = "VIDEO_CODEC"
|
|
69
|
+
ENCODING_SPEED = "ENCODING_SPEED"
|
|
70
|
+
CRF = "CRF"
|
|
61
71
|
DATETIME_FORMAT = "%Y-%m-%d_%H-%M-%S"
|
|
62
72
|
DEFAULT_EXPECTED_DURATION: timedelta = timedelta(minutes=15)
|
|
63
73
|
"""Default length of a video is 15 minutes."""
|
|
@@ -289,6 +299,10 @@ class DetectConfig:
|
|
|
289
299
|
start_time: datetime | None = None
|
|
290
300
|
detect_start: int | None = None
|
|
291
301
|
detect_end: int | None = None
|
|
302
|
+
write_video: bool = False
|
|
303
|
+
video_codec: VideoCodec = VideoCodec.H264_SOFTWARE
|
|
304
|
+
encoding_speed: EncodingSpeed = EncodingSpeed.FAST
|
|
305
|
+
crf: ConstantRateFactor = ConstantRateFactor.DEFAULT
|
|
292
306
|
|
|
293
307
|
def to_dict(self) -> dict:
|
|
294
308
|
expected_duration = (
|
|
@@ -306,6 +320,10 @@ class DetectConfig:
|
|
|
306
320
|
START_TIME: self.start_time,
|
|
307
321
|
DETECT_START: self.detect_start,
|
|
308
322
|
DETECT_END: self.detect_end,
|
|
323
|
+
WRITE_VIDEO: self.write_video,
|
|
324
|
+
VIDEO_CODEC: self.video_codec.value,
|
|
325
|
+
ENCODING_SPEED: self.encoding_speed.value,
|
|
326
|
+
CRF: self.crf.name,
|
|
309
327
|
}
|
|
310
328
|
|
|
311
329
|
|
|
@@ -5,12 +5,14 @@ from OTVision.application.config import (
|
|
|
5
5
|
COL_WIDTH,
|
|
6
6
|
CONF,
|
|
7
7
|
CONVERT,
|
|
8
|
+
CRF,
|
|
8
9
|
DATETIME_FORMAT,
|
|
9
10
|
DEFAULT_FILETYPE,
|
|
10
11
|
DELETE_INPUT,
|
|
11
12
|
DETECT,
|
|
12
13
|
DETECT_END,
|
|
13
14
|
DETECT_START,
|
|
15
|
+
ENCODING_SPEED,
|
|
14
16
|
EXPECTED_DURATION,
|
|
15
17
|
FLUSH_BUFFER_SIZE,
|
|
16
18
|
FONT,
|
|
@@ -51,8 +53,10 @@ from OTVision.application.config import (
|
|
|
51
53
|
TRANSFORM,
|
|
52
54
|
UNDISTORT,
|
|
53
55
|
VID,
|
|
56
|
+
VIDEO_CODEC,
|
|
54
57
|
WEIGHTS,
|
|
55
58
|
WINDOW,
|
|
59
|
+
WRITE_VIDEO,
|
|
56
60
|
YOLO,
|
|
57
61
|
Config,
|
|
58
62
|
ConvertConfig,
|
|
@@ -71,14 +75,21 @@ from OTVision.application.config import (
|
|
|
71
75
|
from OTVision.domain.serialization import Deserializer
|
|
72
76
|
|
|
73
77
|
|
|
78
|
+
class InvalidOtvisionConfigError(Exception):
|
|
79
|
+
pass
|
|
80
|
+
|
|
81
|
+
|
|
74
82
|
class ConfigParser:
|
|
75
83
|
|
|
76
84
|
def __init__(self, deserializer: Deserializer) -> None:
|
|
77
85
|
self._deserialize = deserializer
|
|
78
86
|
|
|
79
87
|
def parse(self, file: Path) -> Config:
|
|
80
|
-
|
|
81
|
-
|
|
88
|
+
raw_data = self._deserialize.deserialize(file)
|
|
89
|
+
config = self.parse_from_dict(raw_data)
|
|
90
|
+
|
|
91
|
+
self.validate_config(config)
|
|
92
|
+
return config
|
|
82
93
|
|
|
83
94
|
def parse_from_dict(self, d: dict) -> Config:
|
|
84
95
|
log_dict = d.get(LOG)
|
|
@@ -181,15 +192,19 @@ class ConfigParser:
|
|
|
181
192
|
|
|
182
193
|
start_time = self._parse_start_time(data)
|
|
183
194
|
return DetectConfig(
|
|
184
|
-
sources,
|
|
185
|
-
data.get(RUN_CHAINED, DetectConfig.run_chained),
|
|
186
|
-
yolo_config,
|
|
187
|
-
expected_duration,
|
|
188
|
-
data.get(OVERWRITE, DetectConfig.overwrite),
|
|
189
|
-
data.get(HALF_PRECISION, DetectConfig.half_precision),
|
|
190
|
-
start_time,
|
|
191
|
-
data.get(DETECT_START, DetectConfig.detect_start),
|
|
192
|
-
data.get(DETECT_END, DetectConfig.detect_end),
|
|
195
|
+
paths=sources,
|
|
196
|
+
run_chained=data.get(RUN_CHAINED, DetectConfig.run_chained),
|
|
197
|
+
yolo_config=yolo_config,
|
|
198
|
+
expected_duration=expected_duration,
|
|
199
|
+
overwrite=data.get(OVERWRITE, DetectConfig.overwrite),
|
|
200
|
+
half_precision=data.get(HALF_PRECISION, DetectConfig.half_precision),
|
|
201
|
+
start_time=start_time,
|
|
202
|
+
detect_start=data.get(DETECT_START, DetectConfig.detect_start),
|
|
203
|
+
detect_end=data.get(DETECT_END, DetectConfig.detect_end),
|
|
204
|
+
write_video=data.get(WRITE_VIDEO, DetectConfig.write_video),
|
|
205
|
+
video_codec=data.get(VIDEO_CODEC, DetectConfig.video_codec),
|
|
206
|
+
encoding_speed=data.get(ENCODING_SPEED, DetectConfig.encoding_speed),
|
|
207
|
+
crf=data.get(CRF, DetectConfig.crf),
|
|
193
208
|
)
|
|
194
209
|
|
|
195
210
|
def parse_yolo_config(self, data: dict) -> YoloConfig:
|
|
@@ -278,3 +293,45 @@ class ConfigParser:
|
|
|
278
293
|
save_dir=save_dir,
|
|
279
294
|
flush_buffer_size=flush_buffer_size,
|
|
280
295
|
)
|
|
296
|
+
|
|
297
|
+
def validate_config(self, config: Config) -> None:
|
|
298
|
+
self.validate_flush_buffer_support_track_lifecycle(config)
|
|
299
|
+
|
|
300
|
+
def validate_flush_buffer_support_track_lifecycle(self, config: Config) -> None:
|
|
301
|
+
"""Validate that the flush buffer size supports complete track lifecycle.
|
|
302
|
+
|
|
303
|
+
In streaming mode, the flush buffer size must be larger than the tracking
|
|
304
|
+
parameters t_min and t_miss_max to ensure tracks can complete their full
|
|
305
|
+
lifecycle before being flushed. This prevents premature flushing of
|
|
306
|
+
unfinished tracks.
|
|
307
|
+
|
|
308
|
+
Args:
|
|
309
|
+
config (Config): The configuration to validate
|
|
310
|
+
|
|
311
|
+
Raises:
|
|
312
|
+
InvalidOtvisionConfigError: If the flush buffer size is not greater
|
|
313
|
+
than both t_min and t_miss_max values
|
|
314
|
+
|
|
315
|
+
Note:
|
|
316
|
+
This validation only applies when stream configuration is present.
|
|
317
|
+
The constraint ensures that:
|
|
318
|
+
- Tracks have enough frames to reach minimum track length (t_min)
|
|
319
|
+
- Tracks can handle maximum missing frames (t_miss_max) before completion
|
|
320
|
+
"""
|
|
321
|
+
|
|
322
|
+
if config.stream is None:
|
|
323
|
+
return
|
|
324
|
+
|
|
325
|
+
flush_buffer_size = config.stream.flush_buffer_size
|
|
326
|
+
if (
|
|
327
|
+
config.track.t_min < flush_buffer_size
|
|
328
|
+
and config.track.t_miss_max < flush_buffer_size
|
|
329
|
+
):
|
|
330
|
+
return
|
|
331
|
+
|
|
332
|
+
raise InvalidOtvisionConfigError(
|
|
333
|
+
f"The flush buffer size ({flush_buffer_size}) must be greater than the "
|
|
334
|
+
f"t_min ({config.track.t_min}) and t_miss_max "
|
|
335
|
+
f"({config.track.t_miss_max}) values to allow tracks to complete "
|
|
336
|
+
"before flushing."
|
|
337
|
+
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
from typing import
|
|
1
|
+
from typing import Iterator
|
|
2
2
|
|
|
3
3
|
from OTVision.abstraction.pipes_and_filter import Filter
|
|
4
4
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
@@ -33,7 +33,5 @@ class CurrentObjectDetector(Filter[Frame, DetectedFrame]):
|
|
|
33
33
|
detect_config = self._get_current_config.get().detect
|
|
34
34
|
return self._factory.create(detect_config)
|
|
35
35
|
|
|
36
|
-
def filter(
|
|
37
|
-
self, pipe: Generator[Frame, None, None]
|
|
38
|
-
) -> Generator[DetectedFrame, None, None]:
|
|
36
|
+
def filter(self, pipe: Iterator[Frame]) -> Iterator[DetectedFrame]:
|
|
39
37
|
return self.get().detect(pipe)
|
|
@@ -84,6 +84,22 @@ class UpdateDetectConfigWithCliArgs:
|
|
|
84
84
|
if cli_args.detect_end is not None
|
|
85
85
|
else detect_config.detect_end
|
|
86
86
|
),
|
|
87
|
+
write_video=(
|
|
88
|
+
cli_args.write_video
|
|
89
|
+
if cli_args.write_video is not None
|
|
90
|
+
else detect_config.write_video
|
|
91
|
+
),
|
|
92
|
+
video_codec=(
|
|
93
|
+
cli_args.video_codec
|
|
94
|
+
if cli_args.video_codec is not None
|
|
95
|
+
else detect_config.video_codec
|
|
96
|
+
),
|
|
97
|
+
encoding_speed=(
|
|
98
|
+
cli_args.encoding_speed
|
|
99
|
+
if cli_args.encoding_speed is not None
|
|
100
|
+
else detect_config.encoding_speed
|
|
101
|
+
),
|
|
102
|
+
crf=cli_args.crf if cli_args.crf is not None else detect_config.crf,
|
|
87
103
|
)
|
|
88
104
|
|
|
89
105
|
def _update_log_config(self, config: Config, cli_args: DetectCliArgs) -> _LogConfig:
|
|
File without changes
|
OTVision/application/{detect/detection_file_save_path_provider.py → otvision_save_path_provider.py}
RENAMED
|
@@ -3,8 +3,9 @@ from pathlib import Path
|
|
|
3
3
|
from OTVision.application.get_current_config import GetCurrentConfig
|
|
4
4
|
|
|
5
5
|
|
|
6
|
-
class
|
|
7
|
-
"""Provides a mechanism to generate file save paths for
|
|
6
|
+
class OtvisionSavePathProvider:
|
|
7
|
+
"""Provides a mechanism to generate file save paths for OTVision tasks such as
|
|
8
|
+
detect and track.
|
|
8
9
|
|
|
9
10
|
This class is responsible for deriving the appropriate filenames for
|
|
10
11
|
detection files based on the source and current configuration
|
|
@@ -19,11 +20,11 @@ class DetectionFileSavePathProvider:
|
|
|
19
20
|
def __init__(self, get_current_config: GetCurrentConfig) -> None:
|
|
20
21
|
self._get_current_config = get_current_config
|
|
21
22
|
|
|
22
|
-
def provide(self, source: str) -> Path:
|
|
23
|
+
def provide(self, source: str, file_type: str) -> Path:
|
|
23
24
|
config = self._get_current_config.get()
|
|
24
25
|
return derive_filename(
|
|
25
26
|
video_file=Path(source),
|
|
26
|
-
|
|
27
|
+
file_type=file_type,
|
|
27
28
|
detect_start=config.detect.detect_start,
|
|
28
29
|
detect_end=config.detect.detect_end,
|
|
29
30
|
)
|
|
@@ -31,7 +32,7 @@ class DetectionFileSavePathProvider:
|
|
|
31
32
|
|
|
32
33
|
def derive_filename(
|
|
33
34
|
video_file: Path,
|
|
34
|
-
|
|
35
|
+
file_type: str,
|
|
35
36
|
detect_start: int | None = None,
|
|
36
37
|
detect_end: int | None = None,
|
|
37
38
|
) -> Path:
|
|
@@ -45,7 +46,7 @@ def derive_filename(
|
|
|
45
46
|
If None, no starting marker will be appended.
|
|
46
47
|
detect_end (int | None): The ending marker to append to the filename. If None,
|
|
47
48
|
no ending marker will be appended.
|
|
48
|
-
|
|
49
|
+
file_type (str): The file suffix to apply to the derived filename.
|
|
49
50
|
|
|
50
51
|
Returns:
|
|
51
52
|
Path: The modified video file path with the updated stem and suffix applied.
|
|
@@ -56,4 +57,4 @@ def derive_filename(
|
|
|
56
57
|
if detect_end is not None:
|
|
57
58
|
cutout += f"_end_{detect_end}"
|
|
58
59
|
new_stem = f"{video_file.stem}{cutout}"
|
|
59
|
-
return video_file.with_stem(new_stem).with_suffix(
|
|
60
|
+
return video_file.with_stem(new_stem).with_suffix(file_type)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from collections import defaultdict
|
|
2
|
+
from dataclasses import dataclass
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from typing import Self
|
|
5
|
+
|
|
6
|
+
from OTVision import dataformat, version
|
|
7
|
+
from OTVision.detect.otdet import (
|
|
8
|
+
OtdetBuilderConfig,
|
|
9
|
+
OtdetMetadataBuilder,
|
|
10
|
+
extract_expected_duration_from_otdet,
|
|
11
|
+
extract_start_date_from_otdet,
|
|
12
|
+
)
|
|
13
|
+
from OTVision.domain.detection import TrackedDetection, TrackId
|
|
14
|
+
from OTVision.domain.frame import FrameNo, TrackedFrame
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class OttrkBuilderConfig:
|
|
19
|
+
otdet_builder_config: OtdetBuilderConfig
|
|
20
|
+
number_of_frames: int
|
|
21
|
+
sigma_l: float
|
|
22
|
+
sigma_h: float
|
|
23
|
+
sigma_iou: float
|
|
24
|
+
t_min: int
|
|
25
|
+
t_miss_max: int
|
|
26
|
+
tracking_run_id: str
|
|
27
|
+
frame_group: int
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class OttrkBuilderError(Exception):
|
|
31
|
+
pass
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class OttrkBuilder:
|
|
35
|
+
@property
|
|
36
|
+
def config(self) -> OttrkBuilderConfig:
|
|
37
|
+
if self._config is None:
|
|
38
|
+
raise OttrkBuilderError("Ottrk builder config is not set")
|
|
39
|
+
return self._config
|
|
40
|
+
|
|
41
|
+
def __init__(self, otdet_metadata_builder: OtdetMetadataBuilder) -> None:
|
|
42
|
+
self._config: OttrkBuilderConfig | None = None
|
|
43
|
+
self._otdet_metadata_builder = otdet_metadata_builder
|
|
44
|
+
self._tracked_detections: dict[TrackId, list[dict]] = defaultdict(list)
|
|
45
|
+
|
|
46
|
+
def build(self) -> dict:
|
|
47
|
+
result = {
|
|
48
|
+
dataformat.METADATA: self.build_metadata(),
|
|
49
|
+
dataformat.DATA: {
|
|
50
|
+
dataformat.DETECTIONS: self._build_data(),
|
|
51
|
+
},
|
|
52
|
+
}
|
|
53
|
+
self.reset()
|
|
54
|
+
return result
|
|
55
|
+
|
|
56
|
+
def build_metadata(self) -> dict:
|
|
57
|
+
otdet_metadata = self._otdet_metadata_builder.build(
|
|
58
|
+
self.config.number_of_frames
|
|
59
|
+
)
|
|
60
|
+
start_date = extract_start_date_from_otdet(otdet_metadata)
|
|
61
|
+
duration = extract_expected_duration_from_otdet(otdet_metadata)
|
|
62
|
+
end_date = start_date + duration
|
|
63
|
+
ottrk_metadata = self._build_track_metadata(start_date, end_date)
|
|
64
|
+
return {
|
|
65
|
+
**otdet_metadata,
|
|
66
|
+
**ottrk_metadata,
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def _build_track_metadata(self, start_date: datetime, end_date: datetime) -> dict:
|
|
70
|
+
result = create_ottrk_metadata_entry(
|
|
71
|
+
start_date=start_date,
|
|
72
|
+
end_date=end_date,
|
|
73
|
+
sigma_l=self.config.sigma_l,
|
|
74
|
+
sigma_h=self.config.sigma_h,
|
|
75
|
+
sigma_iou=self.config.sigma_iou,
|
|
76
|
+
t_min=self.config.t_min,
|
|
77
|
+
t_miss_max=self.config.t_miss_max,
|
|
78
|
+
)
|
|
79
|
+
result[dataformat.TRACKING_RUN_ID] = self.config.tracking_run_id
|
|
80
|
+
result[dataformat.FRAME_GROUP] = self.config.frame_group
|
|
81
|
+
return result
|
|
82
|
+
|
|
83
|
+
def _build_data(self) -> list[dict]:
|
|
84
|
+
return sorted(
|
|
85
|
+
(
|
|
86
|
+
detection
|
|
87
|
+
for detections in self._tracked_detections.values()
|
|
88
|
+
for detection in detections
|
|
89
|
+
),
|
|
90
|
+
key=lambda detection: (
|
|
91
|
+
detection[dataformat.FRAME],
|
|
92
|
+
detection[dataformat.OCCURRENCE],
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
def set_config(self, config: OttrkBuilderConfig) -> Self:
|
|
97
|
+
self._config = config
|
|
98
|
+
self._otdet_metadata_builder.add_config(config.otdet_builder_config)
|
|
99
|
+
return self
|
|
100
|
+
|
|
101
|
+
def add_tracked_frames(self, tracked_frames: list[TrackedFrame]) -> Self:
|
|
102
|
+
finished_tracks = set()
|
|
103
|
+
discarded_tracks = set()
|
|
104
|
+
|
|
105
|
+
for tracked_frame in tracked_frames:
|
|
106
|
+
finished_tracks.update(tracked_frame.finished_tracks)
|
|
107
|
+
discarded_tracks.update(tracked_frame.discarded_tracks)
|
|
108
|
+
|
|
109
|
+
for detection in tracked_frame.detections:
|
|
110
|
+
self._tracked_detections[detection.track_id].append(
|
|
111
|
+
self._serialize_tracked_detection(
|
|
112
|
+
frame_no=tracked_frame.no,
|
|
113
|
+
detection=detection,
|
|
114
|
+
occurrence=tracked_frame.occurrence,
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
self.__sort_detections()
|
|
118
|
+
self.discard_tracks(discarded_tracks)
|
|
119
|
+
self.finish_tracks(finished_tracks)
|
|
120
|
+
return self
|
|
121
|
+
|
|
122
|
+
def _serialize_tracked_detection(
|
|
123
|
+
self,
|
|
124
|
+
frame_no: FrameNo,
|
|
125
|
+
detection: TrackedDetection,
|
|
126
|
+
occurrence: datetime,
|
|
127
|
+
) -> dict:
|
|
128
|
+
result = detection.to_otdet()
|
|
129
|
+
result[dataformat.INTERPOLATED_DETECTION] = False
|
|
130
|
+
result[dataformat.FIRST] = detection.is_first
|
|
131
|
+
result[dataformat.FINISHED] = False
|
|
132
|
+
result[dataformat.TRACK_ID] = detection.track_id
|
|
133
|
+
result[dataformat.FRAME] = frame_no
|
|
134
|
+
result[dataformat.OCCURRENCE] = occurrence.timestamp()
|
|
135
|
+
result[dataformat.INPUT_FILE_PATH] = self.config.otdet_builder_config.source
|
|
136
|
+
return result
|
|
137
|
+
|
|
138
|
+
def __sort_detections(self) -> None:
|
|
139
|
+
for detections in self._tracked_detections.values():
|
|
140
|
+
detections.sort(key=lambda detection: (detection[dataformat.OCCURRENCE]))
|
|
141
|
+
|
|
142
|
+
def reset(self) -> Self:
|
|
143
|
+
self._config = None
|
|
144
|
+
self._otdet_metadata_builder.reset()
|
|
145
|
+
self._tracked_detections = defaultdict(list)
|
|
146
|
+
return self
|
|
147
|
+
|
|
148
|
+
def discard_tracks(self, tracks: set[TrackId]) -> Self:
|
|
149
|
+
for track_id in tracks:
|
|
150
|
+
self.discard_track(track_id)
|
|
151
|
+
return self
|
|
152
|
+
|
|
153
|
+
def discard_track(self, track_id: TrackId) -> Self:
|
|
154
|
+
try:
|
|
155
|
+
del self._tracked_detections[track_id]
|
|
156
|
+
except KeyError:
|
|
157
|
+
pass
|
|
158
|
+
return self
|
|
159
|
+
|
|
160
|
+
def finish_tracks(self, tracks: set[TrackId]) -> Self:
|
|
161
|
+
for track_id in tracks:
|
|
162
|
+
self.finish_track(track_id)
|
|
163
|
+
return self
|
|
164
|
+
|
|
165
|
+
def finish_track(self, track_id: TrackId) -> Self:
|
|
166
|
+
if track_id in self._tracked_detections:
|
|
167
|
+
self._tracked_detections[track_id][-1][dataformat.FINISHED] = True
|
|
168
|
+
return self
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def create_ottrk_metadata_entry(
|
|
172
|
+
start_date: datetime,
|
|
173
|
+
end_date: datetime,
|
|
174
|
+
sigma_l: float,
|
|
175
|
+
sigma_h: float,
|
|
176
|
+
sigma_iou: float,
|
|
177
|
+
t_min: int,
|
|
178
|
+
t_miss_max: int,
|
|
179
|
+
) -> dict:
|
|
180
|
+
return {
|
|
181
|
+
dataformat.OTTRACK_VERSION: version.ottrack_version(),
|
|
182
|
+
dataformat.TRACKING: {
|
|
183
|
+
dataformat.OTVISION_VERSION: version.otvision_version(),
|
|
184
|
+
dataformat.FIRST_TRACKED_VIDEO_START: start_date.timestamp(),
|
|
185
|
+
dataformat.LAST_TRACKED_VIDEO_END: end_date.timestamp(),
|
|
186
|
+
dataformat.TRACKER: create_tracker_metadata(
|
|
187
|
+
sigma_l, sigma_h, sigma_iou, t_min, t_miss_max
|
|
188
|
+
),
|
|
189
|
+
},
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def create_tracker_metadata(
|
|
194
|
+
sigma_l: float, sigma_h: float, sigma_iou: float, t_min: int, t_miss_max: int
|
|
195
|
+
) -> dict:
|
|
196
|
+
return {
|
|
197
|
+
dataformat.NAME: "IOU",
|
|
198
|
+
dataformat.SIGMA_L: sigma_l,
|
|
199
|
+
dataformat.SIGMA_H: sigma_h,
|
|
200
|
+
dataformat.SIGMA_IOU: sigma_iou,
|
|
201
|
+
dataformat.T_MIN: t_min,
|
|
202
|
+
dataformat.T_MISS_MAX: t_miss_max,
|
|
203
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
|
|
3
|
+
StrIdGenerator = Callable[[], str]
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class CurrentTrackingRunId:
|
|
7
|
+
def __init__(self) -> None:
|
|
8
|
+
self._current_tracking_run_id: str | None = None
|
|
9
|
+
|
|
10
|
+
def update(self, tracking_run_id: str) -> None:
|
|
11
|
+
self._current_tracking_run_id = tracking_run_id
|
|
12
|
+
|
|
13
|
+
def get(self) -> str:
|
|
14
|
+
if self._current_tracking_run_id is None:
|
|
15
|
+
raise ValueError("Tracking run id is not set!")
|
|
16
|
+
return self._current_tracking_run_id
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class GenerateNewTrackingRunId:
|
|
20
|
+
def __init__(
|
|
21
|
+
self, id_generator: StrIdGenerator, current: CurrentTrackingRunId
|
|
22
|
+
) -> None:
|
|
23
|
+
self._generate_id = id_generator
|
|
24
|
+
self._current = current
|
|
25
|
+
|
|
26
|
+
def generate(self) -> None:
|
|
27
|
+
self._current.update(self._generate_id())
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GetCurrentTrackingRunId:
|
|
31
|
+
def __init__(self, current: CurrentTrackingRunId) -> None:
|
|
32
|
+
self._current = current
|
|
33
|
+
|
|
34
|
+
def get(self) -> str:
|
|
35
|
+
return self._current.get()
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
from OTVision.abstraction.pipes_and_filter import Filter
|
|
2
|
+
from OTVision.domain.frame import Frame
|
|
3
|
+
from OTVision.domain.input_source_detect import InputSourceDetect
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class GenerateVideo:
|
|
7
|
+
def __init__(
|
|
8
|
+
self, input_source: InputSourceDetect, video_writer: Filter[Frame, Frame]
|
|
9
|
+
) -> None:
|
|
10
|
+
self._input_source = input_source
|
|
11
|
+
self._video_writer = video_writer
|
|
12
|
+
|
|
13
|
+
def generate(self) -> None:
|
|
14
|
+
for frame in self._video_writer.filter(self._input_source.produce()):
|
|
15
|
+
pass
|
OTVision/config.py
CHANGED
|
@@ -77,6 +77,7 @@ from OTVision.application.config import (
|
|
|
77
77
|
VIDEOS,
|
|
78
78
|
WEIGHTS,
|
|
79
79
|
WINDOW,
|
|
80
|
+
WRITE_VIDEO,
|
|
80
81
|
YOLO,
|
|
81
82
|
Config,
|
|
82
83
|
)
|
|
@@ -176,6 +177,7 @@ CONFIG[DETECT][OVERWRITE] = True
|
|
|
176
177
|
CONFIG[DETECT][HALF_PRECISION] = False
|
|
177
178
|
CONFIG[DETECT][DETECT_START] = None
|
|
178
179
|
CONFIG[DETECT][DETECT_END] = None
|
|
180
|
+
CONFIG[DETECT][WRITE_VIDEO] = False
|
|
179
181
|
|
|
180
182
|
# TRACK
|
|
181
183
|
CONFIG[TRACK] = {}
|