虚拟人运动合成是我研究生阶段的研究课题,目前我将已完成的工作整理出来,预计写三篇博客介绍,分为:

1.三维人体运动合成系列之一:运动捕捉数据ASF/AMC文件解析与绘制

2.三维人体运动合成系列之二:流形学习的原理、实现及应用

3.三维人体运动合成系列之三:优化的Dijkstra算法原理及实现

  本人目前还在为一篇SCI论文奋斗,选题是基于稀疏样本的三维人体运动合成,那个恐怕要等结果完成之后再整理成文添加到这个系列之中。而这篇要讲的内容如题所述:运动捕捉数据ASF/AMC文件解析与绘制。因为这个步骤是所有后续工作的基础,说白了就是将以文本形式保存的运动数据解析并绘制出来。我将分为几个部分来分别讲述,其中最关键的部分就是所有骨节点全局坐标的计算,全局坐标计算出来之后再用OpenGL或者其他工具绘制出来是很容易的事情。

1.ASF/AMC文件的解析

  ASF/AMC是一种运动数据格式,是由Vicon运动捕捉设备进行运动捕捉时记录的,运动捕捉简而言之就是将运动数据化,将生动的、直观的运动变成一个个枯燥乏味的数据,这么做的好处就是可以引入计算机对其进行分析、处理,因为计算机虽然没有人对运动的美学鉴赏能力,但是却很擅长数据的处理和分析。其中ASF文件保存的是骨架数据,而AMC文件保存的则是运动数据,具体而言,它们是长这样的:

  以上是各个骨节点数据,包括id,名字,从父节点到该节点的向量方向及长度,绑定于各个骨节点的局部坐标系相对全局坐标系的旋转欧拉角,以及各节点的旋转自由度;除以上信息之外ASF文件的末尾还包括各个骨节点之间的继承关系,这个信息同样很重要,因为后续的计算都有赖于它:

  其中每一行第一个骨节点是该行后续骨节点的父亲节点。而AMC文件是长这样的:

  我们可以看到AMC文件就是由一阵阵的数据构成的,每一帧的最开头都会有该帧的编号,每一帧具体存储的其实就是各个骨节点的旋转欧拉角,注意这里的旋转角度是和ASF文件中各个骨节点的旋转自由度一一对应的,没有自由度的分量当然也就没有相应的旋转角度。

  自由度的概念很好理解,人并不是所有的骨头都有三个旋转自由度的,也就是并不是全部都能围绕三个方向旋转。比如你的头有三个旋转自由度,因为头是能够上下左右前后旋转的,但是你试试你的hand看能不能以手臂为轴围绕其旋转,所以这里head有三个自由度然而hand只有两个,其余道理类似。

  为了保留以上信息以及方便后续计算,我一共设计了三个类,分别是:骨骼类(Bone),骨架类(Skeleton),运动类(Motion),很显然这里我们并不需要提取出一个抽象基类出,也无需用到继承和多态等面向对象机制,相反,它们之间是组合的关系。Skeleton由Bone组成,Motion则由一帧帧的Skeleton组成,具体是以成员变量的形式来实现的。代码如下:

//定义骨骼类
class Bone
{
public: Bone();                       //默认构造函数public:VECTOR  direc;               //骨骼初始方向,从父节点到该节点,全局坐标系下的VECTOR  local_coord;          //在其父节点的“初始局部坐标系”下的局部坐标,辅助计算VECTOR  local_coord_axis;     //绑定于该节点的“初始局部坐标系”,重要VECTOR  global_coord;         //*全局坐标,绘制时真正用到的就是这个。mutable VECTOR  aligned_coord;//新增,全局坐标经过“空间对齐”之后的坐标;这里居然用到mutable了。vector<int> child_id;         //该骨骼的儿子节点id,但可能有多个儿子string name;                 //骨骼名称double length;                //骨骼长度double weight;                //*新增。权重值,越靠近根节点权值越大。double dof[3];                //amc文件里面的旋转角度,用以后续坐标的计算int dofflag[3];               //自由度标记数组,用以辅助amc存储int id;                        //骨骼idint father_id;                //该骨骼的父亲节点id,只可能有一个父亲};

//定义骨架类。
class Skeleton
{
public:Skeleton():bone_num(0){};       //构造函数   void set_weight();              //给每一个骨节点设置权重值,离根节点越近越“重”void cal_local_coord();         //计算全部骨节点在其父节点的“初始局部坐标系”下的局部坐标void cal_global_coord();        //*计算全部骨节点的世界坐标public:Bone bones[MAX_BONE_NUM];      //核心数据结构,用以存储骨架骨骼数据,通过对象直接访问map<string,int> name_to_id;    //还是定义为类的成员变量,用于绑定骨骼名字和id的int bone_num;                   //骨骼总数目。};

最后是Motion类:

//定义运动类。
class Motion
{
public:Motion();                                               //构造函数 void Read_ASF(string filename);                          //读取.ASF文件内容。void Read_AMC(string filename);                        //读取.AMC文件内容,存储第N帧数据  void cal_position();                                   //计算每一帧骨架坐标信息private:Skeleton skeletons[5000];                              //难怪之前超过5000帧就不行了~~o(>_<)o ~int total_frame_num;                                   //该运动片段总帧数。//friend可放在任意位置,不受private或者public影响friend void render_text();friend void render_frame();friend void reshape(int w,int h);friend void idle();friend void specialkey(int key,int x,int y);};

2.全局坐标的计算(重点)

  要想正确的绘制出最终的动画效果,就要正确的绘制出每一帧;而要想正确的绘制出每一帧,就要正确的计算出各个骨节点的全局坐标。那么现在的问题就归结为如何算出每一帧各个骨节点的全局坐标了。这个过程大体而言分为两步:求解任一骨节点在其父节点的局部坐标系下的局部坐标;经过旋转和平移变换,求解其在世界坐标系下的全局坐标。先看第一步,局部坐标的求解:

1.每个骨骼由两个骨节点组成,起点和终点。在每块骨骼的起点处绑定了一个“局部坐标系”;

2.“局部坐标系”是由“原始坐标系”旋转变换而来,旋转角度即ASF文件中的axis分量。所有骨节点的“原始坐标系”均平行于“世界坐标系”;

3.ASF文件中的direction是从该节点的父节点指向该节点本身的,且这个方向是“原始坐标系”意义下的向量,要求在“局部坐标系”下的局部坐标需要进行旋转变换。

  基于以上三点,我们就可以求解任何一个骨节点在其父节点的局部坐标系下的局部坐标了,代码如下:

//计算全部骨骼在其“初始局部坐标系”下的局部坐标,之后的变换均围绕它来进行。
void Skeleton::cal_local_coord()
{//1、初始时刻,每块骨骼的“起点”处绑定一个与世界坐标系平行的“原始局部坐标系”;//我们首先计算每块骨骼在“原始局部坐标系”下的原始局部坐标向量;//注1:每个骨骼都有方向;每个骨骼的初始局部坐标系位于该骨骼的起点处骨节点。//注2:这样一来,部分骨节点上可能绑定多个局部坐标系,不过这并不会带来问题。for(int i=1;i<=bone_num;i++)  {bones[i].local_coord.x=bones[i].length*bones[i].direc.x;bones[i].local_coord.y=bones[i].length*bones[i].direc.y;bones[i].local_coord.z=bones[i].length*bones[i].direc.z;}//2、随后,原始局部坐标系经过——旋转——成为该骨骼的“初始局部坐标系”;//接下来我们计算各个骨骼在“初始局部坐标系”下的坐标,由于原始局部坐标系依次绕z-y-x轴旋转而形成初始局部坐标系//要计算初始局部坐标向量,就将原始局部坐标向量按照“相同顺序”“反向”旋转即可得到。//这实质上是一个:向量不变,坐标系旋转,然后计算原向量在新坐标系下的新向量的几何问题。“逐步跟进”策略for(int i=1;i<=bone_num;i++){//注意:1、角度前面加负号;2、旋转顺序应为:z-y-x.bones[i].local_coord=Matrix_RotateZ(-bones[i].local_coord_axis.z,bones[i].local_coord);  bones[i].local_coord=Matrix_RotateY(-bones[i].local_coord_axis.y,bones[i].local_coord);bones[i].local_coord=Matrix_RotateX(-bones[i].local_coord_axis.x,bones[i].local_coord);  }
}

  局部坐标算出来之后,剩下的就是逐步向上追溯至根节点,通过一系列的旋转和迁移变换至世界坐标系下,得到全局坐标。这里重点就是把握两种变换:旋转变换迁移变换。每一次向上追溯都会依次进行这两种变换。我们在这里将所有的局部坐标系都理解为固定不变的,亦即都跟初始时一样,后续的旋转均是旋转的骨节点,坐标系不变,骨节点绕其旋转那么坐标当然会改变,这就是旋转变换的过程。

  而迁移变换的过程要复杂一些,它的目的是很明确的,就是逐步将每一个骨节点向根节点迁移:首先将当前节点在父节点的局部坐标系下的局部坐标迁移至其爷爷节点的局部坐标系下的局部坐标,...依次向上直至根节点。

  一图胜千言:

  

  于是我们可以得到如下推导:

  

  注:这里的Xk当然是任一骨节点在其局部坐标系下的局部坐标,Rk为从原始坐标系(与世界坐标系平行)到局部坐标系的欧拉旋转角;Tk则是原始坐标系和世界坐标系之间的平移向量,因为这两者是平行的,所以只需要一个平移变换即可。细心的读者应该能够发现,这里的Tk - Tk-1其实就等于ASF文件中的direction点乘length,而Rk即是ASF文件中的axis分量,所以综上所述,ASF文件搞定迁移变换,AMC文件搞定旋转变换。

  具体实现步骤我在代码中注释的很详细了,代码如下:

//**非常重要。用以计算所有骨骼的全局坐标;后续绘制都是基于它的计算结果;其余计算均为了辅助它而存在。
void Skeleton::cal_global_coord()
{//1、首先计算各个节点在其初始局部坐标系下的局部坐标;//注:之后各个骨骼绕其初始局部坐标系旋转,假设各个骨骼的初始局部坐标系方向一直不变,这样以来它们之间的关系也不会变化。cal_local_coord();//2、在此坐标基础之上,对各个骨骼进行两种变换:1、“旋转变换”;2、“迁移变换”。for(int i=1;i<=bone_num;i++){//临时变量,用以存储每一块骨骼在其初始局部坐标系下的局部坐标。VECTOR temp=bones[i].local_coord;//对每一个骨骼进行计算,都要追溯到根节点int current_id=i;                          //当前骨骼idint father_id=bones[current_id].father_id;  //当前骨骼的父亲骨骼idwhile(current_id!=0)                        //id==0则表明已经追溯到父节点了{//1、“旋转变换”,围绕当前骨骼的“初始局部坐标系”旋转,旋转角度为.AMC文件中的数据,已经存储在bones[i].dof中//此时问题的实质变为:坐标系固定,向量围绕固定坐标系旋转求解新的向量坐标。//*注:这里遵循的旋转顺序是:x-y-z。temp=Matrix_RotateX(bones[current_id].dof[0],temp);temp=Matrix_RotateY(bones[current_id].dof[1],temp);temp=Matrix_RotateZ(bones[current_id].dof[2],temp);//2、“迁移变换”,将该骨骼在当前骨骼的初始局部坐标系下的局部坐标迁移至其父骨骼的初始局部坐标系下的坐标//注:迁移变换的理论依据:空间任意一点可以平等的在两套坐标系下转换到世界坐标系中去。//现在关键是要理清骨骼局部坐标到世界坐标的转换过程,初始局部坐标系下的局部坐标向量首先还原至在原始局部坐标系下//这是关键的步骤,之后进行平移变换即可转换至世界坐标系下。//问题可概括为两类:一类是在变换后的坐标系下的向量通过逆序还原到以前的坐标系下的向量;//第二类是:在原来的坐标系下的向量逐步顺序跟进到新的坐标系下的向量。//其实可以统一理解为逐步跟进。//===========================首先定义旋转矩阵==============================//定义绕X轴旋转矩阵:Matrix temp_current_1={0.0};Matrix temp_father_1={0.0};temp_current_1.Index[0][0]=1.0;temp_current_1.Index[1][1]=cos(bones[current_id].local_coord_axis.x);temp_current_1.Index[1][2]=-sin(bones[current_id].local_coord_axis.x);temp_current_1.Index[2][1]=sin(bones[current_id].local_coord_axis.x);temp_current_1.Index[2][2]=cos(bones[current_id].local_coord_axis.x);temp_father_1.Index[0][0]=1.0;temp_father_1.Index[1][1]=cos(bones[father_id].local_coord_axis.x);temp_father_1.Index[1][2]=-sin(bones[father_id].local_coord_axis.x);temp_father_1.Index[2][1]=sin(bones[father_id].local_coord_axis.x);temp_father_1.Index[2][2]=cos(bones[father_id].local_coord_axis.x);///定义绕Y轴旋转矩阵:Matrix temp_current_2={0.0};Matrix temp_father_2={0.0};temp_current_2.Index[0][0]=cos(bones[current_id].local_coord_axis.y);temp_current_2.Index[0][2]=sin(bones[current_id].local_coord_axis.y);temp_current_2.Index[1][1]=1.0;temp_current_2.Index[2][0]=-sin(bones[current_id].local_coord_axis.y);temp_current_2.Index[2][2]=cos(bones[current_id].local_coord_axis.y);temp_father_2.Index[0][0]=cos(bones[father_id].local_coord_axis.y);temp_father_2.Index[0][2]=sin(bones[father_id].local_coord_axis.y);temp_father_2.Index[1][1]=1.0;temp_father_2.Index[2][0]=-sin(bones[father_id].local_coord_axis.y);temp_father_2.Index[2][2]=cos(bones[father_id].local_coord_axis.y);///定义绕Z轴旋转矩阵:Matrix temp_current_3={0.0};Matrix temp_father_3={0.0};temp_current_3.Index[0][0]=cos(bones[current_id].local_coord_axis.z);temp_current_3.Index[0][1]=-sin(bones[current_id].local_coord_axis.z);temp_current_3.Index[1][0]=sin(bones[current_id].local_coord_axis.z);temp_current_3.Index[1][1]=cos(bones[current_id].local_coord_axis.z);temp_current_3.Index[2][2]=1.0;temp_father_3.Index[0][0]=cos(bones[father_id].local_coord_axis.z);temp_father_3.Index[0][1]=-sin(bones[father_id].local_coord_axis.z);temp_father_3.Index[1][0]=sin(bones[father_id].local_coord_axis.z);temp_father_3.Index[1][1]=cos(bones[father_id].local_coord_axis.z);temp_father_3.Index[2][2]=1.0;//==============旋转矩阵定义完毕,以下将其相乘构成组合旋转矩阵==============//这里的顺序仍然很重要Matrix rotate_matrix_current=MatrixMult(temp_current_2,temp_current_1);rotate_matrix_current=MatrixMult(temp_current_3,rotate_matrix_current);Matrix rotate_matrix_father=MatrixMult(temp_father_2,temp_father_1);rotate_matrix_father=MatrixMult(temp_father_3,rotate_matrix_father);//===========================组合旋转矩阵定义完毕。==========================//正式开始迁移://迁移步骤一:temp=MatrixMultVec(rotate_matrix_current,temp);//迁移步骤二://首先定义迁移平移向量,是基于原始局部坐标系下的。VECTOR temp_vec;temp_vec.x=bones[father_id].length*bones[father_id].direc.x;temp_vec.y=bones[father_id].length*bones[father_id].direc.y;temp_vec.z=bones[father_id].length*bones[father_id].direc.z;temp.x+=temp_vec.x;temp.y+=temp_vec.y;temp.z+=temp_vec.z;//迁移步骤三://这里就只用调用一次矩阵求逆,避免之前的四次调用,提高程序效率。temp=MatrixMultVec(MatrixInverse(rotate_matrix_father),temp);//以上,实现将当前骨骼的局部坐标迁移至其父骨骼的局部坐标系下。//最后更新临时变量:当前骨骼->父亲骨骼;父亲骨骼—>爷爷骨骼。current_id=father_id;father_id=bones[current_id].father_id;}   //End of while//此时,current_id=0,即表示它的父亲是“根节点”了。此时已经将其迁移至根节点初始局部坐标系下的局部坐标了。//接下来完成最后一步,将其旋转、迁移至世界坐标系下,这一步是平凡的。//整个骨架的“旋转变换”,依旧遵循X-Y-Z的顺序进行。temp=Matrix_RotateX(bones[0].dof[0],temp);temp=Matrix_RotateY(bones[0].dof[1],temp);temp=Matrix_RotateZ(bones[0].dof[2],temp);//整个骨架的“平移变换”。temp.x+=bones[0].global_coord.x;temp.y+=bones[0].global_coord.y;temp.z+=bones[0].global_coord.z;//至此,两种变换完毕,更新全局坐标bones[i].global_coord=temp;}//End of for() 计算完毕!}

3.绘制结果展示

Skeleton骨架绘制出来是这样的:

以jump运动为例绘制出来的效果是这样的:(由于插入视频不方便,就放几个关键帧的截图展示一下最终的效果)

起跳:

最高点:

落地:

  至此,我们就成功的实现了从静态的文本运动数据到动态的三维人物动画的转换,万里长征走完了第一步,欲知后续,且听下回分解吧。刚统计了下,我个人实现的整个代码量在1200行左右,是用C++结合OpenGL在Visual Studio 2010平台下实现的。关键代码其实都已经放在前面了,如若需要完整的实现代码可E-Mail联系本人:floristt#126.com ,后续我也会将项目代码上传到个人的GitHub上面去。

转载于:https://www.cnblogs.com/floristt/p/4720080.html

三维人体运动合成系列之一:运动捕捉数据ASF/AMC文件解析与绘制相关推荐

  1. 【深度学习】深度学习三维人体建模最新论文、资源、数据、代码整理分享

    三维人体建模作为计算机人体仿真的一个组成部分,一直是人们研究的热点之一.自交互式计算机图形学诞生之日起,就有学者不断探索计算机人体建模技术.从线框建模.实体建模.曲面建模发展到基于物理的建模,已取得重 ...

  2. 深度学习三维人体建模最新论文、资源、数据、代码整理分享

    三维人体建模作为计算机人体仿真的一个组成部分,一直是人们研究的热点之一.自交互式计算机图形学诞生之日起,就有学者不断探索计算机人体建模技术.从线框建模.实体建模.曲面建模发展到基于物理的建模,已取得重 ...

  3. Android官方开发文档Training系列课程中文版:数据存储之文件存储

    原文地址:http://android.xsoftlab.net/training/basics/data-storage/files.html Android使用的文件系统和其它平台的磁碟式文件系统 ...

  4. python数据处理系列之读写csv数据

    python数据处理系列之读写csv数据 导入csv文件 pandas中导入csv数据用的方法是read_csv() import pandas as pd df = pd.read_csv(r'c: ...

  5. 用于三维人体运动跟踪的架构

    本周还了解了一种新的用于三维人体运动跟踪的框架,该框架采用随机采样和局部最优化的方法来取得鲁棒性和有效性的较好折中,引入了基于模拟人体运动的局部最优化方法来提高跟踪的优越性. 本框架在对三维人体进行建 ...

  6. 运动合成——机器学习技术

    参考文献:<人体运动合成中的机器学习技术合成综述> 根据机器学习的用途分类,在图形学中使用到的大致如下: 1>    回归和函数逼近.回归是一种插值技术,分析已知数据点来合成新的数据 ...

  7. 运动捕捉数据的描述ASF/AMC

    运动捕捉数据有多种格式:ASF/AMC,BVH,C3D等,这三个是比较常用的,一般的matlab实验用的是ASF/AMC,其次就是BVH. ASF/AMC文件格式是Acclaim Games公司设计开 ...

  8. boid模型的Matlab程序,动物集群运动行为模型系列之五-—本科毕业设计.doc

    动物集群运动行为模型系列之五--本科毕业设计 动物集群运动模型 摘要 本文主要模拟了鱼群的集群运动.鱼群躲避捕食者追捕的运动情况以及鸟群觅食运动的模拟,以此研究动物个体间的信息传递机制,同时也是对群体 ...

  9. 新品周刊 | LINE FRIENDS李宁推出联名系列;FILA推出全新高级运动羽绒系列

    LINE FRIENDS.FILA斐乐.泰国Mahanakhon.阿迪达斯.宇舶表.卡西欧发布新品. 服饰 LINE FRIENDS李宁联名系列 LINE FRIENDS携手李宁发布联名系列服饰.LI ...

最新文章

  1. 服务器处理 json 数据
  2. 青海省计算机应用能力考试,青海省2015年职称计算机应用能力考试西宁考区四月份考试安排通知...
  3. 一点通路由器模拟软件最新版_2019年高压电工作业考试最新版题库及答案(全部判断题)...
  4. HTTP 错误 404.15 - Not Found请求筛选模块被配置为拒绝包含的查询字符串过长的请求...
  5. 十分钟了解分布式计算:Petuum
  6. 数据丢包怎么修复_交换机发生网络通信故障怎么解决?
  7. linux cgi命令,Linux之cgi实现系统主机监控
  8. Quartus II 8.1 详解--有图---图片详解 【1讲】
  9. 物理层接口特性、数据通信模型、物理层基本概念(数据、信号、码元 、信源、信道、信宿 、速率、波特、带宽)...
  10. 移动端html头部meta标签的含义
  11. 高一计算机专业班主任工作总结,07计算机5班班主任工作总结
  12. hodj 1008 Elevator (模拟题)
  13. 用vector实现通用堆栈的类模板
  14. python sanic_Python Web框架Sanic Streaming – 流式传输
  15. android 获取邮箱账号,android获取google邮箱
  16. linux如何破解密码
  17. xHCI1.1-TRB Ring
  18. Saas平台级架构:统一身份认证管理系统用户权限管理
  19. windows文件共享客户端的一些事——samba文件共享
  20. elementUI 表格宽度自适应、不换行

热门文章

  1. 名悦集团:那些容易伤人又伤车的汽车饰品,你还在用吗
  2. 宜人贷第四季度净营收12.71亿元 同比下滑30%
  3. 如何快速批量转换图片格式?
  4. Connection reset by peer 的解决
  5. 批量打印CAD文件怎么做?来试试这种方法
  6. linux退出热键_linux常用的28个快捷键
  7. python基础语法-温度转换
  8. MM01创建提示:公司代码 XXXX 不存在或没有完全维护
  9. 电动汽车频起火,锂电池能量密度跃进就是一场赌博
  10. 中科院院士怒怼“新型”石墨烯电池:是黑科技还是韭菜收割机?