""" Per-frame time series for the joints relevant to a screening test. Builds angle-over-time series (used by the angle graph) and a per-joint flexion summary at a chosen frame (degrees + open/closed label). Reuses the biomechanics geometry helpers so angle definitions never diverge. """ from __future__ import annotations import math from formscout.agents.biomechanics import _angle_between_points, _get_joint from formscout.analysis.relevant_joints import openness_label, relevant_angles def angle_series(pose2d, test_name: str) -> dict[str, list[float]]: """Return {angle_name: [deg per frame]} for this test's relevant angles. Frames where the angle cannot be measured hold NaN. """ angles = relevant_angles(test_name) series: dict[str, list[float]] = {name: [] for name in angles} for kps in pose2d.keypoints: for name, (a, b, c) in angles.items(): pa, pb, pc = _get_joint(kps, a), _get_joint(kps, b), _get_joint(kps, c) if pa and pb and pc: series[name].append(_angle_between_points(pa, pb, pc)) else: series[name].append(float("nan")) return series def relevant_flexion_at(pose2d, test_name: str, frame_idx: int) -> dict[str, dict]: """At one frame, the relevant joint angles with degree value + openness label. Returns {angle_name: {"deg": float, "openness": str}}; angles that cannot be measured at that frame are omitted. """ out: dict[str, dict] = {} if not (0 <= frame_idx < len(pose2d.keypoints)): return out kps = pose2d.keypoints[frame_idx] for name, (a, b, c) in relevant_angles(test_name).items(): pa, pb, pc = _get_joint(kps, a), _get_joint(kps, b), _get_joint(kps, c) if pa and pb and pc: deg = _angle_between_points(pa, pb, pc) if not math.isnan(deg): out[name] = {"deg": deg, "openness": openness_label(deg)} return out