tests/test_camera.cs
using System.Numerics;
using HumanoidMocapX;
static class CameraTests
{
internal static void Run()
{
Sampling(); Conversion(); ShortestTurn(); InvalidInputs(); Allocations(); Travel();
Console.WriteLine("PASS: VGGT camera sampling, original-pixel intrinsics, shortest-arc interpolation, relative rotations, camera travel and allocation-free decoding");
}
static void Sampling()
{
foreach (var frames in new[] { 1, 2, 10, 11, 30, 31, 150, 300, 5000 })
foreach (var interval in new[] { 1, 10, 30 })
{
var indices = Enumerable.Range(0, frames).Where(frame => frame % interval == 0 || frame == frames - 1).ToArray();
MotionTests.Equal(indices.Length, FlowCamera.SampleCount(frames, interval));
for (var i = 0; i < indices.Length; i++) MotionTests.Equal(indices[i], FlowCamera.SampleFrame(i, frames, interval));
MotionTests.Throws<ArgumentOutOfRangeException>(() => FlowCamera.SampleFrame(-1, frames, interval));
MotionTests.Throws<ArgumentOutOfRangeException>(() => FlowCamera.SampleFrame(indices.Length, frames, interval));
}
}
static void Conversion()
{
const int frames = 24, width = 1920, height = 1080;
var baseline = Quaternion.CreateFromAxisAngle(Vector3.UnitX, .7f);
foreach (var interval in new[] { 1, 10, 30 })
{
var count = FlowCamera.SampleCount(frames, interval);
var poses = new float[count * 9];
for (var sample = 0; sample < count; sample++)
{
var frame = FlowCamera.SampleFrame(sample, frames, interval);
var camera = Quaternion.CreateFromAxisAngle(Vector3.UnitY, frame * .03f) * baseline;
Pose(poses.AsSpan(sample * 9, 9), camera, width, height, 800 + frame * 7, 450 + frame * 3);
}
var rotations = new float[frames * 9]; var intrinsics = new float[frames * 9];
FlowCamera.Decode(poses, frames, interval, width, height, rotations, intrinsics);
for (var frame = 0; frame < frames; frame++)
{
var angle = frame * .03;
var c = Math.Cos(angle); var s = Math.Sin(angle);
double[] expected = [c, 0, s, 0, 1, 0, -s, 0, c];
double[] expectedK = [800 + frame * 7, 0, width * .5, 0, 450 + frame * 3, height * .5, 0, 0, 1];
for (var i = 0; i < 9; i++)
{
MotionTests.Near(expected[i], rotations[frame * 9 + i], 3e-6);
MotionTests.Near(expectedK[i], intrinsics[frame * 9 + i], .0002);
}
}
var original = (float[])rotations.Clone();
var worldChange = Quaternion.CreateFromAxisAngle(Vector3.UnitZ, .4f);
for (var sample = 0; sample < count; sample++)
{
var offset = sample * 9;
var quaternion = new Quaternion(poses[offset + 3], poses[offset + 4], poses[offset + 5], poses[offset + 6]) * worldChange;
for (var axis = 0; axis < 4; axis++) poses[offset + 3 + axis] = quaternion[axis];
}
FlowCamera.Decode(poses, frames, interval, width, height, rotations, intrinsics);
for (var i = 0; i < rotations.Length; i++) MotionTests.Near(original[i], rotations[i], 3e-6);
}
}
static void ShortestTurn()
{
var poses = new float[18];
Pose(poses.AsSpan(0, 9), Quaternion.CreateFromAxisAngle(Vector3.UnitZ, 170 * MathF.PI / 180));
Pose(poses.AsSpan(9, 9), Quaternion.CreateFromAxisAngle(Vector3.UnitZ, -170 * MathF.PI / 180));
var rotations = new float[45]; var intrinsics = new float[45];
foreach (var scale in new[] { 1f, -1f, 1e-35f, 1e35f })
{
var scaled = (float[])poses.Clone();
for (var i = 3; i < 7; i++) scaled[i + 9] *= scale;
FlowCamera.Decode(scaled, 5, 10, 640, 480, rotations, intrinsics);
for (var frame = 0; frame < 5; frame++)
{
var angle = frame * 5 * Math.PI / 180;
MotionTests.Near(Math.Cos(angle), rotations[frame * 9], 3e-6);
MotionTests.Near(-Math.Sin(angle), rotations[frame * 9 + 1], 3e-6);
MotionTests.Near(Math.Sin(angle), rotations[frame * 9 + 3], 3e-6);
}
}
FlowCamera.Decode(poses.AsSpan(0, 9), 1, 30, 640, 480, rotations.AsSpan(0, 9), intrinsics.AsSpan(0, 9));
double[] identity = [1, 0, 0, 0, 1, 0, 0, 0, 1];
for (var i = 0; i < 9; i++) MotionTests.Near(identity[i], rotations[i], 1e-6);
}
static void InvalidInputs()
{
var poses = new float[9]; Pose(poses, Quaternion.Identity);
var rotations = new float[9]; var intrinsics = new float[9];
foreach (var frames in new[] { 0, 5001, int.MaxValue })
MotionTests.Throws<ArgumentOutOfRangeException>(() => FlowCamera.SampleCount(frames, 1));
MotionTests.Throws<ArgumentOutOfRangeException>(() => FlowCamera.SampleCount(5, 2));
MotionTests.Throws<ArgumentOutOfRangeException>(() => FlowCamera.Decode(poses, 1, 1, 0, 480, rotations, intrinsics));
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(new float[8], 1, 1, 640, 480, rotations, intrinsics));
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(poses, 1, 1, 640, 480, new float[10], intrinsics));
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(poses, 1, 1, 640, 480, rotations, new float[8]));
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(poses, 1, 1, 640, 480, rotations, rotations));
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(poses, 1, 1, 640, 480, poses, intrinsics));
var overlapping = new float[10]; poses.CopyTo(overlapping, 0);
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(overlapping.AsSpan(0, 9), 1, 1, 640, 480, rotations, overlapping.AsSpan(1, 9)));
for (var channel = 0; channel < 9; channel++)
{
var invalid = (float[])poses.Clone(); invalid[channel] = float.NaN;
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(invalid, 1, 1, 640, 480, rotations, intrinsics));
}
foreach (var fov in new[] { 0f, -.1f, MathF.PI, 4f, float.Epsilon })
{
var invalid = (float[])poses.Clone(); invalid[7] = fov;
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(invalid, 1, 1, 640, 480, rotations, intrinsics));
}
var zero = (float[])poses.Clone(); zero[6] = 0;
MotionTests.Throws<ArgumentException>(() => FlowCamera.Decode(zero, 1, 1, 640, 480, rotations, intrinsics));
using var cancelled = new CancellationTokenSource(); cancelled.Cancel();
MotionTests.Throws<OperationCanceledException>(() => FlowCamera.Decode(poses, 1, 1, 640, 480, rotations, intrinsics, cancelled.Token));
}
static void Allocations()
{
var poses = new float[18]; Pose(poses.AsSpan(0, 9), Quaternion.Identity); Pose(poses.AsSpan(9, 9), Quaternion.Identity);
var rotations = new float[99]; var intrinsics = new float[99];
for (var i = 0; i < 20; i++) FlowCamera.Decode(poses, 11, 10, 640, 480, rotations, intrinsics);
var before = GC.GetAllocatedBytesForCurrentThread();
for (var i = 0; i < 100; i++) FlowCamera.Decode(poses, 11, 10, 640, 480, rotations, intrinsics);
MotionTests.Equal(0L, GC.GetAllocatedBytesForCurrentThread() - before);
}
static void Travel()
{
// A turning camera that stays put has no travel, whatever its rotation and translation.
var poses = new float[3 * 9];
Vector3[] centres = { new(1, 2, 3), new(1, 2, 3), new(1.3f, 2, 2.6f) };
for (var sample = 0; sample < 3; sample++)
{
var rotation = Quaternion.CreateFromAxisAngle(Vector3.Normalize(new Vector3(1, 2, 3)), sample * .4f);
Pose(poses.AsSpan(sample * 9, 9), rotation);
var translation = -Vector3.Transform(centres[sample], rotation);
poses[sample * 9] = translation.X; poses[sample * 9 + 1] = translation.Y; poses[sample * 9 + 2] = translation.Z;
}
MotionTests.Near(0, FlowCamera.Travel(poses.AsSpan(0, 18)), 1e-5);
MotionTests.Near(.5, FlowCamera.Travel(poses), 1e-5);
MotionTests.Throws<ArgumentException>(() => FlowCamera.Travel(poses.AsSpan(1)));
}
static void Pose(Span<float> pose, Quaternion rotation, int width = 640, int height = 480, float fx = 320, float fy = 240)
{
pose[0] = 4; pose[1] = -5; pose[2] = 6;
pose[3] = rotation.X; pose[4] = rotation.Y; pose[5] = rotation.Z; pose[6] = rotation.W;
pose[7] = (float)(2 * Math.Atan(height * .5 / fy)); pose[8] = (float)(2 * Math.Atan(width * .5 / fx));
}
}