Code/HumanoidMocap/Motion/CaptureViewpoint.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
using HumanoidMocap.Mapping;

namespace HumanoidMocap.Motion;
using Vector3 = System.Numerics.Vector3;

/// <summary>Whether a hand capture was filmed from the performer's own head or chest, or by a camera
/// facing them. Seen from their own head a person's right hand is on the right of the picture; seen
/// from in front it is on the left. Treating footage of someone facing the camera as head-mounted put
/// the character's right hand on its left and crossed its arms. This reads only the side of the
/// picture each reconstructed wrist occupies; it is a viewpoint guess, not camera recovery.</summary>
public static class CaptureViewpoint
{
    /// <summary>Distance in front of the character's shoulders for a camera that faces them, in metres:
    /// the 0.45 m typical hand distance plus hands held about 0.35 m in front of the body.</summary>
    public const float FacingCameraDistance=.8f;
    /// <summary>Chest height below the shoulder line on a 1.45 m shoulder-height figure, in metres.</summary>
    public const float ChestBelowShoulders=.22f;
    public const float MinimumSeparation=.05f,MinimumOffset=.06f;
    public static bool FacesSubject(MotionDocument document)
    {
        if(document.Space!=MotionSpace.CameraRelative)return false;
        var left=document.Bones.FindIndex(b=>b.Role==BoneRole.HandL);var right=document.Bones.FindIndex(b=>b.Role==BoneRole.HandR);
        var both=new List<float>();var onlyLeft=new List<float>();var onlyRight=new List<float>();
        foreach(var frame in document.Frames)
        {
            var l=left>=0&&frame.Evidence[left]==JointEvidence.Reconstructed;var r=right>=0&&frame.Evidence[right]==JointEvidence.Reconstructed;
            // Document space keeps the camera's left-right axis: +X is the right of the picture.
            if(l&&r)both.Add(frame.Positions[right][0]-frame.Positions[left][0]);
            else if(l)onlyLeft.Add(frame.Positions[left][0]);else if(r)onlyRight.Add(frame.Positions[right][0]);
        }
        static float Median(List<float> values){values.Sort();return values[values.Count/2];}
        if(both.Count>=8)return Median(both)<-MinimumSeparation;
        // One hand: a wearer's right hand works on the right of the picture, a facing subject's on the left.
        if(onlyRight.Count>=8&&onlyRight.Count>=onlyLeft.Count)return Median(onlyRight)<-MinimumOffset;
        if(onlyLeft.Count>=8)return Median(onlyLeft)>MinimumOffset;
        return false;
    }
    /// <summary>Placement for a camera facing the character: in front of its shoulders, looking back at it.</summary>
    public static void PlaceFacingCamera(TargetCorrectionSettings settings)
    {
        var looking=Vector3.Transform(-Vector3.UnitZ,Quaternion.CreateFromYawPitchRoll(settings.CaptureCameraYawDegrees*MathF.PI/180,0,0));
        looking.Y=0;if(looking.LengthSquared()<1e-8f)return;looking=Vector3.Normalize(looking);
        var shoulders=(settings.LeftShoulder+settings.RightShoulder)*.5f;
        // Hands filmed from in front are usually held at chest height and framed near the picture's centre.
        settings.CaptureCameraPosition=shoulders-Vector3.UnitY*(ChestBelowShoulders*shoulders.Y/1.45f)+looking*FacingCameraDistance;
        var yaw=settings.CaptureCameraYawDegrees+180;settings.CaptureCameraYawDegrees=yaw>180?yaw-360:yaw;
        settings.CaptureCameraPitchDegrees=0;settings.CaptureFacesSubject=true;
    }
}