tests/test_decoder.cs
using System.Numerics;
using System.IO.Compression;
using System.Text;
using HumanoidMocapX.Core.Maths;
using HumanoidMocapX.Core.Motion;
using HumanoidMocapX.Inference;
using HumanoidMocapX;
using HumanoidMocapX.EditorTools;
static class DecoderTests
{
internal static void Run()
{
foreach (var frames in new[] { 1, 3, 150 }) Conversion(frames);
RejectedInputs(); Checkpoint();
Console.WriteLine("PASS: latent decoding, shape averaging, root travel, rotation conventions, contacts and decoder checkpoint buffers");
}
internal static void Released(string checkpoint, string bodyPath)
{
var joints = SmplModel.Load(bodyPath);
MotionTests.Equal(156, joints.Template.Length); MotionTests.Equal(2496, joints.Directions.Length);
foreach (var value in joints.Template.Concat(joints.Directions))
MotionTests.Check(float.IsFinite(value), "Released SMPL-H regression produced a non-finite value");
var motion = FlowCheckpoint.LoadDecoder(checkpoint, bodyPath).Decode(new float[3 * FlowSampler.Channels]);
motion.Validate();
Console.WriteLine("PASS: released " + Path.GetFileName(checkpoint) + " decoder with separate SMPL-H asset (mean latent, not inferred motion)");
}
static void Conversion(int frames)
{
var fixture = Fixture(frames);
var decoder = new FlowDecoder(fixture.Mean, fixture.Deviation, fixture.Template, fixture.Directions);
var motion = decoder.Decode(fixture.Latent);
var document = motion.ToDocument();
for (var joint = 0; joint < 52; joint++)
for (var axis = 0; axis < 3; axis++)
{
var index = joint * 3 + axis; double expected = fixture.Template[index];
for (var shape = 0; shape < 16; shape++)
expected += ((frames - 1) / 2d + shape * .1) * fixture.Directions[index * 16 + shape];
MotionTests.Near(expected, motion.Rest[joint][axis], 2e-6);
}
for (var frame = 0; frame < frames; frame++)
{
var expectedX = -(.3f + .1f * frame + frame * (frame + 1) / 2f - motion.Rest[0][0]);
var expectedY = 1.4f + .05f * frame - motion.Rest[0][1];
var expectedZ = -(-.2f + .07f * frame - frame * (frame + 1) - motion.Rest[0][2]);
MotionTests.Near(expectedX, motion.Translations[frame][0], .002);
MotionTests.Near(expectedY, motion.Translations[frame][1]);
MotionTests.Near(expectedZ, motion.Translations[frame][2], .004);
MotionTests.Near(1.4f + .05f * frame, document.Frames[frame].Positions[0][1]);
for (var joint = 0; joint < 52; joint++)
{
var expected = Rotation(joint);
if (joint == 0) expected = new Quaternion(0, 1, 0, 0) * expected;
var actual = MotionDocument.Q(document.Frames[frame].Rotations[joint]);
MotionTests.Near(0, MathQ.AngleBetween(expected, actual), 1e-6);
}
MotionTests.Near(0, document.StationaryJoints[0].Probability[frame]);
MotionTests.Near(.25, document.StationaryJoints[1].Probability[frame]);
MotionTests.Near(.8, document.StationaryJoints[2].Probability[frame]);
MotionTests.Near(1, document.StationaryJoints[3].Probability[frame]);
}
// Caller buffers and earlier outputs cannot change a cached decoder's next result.
var expectedRest = motion.Rest[0][1]; motion.Rest[0][1] += 10;
Array.Fill(fixture.Mean, float.NaN); Array.Fill(fixture.Deviation, float.NaN);
Array.Fill(fixture.Template, float.NaN); Array.Fill(fixture.Directions, float.NaN);
MotionTests.Near(expectedRest, decoder.Decode(fixture.Latent).Rest[0][1]);
}
static void RejectedInputs()
{
var fixture = Fixture(1);
FlowDecoder Create() => new(fixture.Mean, fixture.Deviation, fixture.Template, fixture.Directions);
var decoder = Create();
MotionTests.Throws<ArgumentException>(() => decoder.Decode(Array.Empty<float>()));
MotionTests.Throws<ArgumentException>(() => decoder.Decode(new float[492]));
MotionTests.Throws<ArgumentException>(() => decoder.Decode(new float[5001 * 491]));
using var cancellation = new CancellationTokenSource(); cancellation.Cancel();
MotionTests.Throws<OperationCanceledException>(() => decoder.Decode(fixture.Latent, cancellation.Token));
fixture.Latent[10] = float.NaN;
MotionTests.Throws<InvalidDataException>(() => decoder.Decode(fixture.Latent));
fixture = Fixture(1);
for (var component = 0; component < 6; component++) Set(fixture, 0, 159 + component, 0);
try { decoder.Decode(fixture.Latent); throw new Exception("Expected a degenerate rotation rejection"); }
catch (InvalidDataException error) { MotionTests.Check(error.Message.Contains("frame 0, joint 0"), error.Message); }
fixture = Fixture(1); fixture.Deviation[0] = 0;
MotionTests.Throws<InvalidDataException>(() => Create());
fixture = Fixture(1); fixture.Directions[200] = float.NaN;
MotionTests.Throws<InvalidDataException>(() => Create());
fixture = Fixture(1);
MotionTests.Throws<InvalidDataException>(() => new FlowDecoder(fixture.Mean, fixture.Deviation, new float[3], fixture.Directions));
fixture.Mean[0] = float.NaN; MotionTests.Throws<InvalidDataException>(() => Create());
fixture = Fixture(2); fixture.Deviation[0] = float.MaxValue;
decoder = Create(); fixture.Latent[491] = float.MaxValue;
MotionTests.Throws<InvalidDataException>(() => decoder.Decode(fixture.Latent));
}
static void Checkpoint()
{
var fixture = Fixture(3); var buffers = Buffers(fixture);
var folder = Path.Combine(Path.GetTempPath(), "flow-decoder-" + Guid.NewGuid().ToString("N")); Directory.CreateDirectory(folder);
try
{
var path = Path.Combine(folder, "decoder.ckpt"); CheckpointFixture.Write(path, buffers, prefix: "");
var bodyPath = Path.Combine(folder, "body.npz"); WriteBody(bodyPath, fixture);
var actual = FlowCheckpoint.LoadDecoder(path, bodyPath).Decode(fixture.Latent);
var expected = new FlowDecoder(fixture.Mean, fixture.Deviation, fixture.Template, fixture.Directions).Decode(fixture.Latent);
for (var frame = 0; frame < 3; frame++)
{
MotionTests.Check(actual.Poses[frame].AsSpan().SequenceEqual(expected.Poses[frame]), "Checkpoint rotation statistics changed");
MotionTests.Check(actual.Translations[frame].AsSpan().SequenceEqual(expected.Translations[frame]), "Checkpoint root statistics changed");
MotionTests.Check(actual.Contacts[frame].AsSpan().SequenceEqual(expected.Contacts[frame]), "Checkpoint contact statistics changed");
}
for (var joint = 0; joint < 52; joint++)
MotionTests.Check(actual.Rest[joint].AsSpan().SequenceEqual(expected.Rest[joint]), "Checkpoint shaped rest changed");
using var cancellation = new CancellationTokenSource(); cancellation.Cancel();
MotionTests.Throws<OperationCanceledException>(() => FlowCheckpoint.LoadDecoder(path, bodyPath, cancellation.Token));
var index = 0;
foreach (var corrupt in new Action<Dictionary<string, Tensor>>[] {
b => b.Remove("rep_shapes_mean"), b => b["local_rot_data_std"].Data[0] = -1,
b => b["smooth_root_vel_mean"].Data[0] = float.NaN
})
{
buffers = Buffers(fixture); corrupt(buffers);
var bad = Path.Combine(folder, "bad-" + index++ + ".ckpt"); CheckpointFixture.Write(bad, buffers, prefix: "");
MotionTests.Throws<InvalidDataException>(() => FlowCheckpoint.LoadDecoder(bad, bodyPath));
}
foreach (var corruption in new[] { "hierarchy", "ids", "shape", "nan", "missing" })
{
var bad = Path.Combine(folder, corruption + ".npz"); WriteBody(bad, fixture, corruption);
MotionTests.Throws<InvalidDataException>(() => FlowCheckpoint.LoadDecoder(path, bad));
}
}
finally { Directory.Delete(folder, true); }
}
static Dictionary<string, Tensor> Buffers(Values fixture)
{
var buffers = new Dictionary<string, Tensor>();
Pair("smooth_root_vel", new[] { fixture.Mean[0], 123f, fixture.Mean[1] }, new[] { fixture.Deviation[0], 456f, fixture.Deviation[1] }, 3);
Pair("smooth_root_pos", new[] { 123f, fixture.Mean[2], 234f }, new[] { 345f, fixture.Deviation[2], 456f }, 3);
Pair("local_joints_positions", fixture.Mean[3..159], fixture.Deviation[3..159], 52, 3);
Pair("local_rot_data", fixture.Mean[159..471], fixture.Deviation[159..471], 52, 6);
Pair("rep_shapes", fixture.Mean[471..487], fixture.Deviation[471..487], 16);
Pair("rep_foot_contacts", fixture.Mean[487..491], fixture.Deviation[487..491], 4);
return buffers;
void Pair(string name, float[] mean, float[] deviation, params int[] shape)
{
var fullShape = new[] { 1, 1 }.Concat(shape).ToArray();
Add(name + "_mean", mean, fullShape); Add(name + "_std", deviation, fullShape);
}
void Add(string name, float[] values, params int[] shape) => buffers.Add(name, new Tensor(name, values.ToArray(), shape.Reverse().ToArray()));
}
static void WriteBody(string path, Values fixture, string corruption = "")
{
using var archive = ZipFile.Open(path, ZipArchiveMode.Create);
var hierarchy = new double[104];
for (var joint = 0; joint < 52; joint++) { hierarchy[joint] = FlowMotion.JointParents[joint]; hierarchy[52 + joint] = joint; }
hierarchy[0] = uint.MaxValue;
if (corruption == "hierarchy") hierarchy[2] = .5;
if (corruption == "ids") hierarchy[53] = 2;
Write("kintree_table", hierarchy, 2, 52);
var regressor = new double[52 * 6890]; var template = new double[6890 * 3]; var directions = new double[6890 * 3 * 16];
for (var joint = 0; joint < 52; joint++)
{
regressor[joint * 6890 + joint * 2] = .25; regressor[joint * 6890 + joint * 2 + 1] = .75;
for (var axis = 0; axis < 3; axis++)
{
// Weighted regression must recover the expected joint, not select a vertex.
template[joint * 6 + axis] = fixture.Template[joint * 3 + axis] * 4;
for (var shape = 0; shape < 16; shape++)
directions[(joint * 6 + axis) * 16 + shape] = fixture.Directions[(joint * 3 + axis) * 16 + shape] * 4;
}
}
if (corruption != "missing") Write("J_regressor", regressor, 52, 6890);
if (corruption == "nan") template[0] = double.NaN;
Write("v_template", template, 6890, 3);
Write("shapedirs", directions, corruption == "shape" ? new[] { 6890, 16, 3 } : new[] { 6890, 3, 16 });
void Write(string name, double[] values, params int[] shape)
{
using var stream = archive.CreateEntry(name + ".npy").Open(); using var writer = new BinaryWriter(stream, Encoding.ASCII);
var header = "{'descr': '<f8', 'fortran_order': False, 'shape': (" + string.Join(", ", shape) + "), }\n";
writer.Write(new byte[] { 0x93, 78, 85, 77, 80, 89, 1, 0 }); writer.Write((ushort)header.Length);
writer.Write(Encoding.ASCII.GetBytes(header)); foreach (var value in values) writer.Write(value);
}
}
sealed record Values(float[] Mean, float[] Deviation, float[] Template, float[] Directions, float[] Latent);
static Values Fixture(int frames)
{
var fixture = new Values(Enumerable.Range(0, 491).Select(i => .01f * (i % 7 - 3)).ToArray(),
Enumerable.Range(0, 491).Select(i => .5f + (i * 3 % 5) * .25f).ToArray(),
MotionTests.Fixture().Rest.SelectMany(r => r).ToArray(), new float[52 * 3 * 16], new float[frames * 491]);
fixture.Template[0] = .2f; fixture.Template[2] = -.1f;
for (var coordinate = 0; coordinate < 156; coordinate++)
for (var shape = 0; shape < 16; shape++) fixture.Directions[coordinate * 16 + shape] = .001f * (1 + (coordinate + shape) % 7);
for (var frame = 0; frame < frames; frame++)
{
Set(fixture, frame, 0, frame == 0 ? 900 : frame * 30); Set(fixture, frame, 1, frame == 0 ? 800 : frame * -60);
Set(fixture, frame, 2, 700); // This smooth-root Y must not be added to absolute pelvis Y.
Set(fixture, frame, 3, .3f + .1f * frame); Set(fixture, frame, 4, 1.4f + .05f * frame); Set(fixture, frame, 5, -.2f + .07f * frame);
for (var joint = 0; joint < 52; joint++)
{
var rotation = Rotation(joint);
var first = Vector3.Transform(Vector3.UnitX, rotation);
var second = Vector3.Transform(Vector3.UnitY, rotation) * 3 + first * .4f;
first *= 2;
float[] six = { first.X, second.X, first.Y, second.Y, first.Z, second.Z };
for (var channel = 0; channel < 6; channel++) Set(fixture, frame, 159 + joint * 6 + channel, six[channel]);
}
for (var shape = 0; shape < 16; shape++) Set(fixture, frame, 471 + shape, frame + shape * .1f);
float[] contacts = { -.2f, .25f, .8f, 1.4f };
for (var contact = 0; contact < 4; contact++) Set(fixture, frame, 487 + contact, contacts[contact]);
}
return fixture;
}
static Quaternion Rotation(int joint)
{
var angle = (joint % 7) switch { 0 => .37f, 1 => 0, 2 => 1e-9f, 3 => MathF.PI, 4 => -1.7f, 5 => -MathF.PI + 1e-5f, _ => 4.1f };
var axis = joint % 3 == 0 ? Vector3.UnitX : joint % 3 == 1 ? Vector3.UnitY : Vector3.UnitZ;
return Quaternion.CreateFromAxisAngle(axis, angle);
}
static void Set(Values fixture, int frame, int channel, float physical)
=> fixture.Latent[frame * 491 + channel] = (physical - fixture.Mean[channel]) / fixture.Deviation[channel];
}