Skip to content

Commit 5048145

Browse files
committedFeb 11, 2025
Inject OtdetBuilder in OTVisionDetect class
1 parent c1c1925 commit 5048145

File tree

7 files changed

+374
-119
lines changed

7 files changed

+374
-119
lines changed
 

‎OTVision/detect/builder.py

+5
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
from OTVision.application.get_config import GetConfig
1010
from OTVision.config import ConfigParser
1111
from OTVision.detect.cli import ArgparseDetectCliParser
12+
from OTVision.detect.otdet import OtdetBuilder
1213
from OTVision.domain.cli import DetectCliParser
1314

1415

@@ -39,5 +40,9 @@ def update_detect_config_with_ci_args(self) -> UpdateDetectConfigWithCliArgs:
3940
def configure_logger(self) -> ConfigureLogger:
4041
return ConfigureLogger()
4142

43+
@cached_property
44+
def otdet_builder(self) -> OtdetBuilder:
45+
return OtdetBuilder()
46+
4247
def __init__(self, argv: list[str] | None = None) -> None:
4348
self.argv = argv

‎OTVision/detect/detect.py

+45-36
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828

2929
from OTVision.config import Config
3030
from OTVision.dataformat import DATA, LENGTH, METADATA, RECORDED_START_DATE, VIDEO
31-
from OTVision.detect.otdet import OtdetBuilder
31+
from OTVision.detect.otdet import OtdetBuilder, OtdetBuilderConfig
3232
from OTVision.detect.yolo import create_model
3333
from OTVision.helpers.date import parse_date_string_to_utc_datime
3434
from OTVision.helpers.files import (
@@ -46,8 +46,15 @@
4646

4747

4848
class OTVisionDetect:
49-
def __init__(self, config: Config) -> None:
50-
self._config = config
49+
@property
50+
def config(self) -> Config:
51+
if self._config is None:
52+
raise ValueError("Config is missing!")
53+
return self._config
54+
55+
def __init__(self, otdet_builder: OtdetBuilder) -> None:
56+
self._config: Config | None = None
57+
self._otdet_builder = otdet_builder
5158

5259
def update_config(self, config: Config) -> None:
5360
self._config = config
@@ -58,8 +65,8 @@ def start(self) -> None:
5865
Writes detections to one file per video/object.
5966
6067
"""
61-
filetypes = self._config.filetypes.video_filetypes.to_list()
62-
video_files = get_files(paths=self._config.detect.paths, filetypes=filetypes)
68+
filetypes = self.config.filetypes.video_filetypes.to_list()
69+
video_files = get_files(paths=self.config.detect.paths, filetypes=filetypes)
6370

6471
start_msg = f"Start detection of {len(video_files)} video files"
6572
log.info(start_msg)
@@ -70,22 +77,22 @@ def start(self) -> None:
7077
return
7178

7279
model = create_model(
73-
weights=self._config.detect.yolo_config.weights,
74-
confidence=self._config.detect.yolo_config.conf,
75-
iou=self._config.detect.yolo_config.iou,
76-
img_size=self._config.detect.yolo_config.img_size,
77-
half_precision=self._config.detect.half_precision,
78-
normalized=self._config.detect.yolo_config.normalized,
80+
weights=self.config.detect.yolo_config.weights,
81+
confidence=self.config.detect.yolo_config.conf,
82+
iou=self.config.detect.yolo_config.iou,
83+
img_size=self.config.detect.yolo_config.img_size,
84+
half_precision=self.config.detect.half_precision,
85+
normalized=self.config.detect.yolo_config.normalized,
7986
)
8087
for video_file in tqdm(video_files, desc="Detected video files", unit=" files"):
8188
detections_file = derive_filename(
8289
video_file=video_file,
83-
detect_start=self._config.detect.detect_start,
84-
detect_end=self._config.detect.detect_end,
85-
detect_suffix=self._config.filetypes.detect,
90+
detect_start=self.config.detect.detect_start,
91+
detect_end=self.config.detect.detect_end,
92+
detect_suffix=self.config.filetypes.detect,
8693
)
8794

88-
if not self._config.detect.overwrite and detections_file.is_file():
95+
if not self.config.detect.overwrite and detections_file.is_file():
8996
log.warning(
9097
f"{detections_file} already exists. To overwrite, set overwrite "
9198
"to True"
@@ -96,10 +103,10 @@ def start(self) -> None:
96103

97104
video_fps = get_fps(video_file)
98105
detect_start_in_frames = convert_seconds_to_frames(
99-
self._config.detect.detect_start, video_fps
106+
self.config.detect.detect_start, video_fps
100107
)
101108
detect_end_in_frames = convert_seconds_to_frames(
102-
self._config.detect.detect_end, video_fps
109+
self.config.detect.detect_end, video_fps
103110
)
104111
detections = model.detect(
105112
file=video_file,
@@ -110,34 +117,36 @@ def start(self) -> None:
110117
video_width, video_height = get_video_dimensions(video_file)
111118
actual_duration = get_duration(video_file)
112119
actual_frames = len(detections)
113-
if (expected_duration := self._config.detect.expected_duration) is not None:
120+
if (expected_duration := self.config.detect.expected_duration) is not None:
114121
actual_fps = actual_frames / expected_duration.total_seconds()
115122
else:
116123
actual_fps = actual_frames / actual_duration.total_seconds()
117-
otdet = OtdetBuilder(
118-
conf=model.confidence,
119-
iou=model.iou,
120-
video=video_file,
121-
video_width=video_width,
122-
video_height=video_height,
123-
expected_duration=expected_duration,
124-
recorded_fps=video_fps,
125-
actual_fps=actual_fps,
126-
actual_frames=actual_frames,
127-
detection_img_size=model.img_size,
128-
normalized=model.normalized,
129-
detection_model=model.weights,
130-
half_precision=model.half_precision,
131-
chunksize=1,
132-
classifications=model.classifications,
124+
otdet = self._otdet_builder.add_config(
125+
OtdetBuilderConfig(
126+
conf=model.confidence,
127+
iou=model.iou,
128+
video=video_file,
129+
video_width=video_width,
130+
video_height=video_height,
131+
expected_duration=expected_duration,
132+
recorded_fps=video_fps,
133+
actual_fps=actual_fps,
134+
actual_frames=actual_frames,
135+
detection_img_size=model.img_size,
136+
normalized=model.normalized,
137+
detection_model=model.weights,
138+
half_precision=model.half_precision,
139+
chunksize=1,
140+
classifications=model.classifications,
141+
)
133142
).build(detections)
134143

135144
stamped_detections = add_timestamps(otdet, video_file, expected_duration)
136145
write_json(
137146
stamped_detections,
138147
file=detections_file,
139-
filetype=self._config.filetypes.detect,
140-
overwrite=self._config.detect.overwrite,
148+
filetype=self.config.filetypes.detect,
149+
overwrite=self.config.detect.overwrite,
141150
)
142151

143152
log.info(f"Successfully detected and wrote {detections_file}")

‎OTVision/detect/otdet.py

+61-51
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,60 @@
1+
from dataclasses import dataclass
12
from datetime import timedelta
23
from pathlib import Path
4+
from typing import Self
35

46
from OTVision import dataformat, version
57
from OTVision.track.preprocess import Detection
68

79

10+
@dataclass
11+
class OtdetBuilderConfig:
12+
conf: float
13+
iou: float
14+
video: Path
15+
video_width: int
16+
video_height: int
17+
expected_duration: timedelta | None
18+
recorded_fps: float
19+
actual_fps: float
20+
actual_frames: int
21+
detection_img_size: int
22+
normalized: bool
23+
detection_model: str | Path
24+
half_precision: bool
25+
chunksize: int
26+
classifications: dict[int, str]
27+
28+
29+
class OtdetBuilderError(Exception):
30+
pass
31+
32+
833
class OtdetBuilder:
9-
def __init__(
10-
self,
11-
conf: float,
12-
iou: float,
13-
video: Path,
14-
video_width: int,
15-
video_height: int,
16-
expected_duration: timedelta | None,
17-
recorded_fps: float,
18-
actual_fps: float,
19-
actual_frames: int,
20-
detection_img_size: int,
21-
normalized: bool,
22-
detection_model: str | Path,
23-
half_precision: bool,
24-
chunksize: int,
25-
classifications: dict[int, str],
26-
) -> None:
27-
self._conf = conf
28-
self._iou = iou
29-
self._video = video
30-
self._video_width = video_width
31-
self._video_height = video_height
32-
self._expected_duration = expected_duration
33-
self._recorded_fps = recorded_fps
34-
self._actual_fps = actual_fps
35-
self._actual_frames = actual_frames
36-
self._detection_img_size = detection_img_size
37-
self._normalized = normalized
38-
self._detection_model = detection_model
39-
self._half_precision = half_precision
40-
self._chunksize = chunksize
41-
self._classifications = classifications
34+
@property
35+
def config(self) -> OtdetBuilderConfig:
36+
if self._config is None:
37+
raise OtdetBuilderError("Otdet builder config is not set")
38+
return self._config
39+
40+
def __init__(self) -> None:
41+
self._config: OtdetBuilderConfig | None = None
42+
43+
def add_config(self, config: OtdetBuilderConfig) -> Self:
44+
self._config = config
45+
return self
46+
47+
def reset(self) -> Self:
48+
self._config = None
49+
return self
4250

4351
def build(self, detections: list[list[Detection]]) -> dict:
44-
return {
52+
result = {
4553
dataformat.METADATA: self._build_metadata(),
4654
dataformat.DATA: self._build_data(detections),
4755
}
56+
self.reset()
57+
return result
4858

4959
def _build_metadata(self) -> dict:
5060
return {
@@ -62,17 +72,17 @@ def _build_data(self, frames: list[list[Detection]]) -> dict:
6272

6373
def _build_video_config(self) -> dict:
6474
video_config = {
65-
dataformat.FILENAME: str(self._video.stem),
66-
dataformat.FILETYPE: str(self._video.suffix),
67-
dataformat.WIDTH: self._video_width,
68-
dataformat.HEIGHT: self._video_height,
69-
dataformat.RECORDED_FPS: self._recorded_fps,
70-
dataformat.ACTUAL_FPS: self._actual_fps,
71-
dataformat.NUMBER_OF_FRAMES: self._actual_frames,
75+
dataformat.FILENAME: str(self.config.video.stem),
76+
dataformat.FILETYPE: str(self.config.video.suffix),
77+
dataformat.WIDTH: self.config.video_width,
78+
dataformat.HEIGHT: self.config.video_height,
79+
dataformat.RECORDED_FPS: self.config.recorded_fps,
80+
dataformat.ACTUAL_FPS: self.config.actual_fps,
81+
dataformat.NUMBER_OF_FRAMES: self.config.actual_frames,
7282
}
73-
if self._expected_duration is not None:
83+
if self.config.expected_duration is not None:
7484
video_config[dataformat.EXPECTED_DURATION] = int(
75-
self._expected_duration.total_seconds()
85+
self.config.expected_duration.total_seconds()
7686
)
7787
return video_config
7888

@@ -81,13 +91,13 @@ def _build_detection_config(self) -> dict:
8191
dataformat.OTVISION_VERSION: version.otvision_version(),
8292
dataformat.MODEL: {
8393
dataformat.NAME: "YOLOv8",
84-
dataformat.WEIGHTS: str(self._detection_model),
85-
dataformat.IOU_THRESHOLD: self._iou,
86-
dataformat.IMAGE_SIZE: self._detection_img_size,
87-
dataformat.MAX_CONFIDENCE: self._conf,
88-
dataformat.HALF_PRECISION: self._half_precision,
89-
dataformat.CLASSES: self._classifications,
94+
dataformat.WEIGHTS: str(self.config.detection_model),
95+
dataformat.IOU_THRESHOLD: self.config.iou,
96+
dataformat.IMAGE_SIZE: self.config.detection_img_size,
97+
dataformat.MAX_CONFIDENCE: self.config.conf,
98+
dataformat.HALF_PRECISION: self.config.half_precision,
99+
dataformat.CLASSES: self.config.classifications,
90100
},
91-
dataformat.CHUNKSIZE: self._chunksize,
92-
dataformat.NORMALIZED_BBOX: self._normalized,
101+
dataformat.CHUNKSIZE: self.config.chunksize,
102+
dataformat.NORMALIZED_BBOX: self.config.normalized,
93103
}

‎detect.py

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,10 @@ def main(argv: list[str] | None = None) -> None: # sourcery skip: assign-if-exp
4444
log.info(f"Arguments: {vars(cli_args)}")
4545

4646
try:
47-
OTVisionDetect(config).start()
47+
detect = OTVisionDetect(otdet_builder=builder.otdet_builder)
48+
detect.update_config(config)
49+
detect.start()
50+
4851
except FileNotFoundError:
4952
log.exception(f"One of the following files cannot be found: {cli_args.paths}")
5053
raise

‎tests/cli/test_detect_cli.py

+2-6
Original file line numberDiff line numberDiff line change
@@ -198,11 +198,7 @@ class TestDetectCLI:
198198
TEST_DATA_PARAMS_FROM_CUSTOM_CONFIG,
199199
],
200200
)
201-
def test_pass_detect_cli(
202-
self,
203-
test_data: dict,
204-
detect_cli: Callable,
205-
) -> None:
201+
def test_pass_detect_cli(self, test_data: dict, detect_cli: Callable) -> None:
206202
with patch("detect.OTVisionDetect") as mock_detect:
207203
mock_detect_instance = Mock()
208204
mock_detect.return_value = mock_detect_instance
@@ -224,7 +220,7 @@ def test_pass_detect_cli(
224220
detect_cli(argv=list(filter(None, command)))
225221
expected_config = create_expected_config_from_test_data(test_data)
226222

227-
mock_detect.assert_called_once_with(expected_config)
223+
mock_detect_instance.update_config.assert_called_once_with(expected_config)
228224
mock_detect_instance.start.assert_called_once()
229225

230226
@pytest.mark.parametrize(argnames="test_fail_data", argvalues=TEST_FAIL_DATA)

‎tests/detect/detect_test.py

+40-25
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
from jsonschema import validate
1313

1414
import OTVision.config as config
15-
from OTVision.config import DEFAULT_EXPECTED_DURATION
1615
from OTVision.dataformat import (
1716
CLASS,
1817
CONFIDENCE,
@@ -31,6 +30,7 @@
3130
Y,
3231
)
3332
from OTVision.detect.detect import OTVisionDetect, Timestamper, derive_filename
33+
from OTVision.detect.otdet import OtdetBuilder
3434
from OTVision.detect.yolo import Yolov8, create_model
3535
from tests.conftest import YieldFixture
3636

@@ -259,27 +259,29 @@ class TestDetect:
259259
def result_cyclist_otdet(
260260
self, yolov8m: Yolov8, cyclist_mp4: Path, detect_test_tmp_dir: Path
261261
) -> Path:
262-
OTVisionDetect(
262+
target = create_otvision_detect(
263263
create_config_from(
264264
paths=[cyclist_mp4],
265265
weights=MODEL_WEIGHTS,
266-
expected_duration=DEFAULT_EXPECTED_DURATION,
266+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
267267
)
268-
).start()
268+
)
269+
target.start()
269270

270271
return detect_test_tmp_dir / f"{cyclist_mp4.stem}.otdet"
271272

272273
def test_detect_emptyDirAsParam(self, detect_test_tmp_dir: Path) -> None:
273274
empty_dir = detect_test_tmp_dir / "empty"
274275
empty_dir.mkdir()
275276

276-
OTVisionDetect(
277+
target = create_otvision_detect(
277278
create_config_from(
278279
paths=[empty_dir],
279280
weights=MODEL_WEIGHTS,
280-
expected_duration=DEFAULT_EXPECTED_DURATION,
281+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
281282
)
282-
).start()
283+
)
284+
target.start()
283285

284286
assert os.listdir(empty_dir) == []
285287

@@ -332,10 +334,10 @@ def test_detect_error_raised_on_wrong_filetype(
332334
_config = create_config_from(
333335
paths=[video_path],
334336
weights=MODEL_WEIGHTS,
335-
expected_duration=DEFAULT_EXPECTED_DURATION,
337+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
336338
)
337-
338-
OTVisionDetect(_config).start
339+
target = create_otvision_detect(_config)
340+
target.start()
339341

340342
assert os.listdir(detect_error_wrong_filetype_dir) == [video_file_name]
341343

@@ -347,9 +349,11 @@ def test_detect_bboxes_normalized(self, truck_mp4: Path) -> None:
347349
weights=MODEL_WEIGHTS,
348350
confidence=0.25,
349351
normalized=True,
350-
expected_duration=DEFAULT_EXPECTED_DURATION,
352+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
351353
)
352-
OTVisionDetect(_config).start()
354+
target = create_otvision_detect(_config)
355+
target.start()
356+
353357
otdet_dict = read_bz2_otdet(otdet_file)
354358

355359
detections = [
@@ -367,10 +371,11 @@ def test_detect_bboxes_denormalized(self, truck_mp4: Path) -> None:
367371
_config = create_config_from(
368372
paths=[truck_mp4],
369373
weights=MODEL_WEIGHTS,
370-
expected_duration=DEFAULT_EXPECTED_DURATION,
374+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
371375
normalized=False,
372376
)
373-
OTVisionDetect(_config).start()
377+
target = create_otvision_detect(_config)
378+
target.start()
374379
otdet_dict = read_bz2_otdet(otdet_file)
375380

376381
frames = [
@@ -395,10 +400,11 @@ def test_detect_conf_bbox_above_thresh(
395400
_config = create_config_from(
396401
paths=[truck_mp4],
397402
weights=MODEL_WEIGHTS,
398-
expected_duration=DEFAULT_EXPECTED_DURATION,
403+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
399404
confidence=conf,
400405
)
401-
OTVisionDetect(_config).start()
406+
target = create_otvision_detect(_config)
407+
target.start()
402408
otdet_dict = read_bz2_otdet(otdet_file)
403409

404410
detections = [
@@ -416,24 +422,26 @@ def test_detect_overwrite(
416422
otdet_file = truck_mp4.parent / truck_mp4.with_suffix(".otdet")
417423
otdet_file.unlink(missing_ok=True)
418424

419-
OTVisionDetect(
425+
target = create_otvision_detect(
420426
create_config_from(
421427
paths=[truck_mp4],
422428
weights=MODEL_WEIGHTS,
423-
expected_duration=DEFAULT_EXPECTED_DURATION,
429+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
424430
overwrite=True,
425431
)
426-
).start()
432+
)
433+
target.start()
427434

428435
first_mtime = otdet_file.stat().st_mtime_ns
429-
OTVisionDetect(
436+
target = create_otvision_detect(
430437
create_config_from(
431438
paths=[truck_mp4],
432439
weights=MODEL_WEIGHTS,
433-
expected_duration=DEFAULT_EXPECTED_DURATION,
440+
expected_duration=config.DEFAULT_EXPECTED_DURATION,
434441
overwrite=overwrite,
435442
)
436-
).start()
443+
)
444+
target.start()
437445
second_mtime = otdet_file.stat().st_mtime_ns
438446

439447
if overwrite:
@@ -476,16 +484,17 @@ def test_detection_in_rotated_video(
476484
def _get_detection_counts_for(
477485
self,
478486
converted_video: Path,
479-
expected_duration: timedelta = DEFAULT_EXPECTED_DURATION,
487+
expected_duration: timedelta = config.DEFAULT_EXPECTED_DURATION,
480488
) -> dict[str, float]:
481-
OTVisionDetect(
489+
target = create_otvision_detect(
482490
create_config_from(
483491
paths=[converted_video],
484492
weights=MODEL_WEIGHTS,
485493
expected_duration=expected_duration,
486494
confidence=0.5,
487495
)
488-
).start()
496+
)
497+
target.start()
489498
result_otdet = converted_video.parent / converted_video.with_suffix(".otdet")
490499
otdet_dict = read_bz2_otdet(result_otdet)
491500
frames = [
@@ -580,3 +589,9 @@ def create_config_from(
580589
temp_config[config.DETECT][config.OVERWRITE] = overwrite
581590

582591
return config.Config.from_dict(temp_config)
592+
593+
594+
def create_otvision_detect(otvision_config: config.Config) -> OTVisionDetect:
595+
detect = OTVisionDetect(OtdetBuilder())
596+
detect.update_config(otvision_config)
597+
return detect

‎tests/detect/test_odet.py

+217
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
from datetime import timedelta
2+
from pathlib import Path
3+
from unittest.mock import MagicMock
4+
5+
import pytest
6+
7+
from OTVision import dataformat, version
8+
from OTVision.detect.otdet import OtdetBuilder, OtdetBuilderConfig, OtdetBuilderError
9+
from OTVision.track.preprocess import Detection
10+
11+
12+
def create_expected_video_metadata(config: OtdetBuilderConfig) -> dict:
13+
"""Create the expected video metadata based on the given config."""
14+
video_metadata = {
15+
dataformat.FILENAME: str(config.video.stem),
16+
dataformat.FILETYPE: str(config.video.suffix),
17+
dataformat.WIDTH: config.video_width,
18+
dataformat.HEIGHT: config.video_height,
19+
dataformat.RECORDED_FPS: config.recorded_fps,
20+
dataformat.ACTUAL_FPS: config.actual_fps,
21+
dataformat.NUMBER_OF_FRAMES: config.actual_frames,
22+
}
23+
if config.expected_duration:
24+
video_metadata[dataformat.EXPECTED_DURATION] = int(
25+
config.expected_duration.total_seconds()
26+
)
27+
return video_metadata
28+
29+
30+
def create_expected_detection_metadata(config: OtdetBuilderConfig) -> dict:
31+
"""Create the expected detection metadata based on the given config."""
32+
return {
33+
dataformat.OTVISION_VERSION: version.otvision_version(),
34+
dataformat.MODEL: {
35+
dataformat.NAME: "YOLOv8",
36+
dataformat.WEIGHTS: str(config.detection_model),
37+
dataformat.IOU_THRESHOLD: config.iou,
38+
dataformat.IMAGE_SIZE: config.detection_img_size,
39+
dataformat.MAX_CONFIDENCE: config.conf,
40+
dataformat.HALF_PRECISION: config.half_precision,
41+
dataformat.CLASSES: config.classifications,
42+
},
43+
dataformat.CHUNKSIZE: config.chunksize,
44+
dataformat.NORMALIZED_BBOX: config.normalized,
45+
}
46+
47+
48+
def create_expected_metadata(config: OtdetBuilderConfig) -> dict:
49+
"""Create the full expected metadata based on the given config."""
50+
return {
51+
dataformat.OTDET_VERSION: version.otdet_version(),
52+
dataformat.VIDEO: create_expected_video_metadata(config),
53+
dataformat.DETECTION: create_expected_detection_metadata(config),
54+
}
55+
56+
57+
def create_expected_data(
58+
detections: list[list[Detection]],
59+
) -> dict:
60+
"""Create the expected data dictionary based on input detections."""
61+
data = {}
62+
for frame, detection_list in enumerate(detections, start=1):
63+
data[str(frame)] = {
64+
dataformat.DETECTIONS: [d.to_otdet() for d in detection_list]
65+
}
66+
return data
67+
68+
69+
class TestOtdetBuilder:
70+
@pytest.fixture
71+
def builder(self) -> OtdetBuilder:
72+
"""Fixture to provide a new instance of OtdetBuilder for every test."""
73+
return OtdetBuilder()
74+
75+
@pytest.fixture
76+
def config(self) -> OtdetBuilderConfig:
77+
"""Fixture to provide a predefined valid OtdetBuilderConfig."""
78+
return OtdetBuilderConfig(
79+
conf=0.5,
80+
iou=0.4,
81+
video=Path("video.mp4"),
82+
video_width=1920,
83+
video_height=1080,
84+
expected_duration=timedelta(seconds=300),
85+
recorded_fps=30.0,
86+
actual_fps=29.97,
87+
actual_frames=1000,
88+
detection_img_size=640,
89+
normalized=True,
90+
detection_model=Path("model.pt"),
91+
half_precision=False,
92+
chunksize=32,
93+
classifications={0: "person", 1: "car"},
94+
)
95+
96+
@pytest.fixture
97+
def mock_detection(self) -> MagicMock:
98+
"""Fixture to provide a mocked Detection object."""
99+
detection: MagicMock = MagicMock(spec=Detection)
100+
detection.to_otdet.return_value = {
101+
dataformat.CLASS: "person",
102+
dataformat.CONFIDENCE: 0.9,
103+
}
104+
return detection
105+
106+
def test_builder_without_config_raises_error(self, builder: OtdetBuilder) -> None:
107+
"""Test that accessing config without setting it raises an error.
108+
109+
#Requirement https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
110+
111+
""" # noqa
112+
with pytest.raises(OtdetBuilderError, match="Otdet builder config is not set"):
113+
_ = builder.config
114+
115+
def test_builder_add_config_successfully(
116+
self, builder: OtdetBuilder, config: OtdetBuilderConfig
117+
) -> None:
118+
"""Test that configuration can be added to the builder.
119+
120+
#Requirement https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
121+
122+
""" # noqa
123+
actual = builder.add_config(config)
124+
assert builder.config == config
125+
assert actual == builder
126+
127+
def test_build_metadata(
128+
self, builder: OtdetBuilder, config: OtdetBuilderConfig
129+
) -> None:
130+
"""Test the metadata generation.
131+
132+
https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
133+
134+
""" # noqa
135+
builder.add_config(config)
136+
actual = builder._build_metadata()
137+
138+
expected = create_expected_metadata(config)
139+
assert actual == expected
140+
141+
def test_build_data(
142+
self,
143+
builder: OtdetBuilder,
144+
config: OtdetBuilderConfig,
145+
mock_detection: MagicMock,
146+
) -> None:
147+
"""Test the data generation with detection objects.
148+
149+
https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
150+
151+
""" # noqa
152+
builder.add_config(config)
153+
154+
# Provide mock detections as input
155+
given: list[list[Detection]] = [[mock_detection] * 2, [mock_detection]]
156+
actual = builder._build_data(given)
157+
158+
expected = create_expected_data(given)
159+
assert actual == expected
160+
161+
def test_build_full_result(
162+
self,
163+
builder: OtdetBuilder,
164+
config: OtdetBuilderConfig,
165+
mock_detection: MagicMock,
166+
) -> None:
167+
"""Test the full build method.
168+
169+
170+
https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
171+
""" # noqa
172+
builder.add_config(config)
173+
174+
# Provide mock detections
175+
given: list[list[Detection]] = [[mock_detection] * 3]
176+
actual = builder.build(given)
177+
178+
expected_metadata = create_expected_metadata(config)
179+
expected_data = create_expected_data(given)
180+
expected = {
181+
dataformat.METADATA: expected_metadata,
182+
dataformat.DATA: expected_data,
183+
}
184+
assert actual == expected
185+
186+
def test_reset_builder(
187+
self, builder: OtdetBuilder, config: OtdetBuilderConfig
188+
) -> None:
189+
"""Test that the builder is reset after building.
190+
191+
https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
192+
193+
""" # noqa
194+
builder.add_config(config)
195+
assert builder.config == config
196+
197+
actual = builder.reset()
198+
assert actual == builder
199+
assert builder._config is None
200+
201+
def test_empty_detections_builds_valid_data(
202+
self, builder: OtdetBuilder, config: OtdetBuilderConfig
203+
) -> None:
204+
"""Test that an empty detection list builds valid data.
205+
206+
https://openproject.platomo.de/projects/001-opentrafficcam-live/work_packages/7188
207+
208+
""" # noqa
209+
builder.add_config(config)
210+
actual = builder.build([])
211+
212+
expected = {
213+
dataformat.METADATA: create_expected_metadata(config),
214+
dataformat.DATA: {},
215+
}
216+
217+
assert actual == expected

0 commit comments

Comments
 (0)
Please sign in to comment.