-
Notifications
You must be signed in to change notification settings - Fork 46
SMPL data converter #151
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
SMPL data converter #151
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -6,13 +6,16 @@ | |||||
| import torch | ||||||
| import uuid | ||||||
| from flask import session | ||||||
| from flask_socketio import SocketIO, emit | ||||||
| from flask_socketio import SocketIO | ||||||
| from threading import RLock | ||||||
| from typing import Union | ||||||
| from xrprimer.utils.log_utils import logging | ||||||
|
|
||||||
| from xrmocap.data_structure.body_model import auto_load_smpl_data | ||||||
| from xrmocap.model.body_model.builder import build_body_model | ||||||
| from xrmocap.utils.data_convert_utils import ( | ||||||
| SMPLDataConverter, SMPLDataTypeEnum, | ||||||
| ) | ||||||
| from xrmocap.utils.time_utils import Timer | ||||||
| from .base_flask_service import BaseFlaskService | ||||||
|
|
||||||
|
|
@@ -144,6 +147,8 @@ def __init__(self, | |||||
| logger=self.logger, | ||||||
| ) | ||||||
|
|
||||||
| self.data_converter = SMPLDataConverter(logger=self.logger) | ||||||
|
|
||||||
| def run(self): | ||||||
| """Run this flask service according to configuration. | ||||||
|
|
||||||
|
|
@@ -199,6 +204,28 @@ def upload_smpl_data(self, data: dict) -> dict: | |||||
| file_path = os.path.join(self.work_dir, f'{uuid_str}_{file_name}.npz') | ||||||
| with open(file_path, 'wb') as file: | ||||||
| file.write(file_data) | ||||||
| data_type = self.data_converter.get_data_type(file_path) | ||||||
| # organize the input data as the smpl data | ||||||
| if data_type is SMPLDataTypeEnum.AMASS: | ||||||
| self.logger.info('Received AMASS data, converting to SMPL(X) data') | ||||||
| data = self.data_converter.from_amass(file_path) | ||||||
| data.dump(file_path) | ||||||
| elif data_type is SMPLDataTypeEnum.HUMANDATA: | ||||||
| self.logger.info('Received HumanData, converting to SMPL(X) data') | ||||||
| data = self.data_converter.from_humandata(file_path) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Easier to find out what type the return value is.
Suggested change
What if |
||||||
| data.dump(file_path) | ||||||
| elif data_type is SMPLDataTypeEnum.UNKNOWN: | ||||||
| vals = [ | ||||||
| e.value for e in SMPLDataTypeEnum | ||||||
| if e is not SMPLDataTypeEnum.UNKNOWN | ||||||
| ] | ||||||
| error_msg = 'Failed to convert uploaded data due to ' + \ | ||||||
| f'unknown data type, supported data types: {vals}' | ||||||
|
|
||||||
| self.logger.error(error_msg) | ||||||
| resp_dict['msg'] = f'Error: {error_msg}' | ||||||
| resp_dict['status'] = 'fail' | ||||||
| return resp_dict | ||||||
| # load smpl data | ||||||
| smpl_data, class_name = auto_load_smpl_data( | ||||||
| npz_path=file_path, logger=self.logger) | ||||||
|
|
@@ -212,7 +239,7 @@ def upload_smpl_data(self, data: dict) -> dict: | |||||
| 'but no corresponding body model config found.' | ||||||
| resp_dict['msg'] = f'Error: {error_msg}' | ||||||
| self.logger.error(error_msg) | ||||||
| emit('upload_response', resp_dict) | ||||||
| return resp_dict | ||||||
| # build body model | ||||||
| body_model_cfg = self.body_model_configs[smpl_type][smpl_gender] | ||||||
| body_model = build_body_model(body_model_cfg).to(self.device) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,248 @@ | ||||||
| # yapf: disable | ||||||
| import logging | ||||||
| import numpy as np | ||||||
| from enum import Enum | ||||||
| from mmhuman3d.data.data_structures.human_data import HumanData | ||||||
| from typing import Optional, Union | ||||||
|
|
||||||
| from xrmocap.data_structure.body_model import SMPLData, SMPLXData | ||||||
|
|
||||||
| # yapf: enable | ||||||
|
|
||||||
|
|
||||||
| class SMPLDataTypeEnum(str, Enum): | ||||||
| SMPLDATA = 'smpl data' | ||||||
| HUMANDATA = 'human data' | ||||||
| AMASS = 'AMASS' | ||||||
| UNKNOWN = 'unknown' | ||||||
|
|
||||||
|
|
||||||
| def validate_shape(actual_shape: tuple, expected_shape: tuple) -> bool: | ||||||
| """Compare the shape of two ndarrays. | ||||||
| Args: | ||||||
| actual_shape (tuple): the actual shape. | ||||||
| expected_shape (tuple): the expected shape. | ||||||
| Returns: | ||||||
| bool: returns true if the actual shape is the expected shape. | ||||||
| """ | ||||||
| return all(a == e or e is None | ||||||
| for a, e in zip(actual_shape, expected_shape)) | ||||||
|
|
||||||
|
|
||||||
| def validate_spec(specs: dict, data: dict) -> bool: | ||||||
| """Validate whether the input data conform to the specs. | ||||||
| Args: | ||||||
| specs (dict): rules that should be followed. | ||||||
| data (dict): data to be evaluated. | ||||||
| Returns: | ||||||
| bool: returns true if the data follows the specs. | ||||||
| """ | ||||||
| missing_keys = set(specs.keys()) - set(data.keys()) | ||||||
| if missing_keys: | ||||||
| return False | ||||||
|
|
||||||
| for key, expected_shape in specs.items(): | ||||||
| item = data[key] | ||||||
| if not validate_shape(item.shape, expected_shape): | ||||||
| return False | ||||||
| return True | ||||||
|
|
||||||
|
|
||||||
| class SMPLDataConverter: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We've already got data converters for dataset, shall we have a different name here? |
||||||
| """A class that converts the input data into the smpl data.""" | ||||||
| SMPL_DATA_SPECS = { | ||||||
| 'betas': (1, 10), | ||||||
| 'fullpose': (None, 24, 3), | ||||||
| 'gender': (), | ||||||
| 'mask': (None, ), | ||||||
| 'transl': (None, 3) | ||||||
| } | ||||||
|
|
||||||
| SMPLX_DATA_SPECS = { | ||||||
| 'betas': (1, 10), | ||||||
| 'expression': (1, 10), | ||||||
| 'fullpose': (None, 55, 3), | ||||||
| 'gender': (), | ||||||
| 'mask': (None, ), | ||||||
| 'transl': (None, 3) | ||||||
| } | ||||||
|
|
||||||
| AMASS_SMPL_SPECS = { | ||||||
| 'betas': (16, ), | ||||||
| 'gender': (), | ||||||
| 'poses': (None, 156), | ||||||
| 'trans': (None, 3) | ||||||
| } | ||||||
|
|
||||||
| AMASS_SMPLX_SPECS = { | ||||||
| 'betas': (16, ), | ||||||
| 'gender': (), | ||||||
| 'poses': (None, 165), | ||||||
| 'trans': (None, 3) | ||||||
| } | ||||||
|
|
||||||
| HUMANDATA_SMPL_SPECS = {'meta': (), 'smpl': ()} | ||||||
|
|
||||||
| HUMANDATA_SMPLX_SPECS = {'meta': (), 'smplx': ()} | ||||||
|
|
||||||
| def __init__(self, | ||||||
| logger: Union[None, str, logging.Logger] = None) -> None: | ||||||
| """ | ||||||
| Args: | ||||||
| logger (Union[None, str, logging.Logger], optional): | ||||||
| Logger for logging. If None, root logger will be | ||||||
| selected. Defaults to None. | ||||||
| """ | ||||||
| self.logger = logger | ||||||
|
|
||||||
| def get_data_type(self, filepath: str) -> str: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| """Evaluate the data type and the structure of the motion file. | ||||||
| Args: | ||||||
| filepath (str): file to evaluate. | ||||||
| Returns: | ||||||
| str: the recognized data type. | ||||||
| """ | ||||||
| try: | ||||||
| with np.load(filepath, allow_pickle=True) as npz_file: | ||||||
| data_dict = dict(npz_file) | ||||||
| if (validate_spec(self.SMPL_DATA_SPECS, data_dict) | ||||||
| or validate_spec(self.SMPLX_DATA_SPECS, data_dict)): | ||||||
| return SMPLDataTypeEnum.SMPLDATA | ||||||
| elif (validate_spec(self.AMASS_SMPL_SPECS, data_dict) | ||||||
| or validate_spec(self.AMASS_SMPLX_SPECS, data_dict)): | ||||||
| return SMPLDataTypeEnum.AMASS | ||||||
| elif (validate_spec(self.HUMANDATA_SMPL_SPECS, data_dict) | ||||||
| or validate_spec(self.HUMANDATA_SMPLX_SPECS, data_dict)): | ||||||
| return SMPLDataTypeEnum.HUMANDATA | ||||||
| except Exception as e: | ||||||
| self.logger.error({e}) | ||||||
|
|
||||||
| return SMPLDataTypeEnum.UNKNOWN | ||||||
|
|
||||||
| def from_humandata(self, | ||||||
| filepath: str) -> Optional[Union[SMPLData, SMPLXData]]: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """Convert the humandata into the smpl data. | ||||||
| Args: | ||||||
| filepath (str): path to the humandata. | ||||||
| Returns: | ||||||
| Optional[Union[SMPLData, SMPLXData]]: the resulting smpl data | ||||||
| """ | ||||||
| human_data = HumanData.fromfile(filepath) | ||||||
| gender = human_data['meta'].get('gender', None) | ||||||
| if gender is None: | ||||||
| gender = 'neutral' | ||||||
| self.logger.warning( | ||||||
| f'Cannot find gender record in {human_data}.meta, ' + | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I doubt the readability of this log. Can you provide an example from an actual usage where a warning is raised here? |
||||||
| 'Use neutral as default.') | ||||||
| body_model = None | ||||||
| if 'smpl' in dict(human_data).keys(): | ||||||
| body_model = 'smpl' | ||||||
| elif 'smplx' in dict(human_data).keys(): | ||||||
| body_model = 'smplx' | ||||||
| else: | ||||||
| self.logger.error( | ||||||
| f'Cannot find body model in {human_data}.meta, ' + | ||||||
| 'supported body models: [smpl, smplx].') | ||||||
| return None | ||||||
|
|
||||||
| betas = human_data[body_model]['betas'] | ||||||
| transl = human_data[body_model]['transl'] | ||||||
| body_pose = human_data[body_model]['body_pose'] | ||||||
| global_orient = human_data[body_model]['global_orient'] | ||||||
| n_frames = body_pose.shape[0] | ||||||
| mask = np.ones((n_frames, ), dtype=np.uint8) | ||||||
|
|
||||||
| res = None | ||||||
| if 'smpl' == body_model: | ||||||
| param_dict = dict( | ||||||
| betas=betas, | ||||||
| transl=transl, | ||||||
| global_orient=global_orient, | ||||||
| body_pose=body_pose) | ||||||
| res = SMPLData(gender=gender, logger=self.logger) | ||||||
| res.from_param_dict(param_dict) | ||||||
| res.set_mask(mask) | ||||||
| else: | ||||||
| param_dict = dict( | ||||||
| betas=betas, | ||||||
| transl=transl, | ||||||
| global_orient=global_orient, | ||||||
| body_pose=body_pose, | ||||||
| left_hand_pose=human_data['smplx']['left_hand_pose'], | ||||||
| right_hand_pose=human_data['smplx']['right_hand_pose'], | ||||||
| leye_pose=human_data['smplx']['leye_pose'], | ||||||
| reye_pose=human_data['smplx']['reye_pose'], | ||||||
| jaw_pose=human_data['smplx']['jaw_pose'], | ||||||
| expression=human_data['smplx']['expression'], | ||||||
| ) | ||||||
| res = SMPLXData(gender=gender, logger=self.logger) | ||||||
| res.from_param_dict(param_dict) | ||||||
| res.set_mask(mask) | ||||||
|
|
||||||
| return res | ||||||
|
|
||||||
| def from_amass(self, | ||||||
| filepath: str) -> Optional[Union[SMPLData, SMPLXData]]: | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| """Convert the amass data into the smpl data. | ||||||
| Args: | ||||||
| filepath (str): path to the amass data. | ||||||
| Returns: | ||||||
| Optional[Union[SMPLData, SMPLXData]]: the resulting smpl data. | ||||||
| """ | ||||||
| amass_data = np.load(filepath, allow_pickle=True) | ||||||
| poses = amass_data['poses'] | ||||||
| gender = amass_data['gender'] | ||||||
| betas = amass_data['betas'][:10] | ||||||
| transl = amass_data['trans'] | ||||||
| global_orient = amass_data['poses'][:, :3] | ||||||
|
|
||||||
| n_frames = poses.shape[0] | ||||||
| mask = np.ones((n_frames, ), dtype=np.uint8) | ||||||
| res = None | ||||||
| if poses.shape[1] == 156: # smpl | ||||||
| body_pose = amass_data['poses'][:, 3:72] | ||||||
| param_dict = dict( | ||||||
| betas=betas, | ||||||
| transl=transl, | ||||||
| global_orient=global_orient, | ||||||
| body_pose=body_pose) | ||||||
|
|
||||||
| res = SMPLData(gender=gender, logger=self.logger) | ||||||
| res.from_param_dict(param_dict) | ||||||
| mask = np.ones((n_frames, ), dtype=np.uint8) | ||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| res.set_mask(mask) | ||||||
|
|
||||||
| elif poses.shape[1] == 165: # smplx | ||||||
| body_pose = amass_data['poses'][:, 3:66] | ||||||
| jaw_pose = amass_data['poses'][:, 66:69] | ||||||
| leye_pose = amass_data['poses'][:, 69:72] | ||||||
| reye_pose = amass_data['poses'][:, 72:75] | ||||||
| left_hand_pose = amass_data['poses'][:, 75:120] | ||||||
| right_hand_pose = amass_data['poses'][:, 120:165] | ||||||
| param_dict = dict( | ||||||
| betas=betas, | ||||||
| transl=transl, | ||||||
| global_orient=global_orient, | ||||||
| body_pose=body_pose, | ||||||
| jaw_pose=jaw_pose, | ||||||
| leye_pose=leye_pose, | ||||||
| reye_pose=reye_pose, | ||||||
| left_hand_pose=left_hand_pose, | ||||||
| right_hand_pose=right_hand_pose) | ||||||
| res = SMPLXData(gender=gender, logger=self.logger) | ||||||
| res.from_param_dict(param_dict) | ||||||
| res.set_mask(mask) | ||||||
| else: | ||||||
| self.logger.error('Unsupported AMASS data.') | ||||||
|
|
||||||
| return res | ||||||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Too many file IO, both
get_data_typeandfrom_amassload file fromfile_path. Something like this will be faster: