项目需要通过动捕数据实时判定相关动作组的算法。
最开始使用判定相似度的方式(欧氏距离,也是忽略了时间问题),导致效果非常的不理想。
后来换了点位判定的笨方法(就是预制好一些点位,让动捕人员去匹配那些点),虽然效果比之前的要好很多,但没有使用动捕的实时数据来判定。小伙伴也是查了一些相关的东西,这时候看到了它(DTW),决定试试。。。
参考视频:
/// <summary>
/// 定义一个距离公式
/// </summary>
/// <param name="x"></param>
/// <param name="y"></param>
/// <returns></returns>
float DisAbs(float x, float y)
{
return Mathf.Abs(x - y);
}
DTWData DTWFunc(int[] A, int[] B)
{
// 数据存放格式 [N, D]
// N 代表数据存放数目, D 代表数据特征维度
int N_A = A.Length;
int N_B = B.Length;
// 累计距离矩阵 D
float[,] D = new float[N_A, N_B];
D[0, 0] = DisAbs(A[0], B[0]);
// 左边一列
for (var i = 1; i < N_A; i++)
D[i, 0] = D[i - 1, 0] + DisAbs(A[i], B[0]);
// 最下一行
for (var j = 1; j < N_B; j++)
D[0, j] = D[0, j - 1] + DisAbs(A[0], B[j]);
// 中间部分
for (var i = 1; i < N_A; i++)
for (var j = 1; j < N_B; j++)
D[i, j] = DisAbs(A[i], B[j]) + Mathf.Min(D[i - 1, j], D[i, j - 1], D[i - 1, j - 1]);
// 路径回溯——找路径
// 统计配对点数量
int count = 0;
// 记录配对点的距离(距离累计)
float[] d = new float[Mathf.Max(N_A, N_B) * 3];
// 匹配点的位置坐标
List<Vector2> path = new List<Vector2>();
int k = N_A - 1;
int l = N_B - 1;
while (true)
{
// 保存当前路径点
if (k > 0 && l > 0)
{
path.Add(new Vector2(k, l));
// 寻找下一个点
float m = Mathf.Min(D[k - 1, l], D[k, l - 1], D[k - 1, l - 1]);
// 左下角的点
if (m == D[k - 1, l - 1])
{
d[count] = D[k, l] - D[k - 1, l - 1];
k = k - 1;
l = l - 1;
count = count + 1;
}
else if (m == D[k, l - 1])
{
d[count] = D[k, l] - D[k, l - 1];
l = l - 1;
count = count + 1;
}
else if (m == D[k - 1, l])
{
d[count] = D[k, l] - D[k - 1, l];
k = k - 1;
count = count + 1;
}
}
else if (k == 0 && l == 0)
{
path.Add(new Vector2(k, l));
d[count] = D[k, l];
count = count + 1;
break;
}
else if (k == 0)
{
path.Add(new Vector2(k, l));
d[count] = D[k, l] - D[k, l - 1];
l = l - 1;
count = count + 1;
}
else if (l == 0)
{
path.Add(new Vector2(k, l));
d[count] = D[k, l] - D[k - 1, l];
k = k - 1;
count = count + 1;
}
}
float sum = 0;
for (var i = 0; i < d.Length; i++)
sum += d[i];
// 距离均值
float mean = sum / count;
// 将位置数组进行倒序排列(因为查找时是按照从后向前查找)
path.Reverse();
return new DTWData(mean, path, D);
}
/// <summary>
/// DTW 的数据返回结构
/// </summary>
public class DTWData
{
/// <summary>
/// 均值
/// </summary>
public float mean;
/// <summary>
/// 匹配点的位置坐标
/// </summary>
public List<Vector2> path;
/// <summary>
/// 累计距离矩阵 D
/// </summary>
public float[,] D;
public DTWData(float mean, List<Vector2> path, float[,] D)
{
this.mean = mean;
this.path = path;
this.D = D;
}
}
对以上代码进行测试后结果与视频中相同
[Button("DTW数据测试")]
void DTWTest()
{
int[] a = new int[10] { 1, 3, 4, 9, 8, 2, 1, 5, 7, 3 };
int[] b = new int[10] { 1, 6, 2, 3, 0, 9, 4, 1, 6, 3 };
DTWData d = DTWFunc(a, b);
Debug.Log(d.mean);
//Debug.Log(d.path);
//Debug.Log(d.D);
}
以上是视频中介绍的简易算法改为C#的样子,接下来就是改(重载)为三维向量的形式。
我这边只是修改了距离算法
float DisAbs(Vector3 x, Vector3 y)
{
return (x - y).magnitude;
}
在更改为向量的形式后,要做的就是将录制好的动作数据与游玩人员的实时动作数据进行对比,即相似值的范围取舍。
简单测试了下数据都还OK,那么接下来就是将这套测试代码搬运到主体项目中了。
搬到主项目中后经过一些测试:
- 首先录制一段正常速度动作的数据作为“标准数据”,模型运行中的数据为"实时数据"。3组角色动作 Action_1, Action_2, Action_3, “标准数据”为 Action_1 的动作录制数据
- 将Action_1慢放会得到很多组的“拉长”数据,将Action_1快放后会得到很多组的“缩短”数据,经过多次比对后的 mean会在20-40浮动,其中30上下浮动的次数为多数
- 使用不同于录制动作(Action_1)的动作数据时(Action_2或Action_3),mean在70-90之间,动作组与动作组之间存在使用同一动作的情况,即Action_1,Action_2,Action_3 可能都包含准备动作
- 结论是数据结果不理想,后来发现是数据间隔太长了,期初是用的200ms录的数据以及比对数据,后来改为10ms后数据就正常了
Ps:
- 经过多轮测试后发现,距离仍与数据“长度”有关。
- 当我们的“标准数据”数量 ≈ 900 个,“实时数据”数量 >> 900 个(以3000为例),且动作数据为一个默认动作(例如站立)时,数据结果存在距离 ≈ 35 的情况。当我们取值数量接近 900 个时数据均符合预期。
- 简单来说就是:如果一个10s的动作被录制成“标准数据”,那么就不可以是90s的“实时数据”。