目录
一、前言
《功夫熊猫》、《忍者神龟》等一些大片想必大家都有了解过,里面的一个个建模角色能够出色的完成人类的动作,那么这些都是怎么实现的呢?今天我就和大家一起从零开始实现对一个视频中的人物的动作进行捕捉并将其添加到建模角色上,让建模出来的虚拟角色也可以像人类一样实现多种多样的行为动作,本次的项目实现起来有点麻烦,感兴趣的小伙伴可以一步步尝试下来,完成之后会学到很多东西呢。
二、项目介绍
1.目的
本次项目的名称为
3D运动捕捉
,不难猜到是需要对视频或者是摄像头中的
运动的人物的姿势进行抓取
并以3D的形式展现出来。简单来说就是让人物的动作能够被提取出来,方便后续的研究和观察,也可以用来进行虚拟视频的创作,给观众带来视觉上的刺激。
2.技术要点
(1)读取文件
既然是叫做3D运动捕捉的,那么肯定需要对视频或者摄像头中的人物姿势进行捕捉,第一步当然得需要读取文件,读取到的文件只要是
完整的人物运动视频
即可,因此如果
拍摄视频的摄像头是固定
的,那么将会极大地提高我们项目的准确率。
(2)标记关键节点
得到了视频文件,接下来就要
标记视频中人物的身体的各个关键节点部位
了,这对于Python中的第三方库
cvzone
来说就是小菜一碟。该库对人体的
33个关键节点
做了记录,读取视频后就能对人物的关键节点进行标记,通过节点坐标的变化我们就可以实现对人物运动姿势的把握了。
(3)读取每一帧图像中关键节点的坐标
我们已经得到了
33个关键节点的坐标
,每一帧图像中它们的值都会发生变化,因此我们需要将
每一帧图像中的坐标值
记录下来。记录下来的坐标值我们可以创建一个文件来进行存放,在后续的过程中将会使用到这些坐标点。具体的记录实现方式我将在下面的实现步骤中讲解,
希望大家能够耐住性子继续往下看!!
(4)Unity中创建3D角色
使用 Unity 创建一个简单的由
33个球体和几条线段组成的角色
,Unity 的下载和安装我就不在这里赘述了,大家可以在网上找找,一搜一大堆!!创建3D角色的步骤将会在下面的实现步骤中详细介绍。
(5)将人物动作3D化
主要是在
Unity 中使用 C# 脚本
将我们的坐标点的变化连接到创建的3D角色身上,其他的就是调整Unity 中的相机的一些参数使我们的3D形象更加合理
‘上镜’
即可。
3.需要做的准备
(1)第三方库
在本次项目中我们使用 Python 主要是完成人物运动的捕捉和关键节点的标记,并记录一下简单的坐标点的变化,所以只需要一些基础的视觉库即可:
import cv2
from cvzone.PoseModule import PoseDetector
在使用 cvzone 时我本人出现过一些问题并记录了下来,如果有小伙伴遇到同样的问题应该能简单的帮到你:
cvzone 中 PoseDetector 读取视频坐标信息问题
(2)Unity安装与使用
这个软件的下载和安装没太大的什么问题,我是使用
vx公众号 伙伴神
下载的,但它提供的2020版有问题,下载并安装2019版就可以了。至于使用的话现学现卖吧,反正也不是很难。
(3)C#了解
使用C#语言我们需要编写脚本来连接坐标点和3D模型,可能大家会觉得没学过啊、很难啊啥的,但不用怕,我们也不需要掌握全部,就简单使用几个语法,创建几个列表,能达到我们的目标就好。
三、项目实现步骤
1.Python 部分
我们本次的项目是需要结合多种工具才能完成的,利用 Python 我们可以得到视频文件中的
人物的运动状态并标记身上的33个关键节点
,随后
将这些关键节点的坐标值存储到一个文件
中。
(1)读取人物动作并标记关键节点
读取视频文件后需要对视频中的人物的身体上的关键节点进行标记,这里我们只要使用下面的函数
img = detector.findPose(img)#标记姿势关键节点
就可以标记出我们需要的坐标点了,如下:
但我们也仅仅是标记出了节点,并没有记录每一个节点的坐标值,所以我们接下来要
创建一个列表专门来存储每一帧图像中的关键点的坐标值,来方便我们后续将这些点保存到文件中,当然我们也可以打印我们存储的列表的长度,来判断我们的视频的帧数。
我们需要用到下面的函数来帮我们读取每一个坐标点的坐标值:
lmList, bboxInfo = detector.findPosition(img) #获取视频中的姿势信息
有了上面的步骤我们就能检测视频中人物的运动状态了,阶段性的代码如下(仅用来参考,总体代码文章后面会发):
cap = cv2.VideoCapture('Video.mp4')
detector = PoseDetector()
posList = []
while True:
success, img = cap.read()
img = detector.findPose(img)#标记姿势关键节点
lmList, bboxInfo = detector.findPosition(img) #获取视频中的姿势信息
if bboxInfo: #判断是否检测到一个身体
lmString = '' #创建一个空的字符串
for lm in lmList: #'lm'为'landmark'的缩写,即33个关键节点
lmString += f'{lm[1]},{img.shape[0] - lm[2]},{lm[3]},' #最后一个逗号保留,这样每一个坐标值才能分离开来
posList.append(lmString)
print(len(posList))#打印视频帧数
(2)将坐标值写入文件中保存
上面已经将我们的坐标值存储在了列表中,但为了后续的3D转换,我们需要将这些
坐标值存储到一个 .txt 文件中
。代码如下:
if key == ord('s'): #将坐标点写入一个 .txt文件
with open("AnimationFile.txt", 'w') as f:
f.writelines(["%s\n" % item for item in posList])
当我们在视频结束时按下 ‘s’,就会将我们得到的所有坐标点存储到 ‘AnimationFile.txt’ 文件中,通过前面的得到的帧数我们可以判断我们的文件中的坐标点存储情况。
比如,我们读取到的帧数如下:
然后得到的文件的数据行数如下:
比视频帧数少了几行,但可以接受,因为我们也没办法在视频的最后一帧处按下 ‘s’ 。
2.Unity 部分
使用 Python 实现对人物关键节点坐标值的记录后,我们需要将记录下来的那些坐标点应用到3D模型中去,这里就要使用到 Unity 来进行创建了,总体来说比较简单,大胆尝试就好。
(1)创建项目
首先我们需要
创建一个新的项目,然后在该项目中右键创建一个空白
,命名为 manager,这个空白文件夹是比较重要的,后面我们需要用到它来存储我们的一些 C# 脚本。
(2)创建空白来存储 3D 结构部位
要完成一个3D建模,我们得需要一些线条,球体等物件,而我们
创建的该空白文件夹就是用来存储这些部位的
,至于该文件夹的命名方式可以自己考虑,我这边是命名为 body ,用来存放我们的球体和线条。
(3)创建球体
我们需要33个球体来代表得到的33个关键节点,因此接下来应该要做的就是
右键点击 body 并创建一个 sphere, 在下方的空白处点击右键创建 material 将颜色选为自己喜欢的颜色,并拖动刚刚创建好的 material 到 body 下的 sphere 上,
这样就可以创建一个自定义颜色的球体了,如果需要的话还可以点击 sphere 并在右边展开的栏目中修改 size ,最后
复制 body 中的 sphere 并粘贴到 body 下,一共粘贴 32 次
即可,至于命名可以自己考虑。
(4)关键节点和模型的连接
我们需要将之前在 Python 部分得到的
关键节点坐标值文件复制到我们的项目下面,并创建一个 C# 脚本来关联,这个 C# 文件我们要拖到 manager 文件下进行管理。
上面两幅图就是我们创建的脚本文件以及添加到 manager 下的视图。
接下来我们需要
将 body 类的 size 改为 33 并将下面的 sphere 按顺序拖动到对应的对象中,然后我们编写 C# 脚本进行关联,
脚本代码我会放在文章后面部分。
(5)将分散的球体进行连接
通过前面的步骤,我们已经能够让那 33 个点完成简单的一些动作了,但我们还是尝试一下将那些分散的点连接起来,这样我们的项目才算比较完整。
首先我们需要在
body 下面创建一个空白并命名为 Lins
(因为在该空白下我们要用来存储其他的所有线条,因此命为复数形式),然后按照前面的创建 sphere 的步骤我们可以在 Lines 下面创建一个 line 并选择合适的材料。
接下来我们需要再次创建一个脚本,主要目的是
为每一个线条选择起点和终点
(其实就是每一个之前创建的球体),脚本的代码还是会发布在文章后面,通过该脚本我们就可以实现对线条的位置的设定。
该怎么连接这些线条呢?我们可以观察
mediapipe 中的人体节点
,结合下面这张图,我们就可以设置每一个线条的起点和终点:
然后我们复制之前的第一个线条,并粘贴到 Lines,接下来进行简单枯燥的拖动就可以了(
将对应的 sphere 拖动到线条的起点和终点上,一一对应即可
):
然后我们点击运行,应该就能看见我们的项目成功了。
四、总体代码
1.Python 代码
"""
Author:XiaoMa
CSDN Address:一马归一码
"""
import cv2
from cvzone.PoseModule import PoseDetector
cap = cv2.VideoCapture('Video.mp4')
detector = PoseDetector()
posList = []
while True:
success, img = cap.read()
img = detector.findPose(img)#标记姿势关键节点
lmList, bboxInfo = detector.findPosition(img) #获取视频中的姿势信息
if bboxInfo: #判断是否检测到一个身体
lmString = '' #创建一个空的字符串
for lm in lmList: #'lm'为'landmark'的缩写,即33个关键节点
lmString += f'{lm[1]},{img.shape[0] - lm[2]},{lm[3]},' #最后一个逗号保留,这样每一个坐标值才能分离开来
posList.append(lmString)
print(len(posList))
cv2.imshow("Image", img)
key = cv2.waitKey(1)
if key == ord('s'): #将坐标点写入一个文件
with open("AnimationFile.txt", 'w') as f:
f.writelines(["%s\n" % item for item in posList])
2.C# 脚本
(1)球体动画
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using System.Threading;
public class AnimationCode : MonoBehaviour
{
public GameObject[] Body;
List<string> lines;
int counter = 0;
void Start()
{
lines = System.IO.File.ReadLines("Assets/AnimationFile.txt").ToList();
}
void Update()
{
string[] points = lines[counter].Split(',');
for (int i =0; i<=32;i++)
{
float x = float.Parse(points[0 + (i * 3)]) / 100;
float y = float.Parse(points[1 + (i * 3)]) / 100;
float z = float.Parse(points[2 + (i * 3)]) / 300;
Body[i].transform.localPosition = new Vector3(x, y, z);
}
counter += 1;
if (counter == lines.Count) { counter = 0; }
Thread.Sleep(30);
}
}
(2)线条
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
public class LineCode : MonoBehaviour
{
LineRenderer lineRenderer;
public Transform origin;
public Transform destination;
void Start()
{
lineRenderer = GetComponent<LineRenderer>();
lineRenderer.startWidth = 0.1f;
lineRenderer.endWidth = 0.1f;
}
void Update()
{
lineRenderer.SetPosition(0, origin.position);
lineRenderer.SetPosition(1, destination.position);
}
}
五、结束语
本次项目的实现不算很难,尤其是 Python 部分,但复现起来较为繁琐,一步步地跟着做应该没啥问题。合抱之木,生于毫末,加油!