Исходные данные:
- Нейросеть SMPLer на PyTorch (), которая по изображению с веб-камеры регрессирует параметры SMPL модели (форму и позу). Далее эта нейросеть была экспортирована в onnx и загружена в Godot;
- Приложение на Godot на языке C#, которое запускает нейросеть в onnx и получает позу в виде матрицы вращения. Далее поза применяется к костям 3д модели через перенос вращений
Подозреваю что проблема в том, что локальные оси SMPL модели совпадают с мировыми осями. В то время как в Godot локальные оси костей ориентированы вдоль самой кости ()
Пробовал переставлять оси, делать поворот вокруг них чтобы скорректировать разные системы координат, ничего не помогло. По итогу модель не повторяет движения за человеком с веб-камеры, а больше выглядит как сломанная кукла (локти вывернуты, перекручены суставы и тд).
Подскажите, пожалуйста, как все-таки правильно перенести позу smpl модели на другую 3д модель? Может стоит сменить подход на обратную кинематику? Буду рад за любой совет или код
Ниже код на C#, в котором метод InferenceModelONNX запускает производит инференс модели и отдает полученную матрицу вращения сустава в ApplyToBone, который и применяет вращение
public void LoadModelONNX()
{
string modelPath = @"D:\Projects\SMPLer\SMPLer_h36m.onnx";
var sessionOptions = new SessionOptions();
sessionOptions.AppendExecutionProvider_CUDA(0);
this.session = new InferenceSession(modelPath, sessionOptions);
}
public void InferenceModelONNX()
{
// 2. Инициализация камеры
using var capture = new VideoCapture(0);
if (!capture.IsOpened())
{
GD.Print("Ошибка: Камера не найдена.");
return;
}
using var frame = new Mat();
while (OpenCvSharp.Window.WaitKey(1) == -1)
{
Stopwatch stopWatch = new Stopwatch();
stopWatch.Start();
// var frame = new Mat(@"D:\Projects\SMPLer\samples\im03.png");
capture.Read(frame);
if (frame.Empty()) break;
// --- ПРЕДОБРАБОТКА (Аналог transforms.Compose) ---
// Resize [224, 224]
using var resized = frame.Resize(new OpenCvSharp.Size(224, 224));
// ToTensor + Normalize
// Константы из PyTorch transform
float[] mean = { 0.485f, 0.456f, 0.406f };
float[] std = { 0.229f, 0.224f, 0.225f };
// Подготовка тензора (Format: NCHW - Batch, Channels, Height, Width)
var inputTensor = new DenseTensor<float>(new[] { 1, 3, 224, 224 });
for (int y = 0; y < 224; y++)
{
for (int x = 0; x < 224; x++)
{
var pixel = resized.At<Vec3b>(y, x);
// OpenCV использует BGR, PyTorch ожидает RGB
// Нормализация: (pixel / 255.0 - mean) / std
inputTensor[0, 0, y, x] = ((pixel.Item2 / 255.0f) - mean[0]) / std[0]; // R
inputTensor[0, 1, y, x] = ((pixel.Item1 / 255.0f) - mean[1]) / std[1]; // G
inputTensor[0, 2, y, x] = ((pixel.Item0 / 255.0f) - mean[2]) / std[2]; // B
}
}
// --- ИНФЕРЕНС ---
var inputs = new List<NamedOnnxValue>
{
NamedOnnxValue.CreateFromTensor("input", inputTensor)
};
using IDisposableReadOnlyCollection<DisposableNamedOnnxValue> results =
this.session.Run(inputs);
stopWatch.Stop();
GD.Print(1000 / stopWatch.ElapsedMilliseconds);
// --- ОБРАБОТКА ВЫХОДА ---
//Извлекаем позу theta (матрицу вращений всех 24 суставов)
var thetaValue = results.First(r => r.Name == "theta");
var thetaTensor = thetaValue.AsTensor<float>(); // Получаем доступ как к тензору
int numJoints = thetaTensor.Dimensions[1]; // Обычно 24 для SMPL
Basis modelCorrection = new Basis(Vector3.Up, Mathf.Pi);
for (int j = 0; j < numJoints; j++)
{
//Матрица вращений для одного сустава
Basis smplGlobalBasis = new Basis(
new Vector3(thetaTensor[0, j, 0, 0], thetaTensor[0, j, 0, 1], thetaTensor[0, j, 0, 2]),
new Vector3(thetaTensor[0, j, 1, 0], thetaTensor[0, j, 1, 1], thetaTensor[0, j, 1, 2]),
new Vector3(thetaTensor[0, j, 2, 0], thetaTensor[0, j, 2, 1], thetaTensor[0, j, 2, 2])
);
// Применяем общую коррекцию к глобальному вращению SMPL
Basis correctedGlobal = modelCorrection * smplGlobalBasis;
ApplyToBone(j, correctedGlobal);
}
// Визуализация кадра
Cv2.ImShow("Webcam ONNX GPU", frame);
}
}
// Соответствие SMPL Joint → Bone Name 3д м
private string GetBoneNameForSMPLJoint(int j)
{
switch (j)
{
// Левая рука
case 16: return "arm_stretch.l";
case 18: return "forearm_stretch.l";
case 20: return "hand.l";
// Правая рука
case 17: return "arm_stretch.r";
case 19: return "forearm_stretch.r";
case 21: return "hand.r";
default: return null;
}
}
private void ApplyToBone(int smplJointId, Basis smplGlobalBasis)
{
string boneName = GetBoneNameForSMPLJoint(smplJointId);
if (string.IsNullOrEmpty(boneName)) return;
int boneIdx = skeleton.FindBone(boneName);
if (boneIdx == -1) return;
// 1. Получаем глобальную ПОКОЙНУЮ (rest) позу кости в Godot.
// Это ориентация кости, когда все PoseRotation равны нулю.
Basis globalRestBasis = skeleton.GetBoneGlobalRest(boneIdx).Basis;
// 2. Вычисляем локальную позу, которая повернет кость из rest-состояния
// в целевое мировое состояние smplGlobalBasis.
// Формула: Pose = (GlobalRest)^-1 * TargetGlobal
Basis localPose = globalRestBasis.Inverse() * smplGlobalBasis;
// 3. Применяем
skeleton.SetBonePoseRotation(boneIdx, localPose.GetRotationQuaternion());
}