CMU Mocap 中的 ASF AMC 文件格式介绍

779 阅读4分钟

CMU Mocap

CMU Mocap 是一个广为人知的运动捕捉数据库

mocap.cs.cmu.edu/

其中 ASF 文件用于描述骨架,AMC 文件用于描述运动

在官网的 FAQ 界面给出了一些可视化 ASF AMC 文件的工具

mocap.cs.cmu.edu/faqs.php

在这里介绍了一点关于 ASF 文件的组织

www.cs.cmu.edu/~kiranb/ani…

其中引人注意的是骨架层级

屏幕截图_20230221_084239.png

可见,hierarchy 的一行代表一个父节点和若干个直接相连的子节点

在这里介绍了 AMC 文件如何表示某一帧骨架的位置

research.cs.wisc.edu/graphics/Co…

CalciferZh/AMCParser

光看公式可能还是不懂,可以看看别人是怎么解析的

github.com/CalciferZh/…

这个解析器的使用示例是:

if __name__ == '__main__':
  test_all()
  asf_path = './133.asf'
  amc_path = './133_01.amc'
  joints = parse_asf(asf_path)
  motions = parse_amc(amc_path)
  frame_idx = 0
  joints['root'].set_motion(motions[frame_idx])
  joints['root'].draw()

其中 parse_asfparse_amc 都很好懂,就是一行一行地读取文件

关键在于 set_motion

  def set_motion(self, motion):
    if self.name == 'root':
      self.coordinate = np.reshape(np.array(motion['root'][:3]), [3, 1])
      rotation = np.deg2rad(motion['root'][3:])
      self.matrix = self.C.dot(euler2mat(*rotation)).dot(self.Cinv)
    else:
      idx = 0
      rotation = np.zeros(3)
      for axis, lm in enumerate(self.limits):
        if not np.array_equal(lm, np.zeros(2)):
          rotation[axis] = motion[self.name][idx]
          idx += 1
      rotation = np.deg2rad(rotation)
      self.matrix = self.parent.matrix.dot(self.C).dot(euler2mat(*rotation)).dot(self.Cinv)
      self.coordinate = self.parent.coordinate + self.length * self.matrix.dot(self.direction)
    for child in self.children:
      child.set_motion(motion)

Numpy 测试

  array = np.array([1, 2, 3, 4, 5, 6])
  print(array[0])
  print(array[:3])
  print(array[3:])

输出

1
[1 2 3]
[4 5 6]

所以说他这个取数据写法简单,但是直觉上有点怪

[:] 表示取全部数据

[:idx] 表示取 0~idx-1 的数据,[idx:] 表示取 idx~length-1 的数据

毕竟我还以为要不就是一直不包含 [idx] 要不就是一直包含 [idx]


其中 CinvC 的转置,在初始化 Joint 的时候计算:

这个 axis 读的是 root 骨的 axis,由于是用的 euler2mat,可知 ASF 文件里的角度都是欧拉角

image.png


那么现在应该没有问题了,已知公式是:

vM = vXYZ

L = CinvMCB

对于 root 来说,它的坐标是给定的,它也没有父级,所以不需要计算父级相关的矩阵,所以他的计算里面没有 B,那么他的 rotation = np.deg2rad(motion['root'][3:]) 对应公式中的 M

对于其他节点来说,变换要以父级为基础,所以多出来的那个就是父级相关的 B,所以发现:

self.matrix 对应公式中的 L

self.parent.matrix 对应公式中的 B

这么看的话,原公式不如写成:

Rot = euler2mat(np.deg2rad([X, Y, Z]))

B = self.parent.L if self.parent != None else E

L = Cinv·Rot·C·B

其中 X Y Z 是三个轴的欧拉角,文件中用角度表示


有些骨骼只在某个方向上有自由度,那么其他方向上的旋转自然就是 0

如果在那些没有自由度的方向上有一个固定的旋转角度怎么办?那些是写在 axis 里面的,也就是由 C 计算

jutanke/mocap

这个人的脚本可以将 Human3.6M 和 CMU 数据集的骨架归一化,简化,看上去很有用……?

github.com/jutanke/moc…

虽然介绍中大部分使用 Human3.6M 数据集,但是用 CMU 是差不多的

比如在根目录下创建如下测试脚本:

import mocap.datasets.cmu as CMU

all_subjects = CMU.ALL_SUBJECTS
# different subjects have different actions:
action_for_subject_01 = CMU.GET_ACTIONS('01')

from mocap.visualization.sequence import SequenceVisualizer

ds = CMU.CMU(['01'])

seq = ds[0]

vis_dir = './visualization/'
vis_name = 'test_visualization'

vis = SequenceVisualizer(vis_dir, vis_name,  # mandatory parameters
                         plot_fn=None,  # TODO
                         vmin=-1, vmax=1,  # min and max values of the 3D plot scene
                         to_file=False,  # if True writes files to the given directory
                         subsampling=1,  # subsampling of sequences
                         with_pauses=False,  # if True pauses after each frame
                         fps=20,  # fps for visualization
                         mark_origin=False)  # if True draw cross at origin

# plot single sequence
vis.plot(seq,
         seq2=None,
         parallel=False,
         plot_fn1=None, plot_fn2=None,  # defines how seq/seq2 are drawn
         views=[(45, 45)],  # [(elevation, azimuth)]  # defines the view(s)
         lcolor='#099487', rcolor='#F51836',
         lcolor2='#E1C200', rcolor2='#5FBF43',
         noaxis=False,  # if True draw person against white background
         noclear=False, # if True do not clear the scene for next frame
         toggle_color=False,  # if True toggle color after each frame
         plot_cbc=None,  # alternatve plot function: fn(ax{matplotlib}, seq{n_frames x dim}, frame:{int})
         last_frame=60,
         definite_cbc=None,  # fn(ax{matplotlib}, iii{int}|enueration, frame{int})
         name='',
         plot_jid=False,
         create_video=False,
         video_fps=25,
         if_video_keep_pngs=False)

我在使用的时候遇到了这样的报错

  0%|          | 0/60 [00:00<?, ?it/s]
Traceback (most recent call last):
  File "MyWorkSpace\plugins\python\helpers\pydev\pydevconsole.py", line 364, in runcode
    coro = func()
  File "<input>", line 20, in <module>
  File "MyWorkSpace\mocap-version2\mocap\visualization\sequence.py", line 183, in plot
    if t < n:
UnboundLocalError: local variable 'n' referenced before assignment

一看报错的脚本,确实是没有声明局部变量 n 就直接用了

这是因为源代码只考虑了 last_frame is None 的情况

        if last_frame is None:
            if seq2 is None or parallel:
                last_frame = len(seq1)
                n = last_frame
                if parallel:
                    assert seq2 is not None
                    assert len(seq2) == last_frame
            else:
                n = len(seq1)
                seq1 = np.concatenate([seq1, seq2], axis=0)
                last_frame = len(seq1)

如果给 last_frame 赋了值,就会跳过这个 if,局部变量 n 就没有在这里赋值,之后也没有赋值语句,也就相当于没有定义了

最简单的解决方法就是在代码前面声明一下

但是更好的方法是把他这个 if...else... 补全

但是我看不懂他的代码……所以提了一个 issue

github.com/jutanke/moc…