卡通驱动项目ThreeDPoseTracker——模型驱动解析

前言

之前解析过ThreeDPoseTracker这个项目中的深度学习模型,公众号有兄弟私信一些问题,我刚好对这个项目实现有兴趣,就分析一波源码,顺便把问题解答一下。

这个源码其实包括很多内容:3D姿态估计,坐标平滑,骨骼驱动,物理仿真等,非常值得分析。

参考博客:
ThreeDPoseTracker源码

理论与实现

核心代码是源码中的VNectModel.cs,主要是用预测出的3D坐标驱动卡通人体模型,包括内容有:

  • 根关节位置
  • 各关节旋转信息

其核心在于旋转量的确定,至于根关节位置的确定,感觉涉及到很多乱七八糟的内置参数,就不详细介绍了,但是会额外提供一个我用到天荒地老的计算方法。

如果下面的理论看不懂,推荐看看我按照源码复现的一套简化流程,一千行源码直接重写成两百多行

预备知识——“LookRotation”

源码中有个至关重要的函数LookRotation(a,b),它的作用是:

  • 使得z轴(蓝色)始终精准指向a方向
  • 使得y轴(绿色)始终偏向b方向

为什么一个是精准指向,一个是偏向,因为yz轴是垂直的,如果ab不垂直,那么此函数就会保证za同方向,yb大致同向,看图

在这里插入图片描述

正方体为物体,绿色和蓝色分别为yz轴,两个小球分别为yz的目标方向。

左图为标准的指向,中间和右图为调整了目标方向后,物体的yz的指向,可以发现,蓝色z轴始终指向目标,但是绿色y轴是偏向那个方向,因为是立体图,看着绿轴偏的很远,其实是差不多的。

总而言之,蓝轴始终是向着oa方向,绿轴向着oboa组成的平面中与oa垂直的方向。

驱动问题解读

如果扫过一眼代码,会发现有很多重复代码,无外乎以下几类:

初始化Init()的时候有:

AddSkeleton(xx,yy)
xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy));
xx.InverseRotation = xx.Inverse*xx.InitRotation

驱动PoseUpdate的时候有:

xx = TriangleNormal(aa,bb,cc)
xx.rotation = Quaternion.LookRotation(yy) * xx.InverseRotation;

会发现初始化和驱动时候貌似是一种反向计算,所以才出现这么多逆(inverse)。

为什么要用LookRotation计算各个关节的旋转,而非对某根骨骼直接通过Quaternion.FromToRotation(a,b)计算出初始姿态到新的姿态下需要的旋转矩阵呢?

  • 如果只用FromToRotation计算向量到向量的旋转,只能控制位置正确,无法控制方向正确,比如根关节到颈部是直着上去的,这时候人也可以侧身也可以面向前方,所以对关节必须控制至少两个方向的旋转,因此必须使用LookRotation去控制zy轴朝向。

为什么用了LookRotation还要求这么多逆(inverse)?

  • 同一个模型的不同关节具有不同的初始旋转量(即InitRotation),而且不同人的同一个关节也可能具有不同的旋转量,甚至初始的局部坐标轴都不同,比如源码中提供的两个模型的膝盖部分局部坐标系如下

在这里插入图片描述

此时如果用LookRotation,不同的人就需要设置对应的规则,比如同一个姿态,左边的人蓝色z轴向后,右边人蓝色z轴向前,搞不好还有其它的情况,此时就无法用基于LookRotation的同一套代码去驱动这个人了。

如果还是不懂为什么不能用同一套代码驱动,可以举个例子:小腿向后收起的时候,左图的LookRotation必须保证蓝轴向上,右图必须保证蓝轴向下,如下图:

在这里插入图片描述

由于坐标轴最终的方向都不同,所以即使是同一个姿势,对于具有不同坐标系的相同关节也需要针对性LookRotation

那么为什么源码里面可以使用LookRotation去玩驱动,很简单,因为源码将所有的关节都利用初始姿态做了LookRotation的对齐,得到了一个中间矩阵,即源码中的xx.InverseRotation,利用这个中间矩阵就能在驱动的时候对齐所有坐标系,达到通用目的。

源码分析与复现

如何实现上述问题中的坐标系对齐呢?

利用初始姿态下各关节的坐标和旋转来确定,具体是:
当前关节的lookrotation=初始旋转InitRotation×对齐矩阵当前关节的lookrotation = 初始旋转InitRotation \times 对齐矩阵 当前关节的lookrotation=初始旋转InitRotation×对齐矩阵
所以源码中的下面类似代码就是为了求解对齐矩阵:

xx.Inverse = Quaternion.Inverse(Quaternion.LookRotation(xx.position - xxx.position, yy));
xx.InverseRotation = xx.Inverse*xx.InitRotation

看不懂就可以写成

Quaternion.LookRotation(xx.position - xxx.position, yy) = xx.InitRotation * Quaternion.Inverse(xx.InverseRotation)

这就对应上述公式,其中Quaternion.Inverse(xx.InverseRotation)就对应了对齐矩阵。

因此我在复现时候,以根关节的对齐矩阵为例,把上述代码改成了

root = animator.GetBoneTransform(HumanBodyBones.Hips);
midRoot = Quaternion.Inverse(root.rotation) * Quaternion.LookRotation(forward);

其中forward是按照源码的要求,指示人体的当前方向。

如何设置LookRotation的方向?

继续分析源码,发现对于所有关节都做了

var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Transform.position, jointPoints[PositionIndex.lThighBend.Int()].Transform.position, jointPoints[PositionIndex.rThighBend.Int()].Transform.position);
jointPoint.Inverse = GetInverse(jointPoint, jointPoint.Child, forward);jointPoint.InverseRotation = jointPoint.Inverse * jointPoint.InitRotation;

第一行,基于根关节和左右胯关节坐标计算出人体朝向,然后以此作为所有关节的LookRotationy方向,以及每个关节与其子关节的方向作为z方向,计算出中间矩阵。

注意,在接下来,分别对头部和手掌单独又计算了一遍,因为他俩比较特殊

对于头部,直接求解出头到鼻子的向量作为LookRotationz方向,未设置y方向。

var gaze = jointPoints[PositionIndex.Nose.Int()].Transform.position - jointPoints[PositionIndex.head.Int()].Transform.position; // head的方向是head->Nose
head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));

然后计算头部的中间矩阵

head.Inverse = Quaternion.Inverse(Quaternion.LookRotation(gaze));head.InverseRotation = head.Inverse * head.InitRotation;

对于手腕,直接利用手腕、大拇指、中指的坐标,计算出手掌方向作为LookRotationy方向,

var lf = TriangleNormal(lHand.Pos3D, jointPoints[PositionIndex.lMid1.Int()].Pos3D, jointPoints[PositionIndex.lThumb2.Int()].Pos3D); // 手掌方向
var rf = TriangleNormal(rHand.Pos3D, jointPoints[PositionIndex.rThumb2.Int()].Pos3D, jointPoints[PositionIndex.rMid1.Int()].Pos3D);

而左手腕以大拇指到中指的方向为z方向,而右手腕以中指到大拇指方向为z方向:

lHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.lThumb2.Int()].Transform.position - jointPoints[PositionIndex.lMid1.Int()].Transform.position, lf));
rHand.Inverse = Quaternion.Inverse(Quaternion.LookRotation(jointPoints[PositionIndex.rThumb2.Int()].Transform.position - jointPoints[PositionIndex.rMid1.Int()].Transform.position, rf));

再分别求解出中间矩阵:

lHand.InverseRotation = lHand.Inverse * lHand.InitRotation;
rHand.InverseRotation = rHand.Inverse * rHand.InitRotation;

其实完全没必要区分这么明显,只需要求解和使用的时候对应好就行了,比如我实现的时候就统一大拇指到中指:

midLhand = Quaternion.Inverse(lhand.rotation) * Quaternion.LookRotation(lthumb2.position - lmid1.position,TriangleNormal(lhand.position, lthumb2.position, lmid1.position));
midRhand = Quaternion.Inverse(rhand.rotation) * Quaternion.LookRotation(rthumb2.position - rmid1.position,TriangleNormal(rhand.position, rthumb2.position, rmid1.position));

也就是说,对于某些特定关节,需要单独设置用于计算中间变换矩阵的LookRotation信息。推荐看我实现的源码,分为躯干、头、手掌三个部分,我实现的源码就不贴了,文末找。

注意这里计算初始姿态中各关节的LookRotation方法与运行时从深度学习预测的3D关节坐标中计算的LookRotation方案要一模一样。

如何驱动?

通过
当前关节的lookrotation=初始旋转InitRotation×对齐矩阵当前关节的lookrotation = 初始旋转InitRotation \times 对齐矩阵 当前关节的lookrotation=初始旋转InitRotation×对齐矩阵
得到了每个关节的对齐矩阵,那么这个公式很容易得到每个关节的当前旋转信息:
当前旋转Rotation=当前关节的lookrotation×Quaternion.Inverse(对齐矩阵)当前旋转Rotation = 当前关节的lookrotation \times Quaternion.Inverse(对齐矩阵) 当前旋转Rotation=当前关节的lookrotation×Quaternion.Inverse(对齐矩阵)
然后分析源码,在PoseUpdate()函数中,前面的不用看,是计算根关节坐标的,我们先关注关节旋转。

注意因为用对齐矩阵是从初始姿态获取的,所以如何依据初始姿态计算的lookrotation就要按照同样的方法从深度学习模型预测的3D关节坐标中计算对应的lookrotation参数。

比如人体方向依旧是根、左右胯部的坐标计算:

var forward = TriangleNormal(jointPoints[PositionIndex.hip.Int()].Pos3D, jointPoints[PositionIndex.lThighBend.Int()].Pos3D, jointPoints[PositionIndex.rThighBend.Int()].Pos3D);

而根关节当前的旋转就是根据上述公式计算得到:

jointPoints[PositionIndex.hip.Int()].Transform.rotation = Quaternion.LookRotation(forward) * jointPoints[PositionIndex.hip.Int()].InverseRotation;

其它关节我不贴源码了,直接描述:

躯干关节:以身体方向为LookRotationy方向,以当前关节到其子关节的方向为z方向。

手腕:以手腕、大拇指、中指形成的平面的法线方向为y方向,以拇指到中指的方向为z方向。

比如我随便贴一下我复现的左臂(肩、肘、腕)实时驱动:

// 左臂
lshoulder.rotation = Quaternion.LookRotation(pred3D[5] - pred3D[6], forward) * Quaternion.Inverse(midLshoulder);
lelbow.rotation = Quaternion.LookRotation(pred3D[6] - pred3D[7], forward) * Quaternion.Inverse(midLelbow);
lhand.rotation = Quaternion.LookRotation(pred3D[8] - pred3D[9],TriangleNormal(pred3D[7], pred3D[8], pred3D[9]))*Quaternion.Inverse(midLhand);

其中pred3D就是深度学习模型预测的3D关节坐标。

人体位置

上述讲解了旋转的计算方法,关于整个人体的位置,源码中自有一套方案,但是里面预设了很多固定参数,不是特别想分析,所以用了万年不变的方法,计算unity人物模型腿的长度和深度学习预测的腿部长度,然后计算比例系数,乘到深度学习预测的根关节位置即可。

float tallShin = (Vector3.Distance(pred3D[16], pred3D[17]) + Vector3.Distance(pred3D[20], pred3D[21]))/2.0f;
float tallThigh = (Vector3.Distance(pred3D[15], pred3D[16]) + Vector3.Distance(pred3D[19], pred3D[20]))/2.0f;
float tallUnity = (Vector3.Distance(lhip.position, lknee.position) + Vector3.Distance(lknee.position, lfoot.position)) / 2.0f +(Vector3.Distance(rhip.position, rknee.position) + Vector3.Distance(rknee.position, rfoot.position));
root.position = pred3D[24] * (tallUnity/(tallThigh+tallShin));

是不是超级简单,虽然效果有点偏差,但是后续还是会分析一下源码中更新人体位置的方案。

复现流程

VNectModel.cs中的PoseUpdate函数加入以下代码:

FileStream fs = new FileStream(@"D:\code\Unity\ThreeDExperiment\Assets\Resources\record.txt", FileMode.Append);
StreamWriter sw = new StreamWriter(fs);
//写入
foreach(JointPoint jointPoint in jointPoints)
{sw.Write(jointPoint.Pos3D.x.ToString() + " " + jointPoint.Pos3D.y.ToString() + " " + jointPoint.Pos3D.z.ToString() + " ");
}        
sw.WriteLine();
sw.Flush();
sw.Close();
fs.Close();

将关键点写入到txt中做复现时候用的3D关键点数据

然后按照理论进行复现后效果如下:

在这里插入图片描述

红色为预测的3D坐标,人物模型会做出与红色骨架一样的姿势。

结论

这个感觉还是没有考虑人体运动的动力学特性,如果跑过源码,很容易发现个别姿势会出现奇怪的关节扭曲现象,这就是不考虑动力学的后果,给我自己之前的代码打一波广告,那个绝对比这个好,哈哈。

完整的unity实现放在微信公众号的简介中描述的github中,有兴趣可以去找找。或者在公众号回复“ThreeDPose",同时文章也同步到微信公众号中,有疑问或者兴趣欢迎公众号私信。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/246556.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

卡通驱动项目ThreeDPoseTracker——关键点平滑方案解析

前言 之前对ThreeDPoseTracker的深度学习模型和unity中的驱动方法进行过解析,还有一个比较重要的就是从深度学习模型出来的3D关键点数据会有抖动,在ThreeDPoseTracker源码中有做两次平滑,一部分是卡尔曼滤波,还有一部分是低通滤波…

卡通角色表情驱动系列一

前言 分析完ThreeDPoseTracker来做卡通角色的身体驱动,接下来在卡通驱动领域还有一个是表情驱动。对这个真的是一窍不通啊,只能慢慢看论文了。 国际惯例,参考博客/论文: 《Landmark-guided deformation transfer of template f…

opencv相机标定和人头姿态估计案例

前言 头部驱动除了之前关注的表情驱动外,还有眼球驱动和头部方向驱动。本博客基于opencv官方文档和部分开源代码来研究如何基于人脸关键点获取头部的朝向。 国际惯例,参考博客: opencv:Camera Calibration and 3D Reconstruction opencv:…

卡通角色表情驱动系列二

前言 之前介绍了使用传统算法求解BS系数的表情驱动方法,其中提到过的三种方法之一是基于网格形变迁移做的,那么这篇文章就是对《Deformation Transfer for Triangle Meshes》做表情驱动的解析。 国际惯例,参考博客: 论文原文《…

UE自带重定向原理

UE自带重定向方法验证 核心源码在VS的解决方案中的位置: UE4\Source\Developer\AssetTools\Private\AssetTypeActions\AnimSequence.cpp中第3237行RemapTracksToNewSkeleton函数 跳转方法 AssetTypeActions_AnimationAsset.cpp的RetargetNonSkeletonAnimationHa…

【caffe-Windows】caffe+VS2013+Windows无GPU快速配置教程

前言 首先来一波地址: happynear大神的第三方caffe:http://blog.csdn.net/happynear/article/details/45372231 Neil Z大神的第三方caffe:https://initialneil.wordpress.com/2015/01/11/build-caffe-in-windows-with-visual-studio-2013-…

【caffe-Windows】caffe+VS2013+Windows+GPU配置+cifar使用

前言 国际惯例,先来波地址: CUDA WIN7:链接:http://pan.baidu.com/s/1nvyA3Qp 密码:h0f3 官方网址:https://developer.nvidia.com/cuda-toolkit CUDA WIN10:链接:http://pan.baidu.com/s/1…

【一些网站的收集】包含机器学习深度学习大牛主页等

数学概念部分 旋转矩阵、欧拉角、四元数的比较 欧拉角和四元数的表示 四元数与旋转 B样条曲线 非常好的概率统计学习的主页 误差方差偏差 编程语言学习 C#编程视频 OpenGL编程NeHe OpenGL官网 OpenGL“我叫MT“纯手工3D动画制作之1——基础介绍 【强大】非常好的Op…

Eureka源码分析

Eureka源码分析 Eureka server入口: Spring.factories PS: 意味着如果加载EurekaServerAutoConfiguration成功,需要 ConditionalOnBean(EurekaServerMarkerConfiguration.Marker.class)需加载成功. 通过Bean注入了很多类 本质上, eureka-server包含很多事件: EurekaInstanceC…

matlab程序中,如何解决矢量长度必须相同的问题

主要原因就是画图的x和y长度不一样,我用一个例子说明。 问题代码: clear all;close all;clc;x 0 : 1: 9;y sin(x);n 2*length(x);yi interpft(y, n);xi 0 : 0.5 : 10;hold on ;plot(x, y ,ro);plot(xi, yi, b.-);plot(x, sin(x),m--);legend(原始…

matlab 功率谱分析函数psd用法

psd简介 PSD(power spectrum analysis)功率谱分析,PSD在给定频带上的积分计算信号在该频带上的平均功率。与均值-平方谱相反,这个光谱中的峰值并没有反映出给定频率的能量。 单边PSD包含了信号的总功率在频率间隔从DC到一半的奈奎斯特速率。双侧PSD包含…

linux tar (打包、压缩、解压)命令

打包程序:tar c: 创建文档t: 列出存档内容x:提取存档f: filename 要操作的文档名v:详细信息 一:打包 打包:是指把文件整合在一起,不压缩 1.将文件打包:tar cf a.tar…

虚拟机添加硬盘扩容

1.设置→添加→硬盘 2.选择磁盘类型 3.开启虚拟机 4.用ls 命令查看:ls /dev/sd* 5.最后就可以对sdb进行分区操作 这里好麻烦,等我有空,在补上! . . .

利用matlab将三维数据画成三维立体图

首先先分析对象。将数据利用matlab画出图,最开始是导入数据,然后处理数据,最后将处理的数据画出来。 所以我将它分为三个步骤。 第一步:导入数据 如果是mat数据。可以直接load如果是txt数据。可以用txtread如果是excel数据。可…

世界坐标、相机坐标、图像坐标、像素坐标的原理、关系,并用matlab仿真

世界坐标、相机坐标、图像坐标、像素坐标的原理、关系,并用matlab仿真 照相机是日常生活中最常见的。它能把三维的空间图片等比例缩小投影在照片上,称为一个二维图像。 以下我们就讲一讲原理,并相应的进行matlab仿真。 在学之前&#xff0…

matlab 三维高程根据图片颜色给对应点赋予颜色

目录 1. 问题分析 2. 技术分析 3. 程序代码 4. 代码运行结果 1. 问题分析 日常工作尤其是测绘、地质、遥感行业,需要画DEM模型,并在这个模型的基础上,进行着色、渲染。比如,地质分析地面三维地表形变之时,需要根据D…

matlab 计算N天前(后)的日期

注意时间的格式:是字符串、数字还是日期? 下面是计算明天、今天、昨天的日期。 day1 datetime(datestr(now,yyyy-mm-dd))caldays(1)%tomorrowday0 datetime(datestr(now,yyyy-mm-dd))%todayday_1 datetime(datestr(now,yyyy-mm-dd))-caldays(1)%yest…

CAT arguments dimensions are not consistent.CAT参数的维度不一致。

错误实例: 在写符号矩阵的时候常常会出现下面错误: 错误:CAT arguments dimensions are not consistent. CAT参数的维度不一致。 举个栗子: clear; close all; clc; syms A_0 B_0 B C D E F G H ;T_3 [2*A_0 C-D*1i H G*1i;C…

传感器尺寸、像素、DPI分辨率、英寸、毫米的关系

虽然网上有很多这种资料,但是太过于复杂,每个人的说法都不一样,看的让人云里雾里的,我总结了一下,不知道对不对! 1. 1英寸25.4mm 2. 传感器尺寸:传感器的尺寸是指传感器的大小,一般…

利用PS将n张图制作成动态GIF图

第一步:打开PS,导入图片,文件→脚本→将文件载入堆栈… 数据量大的话,就耐心等待一下。 第二步: 创建祯动画 如果没有这个,可以点击窗口→时间轴 如果祯排列顺序反了,这样 最后按照自己要求设置祯动画时间&…