Files
silver_pose/v1/evidence.py
T

91 lines
3.1 KiB
Python

"""Pose quality and geometric evidence calculations without alarm decisions."""
from dataclasses import dataclass
from math import acos, degrees, hypot
from typing import Optional
from v1.pose import PersonPose
_REQUIRED_JOINTS = (5, 6, 11, 12, 13, 14, 15, 16)
@dataclass(frozen=True)
class PoseQuality:
accepted: bool
reason: str
visible_joint_count: int
@dataclass(frozen=True)
class PoseEvidence:
accepted: bool
horizontal_pose: bool
rapid_vertical_change: bool
horizontal_angle_degrees: Optional[float]
hip_center_y: Optional[float]
torso_length: Optional[float]
reason: str
def assess_pose_quality(
pose: Optional[PersonPose], threshold: float
) -> PoseQuality:
if not 0.0 <= threshold <= 1.0:
raise ValueError("threshold must be between 0 and 1")
if pose is None:
return PoseQuality(False, "missing_pose", 0)
if len(pose.keypoints) != 17:
return PoseQuality(False, "invalid_keypoint_count", 0)
visible_count = sum(
point.confidence >= threshold for point in pose.keypoints
)
if any(pose.keypoints[index].confidence < threshold for index in _REQUIRED_JOINTS):
return PoseQuality(False, "required_joint_low_confidence", visible_count)
return PoseQuality(True, "accepted", visible_count)
def extract_evidence(
pose: Optional[PersonPose],
quality: PoseQuality,
previous: Optional[PoseEvidence],
rapid_drop_torso_ratio: float = 0.5,
horizontal_angle_threshold_degrees: float = 35.0,
) -> PoseEvidence:
"""Return geometric facts; a later state machine decides whether to alarm."""
if not quality.accepted or pose is None:
return PoseEvidence(False, False, False, None, None, None, quality.reason)
if rapid_drop_torso_ratio < 0:
raise ValueError("rapid_drop_torso_ratio must be non-negative")
shoulder_x, shoulder_y = _midpoint(pose, 5, 6)
hip_x, hip_y = _midpoint(pose, 11, 12)
vector_x = hip_x - shoulder_x
vector_y = hip_y - shoulder_y
torso_length = hypot(vector_x, vector_y)
if torso_length == 0:
return PoseEvidence(False, False, False, None, hip_y, 0.0, "degenerate_torso")
cosine_to_horizontal = max(-1.0, min(1.0, abs(vector_x) / torso_length))
horizontal_angle = degrees(acos(cosine_to_horizontal))
horizontal_pose = horizontal_angle <= horizontal_angle_threshold_degrees
rapid_vertical_change = False
if previous is not None and previous.accepted and previous.hip_center_y is not None:
rapid_vertical_change = (
hip_y - previous.hip_center_y >= rapid_drop_torso_ratio * torso_length
)
return PoseEvidence(
accepted=True,
horizontal_pose=horizontal_pose,
rapid_vertical_change=rapid_vertical_change,
horizontal_angle_degrees=horizontal_angle,
hip_center_y=hip_y,
torso_length=torso_length,
reason="accepted",
)
def _midpoint(pose: PersonPose, first_index: int, second_index: int):
first = pose.keypoints[first_index]
second = pose.keypoints[second_index]
return ((first.x + second.x) / 2.0, (first.y + second.y) / 2.0)