chore: update demo runtime, tests, and agent docs

This commit is contained in:
2026-03-02 12:33:17 +08:00
parent 1f8f959ad7
commit cbb3284c13
14 changed files with 1491 additions and 236 deletions
+66 -10
View File
@@ -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