| """pixel_cursor.rigging — 14-joint humanoid skeleton primitives. |
| |
| Session 1 (Phase 3) scope: data type, canonical joint layout, parent-linkage. |
| Detection / segmentation / overlay live in studio.rigging. |
| |
| The skeleton is image-space (pixel coordinates, (y, x) order). Coordinates are |
| floats so sub-pixel joint positions (from distance-transform peak interpolation) |
| survive without rounding loss. |
| """ |
| from __future__ import annotations |
|
|
| from dataclasses import dataclass |
| from typing import Final, Tuple |
|
|
| import numpy as np |
|
|
|
|
| JOINT_NAMES: Final[Tuple[str, ...]] = ( |
| "head", |
| "neck", |
| "l_shoulder", |
| "r_shoulder", |
| "l_elbow", |
| "r_elbow", |
| "l_wrist", |
| "r_wrist", |
| "l_hip", |
| "r_hip", |
| "l_knee", |
| "r_knee", |
| "l_ankle", |
| "r_ankle", |
| ) |
|
|
| NUM_JOINTS: Final[int] = len(JOINT_NAMES) |
|
|
| PARENTS: Final[Tuple[int, ...]] = ( |
| 1, |
| -1, |
| 1, |
| 1, |
| 2, |
| 3, |
| 4, |
| 5, |
| 1, |
| 1, |
| 8, |
| 9, |
| 10, |
| 11, |
| ) |
|
|
| JOINT_INDEX: Final[dict[str, int]] = {name: i for i, name in enumerate(JOINT_NAMES)} |
|
|
| BONES: Final[Tuple[Tuple[int, int], ...]] = tuple( |
| (PARENTS[i], i) for i in range(NUM_JOINTS) if PARENTS[i] >= 0 |
| ) |
|
|
|
|
| @dataclass(frozen=True, eq=False, repr=False) |
| class Skeleton: |
| positions: np.ndarray |
| confidence: np.ndarray |
| image_shape: Tuple[int, int] |
|
|
| def __post_init__(self) -> None: |
| if self.positions.shape != (NUM_JOINTS, 2): |
| raise ValueError( |
| f"positions must be shape ({NUM_JOINTS}, 2), got {self.positions.shape}" |
| ) |
| if self.confidence.shape != (NUM_JOINTS,): |
| raise ValueError( |
| f"confidence must be shape ({NUM_JOINTS},), got {self.confidence.shape}" |
| ) |
| if self.positions.dtype != np.float32: |
| object.__setattr__(self, "positions", self.positions.astype(np.float32)) |
| if self.confidence.dtype != np.float32: |
| object.__setattr__(self, "confidence", self.confidence.astype(np.float32)) |
| |
| self.positions.setflags(write=False) |
| self.confidence.setflags(write=False) |
|
|
| def __repr__(self) -> str: |
| return ( |
| f"Skeleton(image_shape={self.image_shape}, " |
| f"joints={NUM_JOINTS}, " |
| f"mean_confidence={float(self.confidence.mean()):.2f})" |
| ) |
|
|
| @classmethod |
| def from_positions( |
| cls, |
| positions: np.ndarray, |
| image_shape: Tuple[int, int], |
| confidence: np.ndarray | None = None, |
| ) -> "Skeleton": |
| positions = np.asarray(positions, dtype=np.float32) |
| if confidence is None: |
| confidence = np.ones(NUM_JOINTS, dtype=np.float32) |
| else: |
| confidence = np.asarray(confidence, dtype=np.float32) |
| return cls(positions=positions, confidence=confidence, image_shape=image_shape) |
|
|
| def joint(self, name: str) -> Tuple[float, float]: |
| idx = JOINT_INDEX[name] |
| return float(self.positions[idx, 0]), float(self.positions[idx, 1]) |
|
|
| def to_dict(self) -> dict[str, dict[str, float]]: |
| return { |
| name: { |
| "y": float(self.positions[i, 0]), |
| "x": float(self.positions[i, 1]), |
| "confidence": float(self.confidence[i]), |
| } |
| for i, name in enumerate(JOINT_NAMES) |
| } |
|
|
|
|
| __all__ = [ |
| "JOINT_NAMES", |
| "NUM_JOINTS", |
| "PARENTS", |
| "JOINT_INDEX", |
| "BONES", |
| "Skeleton", |
| ] |
|
|