e2f2e6668a
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 <noreply@anthropic.com>
215 lines
7.9 KiB
Python
215 lines
7.9 KiB
Python
"""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)
|