Code/HumanoidMocap/Inference/PalmDetectionFilter.cs
// Weighted suppression adapted from MediaPipe's NonMaxSuppressionCalculator.
// Copyright 2019 The MediaPipe Authors. Licensed under Apache-2.0.
// See Editor/HumanoidMocap/Inference/MediaPipe.LICENSE and THIRD_PARTY_NOTICES.md.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Numerics;
namespace HumanoidMocap.Inference;
using Vector2 = System.Numerics.Vector2;
public sealed record PalmDetection(float Score,float X,float Y,float Width,float Height,Vector2[] Points);
public static class PalmDetectionFilter
{
/// <summary>Merge boxes and keypoints by detection score. Each cluster is
/// compared with its highest-scoring original box, not its moving average.
/// The seed score is retained; averaging does not create a confidence score.</summary>
public static List<PalmDetection> Merge(IEnumerable<PalmDetection> candidates,int limit=2,float threshold=.3f)
{
var remaining=candidates.Where(p=>float.IsFinite(p.Score+p.X+p.Y+p.Width+p.Height)
&&p.Score>0&&p.Width>0&&p.Height>0&&p.Points.All(v=>float.IsFinite(v.X+v.Y)))
.OrderByDescending(p=>p.Score).ToList();
var result=new List<PalmDetection>();
while(remaining.Count>0&&result.Count<limit)
{
var seed=remaining[0];var rest=new List<PalmDetection>();
var points=new Vector2[seed.Points.Length];float weight=0,xmin=0,ymin=0,xmax=0,ymax=0;
foreach(var candidate in remaining)
{
if(Iou(seed,candidate)<=threshold){rest.Add(candidate);continue;}
if(candidate.Points.Length!=points.Length)throw new ArgumentException("Palm keypoint counts differ.");
var score=candidate.Score;weight+=score;
xmin+=(candidate.X-candidate.Width/2)*score;ymin+=(candidate.Y-candidate.Height/2)*score;
xmax+=(candidate.X+candidate.Width/2)*score;ymax+=(candidate.Y+candidate.Height/2)*score;
for(var i=0;i<points.Length;i++)points[i]+=candidate.Points[i]*score;
}
if(weight<=0)break;
xmin/=weight;ymin/=weight;xmax/=weight;ymax/=weight;
for(var i=0;i<points.Length;i++)points[i]/=weight;
result.Add(new(seed.Score,(xmin+xmax)/2,(ymin+ymax)/2,xmax-xmin,ymax-ymin,points));
remaining=rest;
}
return result;
}
static float Iou(PalmDetection a,PalmDetection b)
{
var width=Math.Max(0,Math.Min(a.X+a.Width/2,b.X+b.Width/2)-Math.Max(a.X-a.Width/2,b.X-b.Width/2));
var height=Math.Max(0,Math.Min(a.Y+a.Height/2,b.Y+b.Height/2)-Math.Max(a.Y-a.Height/2,b.Y-b.Height/2));
var intersection=width*height;var union=a.Width*a.Height+b.Width*b.Height-intersection;
return union>0?intersection/union:0;
}
}