Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 81 additions & 4 deletions pymomentum/geometry/parameter_transform_pybind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,27 @@ void registerParameterTransformBindings(
py::init([](const std::vector<std::string>& names,
const mm::Skeleton& skeleton,
const Eigen::SparseMatrix<float, Eigen::RowMajor>& transform) {
const auto nJoints = skeleton.joints.size();
const auto nJointParams = nJoints * mm::kParametersPerJoint;
const auto nModelParams = names.size();

MT_THROW_IF_T(
transform.rows() != nJointParams,
py::value_error,
"Expected parameter transform to have {} rows (7*{} joints), but got {}",
nJointParams,
nJoints,
transform.rows());
MT_THROW_IF_T(
transform.cols() != nModelParams,
py::value_error,
"Expected parameter transform to have {} columns (matching parameter names), but got {}",
nModelParams,
transform.cols());

mm::ParameterTransform parameterTransform;
parameterTransform.name = names;
parameterTransform.transform.resize(
static_cast<int>(skeleton.joints.size()) * mm::kParametersPerJoint,
static_cast<int>(names.size()));
parameterTransform.transform.resize(nJointParams, nModelParams);

for (int i = 0; i < transform.outerSize(); ++i) {
for (Eigen::SparseMatrix<float, Eigen::RowMajor>::InnerIterator it(transform, i); it;
Expand All @@ -59,9 +75,70 @@ void registerParameterTransformBindings(
}
}

parameterTransform.offsets.setZero(skeleton.joints.size() * mm::kParametersPerJoint);
parameterTransform.offsets.setZero(nJointParams);
return parameterTransform;
}),
R"(Create a parameter transform from a sparse matrix.

:param names: List of model parameter names.
:param skeleton: The skeleton that this parameter transform operates on.
:param transform: A sparse matrix of size (7*n_joints x n_params) that maps from model parameters to joint parameters.)",
py::arg("names"),
py::arg("skeleton"),
py::arg("transform"))
.def(
py::init([](const std::vector<std::string>& names,
const mm::Skeleton& skeleton,
const py::array_t<float>& transform) {
const auto nJoints = skeleton.joints.size();
const auto nJointParams = nJoints * mm::kParametersPerJoint;
const auto nModelParams = names.size();

MT_THROW_IF_T(
transform.ndim() != 2,
py::value_error,
"Expected parameter transform to be a 2D array, but got {}D array",
transform.ndim());
MT_THROW_IF_T(
transform.shape(0) != nJointParams,
py::value_error,
"Expected parameter transform to have {} rows (7*{} joints), but got {}",
nJointParams,
nJoints,
transform.shape(0));
MT_THROW_IF_T(
transform.shape(1) != nModelParams,
py::value_error,
"Expected parameter transform to have {} columns (matching {} parameter names), but got {}",
nModelParams,
nModelParams,
transform.shape(1));

mm::ParameterTransform parameterTransform;
parameterTransform.name = names;

std::vector<Eigen::Triplet<float>> triplets;
auto accessor = transform.unchecked<2>();
for (int i = 0; i < accessor.shape(0); ++i) {
for (int j = 0; j < accessor.shape(1); ++j) {
if (accessor(i, j) != 0) {
triplets.emplace_back(i, j, accessor(i, j));
}
}
}

parameterTransform.transform.resize(nJointParams, nModelParams);
parameterTransform.transform.setFromTriplets(triplets.begin(), triplets.end());
parameterTransform.offsets.setZero(nJointParams);
return parameterTransform;
}),
R"(Create a parameter transform from a dense numpy array.

The array will be converted to a sparse matrix internally for efficient storage and computation.

:param names: List of model parameter names.
:param skeleton: The skeleton that this parameter transform operates on.
:param transform: A dense numpy array of size (7*n_joints x n_params) that maps from model parameters to joint parameters.)",
py::arg("names"),
py::arg("skeleton"),
py::arg("transform"))
Expand Down
101 changes: 101 additions & 0 deletions pymomentum/renderer/momentum_render.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,13 @@
#include <momentum/rasterizer/camera.h>

#include <dispenso/parallel_for.h> // @manual
#include <pybind11/numpy.h>
#include <Eigen/Core>

#include <string.h>

namespace py = pybind11;

namespace pymomentum {

// Build a camera to look at an object:
Expand Down Expand Up @@ -246,6 +249,104 @@ std::vector<momentum::rasterizer::Camera> buildCamerasForBody(
return result;
}

momentum::rasterizer::Camera createCameraForBody(
const momentum::Character& character,
const py::array_t<float>& skeletonStates,
int imageHeight,
int imageWidth,
float focalLength_mm,
bool horizontal,
float cameraAngle) {
const size_t nJoints = character.skeleton.joints.size();

if (skeletonStates.ndim() < 2 || skeletonStates.ndim() > 3) {
throw std::runtime_error(fmt::format(
"create_camera_for_body: skeleton_states must be 2D (nJoints x 8), 3D (nFrames x nJoints x 8). Got {} dimensions.",
skeletonStates.ndim()));
}

// Verify the last dimension is 8 (tx, ty, tz, rx, ry, rz, rw, s)
const size_t lastDim = skeletonStates.shape(skeletonStates.ndim() - 1);
if (lastDim != 8) {
throw std::runtime_error(fmt::format(
"create_camera_for_body: Expected last dimension to be 8 (tx, ty, tz, rx, ry, rz, rw, s), but got {}.",
lastDim));
}

// Verify the second-to-last dimension matches the number of joints
const size_t jointsDim = skeletonStates.shape(skeletonStates.ndim() - 2);
if (jointsDim != nJoints) {
throw std::runtime_error(fmt::format(
"create_camera_for_body: Expected {} joints (second-to-last dimension), but got {}.",
nJoints,
jointsDim));
}

// Handle different dimensionalities with appropriate accessors
if (skeletonStates.ndim() == 2) {
// Unbatched: (nJoints, 8)
auto accessor = skeletonStates.unchecked<2>();

std::vector<momentum::SkeletonState> skelStates;
skelStates.reserve(1);

momentum::SkeletonState skelState;
skelState.jointState.resize(nJoints);

for (size_t iJoint = 0; iJoint < nJoints; ++iJoint) {
Eigen::Vector3f translation(accessor(iJoint, 0), accessor(iJoint, 1), accessor(iJoint, 2));

Eigen::Quaternionf rotation(
accessor(iJoint, 6), // rw
accessor(iJoint, 3), // rx
accessor(iJoint, 4), // ry
accessor(iJoint, 5)); // rz

float scale = accessor(iJoint, 7);

skelState.jointState[iJoint].transform.translation = translation;
skelState.jointState[iJoint].transform.rotation = rotation;
skelState.jointState[iJoint].transform.scale = scale;
}

skelStates.push_back(skelState);
return makeOutsideInCameraForBody(
character, skelStates, imageHeight, imageWidth, focalLength_mm, horizontal, cameraAngle);

} else { // skeletonStates.ndim() == 3
// Batched: (nBatch, nJoints, 8)
auto accessor = skeletonStates.unchecked<3>();

std::vector<momentum::SkeletonState> skelStates;
skelStates.reserve(accessor.shape(0));
for (size_t iPose = 0; iPose < accessor.shape(0); ++iPose) {
momentum::SkeletonState skelState;
skelState.jointState.resize(nJoints);

for (size_t iJoint = 0; iJoint < nJoints; ++iJoint) {
Eigen::Vector3f translation(
accessor(iPose, iJoint, 0), accessor(iPose, iJoint, 1), accessor(iPose, iJoint, 2));

Eigen::Quaternionf rotation(
accessor(iPose, iJoint, 6), // rw
accessor(iPose, iJoint, 3), // rx
accessor(iPose, iJoint, 4), // ry
accessor(iPose, iJoint, 5)); // rz

float scale = accessor(iPose, iJoint, 7);

skelState.jointState[iJoint].transform.translation = translation;
skelState.jointState[iJoint].transform.rotation = rotation;
skelState.jointState[iJoint].transform.scale = scale;
}

skelStates.push_back(skelState);
}
return makeOutsideInCameraForBody(
character, skelStates, imageHeight, imageWidth, focalLength_mm, horizontal, cameraAngle);
}
}

template <typename T2, typename T1>
std::vector<Eigen::Vector3<T2>> castVector(const std::vector<Eigen::Vector3<T1>>& vec) {
std::vector<Eigen::Vector3<T2>> result;
Expand Down
11 changes: 11 additions & 0 deletions pymomentum/renderer/momentum_render.h
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
#include <momentum/rasterizer/camera.h>

#include <ATen/ATen.h>
#include <pybind11/numpy.h>
#include <pybind11/pybind11.h>

#include <memory>
#include <vector>
Expand Down Expand Up @@ -39,6 +41,15 @@ std::vector<momentum::rasterizer::Camera> buildCamerasForBody(
bool horizontal,
float cameraAngle = 0.0f);

momentum::rasterizer::Camera createCameraForBody(
const momentum::Character& character,
const pybind11::array_t<float>& skeletonStates,
int imageHeight,
int imageWidth,
float focalLength_mm,
bool horizontal,
float cameraAngle = 0.0f);

Eigen::Matrix<int, Eigen::Dynamic, 3, Eigen::RowMajor> triangulate(
const Eigen::VectorXi& faceIndices,
const Eigen::VectorXi& faceOffsets);
Expand Down
21 changes: 21 additions & 0 deletions pymomentum/renderer/renderer_pybind.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,27 @@ PYBIND11_MODULE(renderer, m) {
py::arg("horizontal") = false,
py::arg("camera_angle") = 0.0f);

m.def(
"create_camera_for_body",
&createCameraForBody,
R"(Create a camera that roughly faces the body (default: face the front of the body). If you pass in multiple frames of animation, the camera will ensure all frames are visible.

:param character: Character to use.
:param skeleton_states: numpy.ndarray of skeleton states with shape (nJoints, 8), (nBatch, nJoints, 8), or (nBatch, nFrames, nJoints, 8). Each joint has 8 values: (tx, ty, tz, rx, ry, rz, rw, s) representing translation, quaternion rotation (x, y, z, w), and uniform scale.
:param image_height: Height of the target image.
:param image_width: Width of the target image.
:param focal_length_mm: 35mm-equivalent focal length; e.g. focal_length_mm=50 corresponds to a "normal" lens. Specified in millimeters because this is what photographers use.
:param horizontal: whether the cameras are placed horizontally, assuming the Y axis is the world up direction.
:param camera_angle: what direction the camera looks at the body. default: 0, looking at front of body. pi/2: at left side of body.
:return: a :class:`Camera`.)",
py::arg("character"),
py::arg("skeleton_states"),
py::arg("image_height"),
py::arg("image_width"),
py::arg("focal_length_mm") = 50.0f,
py::arg("horizontal") = false,
py::arg("camera_angle") = 0.0f);

m.def(
"build_cameras_for_hand",
&buildCamerasForHand,
Expand Down
35 changes: 35 additions & 0 deletions pymomentum/test/test_parameter_transform.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from pymomentum.geometry import (
Character,
create_test_character,
ParameterTransform,
uniform_random_to_model_parameters,
)

Expand All @@ -29,6 +30,40 @@ def test_get_transform(self) -> None:
joint_params_2 = torch.matmul(transform, model_params)
self.assertTrue(torch.allclose(joint_params_1, joint_params_2))

def test_parameter_transform_round_trip(self) -> None:
"""Test that converting parameter transform to dense matrix and back is lossless."""
# Create a test character with parameter transform
character: Character = create_test_character()
original_pt = character.parameter_transform

# Get the transform as a dense matrix
transform_matrix = original_pt.transform
transform_numpy = transform_matrix.numpy()

# Create a new parameter transform from the dense matrix
new_pt = ParameterTransform(
names=original_pt.names,
skeleton=character.skeleton,
transform=transform_numpy,
)

# Verify the transform produces identical results
torch.manual_seed(42)
model_params = uniform_random_to_model_parameters(character, torch.rand(5, 10))

joint_params_original = original_pt.apply(model_params)
joint_params_new = new_pt.apply(model_params)

self.assertTrue(
torch.allclose(joint_params_original, joint_params_new, atol=1e-6)
)

# Verify parameter names are preserved
self.assertEqual(original_pt.names, new_pt.names)

# Verify size is preserved
self.assertEqual(original_pt.size, new_pt.size)


if __name__ == "__main__":
unittest.main()
Loading
Loading