Editor/Inference/YoloDetection.cs
// C# adaptation of YOLOX decoding and FlowHMR's person filtering. See THIRD_PARTY_NOTICES.md.
using System;
using System.Threading;
namespace HumanoidMocapX.Inference;
internal readonly record struct YoloBox(float Left, float Top, float Right, float Bottom, float Score);
/// <summary>Decodes raw YOLOX-L heads and applies class-agnostic suppression before
/// selecting people, as required by FlowHMR. Reuses candidate storage across frames.</summary>
internal sealed class YoloDetection
{
readonly Candidate[] candidates = new Candidate[YoloNetwork.Predictions];
readonly bool[] suppressed = new bool[YoloNetwork.Predictions];
const float ConfidenceThreshold = .5f, OverlapThreshold = .3f;
readonly record struct Candidate(YoloBox Box, int Class, int Anchor) : IComparable<Candidate>
{
public int CompareTo(Candidate other)
{
var order = other.Box.Score.CompareTo(Box.Score);
return order == 0 ? Anchor.CompareTo(other.Anchor) : order;
}
}
/// <summary>Writes original-image pixel boxes to output (capacity at least 8400),
/// returning the count. No detections returns zero; no fallback person is invented.
/// Rejects invalid shapes and non-finite predictions. Instances are not thread safe.</summary>
internal int Decode(ReadOnlySpan<float> logits, int width, int height, Span<YoloBox> output,
CancellationToken cancellation = default)
{
if (width < 1 || height < 1 || logits.Length != YoloNetwork.Predictions * YoloNetwork.PredictionChannels
|| output.Length < YoloNetwork.Predictions)
throw new ArgumentException("YOLOX decoding needs positive image dimensions, [8400,85] logits and capacity for 8400 boxes.");
cancellation.ThrowIfCancellationRequested();
var ratio = Math.Min(YoloNetwork.ImageSize / (double)width, YoloNetwork.ImageSize / (double)height);
var count = 0; var anchor = 0;
for (var stride = 8; stride <= 32; stride *= 2)
{
var size = YoloNetwork.ImageSize / stride;
for (var y = 0; y < size; y++)
for (var x = 0; x < size; x++, anchor++)
{
if ((anchor & 255) == 0) cancellation.ThrowIfCancellationRequested();
var row = logits.Slice(anchor * YoloNetwork.PredictionChannels, YoloNetwork.PredictionChannels);
foreach (var value in row)
if (!float.IsFinite(value)) throw new ArithmeticException("YOLOX prediction contains non-finite values.");
var bestClass = 0; var bestScore = Sigmoid(row[5]);
for (var category = 1; category < YoloNetwork.Classes; category++)
{
var score = Sigmoid(row[5 + category]);
if (score > bestScore) { bestScore = score; bestClass = category; }
}
var confidence = Sigmoid(row[4]) * bestScore;
if (confidence < ConfidenceThreshold) continue;
var centerX = (row[0] + x) * stride; var centerY = (row[1] + y) * stride;
var boxWidth = MathF.Exp(row[2]) * stride; var boxHeight = MathF.Exp(row[3]) * stride;
var box = new YoloBox((float)((centerX - boxWidth / 2) / ratio), (float)((centerY - boxHeight / 2) / ratio),
(float)((centerX + boxWidth / 2) / ratio), (float)((centerY + boxHeight / 2) / ratio), confidence);
if (!float.IsFinite(box.Left) || !float.IsFinite(box.Top) || !float.IsFinite(box.Right) || !float.IsFinite(box.Bottom))
throw new ArithmeticException("YOLOX box coordinates overflowed.");
if (box.Right > box.Left && box.Bottom > box.Top) candidates[count++] = new(box, bestClass, anchor);
}
}
Array.Sort(candidates, 0, count); Array.Clear(suppressed, 0, count);
var people = 0;
for (var index = 0; index < count; index++)
{
cancellation.ThrowIfCancellationRequested();
if (suppressed[index]) continue;
var candidate = candidates[index];
if (candidate.Class == 0) output[people++] = candidate.Box;
for (var next = index + 1; next < count; next++)
if (!suppressed[next] && Overlap(candidate.Box, candidates[next].Box) > OverlapThreshold) suppressed[next] = true;
}
return people;
}
static float Sigmoid(float value) => 1 / (1 + MathF.Exp(-value));
static double Overlap(YoloBox first, YoloBox second)
{
var intersection = Math.Max(0, (double)Math.Min(first.Right, second.Right) - Math.Max(first.Left, second.Left))
* Math.Max(0, (double)Math.Min(first.Bottom, second.Bottom) - Math.Max(first.Top, second.Top));
var firstArea = ((double)first.Right - first.Left) * ((double)first.Bottom - first.Top);
var secondArea = ((double)second.Right - second.Left) * ((double)second.Bottom - second.Top);
return intersection / (firstArea + secondArea - intersection);
}
}