diff --git a/src/python/pose_format/utils/alphapose_133.py b/src/python/pose_format/utils/alphapose_133.py index 8ceec93..78168d4 100644 --- a/src/python/pose_format/utils/alphapose_133.py +++ b/src/python/pose_format/utils/alphapose_133.py @@ -2,74 +2,52 @@ from ..numpy.pose_body import NumPyPoseBody from ..pose import Pose from ..pose_header import PoseHeader, PoseHeaderComponent, PoseHeaderDimensions -from .alphapose import ( - FACE_POINTS, FACE_LIMBS_NAMES, GENERAL_HAND_POINTS, - LEFT_HAND_POINTS, RIGHT_HAND_POINTS, HAND_LIMBS_NAMES, - _map_limbs, load_alphapose_json, parse_keypoints_and_confidence, _apply_metadata, +from .alphapose import load_alphapose_json, parse_keypoints_and_confidence, _apply_metadata +from .cocowholebody133_header import ( + BODY_POINTS, FACE_POINTS, HAND_POINTS, + BODY_LIMBS, FACE_LIMBS, HAND_LIMBS, ) -# 133-keypoint body (no neck, head_top, or pelvis compared to 136) -BODY_POINTS = [ - "nose", "left_eye", "right_eye", "left_ear", "right_ear", - "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", - "left_wrist", "right_wrist", "left_hip", "right_hip", - "left_knee", "right_knee", "left_ankle", "right_ankle", - "left_big_toe", "left_small_toe", "left_heel", - "right_big_toe", "right_small_toe", "right_heel", -] - -BODY_LIMBS_NAMES = [ - ("left_ankle", "left_knee"), ("left_knee", "left_hip"), - ("right_ankle", "right_knee"), ("right_knee", "right_hip"), - ("left_hip", "right_hip"), - ("left_shoulder", "left_hip"), ("right_shoulder", "right_hip"), - ("left_shoulder", "right_shoulder"), - ("left_shoulder", "left_elbow"), ("right_shoulder", "right_elbow"), - ("left_elbow", "left_wrist"), ("right_elbow", "right_wrist"), - ("left_eye", "right_eye"), ("nose", "left_eye"), ("nose", "right_eye"), - ("left_eye", "left_ear"), ("right_eye", "right_ear"), - ("left_ear", "left_shoulder"), ("right_ear", "right_shoulder"), - ("left_ankle", "left_big_toe"), ("left_ankle", "left_small_toe"), ("left_ankle", "left_heel"), - ("right_ankle", "right_big_toe"), ("right_ankle", "right_small_toe"), ("right_ankle", "right_heel"), -] - def get_alphapose_133_components(): """ Returns AlphaPose WholeBody-133 component definitions. + AlphaPose 133 is the COCO Wholebody 133-keypoint format. Point lists and + limb connectivity are shared with cocowholebody133_header; only the component + names differ (suffixed with _133 for backward compatibility). + Returns ------- list of PoseHeaderComponent Components for body, face, left hand, and right hand. """ - hand_limbs = _map_limbs(GENERAL_HAND_POINTS, HAND_LIMBS_NAMES) return [ PoseHeaderComponent( name="BODY_133", points=BODY_POINTS, - limbs=_map_limbs(BODY_POINTS, BODY_LIMBS_NAMES), + limbs=BODY_LIMBS, colors=[(0, 255, 0)], point_format="XYC" ), PoseHeaderComponent( name="FACE_133", points=FACE_POINTS, - limbs=_map_limbs(FACE_POINTS, FACE_LIMBS_NAMES), + limbs=FACE_LIMBS, colors=[(255, 255, 255)], point_format="XYC" ), PoseHeaderComponent( name="LEFT_HAND_133", - points=GENERAL_HAND_POINTS, - limbs=hand_limbs, + points=HAND_POINTS, + limbs=HAND_LIMBS, colors=[(0, 255, 0)], point_format="XYC" ), PoseHeaderComponent( name="RIGHT_HAND_133", - points=GENERAL_HAND_POINTS, - limbs=hand_limbs, + points=HAND_POINTS, + limbs=HAND_LIMBS, colors=[(255, 128, 0)], point_format="XYC" ), diff --git a/src/python/pose_format/utils/cocowholebody133_header.py b/src/python/pose_format/utils/cocowholebody133_header.py new file mode 100644 index 0000000..d242e16 --- /dev/null +++ b/src/python/pose_format/utils/cocowholebody133_header.py @@ -0,0 +1,118 @@ +from ..pose_header import PoseHeaderComponent + +# --- Canonical COCO Wholebody 133 point lists --- + +BODY_POINTS = [ + "nose", "left_eye", "right_eye", "left_ear", "right_ear", + "left_shoulder", "right_shoulder", "left_elbow", "right_elbow", + "left_wrist", "right_wrist", "left_hip", "right_hip", + "left_knee", "right_knee", "left_ankle", "right_ankle", + "left_big_toe", "left_small_toe", "left_heel", + "right_big_toe", "right_small_toe", "right_heel", +] + +# Face uses dash separator ("face-N") matching the COCO Wholebody / MMPose convention. +FACE_POINTS = [f"face-{i}" for i in range(68)] + +# Single hand point list shared by both LEFT_HAND and RIGHT_HAND components. +# The component name (LEFT_HAND / RIGHT_HAND) carries the side; point names are generic. +# This mirrors the AlphaPose convention and allows normalize_hands_3d to work correctly. +HAND_POINTS = [f"hand_{i}" for i in range(21)] + + +# --- Limb connectivity --- + +def _map_limbs(points, limb_names): + index_map = {name: idx for idx, name in enumerate(points)} + return [(index_map[a], index_map[b]) for (a, b) in limb_names] + + +BODY_LIMBS_NAMES = [ + ("left_ankle", "left_knee"), ("left_knee", "left_hip"), + ("right_ankle", "right_knee"), ("right_knee", "right_hip"), + ("left_hip", "right_hip"), + ("left_shoulder", "left_hip"), ("right_shoulder", "right_hip"), + ("left_shoulder", "right_shoulder"), + ("left_shoulder", "left_elbow"), ("right_shoulder", "right_elbow"), + ("left_elbow", "left_wrist"), ("right_elbow", "right_wrist"), + ("left_eye", "right_eye"), ("nose", "left_eye"), ("nose", "right_eye"), + ("left_eye", "left_ear"), ("right_eye", "right_ear"), + ("left_ear", "left_shoulder"), ("right_ear", "right_shoulder"), + ("left_ankle", "left_big_toe"), ("left_ankle", "left_small_toe"), ("left_ankle", "left_heel"), + ("right_ankle", "right_big_toe"), ("right_ankle", "right_small_toe"), ("right_ankle", "right_heel"), +] + +# 68-point face connectivity (jaw, brows, nose bridge, eyes, lips). +FACE_LIMBS_NAMES = [ + ("face-0", "face-1"), ("face-1", "face-2"), ("face-2", "face-3"), ("face-3", "face-4"), + ("face-4", "face-5"), ("face-5", "face-6"), ("face-6", "face-7"), ("face-7", "face-8"), + ("face-8", "face-9"), ("face-9", "face-10"), ("face-10", "face-11"), ("face-11", "face-12"), + ("face-12", "face-13"), ("face-13", "face-14"), ("face-14", "face-15"), ("face-15", "face-16"), + ("face-17", "face-18"), ("face-18", "face-19"), ("face-19", "face-20"), ("face-20", "face-21"), + ("face-22", "face-23"), ("face-23", "face-24"), ("face-24", "face-25"), ("face-25", "face-26"), + ("face-27", "face-28"), ("face-28", "face-29"), ("face-29", "face-30"), + ("face-31", "face-32"), ("face-32", "face-33"), ("face-33", "face-34"), ("face-34", "face-35"), + ("face-36", "face-37"), ("face-37", "face-38"), ("face-38", "face-39"), + ("face-39", "face-40"), ("face-40", "face-41"), + ("face-42", "face-43"), ("face-43", "face-44"), ("face-44", "face-45"), + ("face-45", "face-46"), ("face-46", "face-47"), + ("face-48", "face-49"), ("face-49", "face-50"), ("face-50", "face-51"), ("face-51", "face-52"), + ("face-52", "face-53"), ("face-53", "face-54"), ("face-54", "face-55"), ("face-55", "face-56"), + ("face-56", "face-57"), ("face-57", "face-58"), ("face-58", "face-59"), ("face-59", "face-60"), + ("face-60", "face-61"), ("face-61", "face-62"), ("face-62", "face-63"), ("face-63", "face-64"), + ("face-64", "face-65"), ("face-65", "face-66"), ("face-66", "face-67"), +] + +HAND_LIMBS_NAMES = [ + ("hand_0", "hand_1"), ("hand_1", "hand_2"), ("hand_2", "hand_3"), ("hand_3", "hand_4"), + ("hand_0", "hand_5"), ("hand_5", "hand_6"), ("hand_6", "hand_7"), ("hand_7", "hand_8"), + ("hand_0", "hand_9"), ("hand_9", "hand_10"), ("hand_10", "hand_11"), ("hand_11", "hand_12"), + ("hand_0", "hand_13"), ("hand_13", "hand_14"), ("hand_14", "hand_15"), ("hand_15", "hand_16"), + ("hand_0", "hand_17"), ("hand_17", "hand_18"), ("hand_18", "hand_19"), ("hand_19", "hand_20"), +] + +# Pre-computed limb index pairs — import these instead of recomputing. +BODY_LIMBS = _map_limbs(BODY_POINTS, BODY_LIMBS_NAMES) +FACE_LIMBS = _map_limbs(FACE_POINTS, FACE_LIMBS_NAMES) +HAND_LIMBS = _map_limbs(HAND_POINTS, HAND_LIMBS_NAMES) + + +def cocowholebody_components(): + """ + Creates the four PoseHeaderComponent objects for COCO Wholebody 133. + + Returns + ------- + list of PoseHeaderComponent + [BODY (23 pts), FACE (68 pts), LEFT_HAND (21 pts), RIGHT_HAND (21 pts)] + """ + return [ + PoseHeaderComponent( + name="BODY", + points=BODY_POINTS, + limbs=BODY_LIMBS, + colors=[(0, 255, 0)], + point_format="XYC" + ), + PoseHeaderComponent( + name="FACE", + points=FACE_POINTS, + limbs=FACE_LIMBS, + colors=[(255, 255, 255)], + point_format="XYC" + ), + PoseHeaderComponent( + name="LEFT_HAND", + points=HAND_POINTS, + limbs=HAND_LIMBS, + colors=[(0, 255, 0)], + point_format="XYC" + ), + PoseHeaderComponent( + name="RIGHT_HAND", + points=HAND_POINTS, + limbs=HAND_LIMBS, + colors=[(255, 128, 0)], + point_format="XYC" + ), + ] diff --git a/src/python/pose_format/utils/generic.py b/src/python/pose_format/utils/generic.py index 357aedb..5316a1c 100644 --- a/src/python/pose_format/utils/generic.py +++ b/src/python/pose_format/utils/generic.py @@ -11,11 +11,12 @@ from pose_format.utils.openpose_135 import OpenPose_Components as OpenPose135_Components from pose_format.utils.alphapose import get_alphapose_components from pose_format.utils.alphapose_133 import get_alphapose_133_components +from pose_format.utils.cocowholebody133_header import cocowholebody_components # from pose_format.utils.holistic import holistic_components # The import above creates an error: ImportError: Please install mediapipe with: pip install mediapipe -KnownPoseFormat = Literal["holistic", "openpose", "openpose_135", "alphapose_133", "alphapose_136"] +KnownPoseFormat = Literal["holistic", "openpose", "openpose_135", "alphapose_133", "alphapose_136", "coco_wholebody_133"] def get_component_names( pose_or_header_or_components: Union[Pose,PoseHeader]) -> List[str]: @@ -58,6 +59,11 @@ def detect_known_pose_format(pose_or_header: Union[Pose,PoseHeader]) -> KnownPos if component_name in alphapose_136_components: return "alphapose_136" + # COCO wholebody 133 uses "BODY", "FACE", "LEFT_HAND", "RIGHT_HAND" — distinct from alphapose ("BODY_133" etc.) + # When smplest-x or other formats using "BODY" are added, distinguish by body point names at that point. + if "BODY" in component_names and "LEFT_HAND" in component_names: + return "coco_wholebody_133" + raise ValueError( f"Could not detect pose format, unknown pose header schema with component names: {component_names}" ) @@ -107,6 +113,16 @@ def pose_hide_legs(pose: Pose, remove: bool = False) -> Pose: ] points_to_remove_dict = {f"BODY_{variant}": point_names_to_remove} + elif known_pose_format == "coco_wholebody_133": + point_names_to_remove = [ + "left_hip", "right_hip", + "left_knee", "right_knee", + "left_ankle", "right_ankle", + "left_big_toe", "left_small_toe", "left_heel", + "right_big_toe", "right_small_toe", "right_heel", + ] + points_to_remove_dict = {"BODY": point_names_to_remove} + else: raise NotImplementedError( f"Unsupported pose header schema {known_pose_format} for {pose_hide_legs.__name__}: {pose.header}" @@ -148,6 +164,9 @@ def pose_shoulders(pose_header: PoseHeader) -> Tuple[Tuple[str, str], Tuple[str, variant = known_pose_format[len("alphapose_"):] return (f"BODY_{variant}", "right_shoulder"), (f"BODY_{variant}", "left_shoulder") + if known_pose_format == "coco_wholebody_133": + return ("BODY", "right_shoulder"), ("BODY", "left_shoulder") + raise NotImplementedError( f"Unsupported pose header schema {known_pose_format} for {pose_shoulders.__name__}: {pose_header}" ) @@ -173,6 +192,11 @@ def hands_indexes(pose_header: PoseHeader)-> List[int]: pose_header.get_point_index(f"LEFT_HAND_{variant}", "hand_9"), pose_header.get_point_index(f"RIGHT_HAND_{variant}", "hand_9"), ] + if known_pose_format == "coco_wholebody_133": + return [ + pose_header.get_point_index("LEFT_HAND", "hand_9"), + pose_header.get_point_index("RIGHT_HAND", "hand_9"), + ] raise NotImplementedError( f"Unsupported pose header schema {known_pose_format} for {hands_indexes.__name__}: {pose_header}" ) @@ -198,6 +222,8 @@ def hands_components(pose_header: PoseHeader)-> Tuple[Tuple[str, str], Tuple[str if known_pose_format == "alphapose_133" or known_pose_format == "alphapose_136": variant = known_pose_format[len("alphapose_"):] return (f"LEFT_HAND_{variant}", f"RIGHT_HAND_{variant}"), ("hand_0", "hand_17", "hand_5"), ("hand_0", "hand_9") + if known_pose_format == "coco_wholebody_133": + return ("LEFT_HAND", "RIGHT_HAND"), ("hand_0", "hand_17", "hand_5"), ("hand_0", "hand_9") raise NotImplementedError( f"Unsupported pose header schema '{known_pose_format}' for {hands_components.__name__}: {pose_header}" ) @@ -247,6 +273,8 @@ def get_standard_components_for_known_format(known_pose_format: KnownPoseFormat) return get_alphapose_133_components() if known_pose_format == "alphapose_136": return get_alphapose_components() + if known_pose_format == "coco_wholebody_133": + return cocowholebody_components() raise NotImplementedError(f"Unsupported pose header schema {known_pose_format}") @@ -315,6 +343,8 @@ def get_hand_wrist_index(pose: Pose, hand: str)-> int: if known_pose_format == "alphapose_133" or known_pose_format == "alphapose_136": variant = known_pose_format[len("alphapose_"):] return pose.header.get_point_index(f"{hand.upper()}_HAND_{variant}", "hand_0") + if known_pose_format == "coco_wholebody_133": + return pose.header.get_point_index(f"{hand.upper()}_HAND", "hand_0") raise NotImplementedError( f"Unsupported pose header schema {known_pose_format} for {get_hand_wrist_index.__name__}: {pose.header}" ) @@ -329,6 +359,8 @@ def get_body_hand_wrist_index(pose: Pose, hand: str)-> int: if known_pose_format == "alphapose_133" or known_pose_format == "alphapose_136": variant = known_pose_format[len("alphapose_"):] return pose.header.get_point_index(f"BODY_{variant}", f"{hand.lower()}_wrist") + if known_pose_format == "coco_wholebody_133": + return pose.header.get_point_index("BODY", f"{hand.lower()}_wrist") raise NotImplementedError( f"Unsupported pose header schema {known_pose_format} for {get_body_hand_wrist_index.__name__}: {pose.header}" ) diff --git a/src/python/pose_format/utils/generic_test.py b/src/python/pose_format/utils/generic_test.py index c072bc8..63f57aa 100644 --- a/src/python/pose_format/utils/generic_test.py +++ b/src/python/pose_format/utils/generic_test.py @@ -4,6 +4,7 @@ import pytest from pose_format.pose import Pose from pose_format.pose_header import PoseNormalizationInfo +from pose_format.utils.cocowholebody133_header import cocowholebody_components from pose_format.utils.generic import ( detect_known_pose_format, get_component_names, @@ -240,6 +241,21 @@ def test_pose_remove_legs(fake_poses: List[Pose]): component_index = c_names.index(f"BODY_{known_pose_format[-3:]}") pose_with_legs_removed = pose_hide_legs(pose, remove=True) + for point_name in points_that_should_be_removed: + assert point_name not in pose_with_legs_removed.header.components[component_index].points, f"{pose_with_legs_removed.header.components[component_index].name},{pose_with_legs_removed.header.components[component_index].points}" + assert point_name in pose.header.components[component_index].points + elif known_pose_format == "coco_wholebody_133": + c_names = [c.name for c in pose.header.components] + points_that_should_be_removed = [ + "left_hip", "right_hip", + "left_knee", "right_knee", + "left_ankle", "right_ankle", + "left_big_toe", "left_small_toe", "left_heel", + "right_big_toe", "right_small_toe", "right_heel", + ] + component_index = c_names.index("BODY") + pose_with_legs_removed = pose_hide_legs(pose, remove=True) + for point_name in points_that_should_be_removed: assert point_name not in pose_with_legs_removed.header.components[component_index].points, f"{pose_with_legs_removed.header.components[component_index].name},{pose_with_legs_removed.header.components[component_index].points}" assert point_name in pose.header.components[component_index].points @@ -290,6 +306,8 @@ def test_fake_pose(known_pose_format: KnownPoseFormat): assert point_formats[0] == "XYC" elif detected_format == 'alphapose_133' or detected_format == 'alphapose_136': assert point_formats[0] == "XYC" + elif detected_format == 'coco_wholebody_133': + assert point_formats[0] == "XYC" assert detected_format == known_pose_format @@ -297,3 +315,23 @@ def test_fake_pose(known_pose_format: KnownPoseFormat): assert pose.body.data.shape == (frame_count, 1, pose.header.total_points(), data_dimension_expected) assert pose.body.data.shape[0] == frame_count assert pose.header.num_dims() == pose.body.data.shape[-1] + + +def test_cocowholebody133_total_keypoints(): + components = cocowholebody_components() + total = sum(len(c.points) for c in components) + assert total == 133 + + +def test_cocowholebody133_component_names(): + components = cocowholebody_components() + names = [c.name for c in components] + assert names == ["BODY", "FACE", "LEFT_HAND", "RIGHT_HAND"] + + +def test_cocowholebody133_limb_indices_in_bounds(): + for c in cocowholebody_components(): + n = len(c.points) + for (a, b) in c.limbs: + assert 0 <= a < n, f"{c.name}: limb index {a} out of bounds (n={n})" + assert 0 <= b < n, f"{c.name}: limb index {b} out of bounds (n={n})"