chore: update demo runtime, tests, and agent docs
This commit is contained in:
+66
-10
@@ -7,7 +7,7 @@ from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Final, cast
|
||||
from typing import Final, Literal, cast
|
||||
from unittest import mock
|
||||
|
||||
import numpy as np
|
||||
@@ -693,9 +693,11 @@ class MockVisualizer:
|
||||
self,
|
||||
frame: NDArray[np.uint8],
|
||||
bbox: tuple[int, int, int, int] | None,
|
||||
bbox_mask: tuple[int, int, int, int] | None,
|
||||
track_id: int,
|
||||
mask_raw: NDArray[np.uint8] | None,
|
||||
silhouette: NDArray[np.float32] | None,
|
||||
segmentation_input: NDArray[np.float32] | None,
|
||||
label: str | None,
|
||||
confidence: float | None,
|
||||
fps: float,
|
||||
@@ -704,9 +706,11 @@ class MockVisualizer:
|
||||
{
|
||||
"frame": frame,
|
||||
"bbox": bbox,
|
||||
"bbox_mask": bbox_mask,
|
||||
"track_id": track_id,
|
||||
"mask_raw": mask_raw,
|
||||
"silhouette": silhouette,
|
||||
"segmentation_input": segmentation_input,
|
||||
"label": label,
|
||||
"confidence": confidence,
|
||||
"fps": fps,
|
||||
@@ -761,9 +765,8 @@ def test_pipeline_visualizer_updates_on_no_detection() -> None:
|
||||
visualize=True,
|
||||
)
|
||||
|
||||
# Replace the visualizer with our mock
|
||||
mock_viz = MockVisualizer()
|
||||
pipeline._visualizer = mock_viz # type: ignore[assignment]
|
||||
setattr(pipeline, "_visualizer", mock_viz)
|
||||
|
||||
# Run pipeline
|
||||
_ = pipeline.run()
|
||||
@@ -779,13 +782,14 @@ def test_pipeline_visualizer_updates_on_no_detection() -> None:
|
||||
for call in mock_viz.update_calls:
|
||||
assert call["track_id"] == 0 # Default track_id when no detection
|
||||
assert call["bbox"] is None # No bbox when no detection
|
||||
assert call["bbox_mask"] is None
|
||||
assert call["mask_raw"] is None # No mask when no detection
|
||||
assert call["silhouette"] is None # No silhouette when no detection
|
||||
assert call["segmentation_input"] is None
|
||||
assert call["label"] is None # No label when no detection
|
||||
assert call["confidence"] is None # No confidence when no detection
|
||||
|
||||
|
||||
|
||||
def test_pipeline_visualizer_uses_cached_detection_on_no_detection() -> None:
|
||||
"""Test that visualizer reuses last valid detection when current frame has no detection.
|
||||
|
||||
@@ -818,8 +822,8 @@ def test_pipeline_visualizer_uses_cached_detection_on_no_detection() -> None:
|
||||
mock_detector.track.side_effect = [
|
||||
[mock_result], # Frame 0: valid detection
|
||||
[mock_result], # Frame 1: valid detection
|
||||
[], # Frame 2: no detection
|
||||
[], # Frame 3: no detection
|
||||
[], # Frame 2: no detection
|
||||
[], # Frame 3: no detection
|
||||
]
|
||||
mock_yolo.return_value = mock_detector
|
||||
|
||||
@@ -835,7 +839,12 @@ def test_pipeline_visualizer_uses_cached_detection_on_no_detection() -> None:
|
||||
dummy_mask = np.random.randint(0, 256, (480, 640), dtype=np.uint8)
|
||||
dummy_bbox_mask = (100, 100, 200, 300)
|
||||
dummy_bbox_frame = (100, 100, 200, 300)
|
||||
mock_select_person.return_value = (dummy_mask, dummy_bbox_mask, dummy_bbox_frame, 1)
|
||||
mock_select_person.return_value = (
|
||||
dummy_mask,
|
||||
dummy_bbox_mask,
|
||||
dummy_bbox_frame,
|
||||
1,
|
||||
)
|
||||
|
||||
# Setup mock mask_to_silhouette to return valid silhouette
|
||||
dummy_silhouette = np.random.rand(64, 44).astype(np.float32)
|
||||
@@ -856,9 +865,8 @@ def test_pipeline_visualizer_uses_cached_detection_on_no_detection() -> None:
|
||||
visualize=True,
|
||||
)
|
||||
|
||||
# Replace the visualizer with our mock
|
||||
mock_viz = MockVisualizer()
|
||||
pipeline._visualizer = mock_viz # type: ignore[assignment]
|
||||
setattr(pipeline, "_visualizer", mock_viz)
|
||||
|
||||
# Run pipeline
|
||||
_ = pipeline.run()
|
||||
@@ -886,9 +894,57 @@ def test_pipeline_visualizer_uses_cached_detection_on_no_detection() -> None:
|
||||
"not None/blank"
|
||||
)
|
||||
|
||||
# The cached masks should be copies (different objects) to prevent mutation issues
|
||||
segmentation_inputs = [
|
||||
call["segmentation_input"] for call in mock_viz.update_calls
|
||||
]
|
||||
bbox_mask_calls = [call["bbox_mask"] for call in mock_viz.update_calls]
|
||||
assert segmentation_inputs[0] is not None
|
||||
assert segmentation_inputs[1] is not None
|
||||
assert segmentation_inputs[2] is not None
|
||||
assert segmentation_inputs[3] is not None
|
||||
assert bbox_mask_calls[0] == dummy_bbox_mask
|
||||
assert bbox_mask_calls[1] == dummy_bbox_mask
|
||||
assert bbox_mask_calls[2] == dummy_bbox_mask
|
||||
assert bbox_mask_calls[3] == dummy_bbox_mask
|
||||
|
||||
if mask_raw_calls[1] is not None and mask_raw_calls[2] is not None:
|
||||
assert mask_raw_calls[1] is not mask_raw_calls[2], (
|
||||
"Cached mask should be a copy, not the same object reference"
|
||||
)
|
||||
|
||||
|
||||
def test_frame_pacer_emission_count_24_to_15() -> None:
|
||||
from opengait.demo.pipeline import _FramePacer
|
||||
|
||||
pacer = _FramePacer(15.0)
|
||||
interval_ns = int(1_000_000_000 / 24)
|
||||
emitted = sum(pacer.should_emit(i * interval_ns) for i in range(100))
|
||||
assert 60 <= emitted <= 65
|
||||
|
||||
|
||||
def test_frame_pacer_requires_positive_target_fps() -> None:
|
||||
from opengait.demo.pipeline import _FramePacer
|
||||
|
||||
with pytest.raises(ValueError, match="target_fps must be positive"):
|
||||
_FramePacer(0.0)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("window", "stride", "mode", "expected"),
|
||||
[
|
||||
(30, 30, "manual", 30),
|
||||
(30, 7, "manual", 7),
|
||||
(30, 30, "sliding", 1),
|
||||
(30, 1, "chunked", 30),
|
||||
(15, 3, "chunked", 15),
|
||||
],
|
||||
)
|
||||
def test_resolve_stride_modes(
|
||||
window: int,
|
||||
stride: int,
|
||||
mode: Literal["manual", "sliding", "chunked"],
|
||||
expected: int,
|
||||
) -> None:
|
||||
from opengait.demo.pipeline import resolve_stride
|
||||
|
||||
assert resolve_stride(window, stride, mode) == expected
|
||||
|
||||
Reference in New Issue
Block a user