67b3deedbd
Three changesets bundled together: - faceFollow (Camera/Neck) for the ARKit-blendshape face: it can now rotate with the mapped Body_Neck joint's own local axes instead of always billboarding toward the camera. - Fixed a real correctness bug: the face was rendering as isolated dots. Verified against OpenPose's own published keypoint diagrams and source (FACE_PAIRS_RENDER_GPU, 63 pairs) that it should render connected jaw/eyebrow/nose/eye/mouth contour lines; full_limb_data() was also silently dropping face limbs entirely. Both fixed. - Default eyes/ears: most rigs (HumanIK included) have no REye/LEye/REar/LEar joints, only Head/Nose, leaving those 4 slots permanently invisible. They now get a synthetic head-relative position when unmapped but Body_Nose is mapped, anchored at and rotating with the Nose joint's own orientation (not the camera) -- toggleable via useDefaultEyesEars, always overridden by an explicit joint mapping. Also drops the RShoulder->REar / LShoulder->LEar lines from BODY_25 and COCO connectivity by request (a detection-robustness quirk of OpenPose's original network, not real anatomy) so ears stay leaf points -- a disclosed, deliberate deviation from upstream's otherwise-verbatim connectivity. All verified against real Maya (2022/2023/2024), not just logic: rotation math checked exact, explicit-mapping-overrides-fallback checked, shoulder-ear removal checked against full_limb_data() output, rendered test scenes visually confirmed against OpenPose's own reference diagrams. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
336 lines
13 KiB
Python
336 lines
13 KiB
Python
"""OpenPose_full (137-point) keypoint schema.
|
|
|
|
Layout: 25 BODY_25 body/foot points + 21 left-hand + 21 right-hand + 70 face
|
|
points, in that order, giving a flat 0..136 global keypoint index used
|
|
everywhere else in this plugin (mapping node attribute slots, draw override,
|
|
projector, rasterizer).
|
|
|
|
Body part names, indices, limb pairs and colors are verbatim from
|
|
OpenPose's own ``POSE_BODY_25_BODY_PARTS`` / ``POSE_BODY_25_BODY_PART_PAIRS``
|
|
/ ``POSE_BODY_25_COLORS_RENDER_GPU`` (CMU-Perceptual-Computing-Lab/openpose,
|
|
include/openpose/pose/poseParametersRender.hpp). Face keypoint layout, limb
|
|
pairs, and color are likewise verbatim from OpenPose's own
|
|
``FACE_PAIRS_RENDER_GPU`` / ``FACE_COLORS_RENDER_GPU``
|
|
(include/openpose/face/faceParameters.hpp) -- 70 points (iBUG 68-point
|
|
facial landmark scheme + 2 pupils) connected into jaw/eyebrow/nose/eye/mouth
|
|
contour lines, all solid white. Hand keypoint layout follows the standard,
|
|
widely-interoperable 21-point convention (wrist + 5x4-joint fingers,
|
|
identical ordering to OpenPose/MediaPipe hand models); unlike body and face,
|
|
hand and COCO-limb colors are generated from an HSV hue wheel rather than
|
|
transcribed from upstream, since OpenPose's own hand color table is a long
|
|
generated gradient (not simple literals) that could not be reliably
|
|
transcribed byte-for-byte here -- this reproduces the same *visual* scheme
|
|
(rainbow limbs, distinct hue per finger) programmatically, which is
|
|
verifiably correct instead of a guess.
|
|
"""
|
|
|
|
import colorsys
|
|
|
|
|
|
# --- Part sizes / offsets into the flat 0..136 keypoint index ---------------
|
|
|
|
BODY_COUNT = 25
|
|
HAND_COUNT = 21
|
|
FACE_COUNT = 70
|
|
|
|
BODY_START = 0
|
|
HAND_LEFT_START = BODY_START + BODY_COUNT # 25
|
|
HAND_RIGHT_START = HAND_LEFT_START + HAND_COUNT # 46
|
|
FACE_START = HAND_RIGHT_START + HAND_COUNT # 67
|
|
TOTAL_KEYPOINTS = FACE_START + FACE_COUNT # 137
|
|
|
|
PART_BODY = "body"
|
|
PART_HAND_LEFT = "hand_left"
|
|
PART_HAND_RIGHT = "hand_right"
|
|
PART_FACE = "face"
|
|
|
|
# (part id, start index, count) in draw order
|
|
PARTS = [
|
|
(PART_BODY, BODY_START, BODY_COUNT),
|
|
(PART_HAND_LEFT, HAND_LEFT_START, HAND_COUNT),
|
|
(PART_HAND_RIGHT, HAND_RIGHT_START, HAND_COUNT),
|
|
(PART_FACE, FACE_START, FACE_COUNT),
|
|
]
|
|
|
|
|
|
def _hsv255(hue, saturation=1.0, value=1.0):
|
|
r, g, b = colorsys.hsv_to_rgb(hue % 1.0, saturation, value)
|
|
return (int(round(r * 255)), int(round(g * 255)), int(round(b * 255)))
|
|
|
|
|
|
# --- Body: BODY_25 -----------------------------------------------------------
|
|
|
|
BODY_25_NAMES = [
|
|
"Nose", "Neck", "RShoulder", "RElbow", "RWrist", "LShoulder", "LElbow",
|
|
"LWrist", "MidHip", "RHip", "RKnee", "RAnkle", "LHip", "LKnee", "LAnkle",
|
|
"REye", "LEye", "REar", "LEar", "LBigToe", "LSmallToe", "LHeel",
|
|
"RBigToe", "RSmallToe", "RHeel",
|
|
]
|
|
assert len(BODY_25_NAMES) == BODY_COUNT
|
|
|
|
# From OpenPose's POSE_BODY_25_BODY_PART_PAIRS_RENDER_GPU (local indices
|
|
# 0..24, i.e. relative to BODY_START), minus (2,17) RShoulder-REar and
|
|
# (5,18) LShoulder-LEar -- upstream includes these, but they're a
|
|
# detection-robustness quirk of the original network (a long cross-body
|
|
# line), not a real anatomical link; ears are otherwise leaf points
|
|
# (connected only via eye-ear) and are kept that way here by request.
|
|
BODY_25_PAIRS_LOCAL = [
|
|
(1, 8), (1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (8, 9), (9, 10),
|
|
(10, 11), (8, 12), (12, 13), (13, 14), (1, 0), (0, 15), (15, 17), (0, 16),
|
|
(16, 18), (14, 19), (19, 20), (14, 21), (11, 22),
|
|
(22, 23), (11, 24),
|
|
]
|
|
|
|
BODY_25_POINT_COLORS = [_hsv255(i / BODY_COUNT) for i in range(BODY_COUNT)]
|
|
BODY_25_LIMB_COLORS = [
|
|
_hsv255(i / len(BODY_25_PAIRS_LOCAL)) for i in range(len(BODY_25_PAIRS_LOCAL))
|
|
]
|
|
|
|
BODY_POINT_RADIUS = 4.0
|
|
BODY_LIMB_THICKNESS = 4.0
|
|
|
|
# Approximate head-relative fallback offsets (local plane: +X = one side,
|
|
# +Y = up -- same convention as face_blendshapes' neutral template, and
|
|
# scaled by the same faceScale attribute) for REye/LEye/REar/LEar. Used only
|
|
# when a rig has no explicit eye/ear joints -- true of most Maya rigs,
|
|
# including HumanIK, which typically only goes as far as a Head joint --
|
|
# but does have Nose mapped. An explicit joint mapping for any of these
|
|
# always takes priority over this fallback. Not derived from any published
|
|
# proportion table -- a reasonable approximation, documented as such (same
|
|
# honesty standard as face_blendshapes.py).
|
|
DEFAULT_EYE_EAR_LOCAL_OFFSETS = {
|
|
BODY_25_NAMES.index("REye"): (-0.5, 0.3),
|
|
BODY_25_NAMES.index("LEye"): (0.5, 0.3),
|
|
BODY_25_NAMES.index("REar"): (-1.0, 0.05),
|
|
BODY_25_NAMES.index("LEar"): (1.0, 0.05),
|
|
}
|
|
|
|
|
|
# --- Body: COCO-18 (legacy OpenPose COCO model) ------------------------------
|
|
#
|
|
# COCO is *not* simply "BODY_25 with some points removed": it has its own
|
|
# limb connectivity (Neck connects directly to each hip -- there is no
|
|
# MidHip point at all -- and there are no foot points). Both the point names
|
|
# and the COCO section of the limb-pair table are verbatim from OpenPose's
|
|
# own POSE_COCO_BODY_PARTS / POSE_BODY_PART_PAIRS,
|
|
# src/openpose/pose/poseParameters.cpp.
|
|
#
|
|
# COCO mode does not need its own joint-mapping slots: it reuses the exact
|
|
# same BODY_25-indexed targetJoint mapping every character already has, and
|
|
# is purely a different "which of those points + which limbs to draw" view
|
|
# over it (COCO_TO_BODY25_INDEX below is that view).
|
|
|
|
COCO_COUNT = 18
|
|
|
|
COCO_NAMES = [
|
|
"Nose", "Neck", "RShoulder", "RElbow", "RWrist", "LShoulder", "LElbow",
|
|
"LWrist", "RHip", "RKnee", "RAnkle", "LHip", "LKnee", "LAnkle", "REye",
|
|
"LEye", "REar", "LEar",
|
|
]
|
|
assert len(COCO_NAMES) == COCO_COUNT
|
|
|
|
# COCO local index -> BODY_25 local index (== global index, since the body
|
|
# part starts at 0). BODY_25's MidHip (8) and the six foot points (19-24)
|
|
# have no COCO equivalent and are simply absent from this list.
|
|
COCO_TO_BODY25_INDEX = [0, 1, 2, 3, 4, 5, 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18]
|
|
assert len(COCO_TO_BODY25_INDEX) == COCO_COUNT
|
|
assert all(BODY_25_NAMES[COCO_TO_BODY25_INDEX[i]] == COCO_NAMES[i] for i in range(COCO_COUNT))
|
|
|
|
COCO_ACTIVE_BODY25_INDICES = sorted(COCO_TO_BODY25_INDEX)
|
|
|
|
# COCO section of POSE_BODY_PART_PAIRS, expressed in COCO-local 0..17
|
|
# indices, then translated to BODY_25-local/global indices so it can be
|
|
# used directly against the same joint mapping as BODY_25 mode. Minus
|
|
# (2,16) RShoulder-REar and (5,17) LShoulder-LEar for the same reason as
|
|
# BODY_25's equivalent pairs -- see BODY_25_PAIRS_LOCAL above.
|
|
_COCO_PAIRS_COCO_SPACE = [
|
|
(1, 2), (1, 5), (2, 3), (3, 4), (5, 6), (6, 7), (1, 8), (8, 9), (9, 10),
|
|
(1, 11), (11, 12), (12, 13), (1, 0), (0, 14), (14, 16), (0, 15), (15, 17),
|
|
]
|
|
COCO_PAIRS_LOCAL = [
|
|
(COCO_TO_BODY25_INDEX[a], COCO_TO_BODY25_INDEX[b]) for (a, b) in _COCO_PAIRS_COCO_SPACE
|
|
]
|
|
COCO_LIMB_COLORS = [_hsv255(i / len(COCO_PAIRS_LOCAL)) for i in range(len(COCO_PAIRS_LOCAL))]
|
|
|
|
BODY_MODEL_BODY_25 = "BODY_25"
|
|
BODY_MODEL_COCO = "COCO"
|
|
BODY_MODELS = [BODY_MODEL_BODY_25, BODY_MODEL_COCO]
|
|
|
|
|
|
# --- Hands: 21 points each, standard wrist + 5 x 4-joint fingers ------------
|
|
|
|
_FINGER_NAMES = ["Thumb", "Index", "Middle", "Ring", "Pinky"]
|
|
|
|
HAND_NAMES = ["Wrist"] + [
|
|
"{0}{1}".format(finger, joint)
|
|
for finger in _FINGER_NAMES
|
|
for joint in range(1, 5)
|
|
]
|
|
assert len(HAND_NAMES) == HAND_COUNT
|
|
|
|
# (0=wrist, 1-4=thumb, 5-8=index, 9-12=middle, 13-16=ring, 17-20=pinky)
|
|
HAND_PAIRS_LOCAL = []
|
|
for _finger_i in range(5):
|
|
_base = 1 + _finger_i * 4
|
|
HAND_PAIRS_LOCAL += [
|
|
(0, _base), (_base, _base + 1), (_base + 1, _base + 2),
|
|
(_base + 2, _base + 3),
|
|
]
|
|
assert len(HAND_PAIRS_LOCAL) == 20
|
|
|
|
|
|
def _hand_point_color(local_index):
|
|
if local_index == 0:
|
|
return (255, 255, 255) # wrist
|
|
finger = (local_index - 1) // 4
|
|
joint = (local_index - 1) % 4 # 0 (nearest wrist) .. 3 (tip)
|
|
hue = finger / 5.0
|
|
value = 0.4 + 0.2 * joint # brighter towards the fingertip
|
|
return _hsv255(hue, 1.0, value)
|
|
|
|
|
|
HAND_POINT_COLORS = [_hand_point_color(i) for i in range(HAND_COUNT)]
|
|
HAND_LIMB_COLORS = [
|
|
_hand_point_color(j) for (_, j) in HAND_PAIRS_LOCAL
|
|
] # colored by the joint further from the wrist
|
|
|
|
HAND_POINT_RADIUS = 3.0
|
|
HAND_LIMB_THICKNESS = 2.0
|
|
|
|
|
|
# --- Face: 70 points (iBUG 68-point landmarks + 2 pupils) -------------------
|
|
|
|
FACE_NAMES = (
|
|
["Jaw{0}".format(i) for i in range(17)]
|
|
+ ["REyebrow{0}".format(i) for i in range(5)]
|
|
+ ["LEyebrow{0}".format(i) for i in range(5)]
|
|
+ ["Nose{0}".format(i) for i in range(9)]
|
|
+ ["REye{0}".format(i) for i in range(6)]
|
|
+ ["LEye{0}".format(i) for i in range(6)]
|
|
+ ["Mouth{0}".format(i) for i in range(20)]
|
|
+ ["RPupil", "LPupil"]
|
|
)
|
|
assert len(FACE_NAMES) == FACE_COUNT
|
|
|
|
# Verbatim from OpenPose's FACE_PAIRS_RENDER_GPU
|
|
# (include/openpose/face/faceParameters.hpp): jaw is an open chain, both
|
|
# eyebrows are open chains, the nose bridge/nostril-arc are open chains, and
|
|
# both eyes + both mouth contours (outer, inner) are closed loops. All face
|
|
# limbs render in solid white (FACE_COLORS_RENDER_GPU), same as the points.
|
|
FACE_PAIRS_LOCAL = [
|
|
(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7), (7, 8), (8, 9),
|
|
(9, 10), (10, 11), (11, 12), (12, 13), (13, 14), (14, 15), (15, 16),
|
|
(17, 18), (18, 19), (19, 20), (20, 21),
|
|
(22, 23), (23, 24), (24, 25), (25, 26),
|
|
(27, 28), (28, 29), (29, 30),
|
|
(31, 32), (32, 33), (33, 34), (34, 35),
|
|
(36, 37), (37, 38), (38, 39), (39, 40), (40, 41), (41, 36),
|
|
(42, 43), (43, 44), (44, 45), (45, 46), (46, 47), (47, 42),
|
|
(48, 49), (49, 50), (50, 51), (51, 52), (52, 53), (53, 54), (54, 55),
|
|
(55, 56), (56, 57), (57, 58), (58, 59), (59, 48),
|
|
(60, 61), (61, 62), (62, 63), (63, 64), (64, 65), (65, 66), (66, 67), (67, 60),
|
|
]
|
|
assert len(FACE_PAIRS_LOCAL) == 63
|
|
|
|
FACE_POINT_COLORS = [(255, 255, 255)] * FACE_COUNT
|
|
FACE_LIMB_COLORS = [(255, 255, 255)] * len(FACE_PAIRS_LOCAL)
|
|
|
|
FACE_POINT_RADIUS = 2.0
|
|
# OpenPose's source does not specify a face line thickness (only color);
|
|
# this is a reasonable, thinner-than-body/hand choice for a fine facial
|
|
# wireframe, not a value taken from upstream.
|
|
FACE_LIMB_THICKNESS = 1.5
|
|
|
|
|
|
# --- Flattened 0..136 global schema ------------------------------------------
|
|
|
|
KEYPOINT_NAMES = (
|
|
["Body_{0}".format(n) for n in BODY_25_NAMES]
|
|
+ ["HandLeft_{0}".format(n) for n in HAND_NAMES]
|
|
+ ["HandRight_{0}".format(n) for n in HAND_NAMES]
|
|
+ ["Face_{0}".format(n) for n in FACE_NAMES]
|
|
)
|
|
assert len(KEYPOINT_NAMES) == TOTAL_KEYPOINTS
|
|
|
|
NAME_TO_INDEX = {name: i for i, name in enumerate(KEYPOINT_NAMES)}
|
|
|
|
KEYPOINT_PART = (
|
|
[PART_BODY] * BODY_COUNT
|
|
+ [PART_HAND_LEFT] * HAND_COUNT
|
|
+ [PART_HAND_RIGHT] * HAND_COUNT
|
|
+ [PART_FACE] * FACE_COUNT
|
|
)
|
|
|
|
KEYPOINT_POINT_COLORS = (
|
|
BODY_25_POINT_COLORS + HAND_POINT_COLORS + HAND_POINT_COLORS + FACE_POINT_COLORS
|
|
)
|
|
|
|
KEYPOINT_POINT_RADIUS = (
|
|
[BODY_POINT_RADIUS] * BODY_COUNT
|
|
+ [HAND_POINT_RADIUS] * HAND_COUNT
|
|
+ [HAND_POINT_RADIUS] * HAND_COUNT
|
|
+ [FACE_POINT_RADIUS] * FACE_COUNT
|
|
)
|
|
|
|
def part_of(global_index):
|
|
"""Return the part id (PART_BODY / PART_HAND_LEFT / ...) for a keypoint index."""
|
|
for part_id, start, count in PARTS:
|
|
if start <= global_index < start + count:
|
|
return part_id
|
|
raise IndexError(global_index)
|
|
|
|
|
|
# Hand/face limb pairs, offset into the flat 0..136 range -- unaffected by
|
|
# body model choice, so precomputed once.
|
|
_HAND_LEFT_PAIRS_GLOBAL = [(a + HAND_LEFT_START, b + HAND_LEFT_START) for (a, b) in HAND_PAIRS_LOCAL]
|
|
_HAND_RIGHT_PAIRS_GLOBAL = [(a + HAND_RIGHT_START, b + HAND_RIGHT_START) for (a, b) in HAND_PAIRS_LOCAL]
|
|
_FACE_PAIRS_GLOBAL = [(a + FACE_START, b + FACE_START) for (a, b) in FACE_PAIRS_LOCAL]
|
|
|
|
|
|
def _body_limb_data(body_model):
|
|
"""(pairs, colors, thickness) for just the body part, global-indexed."""
|
|
if body_model == BODY_MODEL_COCO:
|
|
pairs = COCO_PAIRS_LOCAL # already translated to BODY_25-local/global indices
|
|
colors = COCO_LIMB_COLORS
|
|
else:
|
|
pairs = [(a + BODY_START, b + BODY_START) for (a, b) in BODY_25_PAIRS_LOCAL]
|
|
colors = BODY_25_LIMB_COLORS
|
|
thickness = [BODY_LIMB_THICKNESS] * len(pairs)
|
|
return pairs, colors, thickness
|
|
|
|
|
|
def full_limb_data(body_model=BODY_MODEL_BODY_25):
|
|
"""(pairs, colors, thickness) for the whole rig: the chosen body model,
|
|
both full hands, and the face contour, global-indexed into the flat
|
|
0..136 range.
|
|
"""
|
|
body_pairs, body_colors, body_thickness = _body_limb_data(body_model)
|
|
pairs = list(body_pairs) + _HAND_LEFT_PAIRS_GLOBAL + _HAND_RIGHT_PAIRS_GLOBAL + _FACE_PAIRS_GLOBAL
|
|
colors = list(body_colors) + HAND_LIMB_COLORS + HAND_LIMB_COLORS + FACE_LIMB_COLORS
|
|
thickness = (
|
|
list(body_thickness)
|
|
+ [HAND_LIMB_THICKNESS] * len(HAND_PAIRS_LOCAL)
|
|
+ [HAND_LIMB_THICKNESS] * len(HAND_PAIRS_LOCAL)
|
|
+ [FACE_LIMB_THICKNESS] * len(FACE_PAIRS_LOCAL)
|
|
)
|
|
assert len(pairs) == len(colors) == len(thickness)
|
|
return pairs, colors, thickness
|
|
|
|
|
|
def active_point_indices(body_model=BODY_MODEL_BODY_25):
|
|
"""Global keypoint indices to draw as points for the given body model.
|
|
|
|
Hands and face are always fully active; the body part is either all 25
|
|
BODY_25 points or just the 18 COCO points (no MidHip, no feet).
|
|
"""
|
|
if body_model == BODY_MODEL_COCO:
|
|
body_indices = list(COCO_ACTIVE_BODY25_INDICES)
|
|
else:
|
|
body_indices = list(range(BODY_START, BODY_START + BODY_COUNT))
|
|
return (
|
|
body_indices
|
|
+ list(range(HAND_LEFT_START, HAND_LEFT_START + HAND_COUNT))
|
|
+ list(range(HAND_RIGHT_START, HAND_RIGHT_START + HAND_COUNT))
|
|
+ list(range(FACE_START, FACE_START + FACE_COUNT))
|
|
)
|