feat(v1): compose temporal fall event pipeline

This commit is contained in:
ila
2026-07-21 11:57:22 +08:00
parent 906e06565e
commit f03662bf5a
16 changed files with 554 additions and 34 deletions
+121
View File
@@ -0,0 +1,121 @@
import numpy as np
from pathlib import Path
from v1.config import AppConfig, EventConfig
from v1.fall_policy import FallEvidencePolicy
from v1.fall_state import FallState, FallStateMachine
from v1.pipeline import FallPipeline
from v1.pose import Keypoint, PersonPose
from v1.tracking import PersonTracker
from v1.video_source import FramePacket, SourceStatus
class _SequencePoseAdapter:
def __init__(self, frames):
self._frames = iter(frames)
def infer(self, _image):
return next(self._frames)
def _pose(horizontal=False):
points = [Keypoint(float(index), float(index), 0.9) for index in range(17)]
if horizontal:
points[5] = Keypoint(20.0, 80.0, 0.9)
points[6] = Keypoint(30.0, 80.0, 0.9)
points[11] = Keypoint(70.0, 100.0, 0.9)
points[12] = Keypoint(80.0, 100.0, 0.9)
else:
points[5] = Keypoint(30.0, 10.0, 0.9)
points[6] = Keypoint(40.0, 10.0, 0.9)
points[11] = Keypoint(30.0, 30.0, 0.9)
points[12] = Keypoint(40.0, 30.0, 0.9)
return PersonPose(
box_xyxy=(20.0, 20.0, 160.0, 160.0),
box_confidence=0.9,
keypoints=tuple(points),
)
def _packet(timestamp):
return FramePacket(
image=np.zeros((180, 180, 3), dtype=np.uint8),
timestamp_monotonic=timestamp,
status=SourceStatus.CONNECTED,
)
def _pipeline(frames):
machine = FallStateMachine(
confirm_window_seconds=1.0,
recovery_window_seconds=2.0,
config_version="cfg-test-pipeline",
)
return (
FallPipeline(
pose_adapter=_SequencePoseAdapter(frames),
tracker=PersonTracker(),
policy=FallEvidencePolicy(suspect_window_seconds=0.5),
state_machine=machine,
keypoint_confidence_threshold=0.4,
),
machine,
)
def _config():
return AppConfig(
source_id="lobby-camera-01",
source_url="rtsp://not-written-to-disk/live",
model_path=Path("models/best.pt"),
model_sha256="a" * 64,
confidence_threshold=0.25,
event=EventConfig(
keypoint_confidence_threshold=0.4,
suspect_window_seconds=0.5,
confirm_window_seconds=1.0,
recovery_window_seconds=2.0,
cooldown_seconds=3.0,
),
event_dir=Path("artifacts/events"),
)
def test_pipeline_confirms_a_recent_drop_that_remains_horizontal():
pipeline, machine = _pipeline([(_pose(),), (_pose(horizontal=True),), (_pose(horizontal=True),)])
pipeline.process(_packet(0.0))
pipeline.process(_packet(0.1))
result = pipeline.process(_packet(1.1))
assert len(result.events) == 1
assert result.events[0].config_version == "cfg-test-pipeline"
assert machine.state_of("P-0001") is FallState.CONFIRMED
def test_pipeline_rejects_a_suspect_when_the_track_is_missing_for_one_frame():
pipeline, machine = _pipeline([(_pose(),), (_pose(horizontal=True),), (), (_pose(horizontal=True),)])
pipeline.process(_packet(0.0))
pipeline.process(_packet(0.1))
pipeline.process(_packet(0.2))
result = pipeline.process(_packet(1.1))
assert result.events == ()
assert machine.state_of("P-0001") is FallState.NORMAL
def test_pipeline_from_config_uses_runtime_version_for_confirmed_event():
config = _config()
pipeline = FallPipeline.from_config(
config,
pose_adapter=_SequencePoseAdapter(
[(_pose(),), (_pose(horizontal=True),), (_pose(horizontal=True),)]
),
)
pipeline.process(_packet(0.0))
pipeline.process(_packet(0.1))
result = pipeline.process(_packet(1.1))
assert result.events[0].config_version == config.runtime_config_version