Files
duck-preview/tests/test_bbox_overlay.py
bartool e9b474b1ed feat: Add video playback functionality and inference support
- Introduced VideoPlayer class to handle local video playback, emitting frames via frame_ready signal.
- Updated MainWindow to switch between camera and video sources, integrating video playback controls.
- Enhanced AppMenuBar with options to open video files and manage inference models.
- Implemented BboxOverlay for displaying detection results on video frames.
- Added InferenceManager to manage YOLO inference in a separate process, with error handling and restart logic.
- Created tests for BboxOverlay and InferenceManager to ensure functionality and robustness.
- Updated pyproject.toml to include optional dependencies for inference support.
2026-05-13 21:30:13 +02:00

181 lines
6.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tests for BboxOverlay — coordinate mapping and state management."""
from __future__ import annotations
from unittest.mock import MagicMock
import pytest
from PySide6.QtCore import QRect, QSize
from app.inference.bbox_overlay import BboxOverlay, Detection
class TestDetection:
def test_namedtuple_fields(self) -> None:
d = Detection(x1=10.0, y1=20.0, x2=100.0, y2=200.0, conf=0.87, label="label")
assert d.x1 == 10.0
assert d.label == "label"
assert d.conf == pytest.approx(0.87)
def test_immutable(self) -> None:
d = Detection(0, 0, 1, 1, 0.5, "x")
with pytest.raises(AttributeError):
d.conf = 0.9 # type: ignore[misc]
class TestBboxOverlayState:
def setup_method(self) -> None:
self.overlay = BboxOverlay()
def test_initially_no_detections(self) -> None:
assert self.overlay._detections == []
def test_initially_source_size_empty(self) -> None:
assert self.overlay._source_size.isEmpty()
def test_on_detections_stores_data(self) -> None:
dets = [Detection(0, 0, 100, 100, 0.9, "label")]
self.overlay.on_detections(dets, (640, 480))
assert self.overlay._detections == dets
assert self.overlay._source_size == QSize(640, 480)
def test_clear_removes_detections(self) -> None:
self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
self.overlay.clear()
assert self.overlay._detections == []
def test_visible_by_default(self) -> None:
assert self.overlay.visible is True
def test_multiple_detections_stored(self) -> None:
dets = [
Detection(0, 0, 50, 50, 0.9, "label"),
Detection(100, 100, 200, 200, 0.75, "label"),
]
self.overlay.on_detections(dets, (640, 480))
assert len(self.overlay._detections) == 2
def test_replace_detections_on_new_call(self) -> None:
self.overlay.on_detections([Detection(0, 0, 10, 10, 0.5, "x")], (100, 100))
self.overlay.on_detections([], (100, 100))
assert self.overlay._detections == []
class TestBboxOverlayCoordinateMapping:
"""
Verify that BboxOverlay correctly maps source-frame pixel coordinates
onto the letterboxed video_rect when painting.
We don't test actual QPainter output — instead we verify that the
QRect values passed to painter.drawRect() correspond to the expected
scaled coordinates.
"""
def setup_method(self) -> None:
self.overlay = BboxOverlay()
def _make_painter_mock(self):
painter = MagicMock()
fm = MagicMock()
fm.height.return_value = 14
fm.ascent.return_value = 11
fm.horizontalAdvance.return_value = 60
painter.fontMetrics.return_value = fm
return painter
def test_paint_skips_when_no_detections(self) -> None:
painter = self._make_painter_mock()
self.overlay.paint(painter, QRect(0, 0, 640, 480))
painter.drawRect.assert_not_called()
def test_paint_skips_when_source_size_empty(self) -> None:
# detections present but source_size not set
self.overlay._detections = [Detection(0, 0, 100, 100, 0.9, "label")]
painter = self._make_painter_mock()
self.overlay.paint(painter, QRect(0, 0, 640, 480))
painter.drawRect.assert_not_called()
def test_bbox_scaled_to_full_video_rect(self) -> None:
"""
Source: 640×480, covers full frame.
video_rect: 640×480 at origin.
Detection: full-frame box → should map 1:1.
"""
self.overlay.on_detections(
[Detection(0.0, 0.0, 640.0, 480.0, 0.99, "label")],
(640, 480),
)
painter = self._make_painter_mock()
video_rect = QRect(0, 0, 640, 480)
self.overlay.paint(painter, video_rect)
# First drawRect call = the bounding box
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
assert first_call_rect.x() == 0
assert first_call_rect.y() == 0
assert first_call_rect.width() == 640
assert first_call_rect.height() == 480
def test_bbox_scaled_with_half_size_video_rect(self) -> None:
"""
Source: 640×480, video_rect: 320×240 at origin (0.5× scale).
Detection at (64, 48)→(128, 96) should map to (32, 24)→(64, 48).
"""
self.overlay.on_detections(
[Detection(64.0, 48.0, 128.0, 96.0, 0.8, "label")],
(640, 480),
)
painter = self._make_painter_mock()
video_rect = QRect(0, 0, 320, 240)
self.overlay.paint(painter, video_rect)
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
assert first_call_rect.x() == 32
assert first_call_rect.y() == 24
assert first_call_rect.width() == 32 # (128-64) * 0.5
assert first_call_rect.height() == 24 # (96-48) * 0.5
def test_bbox_offset_by_video_rect_origin(self) -> None:
"""
video_rect at (100, 50) — letterboxed with margins.
Detection at origin of source should map to (100, 50).
"""
self.overlay.on_detections(
[Detection(0.0, 0.0, 100.0, 100.0, 0.9, "label")],
(640, 480),
)
painter = self._make_painter_mock()
# video_rect 320×240 starting at (100, 50)
video_rect = QRect(100, 50, 320, 240)
self.overlay.paint(painter, video_rect)
first_call_rect: QRect = painter.drawRect.call_args_list[0][0][0]
# x: 100 + int(0 * 320/640) = 100
# y: 50 + int(0 * 240/480) = 50
assert first_call_rect.x() == 100
assert first_call_rect.y() == 50
class TestBboxOverlayWorkerPacket:
"""Test FramePacket and ResultPacket data structures."""
def test_frame_packet_fields(self) -> None:
from app.inference.worker import FramePacket
pkt = FramePacket(
frame_id=1,
raw_bytes=b"\x00" * 12,
width=2,
height=2,
channels=3,
)
assert pkt.frame_id == 1
assert pkt.width == 2
assert pkt.channels == 3
def test_result_packet_fields(self) -> None:
from app.inference.worker import ResultPacket
pkt = ResultPacket(frame_id=5, detections=[], width=640, height=480)
assert pkt.frame_id == 5
assert pkt.detections == []
assert pkt.width == 640