Code/HumanoidMocap/Motion/MotionDiagnostics.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidMocap.Mapping;
namespace HumanoidMocap.Motion;
using Vector3=System.Numerics.Vector3;
public sealed record MissingObservationInterval(double Start,double End);
public sealed record JointTrackDiagnostics(string Bone,BoneRole? Role,int Reconstructed,int Inferred,int GeneratedIk,int Unobserved,
double LongestMissingSeconds,IReadOnlyList<MissingObservationInterval> MissingIntervals,int Authored=0);
public sealed record MotionDiagnosticsReport(int Frames,double Start,double End,double MinimumFrameInterval,double MaximumFrameInterval,
float MaximumObservedBoneLengthRangeMetres,float MaximumObservedRotationStepDegrees,double MaximumObservedRootSpeedMetresPerSecond,
IReadOnlyList<JointTrackDiagnostics> Tracks)
{
public string Interpretation=>"Derived motion checks, not backend confidence or ground-truth accuracy. Large motion changes can be intentional.";
public string HandSummary=>string.Join(" ",Tracks.Where(t=>t.Role is BoneRole.HandL or BoneRole.HandR)
.Select(t=>t.GeneratedIk>0
?$"{(t.Role==BoneRole.HandL?"Left":"Right")} hand: reconstructed {t.Reconstructed}/{Frames}, generated IK {t.GeneratedIk}/{Frames}; longest unresolved gap {t.LongestMissingSeconds:F2}s."
:$"{(t.Role==BoneRole.HandL?"Left":"Right")} hand: observed {t.Reconstructed}/{Frames} frames; longest unresolved gap {t.LongestMissingSeconds:F2}s."));
}
/// <summary>Measures available observations and transform consistency without assigning confidence.</summary>
public static class MotionDiagnostics
{
public static MotionDiagnosticsReport Analyze(MotionDocument motion)
{
motion.Validate();var frames=motion.Frames;
var tracks=new List<JointTrackDiagnostics>();float lengthRange=0,rotationStep=0;double rootSpeed=0;
var intervals=frames.Zip(frames.Skip(1),(a,b)=>b.Time-a.Time).ToArray();
for(var bone=0;bone<motion.Bones.Count;bone++)
{
int reconstructed=0,inferred=0,ik=0,unobserved=0,authored=0;double? missingStart=null;
var missing=new List<MissingObservationInterval>();
float shortest=float.PositiveInfinity,longest=0;
for(var i=0;i<frames.Count;i++)
{
var frame=frames[i];var evidence=frame.Evidence[bone];
if(evidence==JointEvidence.Unobserved){unobserved++;missingStart??=frame.Time;}
else
{
if(missingStart is double start){missing.Add(new(start,frame.Time));missingStart=null;}
if(evidence==JointEvidence.Reconstructed)reconstructed++;
else if(evidence==JointEvidence.InferredGap)inferred++;
else if(evidence==JointEvidence.GeneratedIk)ik++;
else if(evidence==JointEvidence.Authored)authored++;
}
if(evidence!=JointEvidence.Reconstructed)continue;
if(motion.Bones[bone].Parent>=0)
{
var length=MotionDocument.V(frame.Positions[bone]).Length();
shortest=Math.Min(shortest,length);longest=Math.Max(longest,length);
}
if(i==0||frames[i-1].Evidence[bone]!=JointEvidence.Reconstructed)continue;
var previous=frames[i-1];
var a=Quaternion.Normalize(MotionDocument.Q(previous.Rotations[bone]));var b=Quaternion.Normalize(MotionDocument.Q(frame.Rotations[bone]));
rotationStep=Math.Max(rotationStep,2*MathF.Acos(Math.Clamp(Math.Abs(Quaternion.Dot(a,b)),0,1))*180/MathF.PI);
if(motion.Bones[bone].Parent<0)
rootSpeed=Math.Max(rootSpeed,Vector3.Distance(MotionDocument.V(frame.Positions[bone]),MotionDocument.V(previous.Positions[bone]))/(frame.Time-previous.Time));
}
if(missingStart is double finalStart)missing.Add(new(finalStart,frames[^1].Time+1/motion.SourceFps));
if(float.IsFinite(shortest))lengthRange=Math.Max(lengthRange,longest-shortest);
tracks.Add(new(motion.Bones[bone].Name,motion.Bones[bone].Role,reconstructed,inferred,ik,unobserved,
missing.Count==0?0:missing.Max(m=>m.End-m.Start),missing,authored));
}
return new(frames.Count,frames[0].Time,frames[^1].Time,intervals.Length==0?0:intervals.Min(),intervals.Length==0?0:intervals.Max(),
lengthRange,rotationStep,rootSpeed,tracks);
}
}