Здравствуйте!
Мы – Агафонова Екатерина и Гольцова Мария, студентки 3 курса ВШЭ, направления Информатика и вычислительная техника. В этой статье мы расскажем Вам идею о том, как заставить манекена в Unreal Engine 5 повторять движения за человеком в кадре в реальном времени при помощи Python, нейронных сетей и API-запросов, а также поделимся наработками проекта “Виртуальный аватар без мокап-костюма”.
Расскажем об установке ПО, приведем инструкцию по созданию и анимированию персонажа в Unreal Engine, поговорим о нейросетях и главной причине остановки работы - API.
Это костюм для записи движений на съемочной площадке с помощью датчиков. Несколько камер, расположенных под разными углами к движению, снимают динамику маркеров-отражателей или светодиодов, распределенных по телу движущегося «актёра». Далее, специальное ПО вычисляет координаты каждого маркера в пространстве в определенные моменты времени, соотнося данные с каждой камеры.
В киноиндустрии на данный момент существует ряд проблем, связанных с мокап-костюмом, а именно:
Цена
Мокап-костюм, используемый при создании фильмов, стоит от 400 тыс. руб (!!!), что существенно влияет на бюджет фильма и компании в целом.
Сложность
Подготовка актера к съемке является довольно трудозатратной: чего только стоит надеть этот костюм и настроить все датчики… Да и подготовка съемочной площадки также требует немалых усилий.
Время
Кроме того, после съемки актеров в мокап костюмах требуется монтаж и наложение, что существенно увеличивает время производства фильма.
Выяснив вышеперечисленные недостатки, мы решили создать альтернативный вариант считывания движений человека в кадре.
Перейдем к его реализации!
Сначала через лаунчер Epic Games скачаем движок Unreal Engine версии 5.0 и выше (мы используем v5.3.2) и Quixel Bridge, из которого загружаем MetaHuman.
Обратите внимание!
Движок весит очень много, поэтому необходимо, чтобы на диске было не менее 60 Гб.
После запуска UE, выберем создание проекта от 3-го лица в категории Игры. Зададим название в графе Project Name, а настройки, изображенные справа, оставим без изменений. Create!
Не волнуйтесь, в Unreal-е многие загрузки занимают длительное время.
Теперь мы можем поставить на поле некого робота, которого мы хотим анимировать. Обратите внимание, что сейчас мы не можем сразу поставить детализированного персонажа, похожего на реального человека - пока только доступны роботоподобные манекены. Для добавления такого железного человека перейдем в Content Drawer, перетащим его на поле.
Теперь сделаем импорты для работы с MetaHuman.
Войдем в систему Quixel Bridge. Выберем персонажа Trey, потому что так будет проще сопоставить опорные точки скелета персонажа с опорными точками манекена (в ином случае мы столкнулись с несоответствием роста манекена и персонажа, что привело к необходимости дополнительной настройки).
Для импорта найденного персонажа - навести курсор и нажать на загрузку, затем Export.
После установки Unreal Engine и MetaHuman мы перешли к освоению программы. Начали с простого: повторения мимики с видео на лице аватара.
Для начала мы установили приложение LiveLink, позволяющее снимать движения лица в режиме реального времени с помощью совместимого iPhone или iPad.
Затем мы загрузили видео в Unreal Engine и приступили к созданию нашего аватара. Добавили три фронта: анфас (front), левый и правый профили (left и right).
И последнее – наложение кожи на аватара (для этого необходимо импортировать «человека») и анимация мимики:
Перейдем непосредственно к созданию персонажа в Unreal Engine.
Откроем несколько элементов в рабочем пространстве:
Сontent Drawer -> MetaHumans -> Trey выбирем блупринт BP_Trey, ThirdPerson -> Blueprints -> BP_ThirdPersonCharacter, ThirdPerson -> Blueprints -> BP_ThirdPersonGameMode.
Настроим BP_Trey:
Перейдя в настройки класса (Class Settings), выберем в качестве родительского класса персонажа Trey - BP_ThirdPersonCharacter (вклыдка Details, графа Parent Class).
Далее изменим иерархию. В компонентах (слева, Components) перенесем Body в Mesh, удалим Root.
Если мы скомпилируем проект (Compile), то получим ошибку.
Нажмем на строку с ошибкой. Откроется Set Update Animation in Editor. Перенесем Mesh на рабочее пространство и установим связь с Get Children Components. Теперь компиляция пройдет успешно.
Далее, перейдем на вкладку Viewport, чтобы настроить персонажа с манекеном. Обнулим положение и поворот персонажа в блоке Transform справа.
Нажатием по Live Retarget -> UseLiveRetargetMode откроем детали UseLiveRetargetMode и установим “галочку” в категории Default Value -> UseLiveRetargetMode. Увидим, что MetaHuman принял ту же позу, что и манекен.
Теперь в функциях перейдем по LiveRetargetSetup, где в схеме найдем узел RTGMetaHuman. Нажатием на browse увидим расположение соответствующего Animation Blueptint в Content Drawer.
Необходимо продублировать RTG_metahuman, переименовав его: далее мы будем настраивать этот дубликат. Настроим источник и таргет, выбрав соответствующие шаблоны. Настроим корень, как глобально масштабируемый (Translation Mode при нажатии на Root) для дублирования анимационных ресурсов.
Теперь надо более точно сопоставить тело манекена с персонажем. Перейдем в режим редактирования позы (Edit Retarget Pose), убедимся, что позы не совпадают полностью. Настроим показ костей, а затем, используя повороты суставов, самостоятельно повернем плечи и стопы, чтобы добиться наложения.
Сохраним и изменим ретаргетинг для анимации (RTG_metahuman_base_skel_AnumBP)
Здесь в AnimGraph-е, в позе для ретаргетинга (Retarget Pose and Mesh), изменим ресурс на тот, что был создан только что (RTG_Manny…). Скомпилируем и закроем.
Теперь перейдем в BP_Trey. Видим, что персонаж повторяет движение манекена. Чтобы убрать видимость манекена, нажмем на сетку персонажа, в деталях по поиску найдем видимость и уберем ее. В Visibility Based Anim Tick Option выберем Always Tuck Pose and Refresh Bones, для обеспечения синхронизации.
Вы могли заметить, персонаж не так детализирован, как должен, например, отсутствует борода. Настроим LOD через обнуление Forced LOD для появления деталей.
В режиме игры BP_ThirdPersonGameMode изменим родительский класс по умолчанию на блупринт персонажа Trey.
Последняя настройка. Из режима персонажа от 3 лица скопируем весь график событий кроме Event BeginPlay и вставим в график событий BP_Trey. Соединить добавленный в граф событий персонажа Event BeginPlay и Hair LODSetup со вставленным графиком.
Скомпилируем и сохраним. Готово! Мы настроили синхронизацию манекена с MetaHuman.
У манекенов есть большое количество встроенных готовых функций движения, таких как бег, прыжок. Но у них нет возможности поэлементного изменения локаций частей тела персонажа. Для этого мы прибегли к использованию дополнительных функций.
Мы нашли несколько способов управления костями, однако каждый из них имеет свои существенные недостатки, из-за чего мы, к сожалению, приостановили работу.
Первый способ заключается в использовании метода TwoBoneIK.
Во-первых, он удобен в использовании. С помощью построения небольшого графа, добавления пары узлов программа корректно сработала.
Во_вторых, он предоставляет возможность покоординатно менять положение костей и суставов. Эта задача была важна для нас, поскольку данные о позе человека на изображении приходят в виде массива точек (словарь {кость : [x, y, probability]}).
С помощью TwoBoneIK нам удалось задавать координаты суставов, костей тела и сразу же визуально замечать изменения в позе.
Таким образом, данный метод очень удобен в простоте, в возможности работать с каждой костью отдельно.
Нажатием на персонажа на игровом поле, откроем его блупринт BP_Trey. Зайдем в его сетку в компонентах слева. В деталях сетки увидим класс анимации. Перейдем к его местоположению. Двойным нажатием на подсвеченный элемент в Content Browser откроем ABP.
Теперь построим нехитрую схему с нашей функцией, как показано на изображении. Чтобы найти node с Output Pose - нажмем пару раз на стрелку “назад”. После создания схемы, скомпилируем этап.
Теперь нам надо указать, какую кость мы двигаем. Чтобы посмотреть иерархию костей, надо перейти в соответствующий mesh, найдя его в Content Drawer.
Нажатием на node функции в деталях установим название кости IKBone, EffectorLocationSpace, EffectorSpaceBoneName, Joint Target Location Space, Joint Target Location Space Bone Name.
Скомпилируем, зададим положение координат, которое мы хотим.
Теперь и наш персонаж принял то же положение, что и ABP.
В данном случае удалось удалось делать API-запросы, однако метод не привел к изменению позы манекена на поле.
Приведем пример с запросами API.
Мы перешли к тестированию апи запросов на обычном BP с родительским классом Actor.
Создадим новый блупринт класс, установим ему родительский класс Actor, переименуем.
Откроем созданный объект и добавим в его компоненты сетку.
Перейдем в детали сетки и установим skeletal mesh.
Теперь в графе событий составим схему установки локации кости:
Проблемы состоит в том, что мы не наблюдаем физические изменения вв позе человека таким образом. Мы руководствовались туториалом, но в нашей реализации результат оказался не таким же.
После ознакомления с Unreal Engine мы поняли, что хоть и MetaHuman – отличный вариант для повторения мимики человека, он не работает с движениями тела. Тогда наша команда начала копаться в нейронках и искать подходящие для нас варианты:
Tensorflow - библиотека для машинного обучения, позволяющая автоматически находить и классифицировать образы, достигая качества человеческого восприятия;
Pose AI – плагин для Unreal Engine, захватывающий движение всего тела;
Posenet – модель, предназначенная для обнаружения ключевых точек тела на фигурах людей;
DollarsMono – плагин для получения информации о движении в режиме реального времени и отправки информации о костях в Unity или Unreal Engine для управления моделями скелетов. Минусы – платно;
GoogleAI – модуль для анимации кистей рук. Минусы – получение точек только на кистях рук;
YOLOv8 - новейшее семейство моделей обнаружения объектов, сегментации экземпляров и классификации изображений на базе YOLO от Ultralytics, обеспечивающих самые современные характеристики.
Перебрав и изучив эти варианты, мы наткнулись на easyViTPose – плагин для простой и быстрой 2d-оценки людей и животных в разных позах с использованием SOTA ViTPose в режиме реального времени и с поддержкой нескольких скелетов.
Данный модуль считывает аж 133 точки на теле человека, что и послужило решающим фактором при выборе нейронки для нашего проекта.
Сначала мы скачали easyViTPose с гитхаба:
# Блок загрузки необходимых библиотек и модулей
!git clone https://github.com/JunkyByte/easy_ViTPose.git
!cd easy_ViTPose/ && pip install -r requirements.txt && pip install -e .
!pip install huggingface_hub
python -m easy_ViTPose/easy_ViTPose/vit_utils/visualization.py
Далее мы определили основные константы, такие как путь к файлам, размер модели, датасет и т.д.:
# Определение необходимых констант
MODEL_SIZE = 'b' #@param ['s', 'b', 'l', 'h']
YOLO_SIZE = 's' #@param ['s', 'n']
DATASET = 'wholebody' #@param ['coco_25', 'coco', 'wholebody', 'mpii', 'aic', 'ap10k', 'apt36k']
ext = '.pth'
ext_yolo = '.pt'
# Определение необходимых путей
import os
from huggingface_hub import hf_hub_download
MODEL_TYPE = "torch"
YOLO_TYPE = "torch"
REPO_ID = 'JunkyByte/easy_ViTPose'
FILENAME = os.path.join(MODEL_TYPE, f'{DATASET}/vitpose-' + MODEL_SIZE + f'-{DATASET}') + ext
FILENAME_YOLO = 'yolov8/yolov8' + YOLO_SIZE + ext_yolo
print(f'Downloading model {REPO_ID}/{FILENAME}')
model_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME)
yolo_path = hf_hub_download(repo_id=REPO_ID, filename=FILENAME_YOLO)
Затем мы импортировали предобученную модель
# Загрузка модели
from easy_ViTPose import VitInference
model = VitInference(model_path, yolo_path, MODEL_SIZE,
dataset=DATASET, yolo_size=320, is_video=False)
Далее мы запустили нейронку на одной картинке для демонстрации работоспособности и получили массив опорных точек
# Запуск модели на примере картинки
import numpy as np
from io import BytesIO
from PIL import Image
from urllib.request import urlopen
url = 'https://i.ibb.co/gVQpNqF/imggolf.jpg'
img = np.array(Image.open(BytesIO(urlopen(url).read())), dtype=np.uint8)
frame_keypoints = model.inference(img)
img = model.draw(show_yolo=True)
from google.colab.patches import cv2_imshow
cv2_imshow(img[..., ::-1])
После этого мы преобразовали массив точек в словарь
# Преобразование массива точек в словарь
keypoints = ['nose', 'left_eye','right_eye','left_ear','right_ear','left_shoulder','right_shoulder','left_elbow','right_elbow','left_wrist','right_wrist','left_hip','right_hip','left_knee','right_knee','left_ankle','right_ankle','left_big_toe','left_small_toe','left_heel','right_big_toe','right_small_toe','right_heel','face-0','face-1','face-2','face-3','face-4','face-5','face-6','face-7','face-8','face-9','face-10','face-11','face-12','face-13','face-14','face-15','face-16','face-17','face-18','face-19','face-20','face-21','face-22','face-23','face-24','face-25','face-26','face-27','face-28','face-29','face-30','face-31','face-32','face-33','face-34','face-35','face-36','face-37','face-38','face-39','face-40','face-41','face-42','face-43','face-44','face-45','face-46','face-47','face-48','face-49','face-50','face-51','face-52','face-53','face-54','face-55','face-56','face-57','face-58','face-59','face-60','face-61','face-62','face-63','face-64','face-65','face-66','face-67','left_hand_root','left_thumb1','left_thumb2','left_thumb3','left_thumb4','left_forefinger1','left_forefinger2','left_forefinger3','left_forefinger4','left_middle_finger1','left_middle_finger2','left_middle_finger3','left_middle_finger4','left_ring_finger1','left_ring_finger2','left_ring_finger3','left_ring_finger4','left_pinky_finger1','left_pinky_finger2','left_pinky_finger3','left_pinky_finger4','right_hand_root','right_thumb1','right_thumb2','right_thumb3','right_thumb4','right_forefinger1','right_forefinger2','right_forefinger3','right_forefinger4','right_middle_finger1','right_middle_finger2','right_middle_finger3','right_middle_finger4','right_ring_finger1', 'right_ring_finger2', 'right_ring_finger3', 'right_ring_finger4', 'right_pinky_finger1', 'right_pinky_finger2', 'right_pinky_finger3','right_pinky_finger4']
output = dict(zip(keypoints, frame_keypoints[0]))
В конце мы сделали запрос по API к объекту и функции TwoBoneIK в Unreal Engine
# API - запрос к объекту и функции TwoBoneIK в Unreal Engine
import os
import time
import requests
url = "http://localhost:30000/remote/object/call"
sess = requests.Session()
def change_bone(object_path, ikbone, effector_target, joint_target, x, y):
sess.put(url, json = {
"objectPath" : object_path,
"functionName": "TwoBoneIK",
"parameters" : {
"IKBone" : ikbone,
"Effector Location Space" : "Bone Space",
"Effector Target": effector_target,
"Joint Target Location Space" : "Parent Bone Space",
"Joint Target": joint_target,
"Effector Location": {"X": x, "Y": y, "Z":0}
}
})
change_bone(object_path="D:/Unreal Projects/Avatar/Content/__ExternalActors__/ThirdPerson/Maps/ThirdPersonMap/9/1J/2GPO9HV94ZVG7HY4NP6IZ1.uasset",
ikbone="hand_l", effector_target="hand_l", joint_target="upperarm_l", x=100, y=20)
Также мы продемонстрировали работу нейронной сети на видео
# Загружаем видео файл
video_capture = cv2.VideoCapture('video_file1.mp4')
writer = cv2.VideoWriter(
'output2.mp4',
cv2.VideoWriter_fourcc(*'mp4v'), # codec
25.0, # fps
(int(video_capture.get(3)),int(video_capture.get(4))), # width, height
isColor=len(frame.shape) > 2)
while True:
ret, frame_rgb = video_capture.read()
if not ret:
break
# Преобразуем кадр в RGB формат
#frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
# Детектируем людей и опорные точки скелета на кадре
frame_keypoints = model.inference(frame_rgb)
frame_rgb = model.draw(show_yolo=True)
# Отображаем кадр с прямоугольниками и опорными точками
#cv2_imshow(frame_rgb)
writer.write(frame_rgb)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
writer.release()
Отлично, кости движимы, мы знаем, как получить координаты точек тела, теперь приступим к этапу передачи координат в Unreal через API.
API запрос осуществляется через программу Insomnia.
Перед его созданием необходимо включить плагин API в UE5, перезагрузить проект.
Затем во встроенной консоли cmd слева внизу прописать WebControl.StartServer.
Далее пропишем запрос json формата.
Путь к объекту получаем нажатием правой кнопкой мыши по персонажу
Прописываем название функции и параметры. Отправляем запрос.
Итак, мы подошли к проблеме, с которой мы столкнулись при работе с TwoBoneIK. Мы не смогли подобраться к ней через путь к объекту в запросе API.
Неправильное название вызываемой функции.
TwoBoneIK - название функции в UE. Прописав только ее, мы получаем ошибку.
В документации Unreal Engine мы нашли название AnimNode_TwoBoneIK при обращении в JSON.
Однако тоже получили ошибку.
Неправильный путь к объекту
С помощью команды ниже мы выводим информацию об объектах.
Таким образом была найдена необходимая функция.
Посмотрим информацию по ней
На этом шаге возникли трудности с обращением к параметрам.
Обращение к функции через functionName:
Мы убедились в работоспособности запросов, через изменение и получение локаций костей. Числа, действительно, изменялись. Однако мы не смогли выяснить, куда же именно в UE они отправлялись, почему наш персонаж не двигался.
Запрос на получение локации:
Установка локации:
Получение локации после ее установки. Видим, что координаты изменились.
Значения на выходе запроса GetLocation меняются, то есть обращение по API происходит успешно. Но мы не смогли выявить, где же именно в UE эти изменения происходят.
Конфликт Mesh и PoseableMesh Возможно происходил конфликт сеток персонажа и сетки PoseableMesh. Мы перешли к тестированию апи запросов на обычном BP с родительским классом Actor.
Неправильное соединение с персонажем. Возможно, в графе событий пропущен узел или в некотором месте пропущена связь объектов.
Итак, мы выяснили, что для реализации синхронизации информации о позе человека на видеозаписи с движком Unreal Engine (чтобы создать соответствующей анимации детализированного персонажа) необходимо применить:
Метод машинного обучения на основе компьютерного зрения и алгоритмов динамического отслеживания движений;
Плагины и функции Unreal Engine
API-запросы в формате JSON для автоматической передачи обработанных данных в движок Unreal Engine
В ходе проведения эксперимента мы столкнулись с ошибками работы с API при передаче координат точке тела персонажа, что вызвало приостановление работы.
Мы выяснили, что задача, скорее всего, является выполнимой, однако ввиду малого опыта работы с используемыми программами нам не удалось ее решить. Надеемся, что кому-то все же удастся справиться с этой задачей. Удачи!