fix(demo): pace gait windows before buffering
Make the OpenGait-studio demo drop unpaced frames before they grow the silhouette window. Separate source-frame gap tracking from paced-frame stride tracking so runtime scheduling matches the documented demo-window-and-stride behavior. Add regressions for paced window growth and schedule-frame stride semantics.
This commit is contained in:
@@ -927,6 +927,88 @@ def test_frame_pacer_emission_count_24_to_15() -> None:
|
||||
assert 60 <= emitted <= 65
|
||||
|
||||
|
||||
def test_pipeline_pacing_skips_window_growth_until_emitted() -> None:
|
||||
from opengait_studio.pipeline import ScoliosisPipeline
|
||||
|
||||
with (
|
||||
mock.patch("opengait_studio.pipeline.YOLO") as mock_yolo,
|
||||
mock.patch("opengait_studio.pipeline.create_source") as mock_source,
|
||||
mock.patch("opengait_studio.pipeline.create_publisher") as mock_publisher,
|
||||
mock.patch("opengait_studio.pipeline.ScoNetDemo") as mock_classifier,
|
||||
mock.patch("opengait_studio.pipeline.select_person") as mock_select_person,
|
||||
mock.patch("opengait_studio.pipeline.mask_to_silhouette") as mock_mask_to_sil,
|
||||
):
|
||||
mock_detector = mock.MagicMock()
|
||||
mock_box = mock.MagicMock()
|
||||
mock_box.xyxy = np.array([[100, 100, 200, 300]], dtype=np.float32)
|
||||
mock_box.id = np.array([1], dtype=np.int64)
|
||||
mock_mask = mock.MagicMock()
|
||||
mock_mask.data = np.random.rand(1, 480, 640).astype(np.float32)
|
||||
mock_result = mock.MagicMock()
|
||||
mock_result.boxes = mock_box
|
||||
mock_result.masks = mock_mask
|
||||
mock_detector.track.return_value = [mock_result]
|
||||
mock_yolo.return_value = mock_detector
|
||||
mock_source.return_value = []
|
||||
mock_publisher.return_value = mock.MagicMock()
|
||||
|
||||
mock_model = mock.MagicMock()
|
||||
mock_model.predict.return_value = ("neutral", 0.7)
|
||||
mock_classifier.return_value = mock_model
|
||||
|
||||
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)
|
||||
dummy_silhouette = np.random.rand(64, 44).astype(np.float32)
|
||||
mock_select_person.return_value = (
|
||||
dummy_mask,
|
||||
dummy_bbox_mask,
|
||||
dummy_bbox_frame,
|
||||
1,
|
||||
)
|
||||
mock_mask_to_sil.return_value = dummy_silhouette
|
||||
|
||||
pipeline = ScoliosisPipeline(
|
||||
source="dummy.mp4",
|
||||
checkpoint="dummy.pt",
|
||||
config=str(CONFIG_PATH) if CONFIG_PATH.exists() else "dummy.yaml",
|
||||
device="cpu",
|
||||
yolo_model="dummy.pt",
|
||||
window=2,
|
||||
stride=1,
|
||||
nats_url=None,
|
||||
nats_subject="test",
|
||||
max_frames=None,
|
||||
target_fps=15.0,
|
||||
)
|
||||
frame = np.zeros((480, 640, 3), dtype=np.uint8)
|
||||
|
||||
first = pipeline.process_frame(
|
||||
frame,
|
||||
{"frame_count": 0, "timestamp_ns": 1_000_000_000},
|
||||
)
|
||||
second = pipeline.process_frame(
|
||||
frame,
|
||||
{"frame_count": 1, "timestamp_ns": 1_033_000_000},
|
||||
)
|
||||
third = pipeline.process_frame(
|
||||
frame,
|
||||
{"frame_count": 2, "timestamp_ns": 1_067_000_000},
|
||||
)
|
||||
|
||||
assert first is not None
|
||||
assert second is not None
|
||||
assert third is not None
|
||||
assert first["segmentation_input"] is not None
|
||||
assert second["segmentation_input"] is not None
|
||||
assert third["segmentation_input"] is not None
|
||||
assert first["segmentation_input"].shape[0] == 1
|
||||
assert second["segmentation_input"].shape[0] == 1
|
||||
assert second["label"] is None
|
||||
assert third["segmentation_input"].shape[0] == 2
|
||||
assert third["label"] == "neutral"
|
||||
|
||||
|
||||
def test_frame_pacer_requires_positive_target_fps() -> None:
|
||||
from opengait_studio.pipeline import _FramePacer
|
||||
|
||||
|
||||
@@ -144,6 +144,21 @@ class TestSilhouetteWindow:
|
||||
window.push(sil, frame_idx=7, track_id=1)
|
||||
assert window.should_classify()
|
||||
|
||||
def test_should_classify_uses_schedule_frame_idx(self) -> None:
|
||||
"""Stride should be measured in paced/scheduled frames, not source frames."""
|
||||
window = SilhouetteWindow(window_size=2, stride=2, gap_threshold=50)
|
||||
sil = np.ones((64, 44), dtype=np.float32)
|
||||
|
||||
window.push(sil, frame_idx=0, track_id=1, schedule_frame_idx=0)
|
||||
window.push(sil, frame_idx=10, track_id=1, schedule_frame_idx=1)
|
||||
|
||||
assert window.should_classify()
|
||||
|
||||
window.mark_classified()
|
||||
window.push(sil, frame_idx=20, track_id=1, schedule_frame_idx=2)
|
||||
|
||||
assert not window.should_classify()
|
||||
|
||||
def test_should_classify_not_ready(self) -> None:
|
||||
"""should_classify should return False when window not ready."""
|
||||
window = SilhouetteWindow(window_size=5, stride=1, gap_threshold=10)
|
||||
|
||||
Reference in New Issue
Block a user