InferenceWorker/VideoCrop.cs
// UDP/DARK decoding adapted from OpenMMLab code in GVHMR.
// Copyright (c) OpenMMLab. All rights reserved. See ../Editor/HumanoidMocap/Inference/Gvhmr.LICENSE.
using System.Runtime.InteropServices;
using HumanoidMocap.Inference;
using OpenCvSharp;
namespace HumanoidMocap.Worker;
public static class VideoCrop
{
/// <summary>GVHMR get_batch(path_type="np", img_ds=1): full-resolution RGB, conditional Gaussian antialias,
/// affine square crop to 256, center 192 columns, ImageNet normalization.</summary>
public static float[] Prepare(DecodedVideoFrame frame,GvhmrDecoder.Box box,string? previewFile=null)
{
if(box.Size<=0||!float.IsFinite(box.Size)||!float.IsFinite(box.CenterX)||!float.IsFinite(box.CenterY))throw new ArgumentException("Invalid person crop.");
using var rgba=new Mat(frame.Height,frame.Width,MatType.CV_8UC4);Marshal.Copy(frame.Rgba,0,rgba.Data,frame.Rgba.Length);
using var rgb=new Mat();Cv2.CvtColor(rgba,rgb,ColorConversionCodes.RGBA2RGB);
using var small=rgb.Clone();
var factor=box.Size/256/2;
if(factor>1.1)Cv2.GaussianBlur(small,small,new Size(5,5),(factor-1)/2);
var cx=box.CenterX;var cy=box.CenterY;var half=box.Size*.5f;
using var affine=Cv2.GetAffineTransform(new[]{new Point2f(cx-half,cy-half),new Point2f(cx+half,cy-half),new Point2f(cx,cy)},
new[]{new Point2f(0,0),new Point2f(255,0),new Point2f(127.5f,127.5f)});
using var crop=new Mat();Cv2.WarpAffine(small,crop,affine,new Size(256,256),InterpolationFlags.Linear,BorderTypes.Constant,Scalar.Black);
if(previewFile is not null){using var bgr=new Mat();Cv2.CvtColor(crop,bgr,ColorConversionCodes.RGB2BGR);WriteImage(previewFile,bgr);}
var bytes=new byte[256*256*3];Marshal.Copy(crop.Data,bytes,0,bytes.Length);
float[] mean={.485f,.456f,.406f},std={.229f,.224f,.225f};var normalized=new float[3*256*192];
for(var c=0;c<3;c++)for(var y=0;y<256;y++)for(var x=0;x<192;x++)
normalized[(c*256+y)*192+x]=(bytes[(y*256+x+32)*3+c]/255f-mean[c])/std[c];
return normalized;
}
public static float[] FlipImage(float[] pixels)
{var result=new float[pixels.Length];for(var c=0;c<3;c++)for(var y=0;y<256;y++)for(var x=0;x<192;x++)result[(c*256+y)*192+x]=pixels[(c*256+y)*192+191-x];return result;}
public static float[] AverageFlippedHeatmaps(float[] original,float[] flipped)
{
if(original.Length!=17*64*48||flipped.Length!=original.Length)throw new ArgumentException("Expected COCO17 heatmaps.");
var result=new float[original.Length];
for(var joint=0;joint<17;joint++)for(var y=0;y<64;y++)for(var x=0;x<48;x++)
{var paired=joint==0?0:joint%2==1?joint+1:joint-1;result[(joint*64+y)*48+x]=(original[(joint*64+y)*48+x]+flipped[(paired*64+y)*48+47-x])*.5f;}
return result;
}
/// <summary>GVHMR's COCO UDP/DARK heatmap decoding, including kernel 11 and unblurred maxima.
/// Output scores are actual backend heatmap maxima, not calibrated visibility probabilities.</summary>
public static float[] DecodeHeatmaps(float[] heatmaps,GvhmrDecoder.Box box)
{
if(heatmaps.Length!=17*64*48||heatmaps.Any(v=>!float.IsFinite(v)))throw new ArgumentException("Invalid COCO heatmaps.");
var keypoints=new float[51];
for(var joint=0;joint<17;joint++)
{
var map=heatmaps.AsSpan(joint*64*48,64*48).ToArray();var peak=0;for(var i=1;i<map.Length;i++)if(map[i]>map[peak])peak=i;
var confidence=map[peak];var x=confidence>0?peak%48:-1;var y=confidence>0?peak/48:-1;
using var matrix=new Mat(64,48,MatType.CV_32FC1);Marshal.Copy(map,0,matrix.Data,map.Length);Cv2.GaussianBlur(matrix,matrix,new Size(11,11),0);Marshal.Copy(matrix.Data,map,0,map.Length);
for(var i=0;i<map.Length;i++)map[i]=MathF.Log(Math.Clamp(map[i],.001f,50));
float At(int dx,int dy)=>map[Math.Clamp(y+dy,0,63)*48+Math.Clamp(x+dx,0,47)];
var center=At(0,0);var xp=At(1,0);var xm=At(-1,0);var yp=At(0,1);var ym=At(0,-1);
var dx=.5f*(xp-xm);var dy=.5f*(yp-ym);
double xx=xp-2*center+xm+1.1920928955078125e-7,yy=yp-2*center+ym+1.1920928955078125e-7;
double xy=.5f*(At(1,1)-xp-yp+center+center-xm-ym+At(-1,-1));var determinant=xx*yy-xy*xy;
if(Math.Abs(determinant)<1e-20)throw new ArithmeticException("Singular DARK heatmap refinement.");
var refinedX=x-(yy*dx-xy*dy)/determinant;var refinedY=y-(xx*dy-xy*dx)/determinant;
keypoints[joint*3]=(float)(refinedX*(box.Size*.75f/47)+box.CenterX-box.Size*.375f);
keypoints[joint*3+1]=(float)(refinedY*(box.Size/63)+box.CenterY-box.Size*.5f);keypoints[joint*3+2]=confidence;
}
return keypoints;
}
public static void SaveOverlay(DecodedVideoFrame frame,float[] joints,string path)
{
using var rgba=new Mat(frame.Height,frame.Width,MatType.CV_8UC4);Marshal.Copy(frame.Rgba,0,rgba.Data,frame.Rgba.Length);
using var image=new Mat();Cv2.CvtColor(rgba,image,ColorConversionCodes.RGBA2BGR);
var edges=new[]{(5,6),(5,7),(7,9),(6,8),(8,10),(5,11),(6,12),(11,12),(11,13),(13,15),(12,14),(14,16),(0,1),(0,2),(1,3),(2,4)};
Point P(int j)=>new((int)Math.Round(joints[j*3]),(int)Math.Round(joints[j*3+1]));
foreach(var (a,b) in edges)if(joints[a*3+2]>.3&&joints[b*3+2]>.3)Cv2.Line(image,P(a),P(b),new Scalar(30,240,30),2);
for(var j=0;j<17;j++)if(joints[j*3+2]>.3)Cv2.Circle(image,P(j),4,new Scalar(0,100,255),-1);
WriteImage(path,image);
}
/// <summary>Encodes in memory and writes through .NET; OpenCV's own writer cannot open non-ASCII paths.</summary>
static void WriteImage(string path,Mat image)
{
var extension=Path.GetExtension(path);if(!Cv2.ImEncode(extension.Length>0?extension:".png",image,out var bytes))throw new IOException("Cannot encode "+Path.GetFileName(path));
File.WriteAllBytes(path,bytes);
}
}