Files
PoseRenderer/python/openpose_renderer/render_override.py
T
indigo e2f2e6668a 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 <noreply@anthropic.com>
2026-07-14 08:41:57 +08:00

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)