commit e2f2e6668a755f786acac8755f027757eebec6f5 Author: indigo Date: Tue Jul 14 08:41:57 2026 +0800 Add OpenPose_full renderer plugin for Maya Maya API 2.0 plugin that maps a skeleton to OpenPose_full keypoints (BODY_25 or COCO body, 21+21 hand, 70 face) via an openposeCharacter node, with a live Viewport 2.0 draw override, an "OpenPose" viewport renderer plus openposeRenderSequence for PNG output, and a PySide2 mapping editor. Face can also be driven procedurally from ARKit's 52 blendshape weights instead of facial joints, for rigs with no facial skeleton. Includes VS Code debug configs and Maya test scenes/renders. Co-Authored-By: Claude Sonnet 5 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3bbe7b6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +__pycache__/ +*.pyc +*.pyo diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..97e70b6 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,46 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Maya: Attach (debugpy)", + "type": "debugpy", + "request": "attach", + "connect": { "host": "localhost", "port": 5678 }, + "pathMappings": [ + { "localRoot": "${workspaceFolder}/python", "remoteRoot": "${workspaceFolder}/python" }, + { "localRoot": "${workspaceFolder}/plug-ins", "remoteRoot": "${workspaceFolder}/plug-ins" } + ], + "justMyCode": false + }, + { + "name": "mayapy 2024: Run current file", + "type": "debugpy", + "request": "launch", + "python": "C:\\Program Files\\Autodesk\\Maya2024\\bin\\mayapy.exe", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "PYTHONPATH": "${workspaceFolder}/python" } + }, + { + "name": "mayapy 2023: Run current file", + "type": "debugpy", + "request": "launch", + "python": "C:\\Program Files\\Autodesk\\Maya2023\\bin\\mayapy.exe", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "PYTHONPATH": "${workspaceFolder}/python" } + }, + { + "name": "mayapy 2022: Run current file", + "type": "debugpy", + "request": "launch", + "python": "C:\\Program Files\\Autodesk\\Maya2022\\bin\\mayapy.exe", + "program": "${file}", + "console": "integratedTerminal", + "justMyCode": false, + "env": { "PYTHONPATH": "${workspaceFolder}/python" } + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6727242 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "python.defaultInterpreterPath": "C:\\Program Files\\Autodesk\\Maya2024\\bin\\mayapy.exe", + "python.analysis.extraPaths": ["${workspaceFolder}/python"] +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d0577d5 --- /dev/null +++ b/README.md @@ -0,0 +1,176 @@ +# OpenPose Renderer for Maya + +Turns an animated Maya skeleton into OpenPose-style stick-figure images (up +to 25 BODY_25 body/foot + 21+21 hand + 70 face = 137 keypoints), for use +with ControlNet and similar tools. Built entirely on Maya's Python API 2.0. +Targets Maya 2022-2024 (Python 3.9/3.10, PySide2 -- PySide6 also detected +automatically if present). + +Body output supports two OpenPose skeleton formats, selectable per +character (see *Body model* below): **BODY_25** (25 points, includes +MidHip + feet) and **COCO** (18 points, no MidHip/feet, Neck connects +directly to each hip -- OpenPose's original/legacy body model). Both use +the exact same joint mapping; switching formats does not require remapping. + +## Install + +1. Copy (or symlink) this whole `PoseRenderer` folder somewhere on disk. +2. Add its `plug-ins` folder to `MAYA_PLUG_IN_PATH`, e.g. in + `Maya.env`: + ``` + MAYA_PLUG_IN_PATH = C:\path\to\PoseRenderer\plug-ins + ``` + (The plugin adds its own `python/` folder to `sys.path` automatically at + load time -- you do not need to set `PYTHONPATH` yourself.) +3. In Maya: **Windows > Settings/Preferences > Plug-in Manager**, find + `openposeRenderer.py`, and check *Loaded* (and *Auto load* if desired). + +## Usage + +### 1. Map a character + +- Run `cmds.openposeCreateCharacter(name="myCharacter")` (or use the + mapping editor's *Create Character* button) to create one + `openposeCharacter` node per person you want to output. Scenes with + multiple `openposeCharacter` nodes render as multi-person OpenPose output. +- Open the mapping editor: `cmds.openposeShowMappingEditor()`. +- Select the character node in the dropdown, select a joint in the + viewport, click *Set from Selected* next to the OpenPose_full slot it + corresponds to. Repeat for as many of the 137 slots as your rig has + joints for -- unmapped slots simply render with zero confidence (no + point/limb drawn), matching how OpenPose itself represents undetected + keypoints. Face and hand slots are optional; body-only rigs work fine. +- Mappings are plain Maya connections (`joint.message -> + openposeCharacter.targetJoint[i]`), so they save with the scene and + survive joint renames/reparenting. + +### 2. Viewport preview + +With an `openposeCharacter` node in the scene, a colored OpenPose-style +skeleton overlay is drawn live over the mapped joints in every viewport +(toggle per-character via the node's `enableDisplay` attribute; tune with +`jointRadiusScale` / `limbThicknessScale`). This is for QA of the mapping, +independent of rendering. + +### Body model (BODY_25 / COCO) + +Each `openposeCharacter` node has a `bodyModel` enum attribute (`BODY_25`, +the default, or `COCO`), settable in the mapping editor's *Body Model* +dropdown or directly via `cmds.setAttr("myCharacter.bodyModel", 1)` (0 = +BODY_25, 1 = COCO). It controls both the live viewport overlay and the +rendered output for that character: + +- **BODY_25**: all 25 body/foot points -- MidHip, hips/knees/ankles, eyes, + ears, and the 6 foot points (heels + big/small toes). +- **COCO**: the original 18-point OpenPose body model -- no MidHip, no + feet, and the neck connects directly to each hip instead of through a + pelvis point. Slots you mapped for MidHip/feet are simply not drawn in + this mode; nothing needs to be remapped to switch. + +Hands and face are unaffected by this setting either way. Rendered from the +same mapped HumanIK-named test rig (`test/openpose_humanik_test.ma`): + +| BODY_25 | COCO | +| --- | --- | +| ![BODY_25 output](test/openpose_humanik_test_preview.png) | ![COCO output](test/openpose_humanik_test_preview_coco.png) | + +### Face source: facial joints, or ARKit 52 blendshapes + +Most facial rigs have no joints at all -- they're driven entirely by +blendShape targets, often named per Apple's ARKit convention (from an +iPhone/TrueDepth mocap pipeline, Live Link Face, or a hand-keyed rig built +to match it). Each `openposeCharacter` has a `faceSource` enum (mapping +editor's *Face Source* dropdown, or `cmds.setAttr("myCharacter.faceSource", +1)`; 0 = `Joints`, 1 = `ARKitBlendshapes`) controlling where its 70 face +keypoints come from: + +- **Joints** (default): unchanged from above -- map `Face_*` slots to + facial joints if your rig has them. +- **ARKitBlendshapes**: the 70 face keypoints are instead computed + procedurally from 52 connectable `blendshapeWeight[0..51]` float slots + (in Apple's canonical `ARFaceAnchor.BlendShapeLocation` order/naming -- + `eyeBlinkLeft`, `jawOpen`, `mouthSmileLeft`, etc.). Select your + blendShape node (or an ARKit-mocap-driven control) and click *Auto-Connect + ARKit Blendshapes from Selected* in the mapping editor -- it connects + every one of the 52 canonical names that exists as an attribute on the + selected node. The resulting face is anchored at the character's mapped + `Body_Nose` joint and billboards to always face whichever camera is + drawing (viewport or render camera), sized by the `faceScale` attribute + (world units; default 15 -- tune to your scene's unit scale). + +**Accuracy caveat:** there is no published, authoritative "which ARKit +weight moves which OpenPose landmark, by how much" table anywhere (unlike +BODY_25/COCO, which OpenPose's own source defines exactly). The neutral +face template and per-blendshape displacement rules in +`face_blendshapes.py` are a hand-authored, geometrically-reasoned +approximation -- each shape nudges only the anatomically obvious nearby +landmarks in an intuitive direction. It produces a recognizable, responsive +OpenPose-style face suitable for ControlNet conditioning, but is not a +biomechanically precise simulation. `jawForward` and `tongueOut` have no +representable effect on a frontal 2D landmark set and are no-ops. + +Rendered from `test/openpose_arkit_face_test.ma` (a control node with all 52 +ARKit-named attributes, auto-connected -- see that scene for the pattern): + +| Neutral | `jawOpen` + smile + raised brows | +| --- | --- | +| ![Neutral face](test/openpose_arkit_face_test_neutral.png) | ![Expression face](test/openpose_arkit_face_test_expression.png) | + +### 3. Render OpenPose images + +- Switch a viewport panel's **Renderer** menu to **OpenPose** to make it + the active/selectable renderer for that panel (this is the Viewport 2.0 + render override integration point). +- To actually generate the OpenPose PNG sequence, run: + ```python + cmds.openposeRenderSequence( + startFrame=1, endFrame=48, + camera="renderCam", # optional; defaults to the active view's camera + outputDirectory="C:/out/openpose", # optional; defaults to /images/openpose + showInRenderView=True, + ) + ``` + Each frame is projected through the given camera at the scene's render + resolution (`defaultResolution` node), rasterized as a black-background, + OpenPose-colored image (limbs under joints, canonical BODY_25 palette, + per-finger rainbow hand colors, white face dots), written to disk, and + pushed into Maya's Render View so you can watch it render like any other + renderer. + +## Debugging (VS Code) + +`.vscode/launch.json` provides: + +- **`Maya: Attach (debugpy)`** -- attaches to a running, interactive Maya. + One-time setup: `"\bin\mayapy.exe" -m pip install debugpy`. + Each session, run `scripts/start_debug_server.py` inside Maya's Script + Editor first (see that file's docstring), then launch this config from + VS Code to hit breakpoints anywhere in `plug-ins/` or + `python/openpose_renderer/` -- including the draw override and render + override callbacks, which only run inside a real Maya viewport. +- **`mayapy 2022/2023/2024: Run current file`** -- runs the file you have + open through that Maya version's own `mayapy.exe` interpreter (with + `python/` on `PYTHONPATH`), for headless scripts that call + `maya.standalone.initialize()` themselves. Useful for quick iteration on + `constants.py`/`projector.py`/`rasterizer.py` without opening Maya's UI. + +## Notes / known limitations + +- Colors/connectivity: BODY_25 and COCO keypoint layout and limb + connectivity are taken verbatim from OpenPose's own + `POSE_BODY_25_BODY_PART_PAIRS` / COCO section of `POSE_BODY_PART_PAIRS` + (`src/openpose/pose/poseParameters.cpp`). BODY_25's colors are likewise + verbatim from `POSE_BODY_25_COLORS_RENDER_GPU`. COCO limb colors, and all + hand/face colors, are generated from an HSV hue wheel (rainbow per limb + or finger, white face dots) reproducing OpenPose's own visual scheme + rather than a hardcoded transcription of its longer, generated gradient + tables. +- Projection does not attempt to replicate Maya's film-gate/overscan/fit + reconciliation pixel-for-pixel -- for correct results, set the render + camera's film aspect ratio to match the scene's render resolution, as you + would for any Maya render. +- The "OpenPose" Renderer-menu entry uses a standard scene/HUD/present + Viewport 2.0 operation chain (so switching to it always leaves the panel + usable); the actual OpenPose image generation happens via + `openposeRenderSequence`, not via GPU render-target pixel substitution + inside the viewport override. diff --git a/plug-ins/openposeRenderer.py b/plug-ins/openposeRenderer.py new file mode 100644 index 0000000..dbf8d77 --- /dev/null +++ b/plug-ins/openposeRenderer.py @@ -0,0 +1,75 @@ +"""Maya plugin entry point: OpenPose_full renderer (Maya API 2.0). + +Registers, in order: + * the ``openposeCharacter`` mapping node (mapping_node.py) + * its Viewport 2.0 draw override, the live skeleton overlay (draw_override.py) + * the ``openposeCreateCharacter`` / ``openposeShowMappingEditor`` commands + and the mapping editor UI (commands.py, ui/mapping_editor.py) + * the "OpenPose" viewport renderer + ``openposeRenderSequence`` command + (render_override.py) + +See ../README.md for install and usage instructions. + +Note: the ``python/`` package directory is added to sys.path using +MFnPlugin.loadPath() inside initializePlugin(), not module-level __file__ -- +Maya's plugin loader does not reliably set __file__ when it execs a .py +plugin, so the package import is deferred until loadPath() is available. +""" + +import os +import sys + +import maya.api.OpenMaya as om + +maya_useNewAPI = True + +VENDOR = "OpenPose Renderer Plugin" +VERSION = "1.0.0" + +# Populated by _import_submodules() once the plugin's own directory is known. +_MODULES = [] + + +def _import_submodules(fn_plugin): + python_dir = os.path.normpath(os.path.join(fn_plugin.loadPath(), "..", "python")) + if python_dir not in sys.path: + sys.path.insert(0, python_dir) + + from openpose_renderer import mapping_node + from openpose_renderer import draw_override + from openpose_renderer import commands + from openpose_renderer import render_override + + return [mapping_node, draw_override, commands, render_override] + + +def initializePlugin(m_object): + fn_plugin = om.MFnPlugin(m_object, VENDOR, VERSION, "Any") + + global _MODULES + _MODULES = _import_submodules(fn_plugin) + + for module in _MODULES: + try: + module.register(fn_plugin) + except Exception as error: + om.MGlobal.displayError( + "openposeRenderer: failed to register {0}: {1}".format(module.__name__, error) + ) + raise + + +def uninitializePlugin(m_object): + fn_plugin = om.MFnPlugin(m_object) + + errors = [] + for module in reversed(_MODULES): + try: + module.deregister(fn_plugin) + except Exception as error: + errors.append("{0}: {1}".format(module.__name__, error)) + + if errors: + message = "openposeRenderer: errors during uninitialize -- " + "; ".join(errors) + om.MGlobal.displayError(message) + raise RuntimeError(message) diff --git a/python/openpose_renderer/__init__.py b/python/openpose_renderer/__init__.py new file mode 100644 index 0000000..eff7245 --- /dev/null +++ b/python/openpose_renderer/__init__.py @@ -0,0 +1 @@ +"""OpenPose_full renderer plugin for Maya (Python API 2.0).""" diff --git a/python/openpose_renderer/commands.py b/python/openpose_renderer/commands.py new file mode 100644 index 0000000..fde5e72 --- /dev/null +++ b/python/openpose_renderer/commands.py @@ -0,0 +1,91 @@ +"""MEL/Python-visible commands: ``openposeCreateCharacter`` (undoable node +creation) and ``openposeShowMappingEditor`` (launches the mapping UI). +""" + +import maya.api.OpenMaya as om + +from . import mapping_node + + +class CreateCharacterCommand(om.MPxCommand): + + kCmdName = "openposeCreateCharacter" + + def __init__(self): + om.MPxCommand.__init__(self) + self._modifiers = [] + self._created_node_name = None + + @staticmethod + def creator(): + return CreateCharacterCommand() + + @staticmethod + def createSyntax(): + syntax = om.MSyntax() + syntax.addFlag("-n", "-name", om.MSyntax.kString) + return syntax + + def doIt(self, args): + parser = om.MArgParser(self.syntax(), args) + want_name = parser.flagArgumentString("-n", 0) if parser.isFlagSet("-n") else None + + self._modifiers = [] + + # For a shape type, MDagModifier.createNode() returns the + # auto-created *parent transform*, not the shape -- the shape itself + # is its only child, and only exists once doIt() has run. + create_modifier = om.MDagModifier() + transform_obj = create_modifier.createNode(mapping_node.NODE_NAME) + create_modifier.doIt() + self._modifiers.append(create_modifier) + + shape_obj = om.MFnDagNode(transform_obj).child(0) + + if want_name: + rename_modifier = om.MDagModifier() + rename_modifier.renameNode(shape_obj, want_name) + rename_modifier.doIt() + self._modifiers.append(rename_modifier) + + self._created_node_name = om.MFnDagNode(shape_obj).name() + self.setResult(self._created_node_name) + + def redoIt(self): + for modifier in self._modifiers: + modifier.doIt() + + def undoIt(self): + for modifier in reversed(self._modifiers): + modifier.undoIt() + + def isUndoable(self): + return True + + +class ShowMappingEditorCommand(om.MPxCommand): + + kCmdName = "openposeShowMappingEditor" + + @staticmethod + def creator(): + return ShowMappingEditorCommand() + + def doIt(self, args): + from .ui import mapping_editor + mapping_editor.show() + + def isUndoable(self): + return False + + +def register(fn_plugin): + fn_plugin.registerCommand( + CreateCharacterCommand.kCmdName, CreateCharacterCommand.creator, CreateCharacterCommand.createSyntax + ) + fn_plugin.registerCommand(ShowMappingEditorCommand.kCmdName, ShowMappingEditorCommand.creator) + + +def deregister(fn_plugin): + fn_plugin.deregisterCommand(ShowMappingEditorCommand.kCmdName) + fn_plugin.deregisterCommand(CreateCharacterCommand.kCmdName) diff --git a/python/openpose_renderer/constants.py b/python/openpose_renderer/constants.py new file mode 100644 index 0000000..b89b473 --- /dev/null +++ b/python/openpose_renderer/constants.py @@ -0,0 +1,289 @@ +"""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 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. +""" + +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 + +# Verbatim from OpenPose's POSE_BODY_25_BODY_PART_PAIRS_RENDER_GPU (local +# indices 0..24, i.e. relative to BODY_START). +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), + (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 + + +# --- 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) + +# 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_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 +] +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 + +# OpenPose's default renderer draws the face as keypoints only (no limb +# lines), all white. +FACE_PAIRS_LOCAL = [] +FACE_POINT_COLORS = [(255, 255, 255)] * FACE_COUNT + +FACE_POINT_RADIUS = 2.0 +FACE_LIMB_THICKNESS = 0.0 + + +# --- 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 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] + + +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 + plus both full hands, global-indexed into the flat 0..136 range. Faces + render as dots only and contribute no limbs. + """ + 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 + thickness = ( + list(body_thickness) + + [HAND_LIMB_THICKNESS] * len(HAND_PAIRS_LOCAL) + + [HAND_LIMB_THICKNESS] * len(HAND_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)) + ) diff --git a/python/openpose_renderer/draw_override.py b/python/openpose_renderer/draw_override.py new file mode 100644 index 0000000..46e873e --- /dev/null +++ b/python/openpose_renderer/draw_override.py @@ -0,0 +1,135 @@ +"""Viewport 2.0 draw override for ``openposeCharacter``: a live, colored +skeleton overlay drawn in world space on top of the mapped joints, so a rig +can be visually QA'd against the OpenPose_full mapping without rendering. + +This is independent of the render override (render_override.py) -- it always +draws (when the node's enableDisplay attr is on) in every viewport, in 3D +world space, and tracks animation live via prepareForDraw() being re-run +whenever the node is dirtied. +""" + +import maya.api.OpenMaya as om +import maya.api.OpenMayaRender as omr + +from . import constants +from . import face_blendshapes +from . import mapping_node +from . import projector + + +def _mcolor(rgb255): + return om.MColor((rgb255[0] / 255.0, rgb255[1] / 255.0, rgb255[2] / 255.0, 1.0)) + + +class OpenPoseUserData(om.MUserData): + + def __init__(self): + om.MUserData.__init__(self, False) # Maya should not delete after one draw + self.lines = [] # [(p0, p1, MColor, thickness), ...] + self.points = [] # [(p, MColor, pointSize), ...] + + +class OpenPoseDrawOverride(omr.MPxDrawOverride): + + @staticmethod + def creator(obj): + return OpenPoseDrawOverride(obj) + + def __init__(self, obj): + omr.MPxDrawOverride.__init__(self, obj, None, isAlwaysDirty=True) + + def supportedDrawAPIs(self): + return omr.MRenderer.kAllDevices + + def isBounded(self, obj_path, camera_path): + # Joints being displayed can be anywhere in the scene relative to + # this node's own (irrelevant) transform -- don't cull on bbox. + return False + + def hasUIDrawables(self): + return True + + def prepareForDraw(self, obj_path, camera_path, frame_context, old_data): + data = old_data if isinstance(old_data, OpenPoseUserData) else OpenPoseUserData() + data.lines = [] + data.points = [] + + dep_fn = om.MFnDependencyNode(obj_path.node()) + try: + enabled = dep_fn.findPlug(mapping_node.aEnableDisplay, False).asBool() + except RuntimeError: + enabled = True + if not enabled: + return data + + radius_scale = dep_fn.findPlug(mapping_node.aJointRadiusScale, False).asFloat() + thickness_scale = dep_fn.findPlug(mapping_node.aLimbThicknessScale, False).asFloat() + body_model = mapping_node.get_body_model(dep_fn) + + mapped = mapping_node.get_mapped_joint_paths(dep_fn) + positions = {index: projector.world_position(path) for index, path in mapped.items()} + + if mapping_node.get_face_source(dep_fn) == mapping_node.FACE_SOURCE_ARKIT_BLENDSHAPES: + nose_index = constants.NAME_TO_INDEX["Body_Nose"] + anchor = positions.get(nose_index) + if anchor is not None: + weights = mapping_node.get_blendshape_weights(dep_fn) + face_scale = mapping_node.get_face_scale(dep_fn) + local_points = face_blendshapes.compute_face_local_points(weights) + world_points = projector.billboard_world_points(anchor, camera_path, local_points, face_scale) + for offset, world_point in enumerate(world_points): + positions[constants.FACE_START + offset] = world_point + + active_indices = set(constants.active_point_indices(body_model)) + pairs, colors, thickness_list = constants.full_limb_data(body_model) + + for pair_i, (a, b) in enumerate(pairs): + if a in positions and b in positions: + thickness = thickness_list[pair_i] * thickness_scale + if thickness <= 0.0: + continue + data.lines.append((positions[a], positions[b], _mcolor(colors[pair_i]), thickness)) + + for index, pos in positions.items(): + if index not in active_indices: + continue + radius = constants.KEYPOINT_POINT_RADIUS[index] * radius_scale + if radius <= 0.0: + continue + data.points.append((pos, _mcolor(constants.KEYPOINT_POINT_COLORS[index]), radius * 2.0)) + + return data + + def addUIDrawables(self, obj_path, draw_manager, frame_context, data): + if not isinstance(data, OpenPoseUserData): + return + + draw_manager.beginDrawable() + draw_manager.setDepthPriority(omr.MRenderItem.sActiveWireDepthPriority) + + for p0, p1, color, thickness in data.lines: + draw_manager.setColor(color) + draw_manager.setLineWidth(thickness) + draw_manager.line(p0, p1) + + for pos, color, point_size in data.points: + draw_manager.setColor(color) + draw_manager.setPointSize(point_size) + draw_manager.point(pos) + + draw_manager.endDrawable() + + +def register(fn_plugin): + omr.MDrawRegistry.registerDrawOverrideCreator( + mapping_node.DRAW_CLASSIFICATION, + mapping_node.DRAW_REGISTRANT_ID, + OpenPoseDrawOverride.creator, + ) + + +def deregister(fn_plugin): + omr.MDrawRegistry.deregisterDrawOverrideCreator( + mapping_node.DRAW_CLASSIFICATION, + mapping_node.DRAW_REGISTRANT_ID, + ) diff --git a/python/openpose_renderer/face_blendshapes.py b/python/openpose_renderer/face_blendshapes.py new file mode 100644 index 0000000..a680908 --- /dev/null +++ b/python/openpose_renderer/face_blendshapes.py @@ -0,0 +1,235 @@ +"""Drives the 70-point OpenPose_full face part from ARKit's 52 facial +blendshape weights, for rigs that animate the face with blendShape targets +(or live ARKit/iPhone TrueDepth mocap) instead of facial joints. + +``ARKIT_BLENDSHAPE_NAMES`` (order, spelling) is verbatim from Apple's own +``ARFaceAnchor.BlendShapeLocation`` enumeration. + +There is no published, authoritative "which ARKit weight moves which +OpenPose/iBUG landmark, by how much" table anywhere -- unlike BODY_25/COCO, +which OpenPose's own source defines precisely, this is a genuinely +unstandardized problem. ``NEUTRAL_FACE_TEMPLATE`` and ``BLENDSHAPE_OFFSETS`` +below are therefore a hand-authored, geometrically-reasoned *approximation* +(each shape nudges only the anatomically obvious nearby landmarks, in an +intuitive direction) -- good enough to produce a recognizable, responsive +OpenPose-style face for ControlNet conditioning, but not a biomechanically +accurate face simulation. A few shapes with no representable effect on a +frontal 2D landmark set (``jawForward``, ``tongueOut``, ``mouthFunnel`` -- +visually redundant with ``mouthPucker`` on this coarse a template) are +intentionally no-ops. +""" + +import math + +from . import constants + +# Verbatim from Apple's ARFaceAnchor.BlendShapeLocation. +ARKIT_BLENDSHAPE_NAMES = [ + # Left eye + "eyeBlinkLeft", "eyeLookDownLeft", "eyeLookInLeft", "eyeLookOutLeft", + "eyeLookUpLeft", "eyeSquintLeft", "eyeWideLeft", + # Right eye + "eyeBlinkRight", "eyeLookDownRight", "eyeLookInRight", "eyeLookOutRight", + "eyeLookUpRight", "eyeSquintRight", "eyeWideRight", + # Jaw + "jawForward", "jawLeft", "jawRight", "jawOpen", + # Mouth + "mouthClose", "mouthFunnel", "mouthPucker", "mouthLeft", "mouthRight", + "mouthSmileLeft", "mouthSmileRight", "mouthFrownLeft", "mouthFrownRight", + "mouthDimpleLeft", "mouthDimpleRight", "mouthStretchLeft", "mouthStretchRight", + "mouthRollLower", "mouthRollUpper", "mouthShrugLower", "mouthShrugUpper", + "mouthPressLeft", "mouthPressRight", "mouthLowerDownLeft", "mouthLowerDownRight", + "mouthUpperUpLeft", "mouthUpperUpRight", + # Brow + "browDownLeft", "browDownRight", "browInnerUp", "browOuterUpLeft", "browOuterUpRight", + # Cheek + "cheekPuff", "cheekSquintLeft", "cheekSquintRight", + # Nose + "noseSneerLeft", "noseSneerRight", + # Tongue + "tongueOut", +] +assert len(ARKIT_BLENDSHAPE_NAMES) == 52 +BLENDSHAPE_COUNT = 52 +BLENDSHAPE_NAME_TO_INDEX = {name: i for i, name in enumerate(ARKIT_BLENDSHAPE_NAMES)} + +_FACE_INDEX = {name: i for i, name in enumerate(constants.FACE_NAMES)} + +# Mouth outer contour = Mouth0..11 (12 pts), inner contour = Mouth12..19 (8 +# pts). Within the outer contour: 0 = left corner, 1-5 = upper lip (left to +# right), 6 = right corner, 7-11 = lower lip (right to left). +_MOUTH_UPPER_OUTER = [0, 1, 2, 3, 4, 5, 6] +_MOUTH_LOWER_OUTER = [6, 7, 8, 9, 10, 11, 0] + + +def _mouth_outer(i): + return "Mouth{0}".format(i) + + +def _mouth_inner(i): + return "Mouth{0}".format(12 + i) + + +def _arc(cx, cy, rx, ry, start_deg, end_deg, count): + points = [] + for i in range(count): + t = i / (count - 1) if count > 1 else 0.0 + angle = math.radians(start_deg + (end_deg - start_deg) * t) + points.append((cx + rx * math.cos(angle), cy + ry * math.sin(angle))) + return points + + +def _build_neutral_template(): + """70 (x, y) points in a local, camera-facing "face plane" (roughly a + unit square, +X = screen-right, +Y = up, origin ~ between the eyes), in + the same order as constants.FACE_NAMES. Proportions are a plausible + generic face, not a specific published dataset's exact mean shape. + """ + template = [None] * constants.FACE_COUNT + + # Jaw0..16: jawline, ear to ear around the chin. + for i in range(17): + angle = math.radians(180 + 180 * i / 16.0) + x = 1.05 * math.cos(angle) + y = -0.15 + 0.85 * math.sin(angle) + template[_FACE_INDEX["Jaw{0}".format(i)]] = (x, y) + + # Eyebrows: shallow arcs above each eye. Right eyebrow sits at negative + # local X, left eyebrow mirrors at positive X (an arbitrary but + # internally consistent left/right convention, same idea as BODY_25). + for i, (x, y) in enumerate(_arc(-0.5, 0.62, 0.32, 0.10, 200, 340, 5)): + template[_FACE_INDEX["REyebrow{0}".format(i)]] = (x, y) + for i, (x, y) in enumerate(_arc(0.5, 0.62, 0.32, 0.10, -20, 140, 5)): + template[_FACE_INDEX["LEyebrow{0}".format(i)]] = (x, y) + + # Nose: bridge (top to tip) then the nostril-base arc. + for i in range(4): + t = i / 3.0 + template[_FACE_INDEX["Nose{0}".format(i)]] = (0.0, 0.42 - 0.38 * t) + for i, (x, y) in enumerate(_arc(0.0, -0.02, 0.20, 0.06, 200, 340, 5)): + template[_FACE_INDEX["Nose{0}".format(4 + i)]] = (x, y) + + # Eyes: 6 points each (outer corner, 2x upper lid, inner corner, 2x lower lid). + for i, (x, y) in enumerate(_arc(-0.5, 0.32, 0.30, 0.14, 0, 300, 6)): + template[_FACE_INDEX["REye{0}".format(i)]] = (x, y) + for i, (x, y) in enumerate(_arc(0.5, 0.32, 0.30, 0.14, 180, -120, 6)): + template[_FACE_INDEX["LEye{0}".format(i)]] = (x, y) + + # Mouth: 12-point outer contour, then 8-point inner contour. + for i, (x, y) in enumerate(_arc(0.0, -0.55, 0.42, 0.20, 0, 300, 12)): + template[_FACE_INDEX["Mouth{0}".format(i)]] = (x, y) + for i, (x, y) in enumerate(_arc(0.0, -0.55, 0.28, 0.11, 0, 315, 8)): + template[_FACE_INDEX["Mouth{0}".format(12 + i)]] = (x, y) + + template[_FACE_INDEX["RPupil"]] = (-0.5, 0.32) + template[_FACE_INDEX["LPupil"]] = (0.5, 0.32) + + assert all(p is not None for p in template) + return template + + +NEUTRAL_FACE_TEMPLATE = _build_neutral_template() + + +def _sign(value): + return 1.0 if value >= 0 else -1.0 + + +# {blendshape_name: [(face_point_name, dx, dy), ...]} -- displacement applied +# at weight 1.0, linearly scaled by the live weight. See module docstring for +# the honesty caveat on precision. +BLENDSHAPE_OFFSETS = { + # --- Eyes: eyelid closure / widening (upper lid moves toward or away + # from the lower lid; lower lid nudges slightly the opposite way). + "eyeBlinkLeft": [("LEye1", 0.0, -0.10), ("LEye2", 0.0, -0.10)], + "eyeBlinkRight": [("REye1", 0.0, -0.10), ("REye2", 0.0, -0.10)], + "eyeWideLeft": [("LEye1", 0.0, 0.05), ("LEye2", 0.0, 0.05), ("LEye4", 0.0, -0.03), ("LEye5", 0.0, -0.03)], + "eyeWideRight": [("REye1", 0.0, 0.05), ("REye2", 0.0, 0.05), ("REye4", 0.0, -0.03), ("REye5", 0.0, -0.03)], + "eyeSquintLeft": [("LEye1", 0.0, -0.04), ("LEye2", 0.0, -0.04), ("LEye4", 0.0, 0.02), ("LEye5", 0.0, 0.02)], + "eyeSquintRight": [("REye1", 0.0, -0.04), ("REye2", 0.0, -0.04), ("REye4", 0.0, 0.02), ("REye5", 0.0, 0.02)], + # Gaze direction moves the pupil point within the eye socket. + "eyeLookDownLeft": [("LPupil", 0.0, -0.06)], + "eyeLookUpLeft": [("LPupil", 0.0, 0.06)], + "eyeLookInLeft": [("LPupil", -0.08, 0.0)], + "eyeLookOutLeft": [("LPupil", 0.08, 0.0)], + "eyeLookDownRight": [("RPupil", 0.0, -0.06)], + "eyeLookUpRight": [("RPupil", 0.0, 0.06)], + "eyeLookInRight": [("RPupil", 0.08, 0.0)], + "eyeLookOutRight": [("RPupil", -0.08, 0.0)], + + # --- Brows. + "browDownLeft": [("LEyebrow{0}".format(i), 0.0, -0.08) for i in range(5)], + "browDownRight": [("REyebrow{0}".format(i), 0.0, -0.08) for i in range(5)], + "browInnerUp": [("LEyebrow0", 0.0, 0.09), ("REyebrow4", 0.0, 0.09)], + "browOuterUpLeft": [("LEyebrow3", 0.0, 0.08), ("LEyebrow4", 0.0, 0.08)], + "browOuterUpRight": [("REyebrow0", 0.0, 0.08), ("REyebrow1", 0.0, 0.08)], + + # --- Jaw. jawForward has no representable 2D effect (frontal view) -- + # intentionally omitted below. + "jawOpen": ( + [("Jaw{0}".format(i), 0.0, -0.30) for i in range(6, 11)] + + [(_mouth_outer(i), 0.0, -0.22) for i in _MOUTH_LOWER_OUTER] + + [(_mouth_inner(i), 0.0, -0.16) for i in range(4, 7)] + ), + "jawLeft": [("Jaw{0}".format(i), 0.06, 0.0) for i in range(6, 11)], + "jawRight": [("Jaw{0}".format(i), -0.06, 0.0) for i in range(6, 11)], + + # --- Mouth shape. + "mouthSmileLeft": [(_mouth_outer(0), 0.05, 0.10), (_mouth_outer(1), 0.03, 0.06), (_mouth_outer(11), 0.03, 0.06)], + "mouthSmileRight": [(_mouth_outer(6), -0.05, 0.10), (_mouth_outer(5), -0.03, 0.06), (_mouth_outer(7), -0.03, 0.06)], + "mouthFrownLeft": [(_mouth_outer(0), 0.02, -0.08)], + "mouthFrownRight": [(_mouth_outer(6), -0.02, -0.08)], + "mouthLeft": [(_mouth_outer(i), 0.08, 0.0) for i in range(12)], + "mouthRight": [(_mouth_outer(i), -0.08, 0.0) for i in range(12)], + "mouthDimpleLeft": [(_mouth_outer(0), 0.02, 0.02)], + "mouthDimpleRight": [(_mouth_outer(6), -0.02, 0.02)], + "mouthStretchLeft": [(_mouth_outer(0), 0.10, 0.0)], + "mouthStretchRight": [(_mouth_outer(6), -0.10, 0.0)], + "mouthPucker": [ + (_mouth_outer(i), -0.10 * _sign(NEUTRAL_FACE_TEMPLATE[_FACE_INDEX[_mouth_outer(i)]][0]), 0.0) + for i in range(12) + ], + "mouthFunnel": [], # visually redundant with mouthPucker on this coarse template + "mouthClose": ( + [(_mouth_outer(i), 0.0, -0.02) for i in _MOUTH_UPPER_OUTER] + + [(_mouth_outer(i), 0.0, 0.02) for i in _MOUTH_LOWER_OUTER] + ), + "mouthRollLower": [(_mouth_outer(i), 0.0, 0.03) for i in _MOUTH_LOWER_OUTER], + "mouthRollUpper": [(_mouth_outer(i), 0.0, -0.03) for i in _MOUTH_UPPER_OUTER], + "mouthShrugLower": [(_mouth_outer(i), 0.0, -0.04) for i in _MOUTH_LOWER_OUTER], + "mouthShrugUpper": [(_mouth_outer(i), 0.0, 0.04) for i in _MOUTH_UPPER_OUTER], + "mouthPressLeft": [(_mouth_outer(0), 0.02, -0.01)], + "mouthPressRight": [(_mouth_outer(6), -0.02, -0.01)], + "mouthLowerDownLeft": [(_mouth_outer(7), 0.0, -0.06), (_mouth_outer(8), 0.0, -0.06)], + "mouthLowerDownRight": [(_mouth_outer(10), 0.0, -0.06), (_mouth_outer(11), 0.0, -0.06)], + "mouthUpperUpLeft": [(_mouth_outer(1), 0.0, 0.05), (_mouth_outer(2), 0.0, 0.05)], + "mouthUpperUpRight": [(_mouth_outer(4), 0.0, 0.05), (_mouth_outer(5), 0.0, 0.05)], + + # --- Cheeks / nose: subtle. + "cheekPuff": [("Jaw{0}".format(i), 0.03, 0.0) for i in (3, 4)] + [("Jaw{0}".format(i), -0.03, 0.0) for i in (12, 13)], + "cheekSquintLeft": [("LEye4", 0.0, 0.02), ("LEye5", 0.0, 0.02)], + "cheekSquintRight": [("REye4", 0.0, 0.02), ("REye5", 0.0, 0.02)], + "noseSneerLeft": [("Nose7", 0.0, 0.03), ("Nose8", 0.0, 0.03)], + "noseSneerRight": [("Nose4", 0.0, 0.03), ("Nose5", 0.0, 0.03)], + + # --- No 2D-representable effect on a frontal landmark set. + "jawForward": [], + "tongueOut": [], +} +assert set(BLENDSHAPE_OFFSETS.keys()) == set(ARKIT_BLENDSHAPE_NAMES) + + +def compute_face_local_points(weights): + """``weights``: {blendshape_name: 0..1}. Returns 70 (x, y) local points + (constants.FACE_NAMES order), the neutral template plus every active + blendshape's weighted displacement. + """ + points = list(NEUTRAL_FACE_TEMPLATE) + for name, weight in weights.items(): + if not weight: + continue + for point_name, dx, dy in BLENDSHAPE_OFFSETS.get(name, ()): + idx = _FACE_INDEX[point_name] + x, y = points[idx] + points[idx] = (x + dx * weight, y + dy * weight) + return points diff --git a/python/openpose_renderer/mapping_node.py b/python/openpose_renderer/mapping_node.py new file mode 100644 index 0000000..009c6a6 --- /dev/null +++ b/python/openpose_renderer/mapping_node.py @@ -0,0 +1,228 @@ +"""``openposeCharacter`` node: per-character OpenPose_full joint mapping. + +A pure MPxLocatorNode with one connectable message-attribute slot per +OpenPose_full keypoint (``targetJoint[0..136]``). The mapping lives entirely +as Maya connections (joint.message -> openposeCharacter.targetJoint[i]) so it +survives joint renames/reparenting and round-trips through the scene file +with no separate config to keep in sync. The node has no compute() output -- +it is a passive connection target read directly by the draw override +(live viewport skeleton) and the render override (OpenPose image output). +""" + +import maya.api.OpenMaya as om +import maya.api.OpenMayaUI as omui + +from . import constants +from . import face_blendshapes + +NODE_NAME = "openposeCharacter" +NODE_ID = om.MTypeId(0x0012F5C0) +DRAW_CLASSIFICATION = "drawdb/geometry/openposeCharacter" +DRAW_REGISTRANT_ID = "openposeRendererPlugin" + +# Attribute handles, filled in by initialize() +aTargetJoint = None +aEnableDisplay = None +aJointRadiusScale = None +aLimbThicknessScale = None +aCharacterName = None +aBodyModel = None +aFaceSource = None +aBlendshapeWeight = None +aFaceScale = None + +# aBodyModel enum field values, in registration order (index 0 = BODY_25). +BODY_MODEL_FIELDS = [constants.BODY_MODEL_BODY_25, constants.BODY_MODEL_COCO] + +# aFaceSource enum field values, in registration order (index 0 = Joints). +FACE_SOURCE_JOINTS = "Joints" +FACE_SOURCE_ARKIT_BLENDSHAPES = "ARKitBlendshapes" +FACE_SOURCE_FIELDS = [FACE_SOURCE_JOINTS, FACE_SOURCE_ARKIT_BLENDSHAPES] + +DEFAULT_FACE_SCALE = 15.0 + + +class OpenposeCharacterNode(omui.MPxLocatorNode): + + kTypeName = NODE_NAME + kTypeId = NODE_ID + + def __init__(self): + omui.MPxLocatorNode.__init__(self) + + def compute(self, plug, data_block): + # Pure passive mapping node: nothing to compute. + return None + + def isBounded(self): + return False + + @staticmethod + def creator(): + return OpenposeCharacterNode() + + @staticmethod + def initialize(): + global aTargetJoint, aEnableDisplay, aJointRadiusScale + global aLimbThicknessScale, aCharacterName, aBodyModel + global aFaceSource, aBlendshapeWeight, aFaceScale + + msg_attr = om.MFnMessageAttribute() + aTargetJoint = msg_attr.create("targetJoint", "tj") + msg_attr.array = True + # indexMatters already defaults to True (sparse, index-preserving + # array) -- explicitly assigning True here throws kInvalidParameter + # in Maya's Python binding, so it is deliberately left untouched. + msg_attr.storable = True + msg_attr.readable = True + msg_attr.writable = True + om.MPxNode.addAttribute(aTargetJoint) + + num_attr = om.MFnNumericAttribute() + + aEnableDisplay = num_attr.create( + "enableDisplay", "ed", om.MFnNumericData.kBoolean, True + ) + num_attr.keyable = True + om.MPxNode.addAttribute(aEnableDisplay) + + aJointRadiusScale = num_attr.create( + "jointRadiusScale", "jrs", om.MFnNumericData.kFloat, 1.0 + ) + num_attr.keyable = True + num_attr.setMin(0.0) + om.MPxNode.addAttribute(aJointRadiusScale) + + aLimbThicknessScale = num_attr.create( + "limbThicknessScale", "lts", om.MFnNumericData.kFloat, 1.0 + ) + num_attr.keyable = True + num_attr.setMin(0.0) + om.MPxNode.addAttribute(aLimbThicknessScale) + + str_data = om.MFnStringData() + empty_string_obj = str_data.create("") + typed_attr = om.MFnTypedAttribute() + aCharacterName = typed_attr.create( + "characterName", "cn", om.MFnData.kString, empty_string_obj + ) + typed_attr.storable = True + om.MPxNode.addAttribute(aCharacterName) + + enum_attr = om.MFnEnumAttribute() + aBodyModel = enum_attr.create("bodyModel", "bmd", 0) + for field_index, field_name in enumerate(BODY_MODEL_FIELDS): + enum_attr.addField(field_name, field_index) + enum_attr.keyable = True + om.MPxNode.addAttribute(aBodyModel) + + face_source_attr = om.MFnEnumAttribute() + aFaceSource = face_source_attr.create("faceSource", "fsrc", 0) + for field_index, field_name in enumerate(FACE_SOURCE_FIELDS): + face_source_attr.addField(field_name, field_index) + face_source_attr.keyable = True + om.MPxNode.addAttribute(aFaceSource) + + weight_attr = om.MFnNumericAttribute() + aBlendshapeWeight = weight_attr.create("blendshapeWeight", "bsw", om.MFnNumericData.kFloat, 0.0) + weight_attr.array = True + weight_attr.keyable = True + weight_attr.setMin(0.0) + weight_attr.setMax(1.0) + om.MPxNode.addAttribute(aBlendshapeWeight) + + face_scale_attr = om.MFnNumericAttribute() + aFaceScale = face_scale_attr.create("faceScale", "fsc", om.MFnNumericData.kFloat, DEFAULT_FACE_SCALE) + face_scale_attr.keyable = True + face_scale_attr.setMin(0.0) + om.MPxNode.addAttribute(aFaceScale) + + +def get_mapped_joint_paths(dep_node_fn): + """Return {keypoint_index: MDagPath} for every connected targetJoint slot. + + ``dep_node_fn`` is an MFnDependencyNode wrapping an openposeCharacter. + Unmapped keypoint indices are simply absent from the returned dict. + """ + result = {} + plug = dep_node_fn.findPlug(aTargetJoint, False) + for logical_index in plug.getExistingArrayAttributeIndices(): + if logical_index < 0 or logical_index >= constants.TOTAL_KEYPOINTS: + continue + element_plug = plug.elementByLogicalIndex(logical_index) + if not element_plug.isConnected: + continue + source_plug = element_plug.source() + if source_plug.isNull: + continue + source_node = source_plug.node() + if not source_node.hasFn(om.MFn.kDagNode): + continue + dag_fn = om.MFnDagNode(source_node) + result[logical_index] = dag_fn.getPath() + return result + + +def get_body_model(dep_node_fn): + """Return constants.BODY_MODEL_BODY_25 / BODY_MODEL_COCO for this character.""" + try: + field_index = dep_node_fn.findPlug(aBodyModel, False).asShort() + except RuntimeError: + field_index = 0 + if 0 <= field_index < len(BODY_MODEL_FIELDS): + return BODY_MODEL_FIELDS[field_index] + return constants.BODY_MODEL_BODY_25 + + +def get_face_source(dep_node_fn): + """Return FACE_SOURCE_JOINTS / FACE_SOURCE_ARKIT_BLENDSHAPES for this character.""" + try: + field_index = dep_node_fn.findPlug(aFaceSource, False).asShort() + except RuntimeError: + field_index = 0 + if 0 <= field_index < len(FACE_SOURCE_FIELDS): + return FACE_SOURCE_FIELDS[field_index] + return FACE_SOURCE_JOINTS + + +def get_blendshape_weights(dep_node_fn): + """Return {arkit_blendshape_name: weight} for all 52 slots (0.0 default).""" + plug = dep_node_fn.findPlug(aBlendshapeWeight, False) + weights = {} + for index, name in enumerate(face_blendshapes.ARKIT_BLENDSHAPE_NAMES): + element_plug = plug.elementByLogicalIndex(index) + weights[name] = element_plug.asFloat() + return weights + + +def get_face_scale(dep_node_fn): + try: + return dep_node_fn.findPlug(aFaceScale, False).asFloat() + except RuntimeError: + return DEFAULT_FACE_SCALE + + +def iter_character_nodes(): + """Yield an MFnDependencyNode for every openposeCharacter node in the scene.""" + it = om.MItDependencyNodes(om.MFn.kPluginLocatorNode) + while not it.isDone(): + obj = it.thisNode() + fn = om.MFnDependencyNode(obj) + if fn.typeName == NODE_NAME: + yield fn + it.next() + + +def register(fn_plugin): + fn_plugin.registerNode( + NODE_NAME, + NODE_ID, + OpenposeCharacterNode.creator, + OpenposeCharacterNode.initialize, + om.MPxNode.kLocatorNode, + DRAW_CLASSIFICATION, + ) + + +def deregister(fn_plugin): + fn_plugin.deregisterNode(NODE_ID) diff --git a/python/openpose_renderer/projector.py b/python/openpose_renderer/projector.py new file mode 100644 index 0000000..20dc761 --- /dev/null +++ b/python/openpose_renderer/projector.py @@ -0,0 +1,96 @@ +"""World-space joint -> 2D pixel projection for a given camera + resolution. + +Pure math, no drawing. Shared by the viewport draw override (which draws in +3D world space directly and does not strictly need this) and the render +override (which needs actual 2D pixel coordinates to rasterize the +OpenPose-style image). Kept independent of "which camera" / "which +character" decisions -- callers supply an explicit camera path and mapping. +""" + +import maya.api.OpenMaya as om + +from . import constants + +DEFAULT_RESOLUTION_NODE = "defaultResolution" + + +def get_render_resolution(): + """Return (width, height) in pixels from the scene's defaultResolution node.""" + sel = om.MSelectionList() + sel.add(DEFAULT_RESOLUTION_NODE) + fn = om.MFnDependencyNode(sel.getDependNode(0)) + width = fn.findPlug("width", False).asInt() + height = fn.findPlug("height", False).asInt() + return max(1, width), max(1, height) + + +def world_position(dag_path): + """World-space MPoint for any DAG transform (including joints).""" + world_matrix = dag_path.inclusiveMatrix() + return om.MPoint(om.MTransformationMatrix(world_matrix).translation(om.MSpace.kWorld)) + + +def project_point(world_point, camera_path, width, height): + """Project a world-space MPoint through ``camera_path`` to pixel coordinates. + + Returns (pixel_x, pixel_y, in_front_of_camera). + """ + camera_space = om.MPoint(world_point) * camera_path.inclusiveMatrixInverse() + + camera_fn = om.MFnCamera(camera_path) + # projectionMatrix() returns an MFloatMatrix; MPoint's matrix multiply + # operator only supports MMatrix, hence the explicit conversion. + projection_matrix = om.MMatrix(camera_fn.projectionMatrix()) + clip = om.MPoint(camera_space) * projection_matrix + if abs(clip.w) < 1e-9: + return 0.0, 0.0, False + + ndc_x = clip.x / clip.w + ndc_y = clip.y / clip.w + pixel_x = (ndc_x * 0.5 + 0.5) * width + pixel_y = (1.0 - (ndc_y * 0.5 + 0.5)) * height + # The camera looks down its own -Z axis, so a point in front of it has + # negative camera-space Z. + in_front = camera_space.z < 0.0 + return pixel_x, pixel_y, in_front + + +def billboard_world_points(anchor_world_point, camera_path, local_points_xy, scale): + """Place 2D local (x, y) points on a plane centered at ``anchor_world_point`` + that always faces ``camera_path`` (camera-space X/Y used as the plane's + right/up axes), scaled by ``scale``. + + Used to draw/render the procedural ARKit-blendshape face without needing + any facial joint orientation -- it billboards toward whichever camera is + currently drawing (the active viewport camera for the live overlay, the + render camera for output), sidestepping the rig-specific, unanswerable + question of "which way does this head joint's local frame face". + """ + world_matrix = camera_path.inclusiveMatrix() + right = (om.MVector(1.0, 0.0, 0.0) * world_matrix).normal() + up = (om.MVector(0.0, 1.0, 0.0) * world_matrix).normal() + anchor = om.MPoint(anchor_world_point) + return [ + om.MPoint(anchor + right * (x * scale) + up * (y * scale)) + for x, y in local_points_xy + ] + + +def project_character(mapped_joint_paths, camera_path, width, height): + """Project one character's mapping to OpenPose_full pixel keypoints. + + ``mapped_joint_paths``: {keypoint_index: MDagPath}, as returned by + ``mapping_node.get_mapped_joint_paths``. + + Returns a list of length ``constants.TOTAL_KEYPOINTS`` of + (pixel_x, pixel_y, confidence) tuples. Unmapped or off-camera keypoints + get confidence 0.0 at (0.0, 0.0), matching OpenPose's own convention for + undetected keypoints. + """ + keypoints = [(0.0, 0.0, 0.0)] * constants.TOTAL_KEYPOINTS + for index, joint_path in mapped_joint_paths.items(): + world_point = world_position(joint_path) + pixel_x, pixel_y, in_front = project_point(world_point, camera_path, width, height) + confidence = 1.0 if in_front else 0.0 + keypoints[index] = (pixel_x, pixel_y, confidence) + return keypoints diff --git a/python/openpose_renderer/rasterizer.py b/python/openpose_renderer/rasterizer.py new file mode 100644 index 0000000..f238c3e --- /dev/null +++ b/python/openpose_renderer/rasterizer.py @@ -0,0 +1,143 @@ +"""Draws OpenPose_full-style skeleton images (black background, colored +limbs/joints) and writes them out via maya.api.OpenMaya.MImage. + +This is a self-contained software rasterizer (no OpenGL/render-target +dependency) operating on a flat RGBA bytearray, so it can be driven equally +from a batch/offline context or from inside a render override. Confidence-0 +keypoints (unmapped OpenPose_full slots) are simply not drawn, and any limb +whose either endpoint is confidence-0 is skipped -- matching how OpenPose +itself omits undetected points/limbs. +""" + +import maya.api.OpenMaya as om + +from . import constants + +BYTES_PER_PIXEL = 4 # RGBA + + +def _new_buffer(width, height): + buf = bytearray(width * height * BYTES_PER_PIXEL) + for i in range(0, len(buf), BYTES_PER_PIXEL): + buf[i + 3] = 255 # opaque black background + return buf + + +def _set_pixel(buffer, width, height, x, y, color): + if x < 0 or x >= width or y < 0 or y >= height: + return + offset = (y * width + x) * BYTES_PER_PIXEL + buffer[offset] = color[0] + buffer[offset + 1] = color[1] + buffer[offset + 2] = color[2] + buffer[offset + 3] = 255 + + +def _draw_circle(buffer, width, height, cx, cy, radius, color): + if radius <= 0: + return + x_min = max(0, int(cx - radius)) + x_max = min(width - 1, int(cx + radius)) + y_min = max(0, int(cy - radius)) + y_max = min(height - 1, int(cy + radius)) + radius_sq = radius * radius + for y in range(y_min, y_max + 1): + dy = y + 0.5 - cy + for x in range(x_min, x_max + 1): + dx = x + 0.5 - cx + if dx * dx + dy * dy <= radius_sq: + _set_pixel(buffer, width, height, x, y, color) + + +def _draw_line(buffer, width, height, x0, y0, x1, y1, thickness, color): + if thickness <= 0: + return + half = max(thickness * 0.5, 0.5) + x_min = max(0, int(min(x0, x1) - half)) + x_max = min(width - 1, int(max(x0, x1) + half)) + y_min = max(0, int(min(y0, y1) - half)) + y_max = min(height - 1, int(max(y0, y1) + half)) + + dx = x1 - x0 + dy = y1 - y0 + length_sq = dx * dx + dy * dy + half_sq = half * half + + if length_sq < 1e-9: + _draw_circle(buffer, width, height, x0, y0, half, color) + return + + for y in range(y_min, y_max + 1): + py = y + 0.5 + for x in range(x_min, x_max + 1): + px = x + 0.5 + t = ((px - x0) * dx + (py - y0) * dy) / length_sq + t = max(0.0, min(1.0, t)) + proj_x = x0 + t * dx + proj_y = y0 + t * dy + dist_sq = (px - proj_x) ** 2 + (py - proj_y) ** 2 + if dist_sq <= half_sq: + _set_pixel(buffer, width, height, x, y, color) + + +def draw_character(buffer, width, height, keypoints, radius_scale=1.0, thickness_scale=1.0, + body_model=constants.BODY_MODEL_BODY_25): + """Draw one character's OpenPose skeleton into an existing RGBA buffer. + + ``keypoints``: list of length constants.TOTAL_KEYPOINTS of + (pixel_x, pixel_y, confidence), as returned by + ``projector.project_character``. ``body_model`` selects BODY_25 (all 25 + body/foot points) or COCO (18 points, no MidHip/feet, different limb + connectivity) -- hands and face are unaffected either way. + """ + pairs, colors, thickness_list = constants.full_limb_data(body_model) + for pair_i, (a, b) in enumerate(pairs): + xa, ya, ca = keypoints[a] + xb, yb, cb = keypoints[b] + if ca <= 0.0 or cb <= 0.0: + continue + thickness = thickness_list[pair_i] * thickness_scale + _draw_line(buffer, width, height, xa, ya, xb, yb, thickness, colors[pair_i]) + + active_indices = constants.active_point_indices(body_model) + for index in active_indices: + x, y, confidence = keypoints[index] + if confidence <= 0.0: + continue + radius = constants.KEYPOINT_POINT_RADIUS[index] * radius_scale + _draw_circle(buffer, width, height, x, y, radius, constants.KEYPOINT_POINT_COLORS[index]) + + +def render_characters_to_buffer(characters, width, height): + """``characters``: iterable of (keypoints, radius_scale, thickness_scale, body_model). + + Returns a flat RGBA bytearray of size width*height*4. + """ + buffer = _new_buffer(width, height) + for keypoints, radius_scale, thickness_scale, body_model in characters: + draw_character(buffer, width, height, keypoints, radius_scale, thickness_scale, body_model) + return buffer + + +def _flip_rows(buffer, width, height): + """MImage.setPixels() treats row 0 as the bottom of the image (Maya's + classic bottom-up convention); everything else here (and every PNG + reader downstream) treats row 0 as the top. Flip row order once, right + before handing pixels to MImage, so the written file is a normal + top-down image. + """ + row_bytes = width * BYTES_PER_PIXEL + flipped = bytearray(len(buffer)) + for y in range(height): + src = y * row_bytes + dst = (height - 1 - y) * row_bytes + flipped[dst:dst + row_bytes] = buffer[src:src + row_bytes] + return flipped + + +def render_characters_to_file(characters, width, height, file_path, file_format="png"): + """Render all characters and write the result to ``file_path``.""" + buffer = render_characters_to_buffer(characters, width, height) + image = om.MImage() + image.setPixels(_flip_rows(buffer, width, height), width, height) + image.writeToFile(file_path, outputFormat=file_format) diff --git a/python/openpose_renderer/render_override.py b/python/openpose_renderer/render_override.py new file mode 100644 index 0000000..b0d4e7a --- /dev/null +++ b/python/openpose_renderer/render_override.py @@ -0,0 +1,214 @@ +"""Maya renderer integration for OpenPose_full output. + +Two pieces: + +* ``OpenposeRenderOverride`` -- a Viewport 2.0 ``MRenderOverride`` registered + as "OpenPose" in the viewport panel's Renderer dropdown, using the same + minimal scene/HUD/present operation chain as Maya's own reference render + override sample. This makes "OpenPose" a normal, selectable renderer + rather than gambling on undocumented GPU render-target pixel readback + from Python for the actual image content (see the plan's flagged risk). + +* ``render_frame`` / the ``openposeRenderSequence`` command -- the actual + OpenPose image generation: projects every ``openposeCharacter`` in the + scene through a given camera and rasterizes them (projector.py + + rasterizer.py, pure software, no GPU dependency) to a PNG per frame, and + pushes each frame into Maya's Render View via + ``renderWindowEditor(loadImage=...)`` -- the same mechanism third-party + renderers use to display progress there, so the images are visible in the + normal render workflow, not just written silently to disk. +""" + +import os + +import maya.api.OpenMaya as om +import maya.api.OpenMayaRender as omr +import maya.cmds as cmds + +from . import constants +from . import face_blendshapes +from . import mapping_node +from . import projector +from . import rasterizer + +UI_NAME = "OpenPose" +RENDER_OVERRIDE_NAME = "openposeRenderOverride" + +_override_instance = None + + +class OpenposeRenderOverride(omr.MRenderOverride): + + def __init__(self, name): + omr.MRenderOverride.__init__(self, name) + self._operations = [ + omr.MSceneRender("openposeSceneRender"), + omr.MHUDRender(), + omr.MPresentTarget("openposePresentTarget"), + ] + self._current_operation = -1 + + def uiName(self): + return UI_NAME + + def supportedDrawAPIs(self): + return omr.MRenderer.kAllDevices + + def setup(self, destination): + return + + def cleanup(self): + self._current_operation = -1 + + def startOperationIterator(self): + self._current_operation = 0 + return True + + def renderOperation(self): + if 0 <= self._current_operation < len(self._operations): + return self._operations[self._current_operation] + return None + + def nextRenderOperation(self): + self._current_operation += 1 + return self._current_operation < len(self._operations) + + +def render_frame(camera_path, width, height, output_path): + """Project + rasterize every mapped openposeCharacter to a single PNG.""" + characters = [] + for dep_fn in mapping_node.iter_character_nodes(): + try: + enabled = dep_fn.findPlug(mapping_node.aEnableDisplay, False).asBool() + except RuntimeError: + enabled = True + if not enabled: + continue + radius_scale = dep_fn.findPlug(mapping_node.aJointRadiusScale, False).asFloat() + thickness_scale = dep_fn.findPlug(mapping_node.aLimbThicknessScale, False).asFloat() + body_model = mapping_node.get_body_model(dep_fn) + mapped = mapping_node.get_mapped_joint_paths(dep_fn) + keypoints = projector.project_character(mapped, camera_path, width, height) + + if mapping_node.get_face_source(dep_fn) == mapping_node.FACE_SOURCE_ARKIT_BLENDSHAPES: + nose_index = constants.NAME_TO_INDEX["Body_Nose"] + nose_x, nose_y, nose_confidence = keypoints[nose_index] + if nose_confidence > 0.0 and nose_index in mapped: + anchor = projector.world_position(mapped[nose_index]) + weights = mapping_node.get_blendshape_weights(dep_fn) + face_scale = mapping_node.get_face_scale(dep_fn) + local_points = face_blendshapes.compute_face_local_points(weights) + world_points = projector.billboard_world_points(anchor, camera_path, local_points, face_scale) + for offset, world_point in enumerate(world_points): + px, py, in_front = projector.project_point(world_point, camera_path, width, height) + keypoints[constants.FACE_START + offset] = (px, py, 1.0 if in_front else 0.0) + + characters.append((keypoints, radius_scale, thickness_scale, body_model)) + + rasterizer.render_characters_to_file(characters, width, height, output_path) + return output_path + + +def _default_output_dir(): + images_rule = cmds.workspace(fileRuleEntry="images", query=True) or "images" + workspace_root = cmds.workspace(query=True, rootDirectory=True) + out_dir = os.path.join(workspace_root, images_rule, "openpose") + if not os.path.isdir(out_dir): + os.makedirs(out_dir) + return out_dir + + +def _resolve_camera_path(camera_name): + if camera_name: + selection = om.MSelectionList() + selection.add(camera_name) + dag_path = selection.getDagPath(0) + if dag_path.apiType() == om.MFn.kTransform: + dag_path.extendToShape() + return dag_path + import maya.api.OpenMayaUI as omui + return omui.M3dView.active3dView().getCamera() + + +class RenderSequenceCommand(om.MPxCommand): + + kCmdName = "openposeRenderSequence" + + @staticmethod + def creator(): + return RenderSequenceCommand() + + @staticmethod + def createSyntax(): + syntax = om.MSyntax() + syntax.addFlag("-s", "-startFrame", om.MSyntax.kDouble) + syntax.addFlag("-e", "-endFrame", om.MSyntax.kDouble) + syntax.addFlag("-cam", "-camera", om.MSyntax.kString) + syntax.addFlag("-dir", "-outputDirectory", om.MSyntax.kString) + syntax.addFlag("-sw", "-showInRenderView", om.MSyntax.kBoolean) + return syntax + + def isUndoable(self): + return False + + def doIt(self, args): + parser = om.MArgParser(self.syntax(), args) + + start = ( + parser.flagArgumentDouble("-s", 0) + if parser.isFlagSet("-s") + else cmds.playbackOptions(query=True, minTime=True) + ) + end = ( + parser.flagArgumentDouble("-e", 0) + if parser.isFlagSet("-e") + else cmds.playbackOptions(query=True, maxTime=True) + ) + camera_name = parser.flagArgumentString("-cam", 0) if parser.isFlagSet("-cam") else None + out_dir = parser.flagArgumentString("-dir", 0) if parser.isFlagSet("-dir") else _default_output_dir() + show_in_render_view = parser.flagArgumentBool("-sw", 0) if parser.isFlagSet("-sw") else True + + if not os.path.isdir(out_dir): + os.makedirs(out_dir) + + camera_path = _resolve_camera_path(camera_name) + width, height = projector.get_render_resolution() + + scene_path = cmds.file(query=True, sceneName=True) + scene_name = os.path.splitext(os.path.basename(scene_path))[0] if scene_path else "openpose" + scene_name = scene_name or "openpose" + + if show_in_render_view: + cmds.RenderViewWindow() + + original_time = cmds.currentTime(query=True) + written = [] + frame = int(start) + while frame <= int(end): + cmds.currentTime(frame) + file_path = os.path.join(out_dir, "{0}.{1:04d}.png".format(scene_name, frame)) + render_frame(camera_path, width, height, file_path) + written.append(file_path) + if show_in_render_view: + cmds.renderWindowEditor("renderView", edit=True, loadImage=file_path) + frame += 1 + cmds.currentTime(original_time) + + self.setResult(written) + + +def register(fn_plugin): + global _override_instance + fn_plugin.registerCommand( + RenderSequenceCommand.kCmdName, RenderSequenceCommand.creator, RenderSequenceCommand.createSyntax + ) + _override_instance = OpenposeRenderOverride(RENDER_OVERRIDE_NAME) + omr.MRenderer.registerOverride(_override_instance) + + +def deregister(fn_plugin): + global _override_instance + if _override_instance is not None: + omr.MRenderer.deregisterOverride(_override_instance) + _override_instance = None + fn_plugin.deregisterCommand(RenderSequenceCommand.kCmdName) diff --git a/python/openpose_renderer/ui/__init__.py b/python/openpose_renderer/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/python/openpose_renderer/ui/mapping_editor.py b/python/openpose_renderer/ui/mapping_editor.py new file mode 100644 index 0000000..2694ad6 --- /dev/null +++ b/python/openpose_renderer/ui/mapping_editor.py @@ -0,0 +1,304 @@ +"""PySide2 tool for mapping Maya joints onto the 137 OpenPose_full keypoint +slots of a selected ``openposeCharacter`` node. + +Mapping edits go through ``cmds.connectAttr``/``disconnectAttr`` (not raw +API calls) so every edit is a normal, undoable Maya scene operation. +""" + +from functools import partial + +import maya.cmds as cmds +import maya.OpenMayaUI as omui + +try: + from PySide2 import QtCore, QtWidgets + from shiboken2 import wrapInstance +except ImportError: + from PySide6 import QtCore, QtWidgets + from shiboken6 import wrapInstance + +from .. import constants +from .. import face_blendshapes +from .. import mapping_node + +_PART_LABELS = { + constants.PART_BODY: "Body", + constants.PART_HAND_LEFT: "Left Hand", + constants.PART_HAND_RIGHT: "Right Hand", + constants.PART_FACE: "Face", +} + +_window_instance = None + + +def _maya_main_window(): + ptr = omui.MQtUtil.mainWindow() + return wrapInstance(int(ptr), QtWidgets.QWidget) + + +def _target_plug(character, index): + return "{0}.targetJoint[{1}]".format(character, index) + + +class MappingEditor(QtWidgets.QDialog): + + def __init__(self, parent=None): + super(MappingEditor, self).__init__(parent or _maya_main_window()) + self.setWindowTitle("OpenPose Mapping Editor") + self.setWindowFlags(self.windowFlags() & ~QtCore.Qt.WindowContextHelpButtonHint) + self.resize(620, 720) + + self._row_items = {} # index -> QTreeWidgetItem + self._build_ui() + self.refresh_characters() + + def _build_ui(self): + layout = QtWidgets.QVBoxLayout(self) + + char_row = QtWidgets.QHBoxLayout() + char_row.addWidget(QtWidgets.QLabel("Character:")) + self.character_combo = QtWidgets.QComboBox() + self.character_combo.currentIndexChanged.connect(self.refresh_mapping) + char_row.addWidget(self.character_combo, 1) + + refresh_btn = QtWidgets.QPushButton("Refresh") + refresh_btn.clicked.connect(self.refresh_characters) + char_row.addWidget(refresh_btn) + + create_btn = QtWidgets.QPushButton("Create Character") + create_btn.clicked.connect(self._on_create_character) + char_row.addWidget(create_btn) + + char_row.addWidget(QtWidgets.QLabel("Body Model:")) + self.body_model_combo = QtWidgets.QComboBox() + self.body_model_combo.addItems(constants.BODY_MODELS) + self.body_model_combo.currentIndexChanged.connect(self._on_body_model_changed) + char_row.addWidget(self.body_model_combo) + + layout.addLayout(char_row) + + face_row = QtWidgets.QHBoxLayout() + face_row.addWidget(QtWidgets.QLabel("Face Source:")) + self.face_source_combo = QtWidgets.QComboBox() + self.face_source_combo.addItems( + [mapping_node.FACE_SOURCE_JOINTS, mapping_node.FACE_SOURCE_ARKIT_BLENDSHAPES] + ) + self.face_source_combo.currentIndexChanged.connect(self._on_face_source_changed) + face_row.addWidget(self.face_source_combo) + + face_row.addWidget(QtWidgets.QLabel("Face Scale:")) + self.face_scale_spin = QtWidgets.QDoubleSpinBox() + self.face_scale_spin.setRange(0.0, 100000.0) + self.face_scale_spin.setValue(mapping_node.DEFAULT_FACE_SCALE) + self.face_scale_spin.valueChanged.connect(self._on_face_scale_changed) + face_row.addWidget(self.face_scale_spin) + + autoconnect_btn = QtWidgets.QPushButton("Auto-Connect ARKit Blendshapes from Selected") + autoconnect_btn.setToolTip( + "Connects the selected node's attributes to this character's 52 blendshapeWeight\n" + "slots wherever an attribute matching the canonical ARKit name (e.g. jawOpen,\n" + "mouthSmileLeft) exists on it -- typically a blendShape node whose targets are\n" + "named per the ARKit convention, or an ARKit-mocap-driven control." + ) + autoconnect_btn.clicked.connect(self._on_autoconnect_blendshapes) + face_row.addWidget(autoconnect_btn) + + self.blendshape_status_label = QtWidgets.QLabel("") + face_row.addWidget(self.blendshape_status_label) + face_row.addStretch(1) + + layout.addLayout(face_row) + + self.tree = QtWidgets.QTreeWidget() + self.tree.setColumnCount(3) + self.tree.setHeaderLabels(["Keypoint", "Mapped Joint", ""]) + self.tree.setColumnWidth(0, 220) + self.tree.setColumnWidth(1, 180) + layout.addWidget(self.tree, 1) + + self._build_rows() + + def _build_rows(self): + self.tree.clear() + self._row_items = {} + + part_items = {} + for part_id, _start, _count in constants.PARTS: + part_item = QtWidgets.QTreeWidgetItem([_PART_LABELS[part_id], "", ""]) + self.tree.addTopLevelItem(part_item) + part_items[part_id] = part_item + + for index in range(constants.TOTAL_KEYPOINTS): + part_id = constants.part_of(index) + row = QtWidgets.QTreeWidgetItem([constants.KEYPOINT_NAMES[index], "", ""]) + part_items[part_id].addChild(row) + + button_widget = QtWidgets.QWidget() + button_layout = QtWidgets.QHBoxLayout(button_widget) + button_layout.setContentsMargins(0, 0, 0, 0) + set_btn = QtWidgets.QPushButton("Set from Selected") + set_btn.clicked.connect(partial(self._on_set_joint, index)) + clear_btn = QtWidgets.QPushButton("Clear") + clear_btn.clicked.connect(partial(self._on_clear_joint, index)) + button_layout.addWidget(set_btn) + button_layout.addWidget(clear_btn) + self.tree.setItemWidget(row, 2, button_widget) + + self._row_items[index] = row + + self.tree.expandAll() + + def _current_character(self): + return self.character_combo.currentText() or None + + def refresh_characters(self): + current = self._current_character() + self.character_combo.blockSignals(True) + self.character_combo.clear() + characters = sorted(cmds.ls(type="openposeCharacter") or []) + self.character_combo.addItems(characters) + if current in characters: + self.character_combo.setCurrentText(current) + self.character_combo.blockSignals(False) + self.refresh_mapping() + + def refresh_mapping(self): + character = self._current_character() + for index, row in self._row_items.items(): + label = "" + if character: + plug = _target_plug(character, index) + sources = cmds.listConnections(plug, source=True, destination=False) or [] + if sources: + label = sources[0] + row.setText(1, label) + + self.body_model_combo.blockSignals(True) + if character: + self.body_model_combo.setEnabled(True) + self.body_model_combo.setCurrentIndex(cmds.getAttr(character + ".bodyModel")) + else: + self.body_model_combo.setEnabled(False) + self.body_model_combo.blockSignals(False) + + for widget in (self.face_source_combo, self.face_scale_spin): + widget.blockSignals(True) + if character: + self.face_source_combo.setEnabled(True) + self.face_scale_spin.setEnabled(True) + self.face_source_combo.setCurrentIndex(cmds.getAttr(character + ".faceSource")) + self.face_scale_spin.setValue(cmds.getAttr(character + ".faceScale")) + connected = sum( + 1 for i in range(face_blendshapes.BLENDSHAPE_COUNT) + if cmds.listConnections("{0}.blendshapeWeight[{1}]".format(character, i), source=True, destination=False) + ) + self.blendshape_status_label.setText( + "{0} / {1} blendshapes connected".format(connected, face_blendshapes.BLENDSHAPE_COUNT) + ) + else: + self.face_source_combo.setEnabled(False) + self.face_scale_spin.setEnabled(False) + self.blendshape_status_label.setText("") + for widget in (self.face_source_combo, self.face_scale_spin): + widget.blockSignals(False) + + def _on_body_model_changed(self, index): + character = self._current_character() + if character: + cmds.setAttr(character + ".bodyModel", index) + + def _on_face_source_changed(self, index): + character = self._current_character() + if character: + cmds.setAttr(character + ".faceSource", index) + + def _on_face_scale_changed(self, value): + character = self._current_character() + if character: + cmds.setAttr(character + ".faceScale", value) + + def _on_autoconnect_blendshapes(self): + character = self._current_character() + if not character: + QtWidgets.QMessageBox.warning(self, "OpenPose", "Create or select a character first.") + return + selected = cmds.ls(selection=True) + if not selected: + QtWidgets.QMessageBox.warning( + self, "OpenPose", "Select a blendShape node (or control) with ARKit-named attributes first." + ) + return + source_node = selected[0] + + connected_count = 0 + for index, name in enumerate(face_blendshapes.ARKIT_BLENDSHAPE_NAMES): + if not cmds.attributeQuery(name, node=source_node, exists=True): + continue + plug = "{0}.blendshapeWeight[{1}]".format(character, index) + existing = cmds.listConnections(plug, source=True, destination=False, plugs=True) or [] + for src in existing: + cmds.disconnectAttr(src, plug) + cmds.connectAttr("{0}.{1}".format(source_node, name), plug, force=True) + connected_count += 1 + + self.refresh_mapping() + QtWidgets.QMessageBox.information( + self, "OpenPose", + "Connected {0} / {1} ARKit blendshapes from '{2}'.".format( + connected_count, face_blendshapes.BLENDSHAPE_COUNT, source_node + ), + ) + + def _on_create_character(self): + name, ok = QtWidgets.QInputDialog.getText( + self, "Create Character", "Name:", text="openposeCharacter1" + ) + if not ok: + return + created = cmds.openposeCreateCharacter(name=name) if name else cmds.openposeCreateCharacter() + if isinstance(created, list): # MPxCommand string results come back as a 1-item list + created = created[0] + self.refresh_characters() + combo_index = self.character_combo.findText(created) + if combo_index >= 0: + self.character_combo.setCurrentIndex(combo_index) + + def _on_set_joint(self, index): + character = self._current_character() + if not character: + QtWidgets.QMessageBox.warning(self, "OpenPose", "Create or select a character first.") + return + selected = cmds.ls(selection=True, type="joint") + if not selected: + QtWidgets.QMessageBox.warning(self, "OpenPose", "Select a joint first.") + return + joint = selected[0] + plug = _target_plug(character, index) + existing = cmds.listConnections(plug, source=True, destination=False, plugs=True) or [] + for src in existing: + cmds.disconnectAttr(src, plug) + cmds.connectAttr(joint + ".message", plug, force=True) + self.refresh_mapping() + + def _on_clear_joint(self, index): + character = self._current_character() + if not character: + return + plug = _target_plug(character, index) + existing = cmds.listConnections(plug, source=True, destination=False, plugs=True) or [] + for src in existing: + cmds.disconnectAttr(src, plug) + self.refresh_mapping() + + +def show(): + global _window_instance + if _window_instance is not None: + try: + _window_instance.close() + _window_instance.deleteLater() + except RuntimeError: + pass + _window_instance = MappingEditor() + _window_instance.show() + return _window_instance diff --git a/scripts/start_debug_server.py b/scripts/start_debug_server.py new file mode 100644 index 0000000..97e3414 --- /dev/null +++ b/scripts/start_debug_server.py @@ -0,0 +1,28 @@ +"""Run this inside interactive Maya (Script Editor, Python tab) to enable +the "Maya: Attach (debugpy)" launch config in .vscode/launch.json. + +One-time setup -- install debugpy into Maya's own Python environment +(adjust the path for your Maya version): + + "C:\\Program Files\\Autodesk\\Maya2024\\bin\\mayapy.exe" -m pip install debugpy + +Then, each Maya session, before attaching from VS Code: + + exec(open(r"C:\\workspace\\PoseRenderer\\scripts\\start_debug_server.py").read()) + +Breakpoints hit in plug-ins/ or python/openpose_renderer/ (e.g. inside +draw_override.py's prepareForDraw, or a command's doIt) will then pause in +VS Code once you run "Maya: Attach (debugpy)". +""" + +import debugpy + +_ALREADY_LISTENING_ATTR = "_openpose_renderer_debugpy_listening" + +if not getattr(debugpy, _ALREADY_LISTENING_ATTR, False): + debugpy.listen(("localhost", 5678)) + setattr(debugpy, _ALREADY_LISTENING_ATTR, True) + print("[openpose_renderer] debugpy listening on localhost:5678 -- " + "run 'Maya: Attach (debugpy)' in VS Code to connect.") +else: + print("[openpose_renderer] debugpy already listening on localhost:5678.") diff --git a/test/openpose_arkit_face_test.ma b/test/openpose_arkit_face_test.ma new file mode 100644 index 0000000..1c3e8b8 --- /dev/null +++ b/test/openpose_arkit_face_test.ma @@ -0,0 +1,331 @@ +//Maya ASCII 2024 scene +//Name: openpose_arkit_face_test.ma +//Last modified: Tue, Jul 14, 2026 08:25:25 AM +//Codeset: 950 +requires maya "2024"; +requires -nodeType "openposeCharacter" "openposeRenderer.py" "1.0.0"; +currentUnit -l centimeter -a degree -t film; +fileInfo "application" "maya"; +fileInfo "product" "Maya 2024"; +fileInfo "version" "2024"; +fileInfo "cutIdentifier" "202310181224-69282f2959"; +fileInfo "osv" "Windows 10 Pro v2009 (Build: 19045)"; +fileInfo "UUID" "93C6E059-4B38-C365-F7D5-0FAD491D0DEB"; +createNode transform -s -n "persp"; + rename -uid "D6BB12A4-4481-B70D-81F9-22BC50DBF6A2"; + setAttr ".v" no; + setAttr ".t" -type "double3" 28 21 28 ; + setAttr ".r" -type "double3" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ; +createNode camera -s -n "perspShape" -p "persp"; + rename -uid "3B79812D-4CE2-EF69-FD29-D3819FB20809"; + setAttr -k off ".v" no; + setAttr ".fl" 34.999999999999993; + setAttr ".coi" 44.82186966202994; + setAttr ".imn" -type "string" "persp"; + setAttr ".den" -type "string" "persp_depth"; + setAttr ".man" -type "string" "persp_mask"; + setAttr ".hc" -type "string" "viewSet -p %camera"; +createNode transform -s -n "top"; + rename -uid "7D9B1797-4CCF-180D-3705-9C9C4F956631"; + setAttr ".v" no; + setAttr ".t" -type "double3" 0 1000.1 0 ; + setAttr ".r" -type "double3" -90 0 0 ; +createNode camera -s -n "topShape" -p "top"; + rename -uid "02429A50-43C6-9AEF-C9A9-0EA56778BABE"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "top"; + setAttr ".den" -type "string" "top_depth"; + setAttr ".man" -type "string" "top_mask"; + setAttr ".hc" -type "string" "viewSet -t %camera"; + setAttr ".o" yes; +createNode transform -s -n "front"; + rename -uid "E313E787-411D-6315-E6D5-878346B7C7C7"; + setAttr ".v" no; + setAttr ".t" -type "double3" 0 0 1000.1 ; +createNode camera -s -n "frontShape" -p "front"; + rename -uid "CEB81F55-4481-E767-A275-09ACF3D57563"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "front"; + setAttr ".den" -type "string" "front_depth"; + setAttr ".man" -type "string" "front_mask"; + setAttr ".hc" -type "string" "viewSet -f %camera"; + setAttr ".o" yes; +createNode transform -s -n "side"; + rename -uid "23726E62-4236-BA09-5DDB-A8B4AB6467DE"; + setAttr ".v" no; + setAttr ".t" -type "double3" 1000.1 0 0 ; + setAttr ".r" -type "double3" 0 90 0 ; +createNode camera -s -n "sideShape" -p "side"; + rename -uid "0EF9B535-4F25-E296-6FE9-17897B0CD5F8"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "side"; + setAttr ".den" -type "string" "side_depth"; + setAttr ".man" -type "string" "side_mask"; + setAttr ".hc" -type "string" "viewSet -s %camera"; + setAttr ".o" yes; +createNode joint -n "Head"; + rename -uid "10AD57B3-4A53-D241-702A-E4B87D617D75"; + setAttr ".t" -type "double3" 0 150 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode transform -n "transform1"; + rename -uid "AC0B13DC-4C96-222F-1C95-D881328B821A"; +createNode openposeCharacter -n "openposeCharacter_ARKitFace" -p "transform1"; + rename -uid "91BF3ED3-4092-9E34-128B-1C98376C9C10"; + setAttr -k off ".v"; + setAttr ".fsrc" 1; + setAttr -s 52 ".bsw"; + setAttr -s 52 ".bsw"; +createNode transform -n "faceControl"; + rename -uid "BEF04EC7-4C59-6B22-48FA-208B994FD89B"; + addAttr -ci true -sn "eyeBlinkLeft" -ln "eyeBlinkLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookDownLeft" -ln "eyeLookDownLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookInLeft" -ln "eyeLookInLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookOutLeft" -ln "eyeLookOutLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookUpLeft" -ln "eyeLookUpLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeSquintLeft" -ln "eyeSquintLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeWideLeft" -ln "eyeWideLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeBlinkRight" -ln "eyeBlinkRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookDownRight" -ln "eyeLookDownRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookInRight" -ln "eyeLookInRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookOutRight" -ln "eyeLookOutRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeLookUpRight" -ln "eyeLookUpRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeSquintRight" -ln "eyeSquintRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "eyeWideRight" -ln "eyeWideRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "jawForward" -ln "jawForward" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "jawLeft" -ln "jawLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "jawRight" -ln "jawRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "jawOpen" -ln "jawOpen" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthClose" -ln "mouthClose" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthFunnel" -ln "mouthFunnel" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthPucker" -ln "mouthPucker" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthLeft" -ln "mouthLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthRight" -ln "mouthRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthSmileLeft" -ln "mouthSmileLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthSmileRight" -ln "mouthSmileRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthFrownLeft" -ln "mouthFrownLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthFrownRight" -ln "mouthFrownRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthDimpleLeft" -ln "mouthDimpleLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthDimpleRight" -ln "mouthDimpleRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthStretchLeft" -ln "mouthStretchLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthStretchRight" -ln "mouthStretchRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthRollLower" -ln "mouthRollLower" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthRollUpper" -ln "mouthRollUpper" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthShrugLower" -ln "mouthShrugLower" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthShrugUpper" -ln "mouthShrugUpper" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthPressLeft" -ln "mouthPressLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthPressRight" -ln "mouthPressRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthLowerDownLeft" -ln "mouthLowerDownLeft" -min 0 -max 1 + -at "float"; + addAttr -ci true -sn "mouthLowerDownRight" -ln "mouthLowerDownRight" -min 0 -max + 1 -at "float"; + addAttr -ci true -sn "mouthUpperUpLeft" -ln "mouthUpperUpLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "mouthUpperUpRight" -ln "mouthUpperUpRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "browDownLeft" -ln "browDownLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "browDownRight" -ln "browDownRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "browInnerUp" -ln "browInnerUp" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "browOuterUpLeft" -ln "browOuterUpLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "browOuterUpRight" -ln "browOuterUpRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "cheekPuff" -ln "cheekPuff" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "cheekSquintLeft" -ln "cheekSquintLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "cheekSquintRight" -ln "cheekSquintRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "noseSneerLeft" -ln "noseSneerLeft" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "noseSneerRight" -ln "noseSneerRight" -min 0 -max 1 -at "float"; + addAttr -ci true -sn "tongueOut" -ln "tongueOut" -min 0 -max 1 -at "float"; + setAttr -k on ".eyeBlinkLeft"; + setAttr -k on ".eyeLookDownLeft"; + setAttr -k on ".eyeLookInLeft"; + setAttr -k on ".eyeLookOutLeft"; + setAttr -k on ".eyeLookUpLeft"; + setAttr -k on ".eyeSquintLeft"; + setAttr -k on ".eyeWideLeft" 0.69999998807907104; + setAttr -k on ".eyeBlinkRight"; + setAttr -k on ".eyeLookDownRight"; + setAttr -k on ".eyeLookInRight"; + setAttr -k on ".eyeLookOutRight"; + setAttr -k on ".eyeLookUpRight"; + setAttr -k on ".eyeSquintRight"; + setAttr -k on ".eyeWideRight" 0.69999998807907104; + setAttr -k on ".jawForward"; + setAttr -k on ".jawLeft"; + setAttr -k on ".jawRight"; + setAttr -k on ".jawOpen" 0.60000002384185791; + setAttr -k on ".mouthClose"; + setAttr -k on ".mouthFunnel"; + setAttr -k on ".mouthPucker"; + setAttr -k on ".mouthLeft"; + setAttr -k on ".mouthRight"; + setAttr -k on ".mouthSmileLeft" 1; + setAttr -k on ".mouthSmileRight" 1; + setAttr -k on ".mouthFrownLeft"; + setAttr -k on ".mouthFrownRight"; + setAttr -k on ".mouthDimpleLeft"; + setAttr -k on ".mouthDimpleRight"; + setAttr -k on ".mouthStretchLeft"; + setAttr -k on ".mouthStretchRight"; + setAttr -k on ".mouthRollLower"; + setAttr -k on ".mouthRollUpper"; + setAttr -k on ".mouthShrugLower"; + setAttr -k on ".mouthShrugUpper"; + setAttr -k on ".mouthPressLeft"; + setAttr -k on ".mouthPressRight"; + setAttr -k on ".mouthLowerDownLeft"; + setAttr -k on ".mouthLowerDownRight"; + setAttr -k on ".mouthUpperUpLeft"; + setAttr -k on ".mouthUpperUpRight"; + setAttr -k on ".browDownLeft"; + setAttr -k on ".browDownRight"; + setAttr -k on ".browInnerUp" 0.80000001192092896; + setAttr -k on ".browOuterUpLeft" 0.60000002384185791; + setAttr -k on ".browOuterUpRight" 0.60000002384185791; + setAttr -k on ".cheekPuff"; + setAttr -k on ".cheekSquintLeft"; + setAttr -k on ".cheekSquintRight"; + setAttr -k on ".noseSneerLeft"; + setAttr -k on ".noseSneerRight"; + setAttr -k on ".tongueOut"; +createNode lightLinker -s -n "lightLinker1"; + rename -uid "6F41ABE6-4FC9-CBA9-3A4A-3C88AFFEDA05"; + setAttr -s 2 ".lnk"; + setAttr -s 2 ".slnk"; +createNode displayLayerManager -n "layerManager"; + rename -uid "C5E3AE68-4041-3C21-D33D-2DBCA8E53402"; +createNode displayLayer -n "defaultLayer"; + rename -uid "7E1F3A70-4A01-B94C-04B6-24ABE836AECA"; + setAttr ".ufem" -type "stringArray" 0 ; +createNode renderLayerManager -n "renderLayerManager"; + rename -uid "E2FC1CAE-4BC5-8835-9145-E2AFED84D0D6"; +createNode renderLayer -n "defaultRenderLayer"; + rename -uid "711E2B13-42DB-052A-1562-1C99F88A0E30"; + setAttr ".g" yes; +createNode script -n "uiConfigurationScriptNode"; + rename -uid "F0132003-4E9D-D0A6-A39C-F6958ABF910E"; + setAttr ".b" -type "string" "// Maya Mel UI Configuration File.\n// No UI generated in batch mode.\n"; + setAttr ".st" 3; +createNode script -n "sceneConfigurationScriptNode"; + rename -uid "5E8354DD-4768-B99B-F133-489A3680AEB2"; + setAttr ".b" -type "string" "playbackOptions -min 1 -max 120 -ast 1 -aet 200 "; + setAttr ".st" 6; +select -ne :time1; + setAttr ".o" 1; + setAttr ".unw" 1; +select -ne :hardwareRenderingGlobals; + setAttr ".otfna" -type "stringArray" 22 "NURBS Curves" "NURBS Surfaces" "Polygons" "Subdiv Surface" "Particles" "Particle Instance" "Fluids" "Strokes" "Image Planes" "UI" "Lights" "Cameras" "Locators" "Joints" "IK Handles" "Deformers" "Motion Trails" "Components" "Hair Systems" "Follicles" "Misc. UI" "Ornaments" ; + setAttr ".otfva" -type "Int32Array" 22 0 1 1 1 1 1 + 1 1 1 0 0 0 0 0 0 0 0 0 + 0 0 0 0 ; + setAttr ".fprt" yes; + setAttr ".rtfm" 1; +select -ne :renderPartition; + setAttr -s 2 ".st"; +select -ne :renderGlobalsList1; +select -ne :defaultShaderList1; + setAttr -s 5 ".s"; +select -ne :postProcessList1; + setAttr -s 2 ".p"; +select -ne :defaultRenderingList1; +select -ne :standardSurface1; + setAttr ".bc" -type "float3" 0.40000001 0.40000001 0.40000001 ; + setAttr ".sr" 0.5; +select -ne :initialShadingGroup; + setAttr ".ro" yes; +select -ne :initialParticleSE; + setAttr ".ro" yes; +select -ne :defaultRenderGlobals; + addAttr -ci true -h true -sn "dss" -ln "defaultSurfaceShader" -dt "string"; + setAttr ".dss" -type "string" "standardSurface1"; +select -ne :defaultResolution; + setAttr ".w" 512; + setAttr ".h" 512; + setAttr ".pa" 1; +select -ne :defaultColorMgtGlobals; + setAttr ".cfe" yes; + setAttr ".cfp" -type "string" "/OCIO-configs/Maya2022-default/config.ocio"; + setAttr ".vtn" -type "string" "ACES 1.0 SDR-video (sRGB)"; + setAttr ".vn" -type "string" "ACES 1.0 SDR-video"; + setAttr ".dn" -type "string" "sRGB"; + setAttr ".wsn" -type "string" "ACEScg"; + setAttr ".otn" -type "string" "ACES 1.0 SDR-video (sRGB)"; + setAttr ".potn" -type "string" "ACES 1.0 SDR-video (sRGB)"; +select -ne :hardwareRenderGlobals; + setAttr ".ctrs" 256; + setAttr ".btrs" 512; +connectAttr "Head.msg" "openposeCharacter_ARKitFace.tj[0]"; +connectAttr "faceControl.eyeBlinkLeft" "openposeCharacter_ARKitFace.bsw[0]"; +connectAttr "faceControl.eyeLookDownLeft" "openposeCharacter_ARKitFace.bsw[1]"; +connectAttr "faceControl.eyeLookInLeft" "openposeCharacter_ARKitFace.bsw[2]"; +connectAttr "faceControl.eyeLookOutLeft" "openposeCharacter_ARKitFace.bsw[3]"; +connectAttr "faceControl.eyeLookUpLeft" "openposeCharacter_ARKitFace.bsw[4]"; +connectAttr "faceControl.eyeSquintLeft" "openposeCharacter_ARKitFace.bsw[5]"; +connectAttr "faceControl.eyeWideLeft" "openposeCharacter_ARKitFace.bsw[6]"; +connectAttr "faceControl.eyeBlinkRight" "openposeCharacter_ARKitFace.bsw[7]"; +connectAttr "faceControl.eyeLookDownRight" "openposeCharacter_ARKitFace.bsw[8]"; +connectAttr "faceControl.eyeLookInRight" "openposeCharacter_ARKitFace.bsw[9]"; +connectAttr "faceControl.eyeLookOutRight" "openposeCharacter_ARKitFace.bsw[10]"; +connectAttr "faceControl.eyeLookUpRight" "openposeCharacter_ARKitFace.bsw[11]"; +connectAttr "faceControl.eyeSquintRight" "openposeCharacter_ARKitFace.bsw[12]"; +connectAttr "faceControl.eyeWideRight" "openposeCharacter_ARKitFace.bsw[13]"; +connectAttr "faceControl.jawForward" "openposeCharacter_ARKitFace.bsw[14]"; +connectAttr "faceControl.jawLeft" "openposeCharacter_ARKitFace.bsw[15]"; +connectAttr "faceControl.jawRight" "openposeCharacter_ARKitFace.bsw[16]"; +connectAttr "faceControl.jawOpen" "openposeCharacter_ARKitFace.bsw[17]"; +connectAttr "faceControl.mouthClose" "openposeCharacter_ARKitFace.bsw[18]"; +connectAttr "faceControl.mouthFunnel" "openposeCharacter_ARKitFace.bsw[19]"; +connectAttr "faceControl.mouthPucker" "openposeCharacter_ARKitFace.bsw[20]"; +connectAttr "faceControl.mouthLeft" "openposeCharacter_ARKitFace.bsw[21]"; +connectAttr "faceControl.mouthRight" "openposeCharacter_ARKitFace.bsw[22]"; +connectAttr "faceControl.mouthSmileLeft" "openposeCharacter_ARKitFace.bsw[23]"; +connectAttr "faceControl.mouthSmileRight" "openposeCharacter_ARKitFace.bsw[24]"; +connectAttr "faceControl.mouthFrownLeft" "openposeCharacter_ARKitFace.bsw[25]"; +connectAttr "faceControl.mouthFrownRight" "openposeCharacter_ARKitFace.bsw[26]"; +connectAttr "faceControl.mouthDimpleLeft" "openposeCharacter_ARKitFace.bsw[27]"; +connectAttr "faceControl.mouthDimpleRight" "openposeCharacter_ARKitFace.bsw[28]" + ; +connectAttr "faceControl.mouthStretchLeft" "openposeCharacter_ARKitFace.bsw[29]" + ; +connectAttr "faceControl.mouthStretchRight" "openposeCharacter_ARKitFace.bsw[30]" + ; +connectAttr "faceControl.mouthRollLower" "openposeCharacter_ARKitFace.bsw[31]"; +connectAttr "faceControl.mouthRollUpper" "openposeCharacter_ARKitFace.bsw[32]"; +connectAttr "faceControl.mouthShrugLower" "openposeCharacter_ARKitFace.bsw[33]"; +connectAttr "faceControl.mouthShrugUpper" "openposeCharacter_ARKitFace.bsw[34]"; +connectAttr "faceControl.mouthPressLeft" "openposeCharacter_ARKitFace.bsw[35]"; +connectAttr "faceControl.mouthPressRight" "openposeCharacter_ARKitFace.bsw[36]"; +connectAttr "faceControl.mouthLowerDownLeft" "openposeCharacter_ARKitFace.bsw[37]" + ; +connectAttr "faceControl.mouthLowerDownRight" "openposeCharacter_ARKitFace.bsw[38]" + ; +connectAttr "faceControl.mouthUpperUpLeft" "openposeCharacter_ARKitFace.bsw[39]" + ; +connectAttr "faceControl.mouthUpperUpRight" "openposeCharacter_ARKitFace.bsw[40]" + ; +connectAttr "faceControl.browDownLeft" "openposeCharacter_ARKitFace.bsw[41]"; +connectAttr "faceControl.browDownRight" "openposeCharacter_ARKitFace.bsw[42]"; +connectAttr "faceControl.browInnerUp" "openposeCharacter_ARKitFace.bsw[43]"; +connectAttr "faceControl.browOuterUpLeft" "openposeCharacter_ARKitFace.bsw[44]"; +connectAttr "faceControl.browOuterUpRight" "openposeCharacter_ARKitFace.bsw[45]" + ; +connectAttr "faceControl.cheekPuff" "openposeCharacter_ARKitFace.bsw[46]"; +connectAttr "faceControl.cheekSquintLeft" "openposeCharacter_ARKitFace.bsw[47]"; +connectAttr "faceControl.cheekSquintRight" "openposeCharacter_ARKitFace.bsw[48]" + ; +connectAttr "faceControl.noseSneerLeft" "openposeCharacter_ARKitFace.bsw[49]"; +connectAttr "faceControl.noseSneerRight" "openposeCharacter_ARKitFace.bsw[50]"; +connectAttr "faceControl.tongueOut" "openposeCharacter_ARKitFace.bsw[51]"; +relationship "link" ":lightLinker1" ":initialShadingGroup.message" ":defaultLightSet.message"; +relationship "link" ":lightLinker1" ":initialParticleSE.message" ":defaultLightSet.message"; +relationship "shadowLink" ":lightLinker1" ":initialShadingGroup.message" ":defaultLightSet.message"; +relationship "shadowLink" ":lightLinker1" ":initialParticleSE.message" ":defaultLightSet.message"; +connectAttr "layerManager.dli[0]" "defaultLayer.id"; +connectAttr "renderLayerManager.rlmi[0]" "defaultRenderLayer.rlid"; +connectAttr "defaultRenderLayer.msg" ":defaultRenderingList1.r" -na; +// End of openpose_arkit_face_test.ma diff --git a/test/openpose_arkit_face_test_expression.png b/test/openpose_arkit_face_test_expression.png new file mode 100644 index 0000000..206a826 Binary files /dev/null and b/test/openpose_arkit_face_test_expression.png differ diff --git a/test/openpose_arkit_face_test_neutral.png b/test/openpose_arkit_face_test_neutral.png new file mode 100644 index 0000000..e510546 Binary files /dev/null and b/test/openpose_arkit_face_test_neutral.png differ diff --git a/test/openpose_humanik_test.ma b/test/openpose_humanik_test.ma new file mode 100644 index 0000000..94ff15c --- /dev/null +++ b/test/openpose_humanik_test.ma @@ -0,0 +1,600 @@ +//Maya ASCII 2024 scene +//Name: openpose_humanik_test.ma +//Last modified: Tue, Jul 14, 2026 08:03:24 AM +//Codeset: 950 +requires maya "2024"; +requires -nodeType "openposeCharacter" "openposeRenderer.py" "1.0.0"; +currentUnit -l centimeter -a degree -t film; +fileInfo "application" "maya"; +fileInfo "product" "Maya 2024"; +fileInfo "version" "2024"; +fileInfo "cutIdentifier" "202310181224-69282f2959"; +fileInfo "osv" "Windows 10 Pro v2009 (Build: 19045)"; +fileInfo "UUID" "B6CCE6EA-431E-6CE9-7360-46BABE0883C3"; +createNode transform -s -n "persp"; + rename -uid "8310D31F-4947-8244-A1E4-1EBAEA32C348"; + setAttr ".v" no; + setAttr ".t" -type "double3" 28 21 28 ; + setAttr ".r" -type "double3" -27.938352729602379 44.999999999999972 -5.172681101354183e-14 ; +createNode camera -s -n "perspShape" -p "persp"; + rename -uid "82EA3896-4D08-DC2C-28CD-599B24956C13"; + setAttr -k off ".v" no; + setAttr ".fl" 34.999999999999993; + setAttr ".coi" 44.82186966202994; + setAttr ".imn" -type "string" "persp"; + setAttr ".den" -type "string" "persp_depth"; + setAttr ".man" -type "string" "persp_mask"; + setAttr ".hc" -type "string" "viewSet -p %camera"; +createNode transform -s -n "top"; + rename -uid "DABFE07D-478C-AA01-1F9C-678755F64C09"; + setAttr ".v" no; + setAttr ".t" -type "double3" 0 1000.1 0 ; + setAttr ".r" -type "double3" -90 0 0 ; +createNode camera -s -n "topShape" -p "top"; + rename -uid "D3D3EFDD-4F6D-F68D-9220-0DA16D0A1F71"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "top"; + setAttr ".den" -type "string" "top_depth"; + setAttr ".man" -type "string" "top_mask"; + setAttr ".hc" -type "string" "viewSet -t %camera"; + setAttr ".o" yes; +createNode transform -s -n "front"; + rename -uid "F8E3E41F-4F0B-4D85-E2B1-48A41CAB1138"; + setAttr ".v" no; + setAttr ".t" -type "double3" 0 0 1000.1 ; +createNode camera -s -n "frontShape" -p "front"; + rename -uid "CCC3EC0C-46DA-91A7-0DCC-81BC4C7FAF07"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "front"; + setAttr ".den" -type "string" "front_depth"; + setAttr ".man" -type "string" "front_mask"; + setAttr ".hc" -type "string" "viewSet -f %camera"; + setAttr ".o" yes; +createNode transform -s -n "side"; + rename -uid "2DD1A2E6-45CB-8424-CD34-EA99A6909668"; + setAttr ".v" no; + setAttr ".t" -type "double3" 1000.1 0 0 ; + setAttr ".r" -type "double3" 0 90 0 ; +createNode camera -s -n "sideShape" -p "side"; + rename -uid "DC0110C9-4CDD-4029-B41F-65ABE3304C2D"; + setAttr -k off ".v" no; + setAttr ".rnd" no; + setAttr ".coi" 1000.1; + setAttr ".ow" 30; + setAttr ".imn" -type "string" "side"; + setAttr ".den" -type "string" "side_depth"; + setAttr ".man" -type "string" "side_mask"; + setAttr ".hc" -type "string" "viewSet -s %camera"; + setAttr ".o" yes; +createNode joint -n "Hips"; + rename -uid "E9306125-46F8-B494-7C68-9AB3C96BAC96"; + setAttr ".t" -type "double3" 0 100 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "Spine" -p "Hips"; + rename -uid "58DE271D-43EC-C3AB-7A53-B7BF3FA13790"; + setAttr ".t" -type "double3" 0 8 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "Spine1" -p "Spine"; + rename -uid "F6BC0D78-4658-A609-465B-39ADC157E71F"; + setAttr ".t" -type "double3" 0 8 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "Spine2" -p "Spine1"; + rename -uid "73D40132-43A4-BB37-4641-9898153CE359"; + setAttr ".t" -type "double3" 0 8 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "Neck" -p "Spine2"; + rename -uid "50248927-4A10-1E1A-B0EC-9F9433272563"; + setAttr ".t" -type "double3" 0 14 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "Head" -p "Neck"; + rename -uid "E2E49FA6-40DF-644F-C277-F1A5600E55BA"; + setAttr ".t" -type "double3" 0 10 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "HeadTop_End" -p "Head"; + rename -uid "9F15C7EE-4C19-3979-4323-0F9EB7F7D30F"; + setAttr ".t" -type "double3" 0 17 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftShoulder" -p "Spine2"; + rename -uid "9D4188CC-43BF-05F9-8A26-E9B00AC84698"; + setAttr ".t" -type "double3" 6 6 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftArm" -p "LeftShoulder"; + rename -uid "A82601D5-44B9-0B04-B4DB-B281736DFCD9"; + setAttr ".t" -type "double3" 12 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftForeArm" -p "LeftArm"; + rename -uid "531ACB20-4CA6-6BE7-8AD6-3F97594BB8DD"; + setAttr ".t" -type "double3" 24 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHand" -p "LeftForeArm"; + rename -uid "5584D550-4C96-1A13-866F-45A6511F910E"; + setAttr ".t" -type "double3" 22 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandThumb1" -p "LeftHand"; + rename -uid "6D4E3EC0-4A52-74B4-205B-0681F5FFDE44"; + setAttr ".t" -type "double3" 2 -2 4 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandThumb2" -p "LeftHandThumb1"; + rename -uid "AA96287C-47EC-C043-93CD-BC9A89DE027E"; + setAttr ".t" -type "double3" 3.0000000000000142 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandThumb3" -p "LeftHandThumb2"; + rename -uid "4B6B9912-4582-8B2D-961F-AEBAD5C512E8"; + setAttr ".t" -type "double3" 2.9999999999999716 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandThumb4" -p "LeftHandThumb3"; + rename -uid "B01DBD78-4847-78DD-78C8-D2B20522113E"; + setAttr ".t" -type "double3" 3.0000000000000142 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandIndex1" -p "LeftHand"; + rename -uid "A03138BA-4885-2601-C006-D88B90383B56"; + setAttr ".t" -type "double3" 6 0 3 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandIndex2" -p "LeftHandIndex1"; + rename -uid "398FBD7F-4423-1D30-6922-A590AA87CA98"; + setAttr ".t" -type "double3" 5.9999999999999858 0 0.99999999999999956 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandIndex3" -p "LeftHandIndex2"; + rename -uid "9BB07943-436F-A380-3B61-9A94703D5365"; + setAttr ".t" -type "double3" 5.0000000000000142 0 1.0000000000000009 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandIndex4" -p "LeftHandIndex3"; + rename -uid "4517123B-47F2-4473-C397-368B225A3EBF"; + setAttr ".t" -type "double3" 4.9999999999999858 0 1 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandMiddle1" -p "LeftHand"; + rename -uid "8A3473A1-4000-3BDE-62C8-B2842706D6E1"; + setAttr ".t" -type "double3" 7 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandMiddle2" -p "LeftHandMiddle1"; + rename -uid "97989261-45CF-6F6D-44AE-A291F202CBBA"; + setAttr ".t" -type "double3" 6 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandMiddle3" -p "LeftHandMiddle2"; + rename -uid "26E91C0B-44E4-B3CE-3BEE-0A9BB5B04F72"; + setAttr ".t" -type "double3" 5 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandMiddle4" -p "LeftHandMiddle3"; + rename -uid "AA8533FE-4725-477F-0D4C-A3850047AC5D"; + setAttr ".t" -type "double3" 5 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandRing1" -p "LeftHand"; + rename -uid "EC92DECD-4CDD-7220-9D22-4DB5884D3B6F"; + setAttr ".t" -type "double3" 6 0 -3 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandRing2" -p "LeftHandRing1"; + rename -uid "7DC27F02-4D46-0B9C-6D2C-5ABA3C58B0D5"; + setAttr ".t" -type "double3" 5.9999999999999858 0 -0.99999999999999956 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandRing3" -p "LeftHandRing2"; + rename -uid "C07A75D5-4F15-44BA-BA70-A38B4A5BD455"; + setAttr ".t" -type "double3" 5.0000000000000142 0 -1.0000000000000009 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandRing4" -p "LeftHandRing3"; + rename -uid "19BE3F34-47D8-3586-C9A2-9FBDFA49E044"; + setAttr ".t" -type "double3" 4.9999999999999858 0 -1 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandPinky1" -p "LeftHand"; + rename -uid "7179691A-4710-483B-418D-C5BEC7AF33D2"; + setAttr ".t" -type "double3" 5 0 -6 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandPinky2" -p "LeftHandPinky1"; + rename -uid "6AA6C96A-47A4-0573-057C-709DA0C4094D"; + setAttr ".t" -type "double3" 4.9999999999999858 0 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandPinky3" -p "LeftHandPinky2"; + rename -uid "6A9BC4C2-4EFF-8778-C75F-808DAE8134C6"; + setAttr ".t" -type "double3" 4.0000000000000284 1.4210854715202004e-14 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftHandPinky4" -p "LeftHandPinky3"; + rename -uid "CA4A295D-447B-5283-3FB0-7C91D3D7CD74"; + setAttr ".t" -type "double3" 3.9999999999999858 -1.4210854715202004e-14 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightShoulder" -p "Spine2"; + rename -uid "BE79F239-48FB-A0A7-806C-5BAAB007AAEA"; + setAttr ".t" -type "double3" -6 6 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightArm" -p "RightShoulder"; + rename -uid "70793BE9-41F1-1093-61E1-1096408735AB"; + setAttr ".t" -type "double3" -12 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightForeArm" -p "RightArm"; + rename -uid "891BC1DC-43C2-7372-FC15-1B99956F3C2E"; + setAttr ".t" -type "double3" -24 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHand" -p "RightForeArm"; + rename -uid "DA45E6E9-4B12-EA7D-F342-20AE197FA29B"; + setAttr ".t" -type "double3" -22 -2 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandThumb1" -p "RightHand"; + rename -uid "8E544F46-4E34-33B3-83D1-25998605482F"; + setAttr ".t" -type "double3" -2 -2 4 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandThumb2" -p "RightHandThumb1"; + rename -uid "43B6E543-4D87-4B53-F755-6EB549FB3BC4"; + setAttr ".t" -type "double3" -3.0000000000000142 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandThumb3" -p "RightHandThumb2"; + rename -uid "DC49883E-48D7-286B-5F07-D9982CADE0AF"; + setAttr ".t" -type "double3" -2.9999999999999716 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandThumb4" -p "RightHandThumb3"; + rename -uid "5D206C3A-421E-9936-D390-6B9CB23810DF"; + setAttr ".t" -type "double3" -3.0000000000000142 -1 2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandIndex1" -p "RightHand"; + rename -uid "8C06B2EA-4B98-E406-F401-949FB260613D"; + setAttr ".t" -type "double3" -6 0 3 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandIndex2" -p "RightHandIndex1"; + rename -uid "73F71279-406F-09AD-701D-0BA6B49BE549"; + setAttr ".t" -type "double3" -5.9999999999999858 0 0.99999999999999956 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandIndex3" -p "RightHandIndex2"; + rename -uid "061EF514-4CAA-A47F-DEB6-BFAD2F263C96"; + setAttr ".t" -type "double3" -5.0000000000000142 0 1.0000000000000009 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandIndex4" -p "RightHandIndex3"; + rename -uid "DDF6214D-4359-D570-5413-7C94AB13AC38"; + setAttr ".t" -type "double3" -4.9999999999999858 0 1 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandMiddle1" -p "RightHand"; + rename -uid "6506A5F1-4593-D2B3-C7C5-B4AB35ED7D37"; + setAttr ".t" -type "double3" -7 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandMiddle2" -p "RightHandMiddle1"; + rename -uid "08A64278-4C70-F47A-D18F-CC9C8411BE57"; + setAttr ".t" -type "double3" -6 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandMiddle3" -p "RightHandMiddle2"; + rename -uid "748CD3DA-4B74-0088-C9AB-FA8AE8D7CAB5"; + setAttr ".t" -type "double3" -5 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandMiddle4" -p "RightHandMiddle3"; + rename -uid "7633A974-417A-20FF-334F-C2BB7A7036D6"; + setAttr ".t" -type "double3" -5 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandRing1" -p "RightHand"; + rename -uid "867831A1-417A-2205-69D2-43B66EBD926A"; + setAttr ".t" -type "double3" -6 0 -3 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandRing2" -p "RightHandRing1"; + rename -uid "18D84F5D-47EC-EEC5-8502-4E870F9DE8FE"; + setAttr ".t" -type "double3" -5.9999999999999858 0 -0.99999999999999956 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandRing3" -p "RightHandRing2"; + rename -uid "5F1CCA7A-48C7-8646-DD9D-EABF4C7AB79E"; + setAttr ".t" -type "double3" -5.0000000000000142 0 -1.0000000000000009 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandRing4" -p "RightHandRing3"; + rename -uid "91E8358F-4CA2-A7FC-8E68-EB85D4CEE45A"; + setAttr ".t" -type "double3" -4.9999999999999858 0 -1 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandPinky1" -p "RightHand"; + rename -uid "A6572AF3-4928-F15A-30CC-EA8C0149BBDC"; + setAttr ".t" -type "double3" -5 0 -6 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandPinky2" -p "RightHandPinky1"; + rename -uid "AC21159E-45C1-65B8-2EA9-858DD060F467"; + setAttr ".t" -type "double3" -4.9999999999999858 0 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandPinky3" -p "RightHandPinky2"; + rename -uid "279FA6BA-44C6-3A12-0CEF-E2A65DD954D3"; + setAttr ".t" -type "double3" -4.0000000000000284 1.4210854715202004e-14 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightHandPinky4" -p "RightHandPinky3"; + rename -uid "81A4FC64-4F40-282A-062F-8897DF25690D"; + setAttr ".t" -type "double3" -3.9999999999999858 -1.4210854715202004e-14 -2 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftUpLeg" -p "Hips"; + rename -uid "C6D9AA89-47BB-9803-CA4F-5CAEFC4BD087"; + setAttr ".t" -type "double3" 9 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftLeg" -p "LeftUpLeg"; + rename -uid "657ECF49-4370-B8E1-6E3C-C1AF7725A5B5"; + setAttr ".t" -type "double3" -1.7763568394002505e-15 -45 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftFoot" -p "LeftLeg"; + rename -uid "FCB58E90-49E7-F954-C7DF-6FA091557209"; + setAttr ".t" -type "double3" 1.7763568394002505e-15 -45 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftToeBase" -p "LeftFoot"; + rename -uid "56B5AE88-4263-7496-B6FE-8B854CB1C5E0"; + setAttr ".t" -type "double3" 1.7763568394002505e-15 -10 14 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "LeftToe_End" -p "LeftToeBase"; + rename -uid "F71FD7B7-48CD-2E65-44BD-CE94B6622567"; + setAttr ".t" -type "double3" -3.5527136788005009e-15 0 10 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightUpLeg" -p "Hips"; + rename -uid "A0973F04-41A3-B049-0942-25A06B02173D"; + setAttr ".t" -type "double3" -9 0 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightLeg" -p "RightUpLeg"; + rename -uid "D8909A18-4033-2D8A-76C0-8583C5A52D03"; + setAttr ".t" -type "double3" 1.7763568394002505e-15 -45 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightFoot" -p "RightLeg"; + rename -uid "9706929B-41F9-72B6-AF92-33AE9BFDBD5E"; + setAttr ".t" -type "double3" -1.7763568394002505e-15 -45 0 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightToeBase" -p "RightFoot"; + rename -uid "105CF3D4-40DB-FED2-236B-888495BFF390"; + setAttr ".t" -type "double3" -1.7763568394002505e-15 -10 14 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode joint -n "RightToe_End" -p "RightToeBase"; + rename -uid "C8B059A4-47AA-8A97-8733-D9BE21B009F7"; + setAttr ".t" -type "double3" 3.5527136788005009e-15 0 10 ; + setAttr ".mnrl" -type "double3" -360 -360 -360 ; + setAttr ".mxrl" -type "double3" 360 360 360 ; +createNode transform -n "transform1"; + rename -uid "9D123D7D-4CC9-093E-A509-0690E79315F7"; +createNode openposeCharacter -n "openposeCharacter_HIK" -p "transform1"; + rename -uid "F4C90625-4C92-897E-8D0B-24B04BDDCD5D"; + setAttr -k off ".v"; + setAttr -s 59 ".tj"; +createNode lightLinker -s -n "lightLinker1"; + rename -uid "D9C92F2B-4274-F3E8-55F9-3C9220E74C8C"; + setAttr -s 2 ".lnk"; + setAttr -s 2 ".slnk"; +createNode displayLayerManager -n "layerManager"; + rename -uid "CCEC4154-4F20-8390-7839-93A6B3FE0C0C"; +createNode displayLayer -n "defaultLayer"; + rename -uid "0E4AF3B6-4A43-CBF3-4F49-448E0BA8F555"; + setAttr ".ufem" -type "stringArray" 0 ; +createNode renderLayerManager -n "renderLayerManager"; + rename -uid "40B9D3D6-414F-4FFE-9266-6E83CDFBAACF"; +createNode renderLayer -n "defaultRenderLayer"; + rename -uid "4C2D15F1-4F34-6E9A-D299-50A2D0F719CA"; + setAttr ".g" yes; +createNode script -n "uiConfigurationScriptNode"; + rename -uid "C8AD5306-42A0-C02B-2782-70BFA1B3DBD4"; + setAttr ".b" -type "string" "// Maya Mel UI Configuration File.\n// No UI generated in batch mode.\n"; + setAttr ".st" 3; +createNode script -n "sceneConfigurationScriptNode"; + rename -uid "58E5288A-41F1-3CA0-2020-0F9236560092"; + setAttr ".b" -type "string" "playbackOptions -min 1 -max 120 -ast 1 -aet 200 "; + setAttr ".st" 6; +select -ne :time1; + setAttr ".o" 1; + setAttr ".unw" 1; +select -ne :hardwareRenderingGlobals; + setAttr ".otfna" -type "stringArray" 22 "NURBS Curves" "NURBS Surfaces" "Polygons" "Subdiv Surface" "Particles" "Particle Instance" "Fluids" "Strokes" "Image Planes" "UI" "Lights" "Cameras" "Locators" "Joints" "IK Handles" "Deformers" "Motion Trails" "Components" "Hair Systems" "Follicles" "Misc. UI" "Ornaments" ; + setAttr ".otfva" -type "Int32Array" 22 0 1 1 1 1 1 + 1 1 1 0 0 0 0 0 0 0 0 0 + 0 0 0 0 ; + setAttr ".fprt" yes; + setAttr ".rtfm" 1; +select -ne :renderPartition; + setAttr -s 2 ".st"; +select -ne :renderGlobalsList1; +select -ne :defaultShaderList1; + setAttr -s 5 ".s"; +select -ne :postProcessList1; + setAttr -s 2 ".p"; +select -ne :defaultRenderingList1; +select -ne :standardSurface1; + setAttr ".bc" -type "float3" 0.40000001 0.40000001 0.40000001 ; + setAttr ".sr" 0.5; +select -ne :initialShadingGroup; + setAttr ".ro" yes; +select -ne :initialParticleSE; + setAttr ".ro" yes; +select -ne :defaultRenderGlobals; + addAttr -ci true -h true -sn "dss" -ln "defaultSurfaceShader" -dt "string"; + setAttr ".dss" -type "string" "standardSurface1"; +select -ne :defaultResolution; + setAttr ".pa" 1; +select -ne :defaultColorMgtGlobals; + setAttr ".cfe" yes; + setAttr ".cfp" -type "string" "/OCIO-configs/Maya2022-default/config.ocio"; + setAttr ".vtn" -type "string" "ACES 1.0 SDR-video (sRGB)"; + setAttr ".vn" -type "string" "ACES 1.0 SDR-video"; + setAttr ".dn" -type "string" "sRGB"; + setAttr ".wsn" -type "string" "ACEScg"; + setAttr ".otn" -type "string" "ACES 1.0 SDR-video (sRGB)"; + setAttr ".potn" -type "string" "ACES 1.0 SDR-video (sRGB)"; +select -ne :hardwareRenderGlobals; + setAttr ".ctrs" 256; + setAttr ".btrs" 512; +connectAttr "Hips.s" "Spine.is"; +connectAttr "Spine.s" "Spine1.is"; +connectAttr "Spine1.s" "Spine2.is"; +connectAttr "Spine2.s" "Neck.is"; +connectAttr "Neck.s" "Head.is"; +connectAttr "Head.s" "HeadTop_End.is"; +connectAttr "Spine2.s" "LeftShoulder.is"; +connectAttr "LeftShoulder.s" "LeftArm.is"; +connectAttr "LeftArm.s" "LeftForeArm.is"; +connectAttr "LeftForeArm.s" "LeftHand.is"; +connectAttr "LeftHand.s" "LeftHandThumb1.is"; +connectAttr "LeftHandThumb1.s" "LeftHandThumb2.is"; +connectAttr "LeftHandThumb2.s" "LeftHandThumb3.is"; +connectAttr "LeftHandThumb3.s" "LeftHandThumb4.is"; +connectAttr "LeftHand.s" "LeftHandIndex1.is"; +connectAttr "LeftHandIndex1.s" "LeftHandIndex2.is"; +connectAttr "LeftHandIndex2.s" "LeftHandIndex3.is"; +connectAttr "LeftHandIndex3.s" "LeftHandIndex4.is"; +connectAttr "LeftHand.s" "LeftHandMiddle1.is"; +connectAttr "LeftHandMiddle1.s" "LeftHandMiddle2.is"; +connectAttr "LeftHandMiddle2.s" "LeftHandMiddle3.is"; +connectAttr "LeftHandMiddle3.s" "LeftHandMiddle4.is"; +connectAttr "LeftHand.s" "LeftHandRing1.is"; +connectAttr "LeftHandRing1.s" "LeftHandRing2.is"; +connectAttr "LeftHandRing2.s" "LeftHandRing3.is"; +connectAttr "LeftHandRing3.s" "LeftHandRing4.is"; +connectAttr "LeftHand.s" "LeftHandPinky1.is"; +connectAttr "LeftHandPinky1.s" "LeftHandPinky2.is"; +connectAttr "LeftHandPinky2.s" "LeftHandPinky3.is"; +connectAttr "LeftHandPinky3.s" "LeftHandPinky4.is"; +connectAttr "Spine2.s" "RightShoulder.is"; +connectAttr "RightShoulder.s" "RightArm.is"; +connectAttr "RightArm.s" "RightForeArm.is"; +connectAttr "RightForeArm.s" "RightHand.is"; +connectAttr "RightHand.s" "RightHandThumb1.is"; +connectAttr "RightHandThumb1.s" "RightHandThumb2.is"; +connectAttr "RightHandThumb2.s" "RightHandThumb3.is"; +connectAttr "RightHandThumb3.s" "RightHandThumb4.is"; +connectAttr "RightHand.s" "RightHandIndex1.is"; +connectAttr "RightHandIndex1.s" "RightHandIndex2.is"; +connectAttr "RightHandIndex2.s" "RightHandIndex3.is"; +connectAttr "RightHandIndex3.s" "RightHandIndex4.is"; +connectAttr "RightHand.s" "RightHandMiddle1.is"; +connectAttr "RightHandMiddle1.s" "RightHandMiddle2.is"; +connectAttr "RightHandMiddle2.s" "RightHandMiddle3.is"; +connectAttr "RightHandMiddle3.s" "RightHandMiddle4.is"; +connectAttr "RightHand.s" "RightHandRing1.is"; +connectAttr "RightHandRing1.s" "RightHandRing2.is"; +connectAttr "RightHandRing2.s" "RightHandRing3.is"; +connectAttr "RightHandRing3.s" "RightHandRing4.is"; +connectAttr "RightHand.s" "RightHandPinky1.is"; +connectAttr "RightHandPinky1.s" "RightHandPinky2.is"; +connectAttr "RightHandPinky2.s" "RightHandPinky3.is"; +connectAttr "RightHandPinky3.s" "RightHandPinky4.is"; +connectAttr "Hips.s" "LeftUpLeg.is"; +connectAttr "LeftUpLeg.s" "LeftLeg.is"; +connectAttr "LeftLeg.s" "LeftFoot.is"; +connectAttr "LeftFoot.s" "LeftToeBase.is"; +connectAttr "LeftToeBase.s" "LeftToe_End.is"; +connectAttr "Hips.s" "RightUpLeg.is"; +connectAttr "RightUpLeg.s" "RightLeg.is"; +connectAttr "RightLeg.s" "RightFoot.is"; +connectAttr "RightFoot.s" "RightToeBase.is"; +connectAttr "RightToeBase.s" "RightToe_End.is"; +connectAttr "Head.msg" "openposeCharacter_HIK.tj[0]"; +connectAttr "Neck.msg" "openposeCharacter_HIK.tj[1]"; +connectAttr "RightArm.msg" "openposeCharacter_HIK.tj[2]"; +connectAttr "RightForeArm.msg" "openposeCharacter_HIK.tj[3]"; +connectAttr "RightHand.msg" "openposeCharacter_HIK.tj[4]"; +connectAttr "LeftArm.msg" "openposeCharacter_HIK.tj[5]"; +connectAttr "LeftForeArm.msg" "openposeCharacter_HIK.tj[6]"; +connectAttr "LeftHand.msg" "openposeCharacter_HIK.tj[7]"; +connectAttr "Hips.msg" "openposeCharacter_HIK.tj[8]"; +connectAttr "RightUpLeg.msg" "openposeCharacter_HIK.tj[9]"; +connectAttr "RightLeg.msg" "openposeCharacter_HIK.tj[10]"; +connectAttr "RightFoot.msg" "openposeCharacter_HIK.tj[11]"; +connectAttr "LeftUpLeg.msg" "openposeCharacter_HIK.tj[12]"; +connectAttr "LeftLeg.msg" "openposeCharacter_HIK.tj[13]"; +connectAttr "LeftFoot.msg" "openposeCharacter_HIK.tj[14]"; +connectAttr "LeftToeBase.msg" "openposeCharacter_HIK.tj[19]"; +connectAttr "RightToeBase.msg" "openposeCharacter_HIK.tj[22]"; +connectAttr "LeftHand.msg" "openposeCharacter_HIK.tj[25]"; +connectAttr "LeftHandThumb1.msg" "openposeCharacter_HIK.tj[26]"; +connectAttr "LeftHandThumb2.msg" "openposeCharacter_HIK.tj[27]"; +connectAttr "LeftHandThumb3.msg" "openposeCharacter_HIK.tj[28]"; +connectAttr "LeftHandThumb4.msg" "openposeCharacter_HIK.tj[29]"; +connectAttr "LeftHandIndex1.msg" "openposeCharacter_HIK.tj[30]"; +connectAttr "LeftHandIndex2.msg" "openposeCharacter_HIK.tj[31]"; +connectAttr "LeftHandIndex3.msg" "openposeCharacter_HIK.tj[32]"; +connectAttr "LeftHandIndex4.msg" "openposeCharacter_HIK.tj[33]"; +connectAttr "LeftHandMiddle1.msg" "openposeCharacter_HIK.tj[34]"; +connectAttr "LeftHandMiddle2.msg" "openposeCharacter_HIK.tj[35]"; +connectAttr "LeftHandMiddle3.msg" "openposeCharacter_HIK.tj[36]"; +connectAttr "LeftHandMiddle4.msg" "openposeCharacter_HIK.tj[37]"; +connectAttr "LeftHandRing1.msg" "openposeCharacter_HIK.tj[38]"; +connectAttr "LeftHandRing2.msg" "openposeCharacter_HIK.tj[39]"; +connectAttr "LeftHandRing3.msg" "openposeCharacter_HIK.tj[40]"; +connectAttr "LeftHandRing4.msg" "openposeCharacter_HIK.tj[41]"; +connectAttr "LeftHandPinky1.msg" "openposeCharacter_HIK.tj[42]"; +connectAttr "LeftHandPinky2.msg" "openposeCharacter_HIK.tj[43]"; +connectAttr "LeftHandPinky3.msg" "openposeCharacter_HIK.tj[44]"; +connectAttr "LeftHandPinky4.msg" "openposeCharacter_HIK.tj[45]"; +connectAttr "RightHand.msg" "openposeCharacter_HIK.tj[46]"; +connectAttr "RightHandThumb1.msg" "openposeCharacter_HIK.tj[47]"; +connectAttr "RightHandThumb2.msg" "openposeCharacter_HIK.tj[48]"; +connectAttr "RightHandThumb3.msg" "openposeCharacter_HIK.tj[49]"; +connectAttr "RightHandThumb4.msg" "openposeCharacter_HIK.tj[50]"; +connectAttr "RightHandIndex1.msg" "openposeCharacter_HIK.tj[51]"; +connectAttr "RightHandIndex2.msg" "openposeCharacter_HIK.tj[52]"; +connectAttr "RightHandIndex3.msg" "openposeCharacter_HIK.tj[53]"; +connectAttr "RightHandIndex4.msg" "openposeCharacter_HIK.tj[54]"; +connectAttr "RightHandMiddle1.msg" "openposeCharacter_HIK.tj[55]"; +connectAttr "RightHandMiddle2.msg" "openposeCharacter_HIK.tj[56]"; +connectAttr "RightHandMiddle3.msg" "openposeCharacter_HIK.tj[57]"; +connectAttr "RightHandMiddle4.msg" "openposeCharacter_HIK.tj[58]"; +connectAttr "RightHandRing1.msg" "openposeCharacter_HIK.tj[59]"; +connectAttr "RightHandRing2.msg" "openposeCharacter_HIK.tj[60]"; +connectAttr "RightHandRing3.msg" "openposeCharacter_HIK.tj[61]"; +connectAttr "RightHandRing4.msg" "openposeCharacter_HIK.tj[62]"; +connectAttr "RightHandPinky1.msg" "openposeCharacter_HIK.tj[63]"; +connectAttr "RightHandPinky2.msg" "openposeCharacter_HIK.tj[64]"; +connectAttr "RightHandPinky3.msg" "openposeCharacter_HIK.tj[65]"; +connectAttr "RightHandPinky4.msg" "openposeCharacter_HIK.tj[66]"; +relationship "link" ":lightLinker1" ":initialShadingGroup.message" ":defaultLightSet.message"; +relationship "link" ":lightLinker1" ":initialParticleSE.message" ":defaultLightSet.message"; +relationship "shadowLink" ":lightLinker1" ":initialShadingGroup.message" ":defaultLightSet.message"; +relationship "shadowLink" ":lightLinker1" ":initialParticleSE.message" ":defaultLightSet.message"; +connectAttr "layerManager.dli[0]" "defaultLayer.id"; +connectAttr "renderLayerManager.rlmi[0]" "defaultRenderLayer.rlid"; +connectAttr "defaultRenderLayer.msg" ":defaultRenderingList1.r" -na; +// End of openpose_humanik_test.ma diff --git a/test/openpose_humanik_test_preview.png b/test/openpose_humanik_test_preview.png new file mode 100644 index 0000000..f3dc00c Binary files /dev/null and b/test/openpose_humanik_test_preview.png differ diff --git a/test/openpose_humanik_test_preview_coco.png b/test/openpose_humanik_test_preview_coco.png new file mode 100644 index 0000000..be1f7f7 Binary files /dev/null and b/test/openpose_humanik_test_preview_coco.png differ