A static utility that adjusts wrist positions in a MotionDocument to match 2D pose observations (COCO joints) and optional depth measurements. It detects hidden wrists, pairs hands, computes target world-space wrist moves, smooths them, solves a two-bone IK for each arm, writes corrected joint local rotations into the motion frames, and can save/load/replay those corrections.
using System.Numerics;
using System.Text.Json;
using HumanoidMocap.Core.Inference;
using HumanoidMocap.EditorTools.Inference;
using HumanoidMocap.Core.Mapping;
using HumanoidMocap.Core.Motion;
namespace HumanoidMocap.Worker;
/// <summary>Puts each wrist where the picture shows it. The body network's arms are smooth and plausible but do not
/// always follow the wrists the 2D pose sees: on a dance clip the performer crossed the wrists above the head and the
/// capture left them uncrossed, 34 px (about 17 cm) off; on the "Take the L" emote the hand held to the forehead stood
/// 34 cm in front of the head, because from one camera a hand on the forehead and a hand reaching toward the lens look
/// the same. Per frame, a confidently seen wrist more than <see cref="TolerancePicture"/> of the body's height off
/// its 2D position is moved across the picture, at its own distance from the camera, until it is only that far off
/// (at most <see cref="MaximumMetres"/>); a wrist the picture shows above the eyes and within the head's width is
/// then slid along its line of sight to within <see cref="FrontMetres"/> in front of the head (or
/// <see cref="BehindMetres"/> behind it). A boxing guard held before the chin keeps its reach. A wrist the picture
/// places inside the torso's outline but can barely see (score under <see cref="HiddenScore"/>, its elbow well seen) is
/// hidden by the body (unless the network put it clearly in front of the torso: a blurred hand swinging before the belly),
/// so it is slid along its line of sight to behind the torso's back at its height: in a floss dance
/// the hand swinging behind the hips stood at the hip's side, level with the body. The moves are smoothed over time and
/// the arm follows by two-bone IK; the hand keeps its orientation.</summary>
public static class WristPictureFit
{
public const string File="wrist-picture-fit.json";
/// <summary>A wrist inside the torso's outline seen less surely than this is behind the body (seen wrists score about .9;
/// hidden ones .55–.7 on the floss clip, while a hand crossing in front kept .9).</summary>
public const float HiddenScore=.75f,HiddenMinimumScore=.2f,HiddenElbowScore=.7f,BehindTorsoMargin=.05f;
/// <summary>A hidden stretch counts as behind the body even where the network put the hand in front when the wrist moves slower than
/// this (body heights per second; a blurred swing in front moved 2.4-3.7, the floss hand behind the hip about 1) and the performer is
/// at least <see cref="HiddenBodyPixels"/> tall, enough for a vanishing wrist to mean something.</summary>
public const float HiddenSlowSpeed=1.2f,HiddenBodyPixels=200,MaximumBehindMetres=.5f;
public const float FrontMetres=.15f,BehindMetres=.1f,TolerancePicture=.015f,TogetherPicture=.15f,PairPicture=.12f,PairMetres=.2f,PairScore=.4f,MaximumMetres=.25f,MaximumMeasuredMetres=.6f,MinimumScore=.6f;
/// <summary>The corrected local rotations of one arm (upper arm, forearm, hand) in one frame.</summary>
public sealed record Correction(int Frame,bool Left,float[][] Rotations);
/// <param name="observations">Per frame, COCO 2D joints (x, y, score) in the picture.</param>
/// <returns>The corrections made, already applied to <paramref name="motion"/> (camera-relative).</returns>
/// <param name="measured">Per side (left, right) and frame, the wrist's distance from the camera measured by a depth
/// sensor (see <see cref="DepthCorrection"/>), or null; a measurement replaces the head, paired-hands and torso rules.</param>
public static List<Correction> Apply(MotionDocument motion,IReadOnlyList<float[]?> observations,GvhmrDecoder.Camera camera,float?[][]? measured=null)
{
var result=new List<Correction>();var bones=motion.Bones;var count=motion.Frames.Count;
int Role(BoneRole role)=>bones.FindIndex(b=>b.Role==role);
var head=Role(BoneRole.Head);if(head<0||observations.Count!=count||count==0)return result;
// The body's height in the picture (nose to lower ankle), its median over the clip, sets the tolerance.
var heights=observations.Where(o=>o is {Length:>=51}&&o[2]>.5f&&(o[15*3+2]>.5f||o[16*3+2]>.5f))
.Select(o=>Math.Max(o![15*3+2]>.5f?o[15*3+1]:0,o[16*3+2]>.5f?o[16*3+1]:0)-o[1]).Where(h=>h>0).OrderBy(h=>h).ToArray();
if(heights.Length==0)return result;
var bodyHeight=heights[heights.Length/2];var tolerance=TolerancePicture*bodyHeight;
// Wrists the picture shows together (arms folded across the chest seen from the side) are brought to within
// PairMetres of each other's distance from the camera, half each: the network put one 60 cm behind the other.
// Only their being together is used, so a less certain sighting (PairScore) counts.
int handL=Role(BoneRole.HandL),handR=Role(BoneRole.HandR);
int hips=Role(BoneRole.Hips),neck=Role(BoneRole.Neck),shoulderL=Role(BoneRole.UpperArmL),shoulderR=Role(BoneRole.UpperArmR);var pairShift=new[]{new float[count],new float[count]};
if(handL>=0&&handR>=0)for(var t=0;t<count;t++)
{
var o=observations[t];if(o is null||o.Length<51||o[9*3+2]<PairScore||o[10*3+2]<PairScore)continue;
if(MathF.Sqrt((o[9*3]-o[10*3])*(o[9*3]-o[10*3])+(o[9*3+1]-o[10*3+1])*(o[9*3+1]-o[10*3+1]))>PairPicture*bodyHeight)continue;
var (positions,_)=World(motion,t);var apart=-positions[handR].Z-(-positions[handL].Z);
var excess=MathF.Abs(apart)-PairMetres;if(excess<=0)continue;
pairShift[0][t]=MathF.Sign(apart)*excess/2;pairShift[1][t]=-MathF.Sign(apart)*excess/2;
}
// Held two frames past each end, so the smoothing below does not thin it where the hands first come together.
foreach(var shift in pairShift)
{
var grown=(float[])shift.Clone();
for(var t=0;t<count;t++)if(shift[t]==0)
for(var k=1;k<=2;k++){if(t-k>=0&&shift[t-k]!=0){grown[t]=shift[t-k];break;}if(t+k<count&&shift[t+k]!=0){grown[t]=shift[t+k];break;}}
Array.Copy(grown,shift,count);
}
foreach(var left in new[]{true,false})
{
int upper=Role(left?BoneRole.UpperArmL:BoneRole.UpperArmR),lower=Role(left?BoneRole.LowerArmL:BoneRole.LowerArmR),hand=Role(left?BoneRole.HandL:BoneRole.HandR);
if(upper<0||lower<0||hand<0||bones[lower].Parent!=upper||bones[hand].Parent!=lower)continue;
var wristJoint=left?9:10;var elbowJoint=left?7:8;
// Frames where the picture hides this wrist behind the torso, held two frames past each end so the smoothing
// below does not thin a short pass behind the hips.
var hidden=new bool[count];
for(var t=0;t<count;t++)
{
var o=observations[t];if(o is null||o.Length<51)continue;
var score=o[wristJoint*3+2];
hidden[t]=score>=HiddenMinimumScore&&score<HiddenScore&&o[elbowJoint*3+2]>=HiddenElbowScore&&InsideTorsoOutline(o,wristJoint);
}
// Each hidden stretch that is slow (see HiddenSlowSpeed) is trusted over the network's depth.
var trusted=new bool[count];
for(var t=0;t<count;)
{
if(!hidden[t]){t++;continue;}
var end=t;while(end+1<count&&hidden[end+1])end++;
var speeds=new List<float>();
for(var k=t;k<=end;k++)
{
int a=Math.Max(0,k-1),b=Math.Min(count-1,k+1);var span=motion.Frames[b].Time-motion.Frames[a].Time;
if(observations[a] is not {Length:>=51} oa||observations[b] is not {Length:>=51} ob||span<=0)continue;
speeds.Add(MathF.Sqrt((ob[wristJoint*3]-oa[wristJoint*3])*(ob[wristJoint*3]-oa[wristJoint*3])+(ob[wristJoint*3+1]-oa[wristJoint*3+1])*(ob[wristJoint*3+1]-oa[wristJoint*3+1]))/bodyHeight/(float)span);
}
speeds.Sort();
if(bodyHeight>=HiddenBodyPixels&&speeds.Count>0&&speeds[speeds.Count/2]<HiddenSlowSpeed)for(var k=t;k<=end;k++)trusted[k]=true;
t=end+1;
}
var behind=(bool[])hidden.Clone();var sure=(bool[])trusted.Clone();
for(var t=0;t<count;t++)for(var k=-2;k<=2;k++)if(t+k>=0&&t+k<count){if(hidden[t])behind[t+k]=true;if(trusted[t])sure[t+k]=true;}
// Where each wrist should go, as a move from where the network put it.
var move=new Vector3[count];
for(var t=0;t<count;t++)
{
var o=observations[t];if(o is null||o.Length<51)continue;
float Score(int k)=>o[k*3+2];
var seen=Score(wristJoint)>=MinimumScore;
if(!seen&&!behind[t])continue;
var (positions,_)=World(motion,t);var wrist=positions[hand];
// Camera-relative documents are the camera's axes turned half a turn about x: depth is -z.
var z=-wrist.Z;if(z<.05f)continue;
float u=camera.CenterX+camera.FocalLength*wrist.X/z,v=camera.CenterY-camera.FocalLength*wrist.Y/z;
float ou=o[wristJoint*3],ov=o[wristJoint*3+1];var error=MathF.Sqrt((ou-u)*(ou-u)+(ov-v)*(ov-v));
var goal=wrist;
// Wrists close together in the picture (crossed, clasped) keep less slack: their order is what shows.
var slack=tolerance;var otherJoint=left?10:9;
if(Score(otherJoint)>=MinimumScore)
{
var apart=MathF.Sqrt((o[otherJoint*3]-ou)*(o[otherJoint*3]-ou)+(o[otherJoint*3+1]-ov)*(o[otherJoint*3+1]-ov));
slack*=Math.Clamp(apart/(TogetherPicture*bodyHeight),.25f,1);
}
var pushedBehind=false;
if(seen&&error>slack)
{
var keep=slack/error;var tu=ou+(u-ou)*keep;var tv=ov+(v-ov)*keep;
goal=new Vector3((tu-camera.CenterX)/camera.FocalLength*z,-(tv-camera.CenterY)/camera.FocalLength*z,-z);
}
var target=measured?[left?0:1][t];
if(target is float distance&&distance>.05f)goal*=distance/-goal.Z;
else
{
if(Score(1)>=.5f&&Score(2)>=.5f)
{
var eyeY=Math.Min(o[1*3+1],o[2*3+1]);var eyeX=(o[1*3]+o[2*3])/2;
var width=Score(3)>.3f&&Score(4)>.3f?MathF.Abs(o[3*3]-o[4*3]):2.5f*MathF.Abs(o[1*3]-o[2*3]);
if(width>=3&&ov<eyeY&&MathF.Abs(ou-eyeX)<1.2f*width&&ov>eyeY-2.5f*width)
{
var depth=-goal.Z-(-positions[head].Z);var shift=Math.Clamp(depth,-FrontMetres,BehindMetres)-depth;
goal*=(-goal.Z+shift)/-goal.Z;
}
}
if(pairShift[left?0:1][t] is var pair&&pair!=0)goal*=(-goal.Z+pair)/-goal.Z;
// Hidden by the body: behind the torso's back at the hand's height. Only where the network did not put the hand
// clearly in front of the torso: a hand swinging before the belly, blurred, is also hard to see (Orange Justice:
// 21-41 cm in front, scored .66-.73), while the floss hand the body hid stood level with it. A slow hidden stretch on a
// large enough performer is trusted even so: the floss's other hand went behind the hip with the network 25 cm in front.
if(behind[t]&&TorsoDepths(positions,hips,neck,shoulderL,shoulderR,goal) is var (front,back)&&(-wrist.Z>=front||sure[t])&&-goal.Z<back){goal*=back/-goal.Z;pushedBehind=true;}
// Never deeper into the torso than the network had it: pulled toward the other hand or the head, a
// hand went 5 cm into the chest. It slides along its line of sight, so the picture does not change.
if(Torso(positions,hips,neck,shoulderL,shoulderR) is { } torso)
{
var need=Math.Min(1,torso.Clearance(wrist));
if(torso.Clearance(goal)<need)
{
Vector3? best=null;
for(var k=1;k<=80&&best is null;k++)foreach(var scale in new[]{1+k*.005f,1-k*.005f})
if(best is null&&torso.Clearance(goal*scale)>=need)best=goal*scale;
goal=best??wrist;
}
}
}
// A hand passing behind the body may have to cross the whole torso (the floss hand the network put 25 cm in front).
var limit=target is not null?MaximumMeasuredMetres:pushedBehind?MaximumBehindMetres:MaximumMetres;
var change=goal-wrist;if(change.Length()>limit)change=Vector3.Normalize(change)*limit;
move[t]=change;
}
// Smoothed so the arm does not snap, and so a single doubtful 2D frame barely moves it.
var smooth=new Vector3[count];const float sigma=1.3f;
for(var t=0;t<count;t++)
{
var sum=Vector3.Zero;float weights=0;
for(var k=-6;k<=6;k++){var i=Math.Clamp(t+k,0,count-1);var w=MathF.Exp(-k*k/(2*sigma*sigma));sum+=move[i]*w;weights+=w;}
smooth[t]=sum/weights;
}
for(var t=0;t<count;t++)
{
if(smooth[t].Length()<.005f)continue;
var (positions,rotations)=World(motion,t);
var parent=bones[upper].Parent;var parentWorld=parent<0?Quaternion.Identity:rotations[parent];
Vector3 s=positions[upper],e=positions[lower],w=positions[hand];
var goal=w+smooth[t];
float a=Vector3.Distance(s,e),b=Vector3.Distance(e,w);if(a<1e-4f||b<1e-4f)continue;
var toGoal=goal-s;var d=Math.Clamp(toGoal.Length(),MathF.Abs(a-b)+1e-4f,a+b-1e-4f);var direction=Vector3.Normalize(toGoal);
// The elbow stays on the side it was on.
var pole=e-s;pole-=direction*Vector3.Dot(pole,direction);
if(pole.LengthSquared()<1e-10f)continue;
pole=Vector3.Normalize(pole);
var along=(a*a-b*b+d*d)/(2*d);var elbow=s+direction*along+pole*MathF.Sqrt(Math.Max(0,a*a-along*along));var wrist=s+direction*d;
var upperWorld=Quaternion.Normalize(Between(e-s,elbow-s)*rotations[upper]);
var turnedLower=Quaternion.Normalize(Between(e-s,elbow-s)*rotations[lower]);
var lowerWorld=Quaternion.Normalize(Between(Vector3.Transform(w-e,Between(e-s,elbow-s)),wrist-elbow)*turnedLower);
var locals=new[]{Quaternion.Normalize(Quaternion.Inverse(parentWorld)*upperWorld),Quaternion.Normalize(Quaternion.Inverse(upperWorld)*lowerWorld),
Quaternion.Normalize(Quaternion.Inverse(lowerWorld)*rotations[hand])};
var correction=new Correction(t,left,locals.Select(q=>new[]{q.X,q.Y,q.Z,q.W}).ToArray());
Replay(motion,new[]{correction});result.Add(correction);
}
}
return result;
}
/// <summary>Applies saved corrections to another document built on the same skeleton (the world-relative
/// refinement rebuilds the arms from the network's output and would otherwise lose them).</summary>
public static int Replay(MotionDocument motion,IEnumerable<Correction> corrections)
{
var bones=motion.Bones;int Role(BoneRole role)=>bones.FindIndex(b=>b.Role==role);var applied=0;
foreach(var c in corrections)
{
if(c.Frame<0||c.Frame>=motion.Frames.Count)continue;
var chain=c.Left?new[]{BoneRole.UpperArmL,BoneRole.LowerArmL,BoneRole.HandL}:new[]{BoneRole.UpperArmR,BoneRole.LowerArmR,BoneRole.HandR};
var indices=chain.Select(Role).ToArray();if(indices.Any(i=>i<0))continue;
for(var k=0;k<3;k++)motion.Frames[c.Frame].Rotations[indices[k]]=c.Rotations[k].ToArray();
applied++;
}
return applied;
}
public static void Save(string folder,List<Correction> corrections)
{
var path=Path.Combine(folder,File);
if(corrections.Count==0){System.IO.File.Delete(path);return;}
System.IO.File.WriteAllText(path,JsonSerializer.Serialize(corrections));
}
public static List<Correction> Load(string folder)
{
var path=Path.Combine(folder,File);
return System.IO.File.Exists(path)?JsonSerializer.Deserialize<List<Correction>>(System.IO.File.ReadAllText(path))??new():new();
}
/// <summary>The torso as an upright oval tube from the hips to the neck, sized from the shoulders; Clearance is 1
/// on its surface (widened by a forearm's thickness), below 1 inside and above 1 outside.</summary>
sealed record TorsoTube(Vector3 Bottom,Vector3 Up,float Height,Vector3 Across,Vector3 Forward,float Width,float Depth)
{
public float Clearance(Vector3 p)
{
var v=p-Bottom;var along=Vector3.Dot(v,Up)/Height;if(along<.05f||along>.95f)return float.PositiveInfinity;
float x=Vector3.Dot(v,Across)/Width,z=Vector3.Dot(v,Forward)/Depth;return MathF.Sqrt(x*x+z*z);
}
}
static TorsoTube? Torso(Vector3[] p,int hips,int neck,int shoulderL,int shoulderR)
{
if(hips<0||neck<0||shoulderL<0||shoulderR<0)return null;
var axis=p[neck]-p[hips];var height=axis.Length();if(height<1e-4f)return null;var up=axis/height;
var across=p[shoulderL]-p[shoulderR];var span=across.Length()/2;across-=up*Vector3.Dot(across,up);if(across.LengthSquared()<1e-8f||span<1e-4f)return null;
across=Vector3.Normalize(across);
return new(p[hips],up,height,across,Vector3.Cross(across,up),(.7f+.18f)*span,(.55f+.18f)*span);
}
/// <summary>Whether 2D joint <paramref name="k"/> lies inside the shoulders-and-hips outline (all four seen).</summary>
static bool InsideTorsoOutline(float[] o,int k)
{
int[] corners={5,6,12,11};
if(corners.Any(c=>o[c*3+2]<.5f))return false;
float x=o[k*3],y=o[k*3+1];var inside=false;
for(var a=0;a<4;a++)
{
float x1=o[corners[a]*3],y1=o[corners[a]*3+1],x2=o[corners[(a+1)%4]*3],y2=o[corners[(a+1)%4]*3+1];
if((y1>y)!=(y2>y)&&x<(x2-x1)*(y-y1)/(y2-y1)+x1)inside=!inside;
}
return inside;
}
/// <summary>Distances from the camera just in front of and just behind the torso at the height of <paramref name="p"/>: the
/// torso's middle there (between hips and neck), less or plus its half-depth (from the shoulders) and <see cref="BehindTorsoMargin"/>.
/// Both infinite when the joints are missing, so no hand counts as behind.</summary>
static (float Front,float Back) TorsoDepths(Vector3[] positions,int hips,int neck,int shoulderL,int shoulderR,Vector3 p)
{
if(hips<0||neck<0||shoulderL<0||shoulderR<0)return(float.PositiveInfinity,float.PositiveInfinity);
var low=positions[hips];var high=positions[neck];if(MathF.Abs(high.Y-low.Y)<1e-3f)return(float.PositiveInfinity,float.PositiveInfinity);
var f=Math.Clamp((p.Y-low.Y)/(high.Y-low.Y),0,1);var middle=-Vector3.Lerp(low,high,f).Z;
var halfDepth=.55f*Vector3.Distance(positions[shoulderL],positions[shoulderR])/2;
return(middle-halfDepth-BehindTorsoMargin,middle+halfDepth+BehindTorsoMargin);
}
static (Vector3[] Positions,Quaternion[] Rotations) World(MotionDocument motion,int t)
{
var bones=motion.Bones;var frame=motion.Frames[t];var p=new Vector3[bones.Count];var q=new Quaternion[bones.Count];
for(var i=0;i<bones.Count;i++)
{
var parent=bones[i].Parent;var local=Quaternion.Normalize(MotionDocument.Q(frame.Rotations[i]));var offset=MotionDocument.V(frame.Positions[i]);
if(parent<0){p[i]=offset;q[i]=local;}else{p[i]=p[parent]+Vector3.Transform(offset,q[parent]);q[i]=Quaternion.Normalize(q[parent]*local);}
}
return (p,q);
}
static Quaternion Between(Vector3 from,Vector3 to)
{
from=Vector3.Normalize(from);to=Vector3.Normalize(to);var dot=Vector3.Dot(from,to);
if(dot>.999999f)return Quaternion.Identity;
if(dot<-.999999f){var axis=Vector3.Cross(Vector3.UnitX,from);if(axis.LengthSquared()<1e-6f)axis=Vector3.Cross(Vector3.UnitY,from);return Quaternion.CreateFromAxisAngle(Vector3.Normalize(axis),MathF.PI);}
var c=Vector3.Cross(from,to);return Quaternion.Normalize(new Quaternion(c.X,c.Y,c.Z,1+dot));
}
}