HumanoidMocap/Core/Motion/CaptureRootSpikes.cs
using System;
using System.Linq;
namespace HumanoidMocapX.Core.Motion;
using Vector3 = System.Numerics.Vector3;
/// <summary>Rejects brief large translation excursions before filtering a reconstructed body.</summary>
internal static class CaptureRootSpikes
{
const double HalfWindowSeconds = .08;
const float MinimumExcursionMetres = .15f;
internal static Vector3[] Clean( Vector3[] positions, double fps )
{
var result = positions.ToArray();
var radius = Math.Max( 1, (int)Math.Round( HalfWindowSeconds * fps ) );
var values = new float[ radius * 2 + 1 ];
for ( var frame = 0; frame < positions.Length; frame++ )
{
float Median( int axis )
{
for ( var offset = -radius; offset <= radius; offset++ )
{
var position = positions[ Math.Clamp( frame + offset, 0, positions.Length - 1 ) ];
values[ offset + radius ] = axis == 0 ? position.X : axis == 1 ? position.Y : position.Z;
}
Array.Sort( values );
return values[ radius ];
}
var median = new Vector3( Median( 0 ), Median( 1 ), Median( 2 ) );
// A monotonic trajectory, however fast, is its own temporal median. A
// short depth excursion that returns immediately is not sustained travel.
// Leave ordinary gait oscillation and small jump-apex curvature untouched.
if ( Vector3.Distance( positions[ frame ], median ) > MinimumExcursionMetres )
result[ frame ] = median;
}
return result;
}
}