Skip to content

Commit 4debaac

Browse files
JinlongYANGJinlong Yang
andauthored
Add a method to support direct SAM3D-Body output to SMPL(X) (#43)
Co-authored-by: Jinlong Yang <jinlong.yang@oculus.com>
1 parent 98d520f commit 4debaac

File tree

7 files changed

+192
-14
lines changed

7 files changed

+192
-14
lines changed

tools/mhr_smpl_conversion/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The SMPL-MHR Conversion Tool enables seamless conversion between different 3D hu
88

99
- **SMPL/SMPLX → MHR**: Convert SMPL/SMPLX model parameters/meshes to MHR format
1010
- **MHR → SMPL/SMPLX**: Convert MHR parameters back to SMPL/SMPLX format
11+
- **SAM3D Outputs (MHR) → SMPL/SMPLX**: Convert SAM3D output to SMPL/SMPLX model parameters
1112

1213
The tool uses barycentric interpolation for topology mapping and offers multiple optimization backends for parameter fitting.
1314

@@ -64,6 +65,7 @@ mhr_smpl_conversion/
6465
│   ├── smpl_para2mhr_pymomentum
6566
│   ├── mhr_para2smplx_pytorch_single_identity
6667
│   ├── smplx_mesh2mhr_pytorch_single_identity
68+
│   ├── sam3d_output_to_smplx
6769
└──...
6870
```
6971

@@ -88,6 +90,14 @@ converter = Conversion(
8890
method="pytorch" # or "pymomentum"
8991
)
9092

93+
# Convert SAM3D output to SMPL(X)
94+
results = converter.convert_sam3d_output_to_smplx(
95+
sam3d_outputs=sam3d_outputs,
96+
return_smpl_meshes=True,
97+
return_smpl_parameters=True,
98+
return_fitting_errors=True
99+
)
100+
91101
# Convert SMPLX to MHR
92102
results = converter.convert_smpl2mhr(
93103
smpl_parameters=smplx_params,

tools/mhr_smpl_conversion/conversion.py

Lines changed: 127 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@
8282

8383
import logging
8484
from functools import cached_property, lru_cache
85-
from typing import Optional
85+
from typing import Optional, Any
8686

8787
import numpy as np
8888

@@ -322,9 +322,9 @@ def convert_smpl2mhr(
322322
return ConversionResult(
323323
result_meshes=result_meshes if return_mhr_meshes else None,
324324
result_vertices=mhr_vertices if return_mhr_vertices else None,
325-
result_parameters=None
326-
if not return_mhr_parameters
327-
else fitting_parameter_results,
325+
result_parameters=(
326+
None if not return_mhr_parameters else fitting_parameter_results
327+
),
328328
result_errors=errors if return_fitting_errors else None,
329329
)
330330

@@ -401,12 +401,132 @@ def convert_mhr2smpl(
401401
return ConversionResult(
402402
result_meshes=result_meshes if return_smpl_meshes else None,
403403
result_vertices=smpl_vertices if return_smpl_vertices else None,
404-
result_parameters=fitting_parameter_results
405-
if return_smpl_parameters
406-
else None,
404+
result_parameters=(
405+
fitting_parameter_results if return_smpl_parameters else None
406+
),
407407
result_errors=errors if return_fitting_errors else None,
408408
)
409409

410+
def convert_sam3d_output_to_smpl(
411+
self,
412+
sam3d_outputs: list[dict[str, Any]],
413+
return_smpl_meshes: bool = False,
414+
return_smpl_parameters: bool = True,
415+
return_smpl_vertices: bool = False,
416+
return_fitting_errors: bool = True,
417+
batch_size: int = 256,
418+
) -> ConversionResult:
419+
"""
420+
Convert SAM3D output to SMPL model parameters.
421+
422+
Usage:
423+
After initializing a coverter object from Conversion class,
424+
sam3d_outputs = SAM3DBodyEstimator.process_one_image(...)
425+
conversion_results = coverter.convert_sam3d_output_to_smpl(sam3d_outputs)
426+
427+
Args:
428+
sam3d_outputs: List of dictionaries containing the output of the SAM3D model.
429+
return_smpl_meshes: Whether to return the SMPL meshes. If True, the function will return a list
430+
of SMPL meshes.
431+
return_smpl_parameters: Whether to return the SMPL parameters. If True, the function will return a
432+
dictionary of SMPL parameters.
433+
return_smpl_vertices: Whether to return the SMPL vertices. If True, the function will return a numpy
434+
array of SMPL vertices.
435+
return_fitting_errors: Whether to return fitting errors. If True, the function will return errors for
436+
each frame.
437+
batch_size: Number of frames to process in each batch.
438+
439+
Returns:
440+
ConversionResult containing:
441+
- result_meshes: List of SMPL meshes
442+
- result_vertices: Numpy array of SMPL vertices
443+
- result_parameters: Dictionary of SMPL parameters
444+
- result_errors: Numpy array of fitting errors for each frame
445+
"""
446+
447+
# These are the keys that are expected in the sam3d_outputs, from the first three we can extract the MHR vertices. The last one are the MHR vertices.
448+
SAM3D_OUTPUT_VARIABLES = (
449+
"mhr_model_params", # Translation is not included in the model params, but in "pred_cam_t".
450+
"shape_params",
451+
"expr_params",
452+
"pred_vertices", # Translation is not included in the vertices, but in "pred_cam_t".
453+
"pred_cam_t", # This is the camera translation w.r.t. each person.
454+
)
455+
456+
# The result of SAM3DBodyEstimator.process_one_image() is a list of dictionaries, each dictionary contains the outputs of the SAM3D model for one person.
457+
# We concatenate all person's outputs into one dictionary, so that we can convert them to SMPL in batch.
458+
concatenated_sam3d_outputs = {}
459+
for k in SAM3D_OUTPUT_VARIABLES:
460+
concatenated_sam3d_outputs[k] = np.stack(
461+
[
462+
sam3d_output[k]
463+
for sam3d_output in sam3d_outputs
464+
if k in sam3d_output
465+
],
466+
axis=0,
467+
)
468+
concatenated_sam3d_outputs[k] = self._to_tensor(
469+
concatenated_sam3d_outputs[k]
470+
)
471+
472+
# If MHR vertices are available, use them to compute the SMPL parameters. If not, use the MHR model, shape, and expressions to compute the SMPL parameters.
473+
num_people = len(sam3d_outputs)
474+
mhr_vertices = None
475+
if (
476+
"pred_vertices" in concatenated_sam3d_outputs
477+
and concatenated_sam3d_outputs["pred_vertices"].shape[0] == num_people
478+
):
479+
# If pred_vertices is available, use it to compute the target vertices
480+
# Note: SAM3D outputs the vertices in meters, so we need to convert them to centimeters before passing them to the conversion function
481+
mhr_vertices = (
482+
100.0 * concatenated_sam3d_outputs["pred_vertices"]
483+
+ 100.0 * concatenated_sam3d_outputs["pred_cam_t"][:, None, :]
484+
)
485+
elif (
486+
"mhr_model_params" in concatenated_sam3d_outputs
487+
and concatenated_sam3d_outputs["mhr_model_params"].shape[0] == num_people
488+
):
489+
# If pred_vertices is not available, use the mhr_model_params to compute the target vertices
490+
mhr_parameters = {}
491+
mhr_parameters["lbs_model_params"] = concatenated_sam3d_outputs[
492+
"mhr_model_params"
493+
]
494+
495+
assert (
496+
"shape_params" in concatenated_sam3d_outputs
497+
and concatenated_sam3d_outputs["shape_params"].shape[0] == num_people
498+
)
499+
mhr_parameters["identity_coeffs"] = concatenated_sam3d_outputs[
500+
"shape_params"
501+
]
502+
assert (
503+
"expr_params" in concatenated_sam3d_outputs
504+
and concatenated_sam3d_outputs["expr_params"].shape[0] == num_people
505+
)
506+
mhr_parameters["face_expr_coeffs"] = concatenated_sam3d_outputs[
507+
"expr_params"
508+
]
509+
_, mhr_vertices = self._mhr_para2mesh(mhr_parameters, return_mesh=False)
510+
mhr_vertices = self._to_tensor(mhr_vertices)
511+
mhr_vertices[..., [1, 2]] *= -1 # Camera system difference in SAM3D-Body
512+
mhr_vertices += 100.0 * concatenated_sam3d_outputs["pred_cam_t"][:, None, :]
513+
else:
514+
raise ValueError(
515+
"Either pred_vertices or mhr_model_params must be available in the SAM3D output."
516+
)
517+
518+
conversion_results = self.convert_mhr2smpl(
519+
mhr_vertices=mhr_vertices,
520+
single_identity=False,
521+
is_tracking=False,
522+
return_smpl_meshes=return_smpl_meshes,
523+
return_smpl_parameters=return_smpl_parameters,
524+
return_smpl_vertices=return_smpl_vertices,
525+
return_fitting_errors=return_fitting_errors,
526+
batch_size=batch_size,
527+
)
528+
return conversion_results
529+
410530
def _to_tensor(self, data: torch.Tensor | np.ndarray) -> torch.Tensor:
411531
"""Convert input data to tensor on the appropriate device."""
412532
if isinstance(data, torch.Tensor):
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.

tools/mhr_smpl_conversion/example.py

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@
2929
from conversion import Conversion
3030

3131
_INPUT_FILE = "./data/example_smplx_poses.npy" # Directory to store input data
32+
_SAM3D_BODY_OUTPUT_DIR = (
33+
"./data/sam3d_body_outputs" # Directory to store SAM3D body output
34+
)
3235
_OUTPUT_DIR = "./tmp_results" # Directory to store conversion results
3336

3437

@@ -67,6 +70,7 @@ def run_examples(
6770
if not os.path.exists(converted_smpl_model_file):
6871
smpl_model_data = dict(np.load(smpl_model_file))
6972
import pickle
73+
7074
with open(converted_smpl_model_file, "wb") as f:
7175
pickle.dump(smpl_model_data, f)
7276
self.smpl_model = smplx.SMPL(
@@ -97,12 +101,8 @@ def run_examples(
97101

98102
# Example 2: Convert the SMPLX meshes to MHR with PyTorch and single identity
99103
if self.smplx_model is not None:
100-
print(
101-
"\nConverting SMPLX meshes to MHR with PyTorch and single identity."
102-
)
103-
example_output_dir = (
104-
output_dir + "/smplx_mesh2mhr_pytorch_single_identity"
105-
)
104+
print("\nConverting SMPLX meshes to MHR with PyTorch and single identity.")
105+
example_output_dir = output_dir + "/smplx_mesh2mhr_pytorch_single_identity"
106106
os.makedirs(example_output_dir, exist_ok=True)
107107
mhr_parameters = (
108108
self.example_smplx_meshes_to_mhr_with_pytorch_single_identity(
@@ -119,6 +119,54 @@ def run_examples(
119119
mhr_parameters, example_output_dir
120120
)
121121

122+
# Example 4: Convert the SAM3D output to SMPLX
123+
if self.smplx_model is not None:
124+
print("\nConverting SAM3D output to SMPLX.")
125+
example_output_dir = output_dir + "/sam3d_output_to_smplx"
126+
os.makedirs(example_output_dir, exist_ok=True)
127+
self.example_sam3d_output_to_smplx(example_output_dir)
128+
129+
def example_sam3d_output_to_smplx(self, output_dir: str):
130+
"""Convert SAM3D output to SMPLX."""
131+
mhr_model = MHR.from_files(lod=1, device=self._device)
132+
133+
# Load the SAM3D outputs
134+
subject_files = [
135+
f for f in os.listdir(_SAM3D_BODY_OUTPUT_DIR) if f.endswith(".npz")
136+
]
137+
sam3d_outputs = []
138+
139+
for subject_file in subject_files:
140+
subject_data = np.load(os.path.join(_SAM3D_BODY_OUTPUT_DIR, subject_file))
141+
sam3d_outputs.append(subject_data)
142+
mesh = trimesh.Trimesh(
143+
subject_data["pred_vertices"] + subject_data["pred_cam_t"][None, ...],
144+
mhr_model.character.mesh.faces,
145+
process=False,
146+
)
147+
mesh.export(f"{output_dir}/{subject_file[:-4]}_sam3d.ply")
148+
149+
# ***** Core conversion code *****
150+
converter = Conversion(
151+
mhr_model=mhr_model, smpl_model=self.smplx_model, method="pytorch"
152+
)
153+
154+
conversion_results = converter.convert_sam3d_output_to_smpl(
155+
sam3d_outputs=sam3d_outputs,
156+
return_smpl_meshes=True,
157+
return_smpl_parameters=False,
158+
return_smpl_vertices=False,
159+
return_fitting_errors=True,
160+
)
161+
162+
print("Conversion errors:")
163+
print(conversion_results.result_errors)
164+
165+
# Save the results
166+
print(f"Exporting the results to {output_dir}...")
167+
for i, mesh in enumerate(conversion_results.result_meshes):
168+
mesh.export(f"{output_dir}/{subject_files[i][:-4]}_result_smplx.ply")
169+
122170
def example_smpl_parameters_to_mhr_with_pymomentum_multiple_identity(
123171
self, smpl_paras: dict[str, torch.Tensor], output_dir: str
124172
):
@@ -301,7 +349,7 @@ def _parse_arguments():
301349
"--smpl",
302350
type=str,
303351
required=False,
304-
default="./data/SMPL_NEUTRAL.npz",
352+
default="./data/SMPL_NEUTRAL.pkl",
305353
help="Path to the SMPL model file. If not specified, SMPL related conversion will be skipped.",
306354
)
307355

0 commit comments

Comments
 (0)