Files
silver_pose/v2/internal/fall/pipeline_test.go
T

106 lines
3.9 KiB
Go

package fall
import (
"math"
"testing"
"silverpose/v2/internal/pose"
)
func TestEngineConfirmsPersistentHorizontalPoseAtV1Delay(t *testing.T) {
engine, err := NewEngine(EngineConfig{
KeypointConfidenceThreshold: 0.4,
SuspectWindowSeconds: 0.5,
ConfirmWindowSeconds: 1.8,
RecoveryWindowSeconds: 2.0,
CooldownSeconds: 10.0,
RequireRapidDrop: false,
RequireLowerBody: false,
HorizontalAngleThresholdDegrees: 45.0,
ConfigVersion: "cfg-v1-contract",
SessionID: "regression",
})
if err != nil {
t.Fatalf("NewEngine returned an error: %v", err)
}
engine.Process(Frame{Timestamp: 0.0, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(false)}})
engine.Process(Frame{Timestamp: 0.1, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(true)}})
result := engine.Process(Frame{Timestamp: 1.91, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(true)}})
if len(result.Events) != 1 {
t.Fatalf("events = %+v, want one confirmation", result.Events)
}
event := result.Events[0]
if event.TrackID != "P-0001" || math.Abs(event.LatencySeconds-1.81) > 1e-9 || event.ConfigVersion != "cfg-v1-contract" {
t.Fatalf("event = %+v", event)
}
if got := engine.StateOf("P-0001"); got != Confirmed {
t.Fatalf("state = %s, want CONFIRMED", got)
}
}
func TestEngineDoesNotConfirmWhenTrackIsMissing(t *testing.T) {
engine, err := NewEngine(testConfig())
if err != nil {
t.Fatalf("NewEngine returned an error: %v", err)
}
engine.Process(Frame{Timestamp: 0.0, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(false)}})
engine.Process(Frame{Timestamp: 0.1, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(true)}})
engine.Process(Frame{Timestamp: 0.2, Width: 180, Height: 180})
result := engine.Process(Frame{Timestamp: 1.9, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(true)}})
if len(result.Events) != 0 {
t.Fatalf("events = %+v, want none after a missing track", result.Events)
}
}
func TestEngineDoesNotAlarmForUprightSequence(t *testing.T) {
engine, err := NewEngine(testConfig())
if err != nil {
t.Fatalf("NewEngine returned an error: %v", err)
}
for _, timestamp := range []float64{0.0, 0.6, 1.2, 1.8, 2.4} {
result := engine.Process(Frame{Timestamp: timestamp, Width: 180, Height: 180, Poses: []pose.PersonPose{testPose(false)}})
if len(result.Events) != 0 {
t.Fatalf("timestamp %v emitted %+v for upright pose", timestamp, result.Events)
}
}
}
func testConfig() EngineConfig {
return EngineConfig{
KeypointConfidenceThreshold: 0.4,
SuspectWindowSeconds: 0.5,
ConfirmWindowSeconds: 1.8,
RecoveryWindowSeconds: 2.0,
CooldownSeconds: 10.0,
RequireRapidDrop: false,
RequireLowerBody: false,
HorizontalAngleThresholdDegrees: 45.0,
ConfigVersion: "cfg-v1-contract",
SessionID: "regression",
}
}
func testPose(horizontal bool) pose.PersonPose {
person := pose.PersonPose{Box: pose.Box{Left: 20, Top: 20, Right: 160, Bottom: 160}, Confidence: 0.9}
for index := range person.Keypoints {
person.Keypoints[index] = pose.Keypoint{X: float32(index), Y: float32(index), Confidence: 0.9}
}
if horizontal {
person.Keypoints[5] = pose.Keypoint{X: 20, Y: 80, Confidence: 0.9}
person.Keypoints[6] = pose.Keypoint{X: 30, Y: 80, Confidence: 0.9}
person.Keypoints[11] = pose.Keypoint{X: 70, Y: 100, Confidence: 0.9}
person.Keypoints[12] = pose.Keypoint{X: 80, Y: 100, Confidence: 0.9}
} else {
person.Keypoints[5] = pose.Keypoint{X: 30, Y: 10, Confidence: 0.9}
person.Keypoints[6] = pose.Keypoint{X: 40, Y: 10, Confidence: 0.9}
person.Keypoints[11] = pose.Keypoint{X: 30, Y: 30, Confidence: 0.9}
person.Keypoints[12] = pose.Keypoint{X: 40, Y: 30, Confidence: 0.9}
}
return person
}