Add faceFollow, fix face contour rendering, add default eyes/ears
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>
This commit is contained in:
@@ -5,21 +5,23 @@ 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 and limb pairs are verbatim from OpenPose's own
|
||||
``POSE_BODY_25_BODY_PARTS`` / ``POSE_BODY_25_BODY_PART_PAIRS`` (see
|
||||
CMU-Perceptual-Computing-Lab/openpose, include/openpose/pose/
|
||||
poseParametersRender.hpp). Hand and face keypoint layouts follow the
|
||||
standard, widely-interoperable 21-point hand (wrist + 5x4-joint fingers,
|
||||
identical ordering to OpenPose/MediaPipe hand models) and 70-point face
|
||||
(iBUG 68-point facial landmark scheme + 2 pupils, OpenPose's own face model)
|
||||
conventions.
|
||||
|
||||
Colors are generated from an HSV hue wheel per part rather than
|
||||
hardcoded from upstream source: OpenPose's own hand/face color tables are
|
||||
long generated gradients (not simple literals) that could not be reliably
|
||||
transcribed byte-for-byte here, so we reproduce the same *visual* scheme
|
||||
(rainbow limbs, distinct hue per finger, white face dots) programmatically,
|
||||
which is verifiably correct instead of a guess.
|
||||
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
|
||||
@@ -66,12 +68,16 @@ BODY_25_NAMES = [
|
||||
]
|
||||
assert len(BODY_25_NAMES) == BODY_COUNT
|
||||
|
||||
# Verbatim from OpenPose's POSE_BODY_25_BODY_PART_PAIRS_RENDER_GPU (local
|
||||
# indices 0..24, i.e. relative to BODY_START).
|
||||
# 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), (2, 17), (5, 18), (14, 19), (19, 20), (14, 21), (11, 22),
|
||||
(16, 18), (14, 19), (19, 20), (14, 21), (11, 22),
|
||||
(22, 23), (11, 24),
|
||||
]
|
||||
|
||||
@@ -83,6 +89,22 @@ BODY_25_LIMB_COLORS = [
|
||||
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) ------------------------------
|
||||
#
|
||||
@@ -116,13 +138,14 @@ assert all(BODY_25_NAMES[COCO_TO_BODY25_INDEX[i]] == COCO_NAMES[i] for i in rang
|
||||
|
||||
COCO_ACTIVE_BODY25_INDICES = sorted(COCO_TO_BODY25_INDEX)
|
||||
|
||||
# Verbatim (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.
|
||||
# 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),
|
||||
(2, 16), (5, 17),
|
||||
]
|
||||
COCO_PAIRS_LOCAL = [
|
||||
(COCO_TO_BODY25_INDEX[a], COCO_TO_BODY25_INDEX[b]) for (a, b) in _COCO_PAIRS_COCO_SPACE
|
||||
@@ -189,13 +212,34 @@ FACE_NAMES = (
|
||||
)
|
||||
assert len(FACE_NAMES) == FACE_COUNT
|
||||
|
||||
# OpenPose's default renderer draws the face as keypoints only (no limb
|
||||
# lines), all white.
|
||||
FACE_PAIRS_LOCAL = []
|
||||
# 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
|
||||
FACE_LIMB_THICKNESS = 0.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 ------------------------------------------
|
||||
@@ -236,10 +280,11 @@ def part_of(global_index):
|
||||
raise IndexError(global_index)
|
||||
|
||||
|
||||
# Hand limb pairs, offset into the flat 0..136 range -- unaffected by body
|
||||
# model choice, so precomputed once.
|
||||
# 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):
|
||||
@@ -255,17 +300,18 @@ def _body_limb_data(body_model):
|
||||
|
||||
|
||||
def full_limb_data(body_model=BODY_MODEL_BODY_25):
|
||||
"""(pairs, colors, thickness) for the whole rig: the chosen body model
|
||||
plus both full hands, global-indexed into the flat 0..136 range. Faces
|
||||
render as dots only and contribute no limbs.
|
||||
"""(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
|
||||
colors = list(body_colors) + HAND_LIMB_COLORS + HAND_LIMB_COLORS
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user