├── .gitignore ├── 3Dviewer.py ├── LICENSE ├── README.md ├── amc_parser.py ├── demo.gif └── demo_static.png /.gitignore: -------------------------------------------------------------------------------- 1 | # Custom 2 | data/ 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | env/ 15 | build/ 16 | develop-eggs/ 17 | dist/ 18 | downloads/ 19 | eggs/ 20 | .eggs/ 21 | lib/ 22 | lib64/ 23 | parts/ 24 | sdist/ 25 | var/ 26 | wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | 31 | # PyInstaller 32 | # Usually these files are written by a python script from a template 33 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 34 | *.manifest 35 | *.spec 36 | 37 | # Installer logs 38 | pip-log.txt 39 | pip-delete-this-directory.txt 40 | 41 | # Unit test / coverage reports 42 | htmlcov/ 43 | .tox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | .hypothesis/ 51 | 52 | # Translations 53 | *.mo 54 | *.pot 55 | 56 | # Django stuff: 57 | *.log 58 | local_settings.py 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # dotenv 86 | .env 87 | 88 | # virtualenv 89 | .venv 90 | venv/ 91 | ENV/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | -------------------------------------------------------------------------------- /3Dviewer.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | import numpy as np 3 | import time 4 | import transforms3d.euler as euler 5 | from amc_parser import * 6 | 7 | from OpenGL.GL import * 8 | from OpenGL.GLU import * 9 | 10 | 11 | class Viewer: 12 | def __init__(self, joints=None, motions=None): 13 | """ 14 | Display motion sequence in 3D. 15 | 16 | Parameter 17 | --------- 18 | joints: Dict returned from `amc_parser.parse_asf`. Keys are joint names and 19 | values are instance of Joint class. 20 | 21 | motions: List returned from `amc_parser.parse_amc. Each element is a dict 22 | with joint names as keys and relative rotation degree as values. 23 | 24 | """ 25 | self.joints = joints 26 | self.motions = motions 27 | self.frame = 0 # current frame of the motion sequence 28 | self.playing = False # whether is playing the motion sequence 29 | self.fps = 120 # frame rate 30 | 31 | # whether is dragging 32 | self.rotate_dragging = False 33 | self.translate_dragging = False 34 | # old mouse cursor position 35 | self.old_x = 0 36 | self.old_y = 0 37 | # global rotation 38 | self.global_rx = 0 39 | self.global_ry = 0 40 | # rotation matrix for camera moving 41 | self.rotation_R = np.eye(3) 42 | # rotation speed 43 | self.speed_rx = np.pi / 90 44 | self.speed_ry = np.pi / 90 45 | # translation speed 46 | self.speed_trans = 0.25 47 | self.speed_zoom = 0.5 48 | # whether the main loop should break 49 | self.done = False 50 | # default translate set manually to make sure the skeleton is in the middle 51 | # of the window 52 | # if you can't see anything in the screen, this is the first parameter you 53 | # need to adjust 54 | self.default_translate = np.array([0, -20, -100], dtype=np.float32) 55 | self.translate = np.copy(self.default_translate) 56 | 57 | pygame.init() 58 | self.screen_size = (1024, 768) 59 | self.screen = pygame.display.set_mode( 60 | self.screen_size, pygame.DOUBLEBUF | pygame.OPENGL 61 | ) 62 | pygame.display.set_caption( 63 | 'AMC Parser - frame %d / %d' % (self.frame, len(self.motions)) 64 | ) 65 | self.clock = pygame.time.Clock() 66 | 67 | glClearColor(0, 0, 0, 0) 68 | glShadeModel(GL_SMOOTH) 69 | glMaterialfv( 70 | GL_FRONT, GL_SPECULAR, np.array([1, 1, 1, 1], dtype=np.float32) 71 | ) 72 | glMaterialfv( 73 | GL_FRONT, GL_SHININESS, np.array([100.0], dtype=np.float32) 74 | ) 75 | glMaterialfv( 76 | GL_FRONT, GL_AMBIENT, np.array([0.7, 0.7, 0.7, 0.7], dtype=np.float32) 77 | ) 78 | glEnable(GL_POINT_SMOOTH) 79 | 80 | glLightfv(GL_LIGHT0, GL_POSITION, np.array([1, 1, 1, 0], dtype=np.float32)) 81 | glEnable(GL_LIGHT0) 82 | glEnable(GL_LIGHTING) 83 | glEnable(GL_DEPTH_TEST) 84 | gluPerspective(45, (self.screen_size[0]/self.screen_size[1]), 0.1, 500.0) 85 | 86 | glPointSize(10) 87 | glLineWidth(2.5) 88 | 89 | def process_event(self): 90 | """ 91 | Handle user interface events: keydown, close, dragging. 92 | 93 | """ 94 | for event in pygame.event.get(): 95 | if event.type == pygame.QUIT: 96 | self.done = True 97 | elif event.type == pygame.KEYDOWN: 98 | if event.key == pygame.K_RETURN: # reset camera 99 | self.translate = self.default_translate 100 | self.global_rx = 0 101 | self.global_ry = 0 102 | elif event.key == pygame.K_SPACE: 103 | self.playing = not self.playing 104 | elif event.type == pygame.MOUSEBUTTONDOWN: # dragging 105 | if event.button == 1: 106 | self.rotate_dragging = True 107 | else: 108 | self.translate_dragging = True 109 | self.old_x, self.old_y = event.pos 110 | elif event.type == pygame.MOUSEBUTTONUP: 111 | if event.button == 1: 112 | self.rotate_dragging = False 113 | else: 114 | self.translate_dragging = False 115 | elif event.type == pygame.MOUSEMOTION: 116 | if self.translate_dragging: 117 | # haven't figure out best way to implement this 118 | pass 119 | elif self.rotate_dragging: 120 | new_x, new_y = event.pos 121 | self.global_ry -= (new_x - self.old_x) / \ 122 | self.screen_size[0] * np.pi 123 | self.global_rx -= (new_y - self.old_y) / \ 124 | self.screen_size[1] * np.pi 125 | self.old_x, self.old_y = new_x, new_y 126 | pressed = pygame.key.get_pressed() 127 | # rotation 128 | if pressed[pygame.K_DOWN]: 129 | self.global_rx -= self.speed_rx 130 | if pressed[pygame.K_UP]: 131 | self. global_rx += self.speed_rx 132 | if pressed[pygame.K_LEFT]: 133 | self.global_ry += self.speed_ry 134 | if pressed[pygame.K_RIGHT]: 135 | self.global_ry -= self.speed_ry 136 | # moving 137 | if pressed[pygame.K_a]: 138 | self.translate[0] -= self.speed_trans 139 | if pressed[pygame.K_d]: 140 | self.translate[0] += self.speed_trans 141 | if pressed[pygame.K_w]: 142 | self.translate[1] += self.speed_trans 143 | if pressed[pygame.K_s]: 144 | self.translate[1] -= self.speed_trans 145 | if pressed[pygame.K_q]: 146 | self.translate[2] += self.speed_zoom 147 | if pressed[pygame.K_e]: 148 | self.translate[2] -= self.speed_zoom 149 | # forward and rewind 150 | if pressed[pygame.K_COMMA]: 151 | self.frame -= 1 152 | if self.frame < 0: 153 | self.frame = len(self.motions) - 1 154 | if pressed[pygame.K_PERIOD]: 155 | self.frame += 1 156 | if self.frame >= len(self.motions): 157 | self.frame = 0 158 | # global rotation 159 | grx = euler.euler2mat(self.global_rx, 0, 0) 160 | gry = euler.euler2mat(0, self.global_ry, 0) 161 | self.rotation_R = grx.dot(gry) 162 | 163 | def set_joints(self, joints): 164 | """ 165 | Set joints for viewer. 166 | 167 | Parameter 168 | --------- 169 | joints: Dict returned from `amc_parser.parse_asf`. Keys are joint names and 170 | values are instance of Joint class. 171 | 172 | """ 173 | self.joints = joints 174 | 175 | def set_motion(self, motions): 176 | """ 177 | Set motion sequence for viewer. 178 | 179 | Paramter 180 | -------- 181 | motions: List returned from `amc_parser.parse_amc. Each element is a dict 182 | with joint names as keys and relative rotation degree as values. 183 | 184 | """ 185 | self.motions = motions 186 | 187 | def draw(self): 188 | """ 189 | Draw the skeleton with balls and sticks. 190 | 191 | """ 192 | glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT) 193 | 194 | glBegin(GL_POINTS) 195 | for j in self.joints.values(): 196 | coord = np.array( 197 | np.squeeze(j.coordinate).dot(self.rotation_R) + \ 198 | self.translate, dtype=np.float32 199 | ) 200 | glVertex3f(*coord) 201 | glEnd() 202 | 203 | glBegin(GL_LINES) 204 | for j in self.joints.values(): 205 | child = j 206 | parent = j.parent 207 | if parent is not None: 208 | coord_x = np.array( 209 | np.squeeze(child.coordinate).dot(self.rotation_R)+self.translate, 210 | dtype=np.float32 211 | ) 212 | coord_y = np.array( 213 | np.squeeze(parent.coordinate).dot(self.rotation_R)+self.translate, 214 | dtype=np.float32 215 | ) 216 | glVertex3f(*coord_x) 217 | glVertex3f(*coord_y) 218 | glEnd() 219 | 220 | def run(self): 221 | """ 222 | Main loop. 223 | 224 | """ 225 | while not self.done: 226 | self.process_event() 227 | self.joints['root'].set_motion(self.motions[self.frame]) 228 | if self.playing: 229 | self.frame += 1 230 | if self.frame >= len(self.motions): 231 | self.frame = 0 232 | self.draw() 233 | pygame.display.set_caption( 234 | 'AMC Parser - frame %d / %d' % (self.frame, len(self.motions)) 235 | ) 236 | pygame.display.flip() 237 | self.clock.tick(self.fps) 238 | pygame.quit() 239 | 240 | 241 | if __name__ == '__main__': 242 | asf_path = './data/01/01.asf' 243 | amc_path = './data/01/01_01.amc' 244 | joints = parse_asf(asf_path) 245 | motions = parse_amc(amc_path) 246 | v = Viewer(joints, motions) 247 | v.run() 248 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Yuxiao Zhou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMCParser 2 | 3 | A lightweight library to parse and visualize asf/amc files from [CMU MoCap](http://mocap.cs.cmu.edu/) dataset using Python 3. 4 | 5 | The main purpose of this library is to help understanding how asf/amc file works, as well as how to use them. Therefore, simplicity and readability are stressed, while robustness is ignored. 6 | 7 | ## Demo 8 | 9 | Demo using PyGame and PyOpenGL: 10 | 11 | ![3D Demo](demo.gif) 12 | 13 | Demo using Matplotlib: 14 | 15 | ![Static Demo](demo_static.png) 16 | 17 | ## Usage 18 | 19 | There's a simple example in the source code as follows: 20 | 21 | ```python 22 | if __name__ == '__main__': 23 | asf_path = './data/01/01.asf' 24 | amc_path = './data/01/01_01.amc' 25 | joints = parse_asf(asf_path) 26 | motions = parse_amc(amc_path) 27 | frame_idx = 180 28 | joints['root'].set_motion(motions[frame_idx]) 29 | joints['root'].draw() 30 | ``` 31 | 32 | And another example in `3Dviewer.py`: 33 | ```python 34 | asf_path = './data/01/01.asf' 35 | amc_path = './data/01/01_01.amc' 36 | joints = parse_asf(asf_path) 37 | motions = parse_amc(amc_path) 38 | v = Viwer(joints, motions) 39 | v.run() 40 | ``` 41 | 42 | The data can be found from CMU MoCap dataset. 43 | 44 | ## Parser 45 | 46 | The asf/amc parsers are straightforward and easy to understand. The parsers are fully tested on the CMU MoCap dataset, but I don't expect it can work on other datasets without any modification. However, it won't be hard to extend it for more complicating asf/amc files. 47 | 48 | ## Visualization 49 | 50 | Matplotlib is used to draw joints and bones in 3D statically; PyGame and PyOpenGL are used to draw motion sequence. 51 | 52 | In 3DViewer, we support: 53 | 54 | * `WASD` to move around. 55 | * `QE` to zoom in/out. 56 | * `↑ ↓ ← →` to rotate. 57 | * `LEFT MOUSE BUTTON` to drag. 58 | * `RETURN` to reset camera view. 59 | * `SPACE` to start/pause. 60 | * `,` and `.` to rewind and forward. 61 | 62 | NOTE that my implementation of changing view is inefficient (but fluent enough) with some small bugs, but I don't have time to improve it. Pull request is welcomed. 63 | 64 | ## Dependencies 65 | 66 | * numpy 67 | * transforms3d 68 | * matplotlib 69 | * pygame 70 | * pyopengl 71 | 72 | All the dependencies are available via `pip install`. 73 | 74 | ## One More Thing 75 | 76 | If this repo is used in any publications or projects, please let me know. I would be happy and encouraged =) 77 | -------------------------------------------------------------------------------- /amc_parser.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import matplotlib.pyplot as plt 3 | from transforms3d.euler import euler2mat 4 | from mpl_toolkits.mplot3d import Axes3D 5 | 6 | 7 | class Joint: 8 | def __init__(self, name, direction, length, axis, dof, limits): 9 | """ 10 | Definition of basic joint. The joint also contains the information of the 11 | bone between it's parent joint and itself. Refer 12 | [here](https://research.cs.wisc.edu/graphics/Courses/cs-838-1999/Jeff/ASF-AMC.html) 13 | for detailed description for asf files. 14 | 15 | Parameter 16 | --------- 17 | name: Name of the joint defined in the asf file. There should always be one 18 | root joint. String. 19 | 20 | direction: Default direction of the joint(bone). The motions are all defined 21 | based on this default pose. 22 | 23 | length: Length of the bone. 24 | 25 | axis: Axis of rotation for the bone. 26 | 27 | dof: Degree of freedom. Specifies the number of motion channels and in what 28 | order they appear in the AMC file. 29 | 30 | limits: Limits on each of the channels in the dof specification 31 | 32 | """ 33 | self.name = name 34 | self.direction = np.reshape(direction, [3, 1]) 35 | self.length = length 36 | axis = np.deg2rad(axis) 37 | self.C = euler2mat(*axis) 38 | self.Cinv = np.linalg.inv(self.C) 39 | self.limits = np.zeros([3, 2]) 40 | for lm, nm in zip(limits, dof): 41 | if nm == 'rx': 42 | self.limits[0] = lm 43 | elif nm == 'ry': 44 | self.limits[1] = lm 45 | else: 46 | self.limits[2] = lm 47 | self.parent = None 48 | self.children = [] 49 | self.coordinate = None 50 | self.matrix = None 51 | 52 | def set_motion(self, motion): 53 | if self.name == 'root': 54 | self.coordinate = np.reshape(np.array(motion['root'][:3]), [3, 1]) 55 | rotation = np.deg2rad(motion['root'][3:]) 56 | self.matrix = self.C.dot(euler2mat(*rotation)).dot(self.Cinv) 57 | else: 58 | idx = 0 59 | rotation = np.zeros(3) 60 | for axis, lm in enumerate(self.limits): 61 | if not np.array_equal(lm, np.zeros(2)): 62 | rotation[axis] = motion[self.name][idx] 63 | idx += 1 64 | rotation = np.deg2rad(rotation) 65 | self.matrix = self.parent.matrix.dot(self.C).dot(euler2mat(*rotation)).dot(self.Cinv) 66 | self.coordinate = self.parent.coordinate + self.length * self.matrix.dot(self.direction) 67 | for child in self.children: 68 | child.set_motion(motion) 69 | 70 | def draw(self): 71 | joints = self.to_dict() 72 | fig = plt.figure() 73 | ax = Axes3D(fig,auto_add_to_figure=False) 74 | fig.add_axes(ax) 75 | 76 | ax.set_xlim3d(-50, 10) 77 | ax.set_ylim3d(-20, 40) 78 | ax.set_zlim3d(-20, 40) 79 | 80 | xs, ys, zs = [], [], [] 81 | for joint in joints.values(): 82 | xs.append(joint.coordinate[0, 0]) 83 | ys.append(joint.coordinate[1, 0]) 84 | zs.append(joint.coordinate[2, 0]) 85 | plt.plot(zs, xs, ys, 'b.') 86 | 87 | for joint in joints.values(): 88 | child = joint 89 | if child.parent is not None: 90 | parent = child.parent 91 | xs = [child.coordinate[0, 0], parent.coordinate[0, 0]] 92 | ys = [child.coordinate[1, 0], parent.coordinate[1, 0]] 93 | zs = [child.coordinate[2, 0], parent.coordinate[2, 0]] 94 | plt.plot(zs, xs, ys, 'r') 95 | plt.show() 96 | 97 | def to_dict(self): 98 | ret = {self.name: self} 99 | for child in self.children: 100 | ret.update(child.to_dict()) 101 | return ret 102 | 103 | def pretty_print(self): 104 | print('===================================') 105 | print('joint: %s' % self.name) 106 | print('direction:') 107 | print(self.direction) 108 | print('limits:', self.limits) 109 | print('parent:', self.parent) 110 | print('children:', self.children) 111 | 112 | 113 | def read_line(stream, idx): 114 | if idx >= len(stream): 115 | return None, idx 116 | line = stream[idx].strip().split() 117 | idx += 1 118 | return line, idx 119 | 120 | 121 | def parse_asf(file_path): 122 | '''read joint data only''' 123 | with open(file_path) as f: 124 | content = f.read().splitlines() 125 | 126 | for idx, line in enumerate(content): 127 | # meta infomation is ignored 128 | if line == ':bonedata': 129 | content = content[idx+1:] 130 | break 131 | 132 | # read joints 133 | joints = {'root': Joint('root', np.zeros(3), 0, np.zeros(3), [], [])} 134 | idx = 0 135 | while True: 136 | # the order of each section is hard-coded 137 | 138 | line, idx = read_line(content, idx) 139 | 140 | if line[0] == ':hierarchy': 141 | break 142 | 143 | assert line[0] == 'begin' 144 | 145 | line, idx = read_line(content, idx) 146 | assert line[0] == 'id' 147 | 148 | line, idx = read_line(content, idx) 149 | assert line[0] == 'name' 150 | name = line[1] 151 | 152 | line, idx = read_line(content, idx) 153 | assert line[0] == 'direction' 154 | direction = np.array([float(axis) for axis in line[1:]]) 155 | 156 | # skip length 157 | line, idx = read_line(content, idx) 158 | assert line[0] == 'length' 159 | length = float(line[1]) 160 | 161 | line, idx = read_line(content, idx) 162 | assert line[0] == 'axis' 163 | assert line[4] == 'XYZ' 164 | 165 | axis = np.array([float(axis) for axis in line[1:-1]]) 166 | 167 | dof = [] 168 | limits = [] 169 | 170 | line, idx = read_line(content, idx) 171 | if line[0] == 'dof': 172 | dof = line[1:] 173 | for i in range(len(dof)): 174 | line, idx = read_line(content, idx) 175 | if i == 0: 176 | assert line[0] == 'limits' 177 | line = line[1:] 178 | assert len(line) == 2 179 | mini = float(line[0][1:]) 180 | maxi = float(line[1][:-1]) 181 | limits.append((mini, maxi)) 182 | 183 | line, idx = read_line(content, idx) 184 | 185 | assert line[0] == 'end' 186 | joints[name] = Joint( 187 | name, 188 | direction, 189 | length, 190 | axis, 191 | dof, 192 | limits 193 | ) 194 | 195 | # read hierarchy 196 | assert line[0] == ':hierarchy' 197 | 198 | line, idx = read_line(content, idx) 199 | 200 | assert line[0] == 'begin' 201 | 202 | while True: 203 | line, idx = read_line(content, idx) 204 | if line[0] == 'end': 205 | break 206 | assert len(line) >= 2 207 | for joint_name in line[1:]: 208 | joints[line[0]].children.append(joints[joint_name]) 209 | for nm in line[1:]: 210 | joints[nm].parent = joints[line[0]] 211 | 212 | return joints 213 | 214 | 215 | def parse_amc(file_path): 216 | with open(file_path) as f: 217 | content = f.read().splitlines() 218 | 219 | for idx, line in enumerate(content): 220 | if line == ':DEGREES': 221 | content = content[idx+1:] 222 | break 223 | 224 | frames = [] 225 | idx = 0 226 | line, idx = read_line(content, idx) 227 | assert line[0].isnumeric(), line 228 | EOF = False 229 | while not EOF: 230 | joint_degree = {} 231 | while True: 232 | line, idx = read_line(content, idx) 233 | if line is None: 234 | EOF = True 235 | break 236 | if line[0].isnumeric(): 237 | break 238 | joint_degree[line[0]] = [float(deg) for deg in line[1:]] 239 | frames.append(joint_degree) 240 | return frames 241 | 242 | 243 | def test_all(): 244 | import os 245 | lv0 = './data' 246 | lv1s = os.listdir(lv0) 247 | for lv1 in lv1s: 248 | lv2s = os.listdir('/'.join([lv0, lv1])) 249 | asf_path = '%s/%s/%s.asf' % (lv0, lv1, lv1) 250 | print('parsing %s' % asf_path) 251 | joints = parse_asf(asf_path) 252 | motions = parse_amc('./nopose.amc') 253 | joints['root'].set_motion(motions[0]) 254 | joints['root'].draw() 255 | 256 | # for lv2 in lv2s: 257 | # if lv2.split('.')[-1] != 'amc': 258 | # continue 259 | # amc_path = '%s/%s/%s' % (lv0, lv1, lv2) 260 | # print('parsing amc %s' % amc_path) 261 | # motions = parse_amc(amc_path) 262 | # for idx, motion in enumerate(motions): 263 | # print('setting motion %d' % idx) 264 | # joints['root'].set_motion(motion) 265 | 266 | 267 | if __name__ == '__main__': 268 | test_all() 269 | # asf_path = './133.asf' 270 | # amc_path = './133_01.amc' 271 | # joints = parse_asf(asf_path) 272 | # motions = parse_amc(amc_path) 273 | # frame_idx = 0 274 | # joints['root'].set_motion(motions[frame_idx]) 275 | # joints['root'].draw() 276 | -------------------------------------------------------------------------------- /demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CalciferZh/AMCParser/08e5a3662e197e7e40eed2c18fc254282eb381f9/demo.gif -------------------------------------------------------------------------------- /demo_static.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CalciferZh/AMCParser/08e5a3662e197e7e40eed2c18fc254282eb381f9/demo_static.png --------------------------------------------------------------------------------