import numpy as np
[docs]
def vector_between(c1, c2):
"""
Returns a NumPy vector from Coordinate c1 to c2.
Parameters
----------
c1 : Coordinate
First point (x,y,z)
c2 : Coordinate
Second point (x,y,z)
Returns
-------
np.array
A NumPy array representing the vector from c1 to c2.
"""
return np.array([c2.x - c1.x, c2.y - c1.y, c2.z - c1.z])
[docs]
def get_finger_length(finger_name: str, pose) -> float:
"""
Calculate the total 3D length of a finger.
Highkey, this information is pretty arbitrary, since unless you normalize scaling, absolute distances aren't
particularly useful, compared to relative distances.
TODO: write a relative bone length function. That would be more useful.
The length is computed by summing the Euclidean distances between
each pair of adjacent joints for the given finger.
Parameters
----------
finger_name : str
Name of the finger (case-insensitive). Must be a key in
`handposeutils.data.constants.FINGER_MAPPING`
(e.g., "thumb", "index", "middle", "ring", "pinky").
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
float
Total finger length in the same units as the pose coordinates.
"""
finger_name = finger_name.upper()
from handposeutils.data.constants import FINGER_MAPPING
indices = FINGER_MAPPING[finger_name]
coords = [pose[i] for i in indices]
# Sum distances between adjacent joints along the finger
length = 0.0
for i in range(len(coords) - 1):
v = vector_between(coords[i], coords[i+1])
length += np.linalg.norm(v)
return length
[docs]
def get_finger_segment_lengths(finger_name: str, pose) -> list[float]:
"""
Get the individual segment lengths of a finger.
Returns the lengths of the proximal, intermediate, and distal segments
by computing Euclidean distances between successive joints.
Parameters
----------
finger_name : str
Name of the finger (case-insensitive). Must be a key in
`handposeutils.data.constants.FINGER_MAPPING`.
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
list of float
Three lengths (same units as coordinates), ordered from base to tip.
See Also
--------
get_finger_length
gets the summed length of the full finger
"""
finger_name = finger_name.upper()
from handposeutils.data.constants import FINGER_MAPPING
indices = FINGER_MAPPING[finger_name]
coords = [pose[i] for i in indices]
# Return lengths between successive joints (3 segments per finger)
return [np.linalg.norm(vector_between(coords[i], coords[i+1])) for i in range(3)]
[docs]
def get_finger_curvature(finger_name: str, pose) -> float:
"""
Estimate the average angular curvature of a finger.
Curvature is measured as the mean angle (in radians) between consecutive
segment vectors. Lower values indicate a straighter finger.
Parameters
----------
finger_name : str
Name of the finger (case-insensitive).
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
float
Average curvature angle in radians.
"""
finger_name = finger_name.upper()
from handposeutils.data.constants import FINGER_MAPPING
indices = FINGER_MAPPING[finger_name]
a, b, c, d = [pose[i] for i in indices]
# Get vectors between adjacent joints
v1 = vector_between(a, b)
v2 = vector_between(b, c)
v3 = vector_between(c, d)
def angle_between(v1, v2):
# Classic cosine angle formula
dot = np.dot(v1, v2)
norms = np.linalg.norm(v1) * np.linalg.norm(v2)
cos_theta = np.clip(dot / (norms + 1e-6), -1.0, 1.0)
return np.arccos(cos_theta)
# Average the two segment angles
return (angle_between(v1, v2) + angle_between(v2, v3)) / 2.0
[docs]
def get_total_hand_span(pose) -> float:
"""
Compute the Euclidean distance between thumb tip and pinky tip.
Parameters
----------
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
float
Distance between landmarks 4 (thumb tip) and 20 (pinky tip).
"""
thumb_tip = pose[4]
pinky_tip = pose[20]
# Simple Euclidean distance
return np.linalg.norm(vector_between(thumb_tip, pinky_tip))
[docs]
def get_finger_spread(pose) -> dict[str, float]:
"""
Measure the angular spread between adjacent fingers at their MCP joints.
Parameters
----------
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
dict of str to float
Mapping from finger pair names (e.g., "INDEX-MIDDLE") to spread angle in radians.
"""
base_indices = [5, 9, 13, 17] # MCPs for index → pinky
names = ["INDEX", "MIDDLE", "RING", "PINKY"]
spread = {}
for i in range(len(base_indices) - 1):
a = pose[base_indices[i]]
b = pose[0] # Wrist
c = pose[base_indices[i + 1]]
# Vectors from wrist to adjacent MCPs
v1 = vector_between(b, a)
v2 = vector_between(b, c)
# Angle between MCP direction vectors
dot = np.dot(v1, v2)
norms = np.linalg.norm(v1) * np.linalg.norm(v2)
angle = np.arccos(np.clip(dot / (norms + 1e-6), -1.0, 1.0))
spread[f"{names[i]}-{names[i+1]}"] = angle
return spread
[docs]
def get_hand_aspect_ratio(pose) -> float:
"""
Calculate the aspect ratio (width / height) of the hand in the XY plane.
Parameters
----------
pose : Pose
Hand pose object
Returns
-------
float
Ratio of hand width to height in the XY plane.
"""
coords = np.array([c.as_tuple() for c in pose.get_all_coordinates()])
min_x, max_x = coords[:, 0].min(), coords[:, 0].max()
min_y, max_y = coords[:, 1].min(), coords[:, 1].max()
width = max_x - min_x
height = max_y - min_y
return width / (height + 1e-6)
[docs]
def get_pose_flatness(pose, axis='z') -> float:
"""
Measure the flatness of the hand pose along a given axis.
Flatness is defined as the standard deviation of all coordinates
along the specified axis.
Parameters
----------
pose : Pose
Hand pose object with `get_all_coordinates()` method.
axis : {'x', 'y', 'z'}, optional
Axis along which to compute flatness. Default is 'z'.
Returns
-------
float
Standard deviation along the specified axis. Lower values mean flatter pose.
"""
coords = np.array([c.as_tuple() for c in pose.get_all_coordinates()])
axis_map = {'x': 0, 'y': 1, 'z': 2}
return np.std(coords[:, axis_map[axis]])
[docs]
def get_joint_angle(triplet: tuple[int, int, int], pose) -> float:
"""
Compute the internal angle at the middle joint of a 3-point chain.
Parameters
----------
triplet : tuple of int
Landmark indices (a, b, c) where `b` is the vertex joint.
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
float
Angle at point `b` in radians.
"""
a, b, c = (pose[i] for i in triplet)
v1 = vector_between(b, a)
v2 = vector_between(b, c)
dot = np.dot(v1, v2)
norms = np.linalg.norm(v1) * np.linalg.norm(v2)
return np.arccos(np.clip(dot / (norms + 1e-6), -1.0, 1.0))
[docs]
def get_palm_normal_vector(pose) -> np.ndarray:
"""
Compute the palm normal vector using three base landmarks.
Uses the cross product of the wrist-to-index_mcp and wrist-to-pinky_mcp
vectors to obtain the palm's outward normal.
Parameters
----------
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
numpy.ndarray
Normalized 3D vector representing the palm normal.
"""
a = np.array(pose[0].as_tuple()) # Wrist
b = np.array(pose[5].as_tuple()) # Index MCP
c = np.array(pose[17].as_tuple()) # Pinky MCP
# Cross product of two vectors from wrist to edges of palm
v1 = b - a
v2 = c - a
normal = np.cross(v1, v2)
return normal / (np.linalg.norm(normal) + 1e-6)
[docs]
def get_cross_finger_angles(pose) -> dict[str, float]:
"""
Measure the angle between direction vectors of adjacent fingers.
Parameters
----------
pose : Pose
Hand pose object or sequence supporting index-based access to `Coordinate` objects.
Returns
-------
dict of str to float
Mapping from adjacent finger name pairs (e.g., "THUMB-INDEX") to
angle in radians.
"""
from handposeutils.data.constants import FINGER_MAPPING
finger_names = ["THUMB", "INDEX", "MIDDLE", "RING", "PINKY"]
vectors = {}
# Compute normalized vector from base to tip for each finger
for name in finger_names:
indices = FINGER_MAPPING[name]
base, tip = pose[indices[0]], pose[indices[-1]]
vec = vector_between(base, tip)
vectors[name] = vec / (np.linalg.norm(vec) + 1e-6)
angles = {}
for i in range(len(finger_names) - 1):
f1, f2 = finger_names[i], finger_names[i+1]
angle = np.arccos(np.clip(np.dot(vectors[f1], vectors[f2]), -1.0, 1.0))
angles[f"{f1}-{f2}"] = angle
return angles