15 ноября 2015 г.

Получение данных с камеры телефона на Unreal Engine 4 + Android

Задача: получить и обработать данные с камеры телефона на Android в рамках проекта на Unreal Engine 4.

Необходимость в этом возникла при попытке использовать камеру на телефоне для определения местоположение телефона относительно помещения, в котором его используют. Это касается и AR- и VR-игр.

Как обычно, я запишу, что я делал для этого, чтобы потом вспомнить. Будет небольшая каша из-за того, что у меня используется моя библиотека markers detector, которая вам, скорее всего, не нужна. Но, надеюсь, вы поймёте ход моих мыслей и сможете увидеть всю картину целиком. Учитывая необходимость править файлы из директории игрового движка, использовать Java, NDK и C++, я бы не назвал весь этот подход очевидным.

Исследование готовых вариантов

Первое, что удалось найти - это использовать OpenCV второй версии, который имеет нативные (.so) библиотеки для работы с камерой на разных телефонах. После многодневных попыток было установлено, что библиотеки не работают на многих устройствах. Кроме того, в OpenCV 3 совсем отказались от использования native-camera. Цитата с официального сайта:
The other, a bit sad but inevitable news - we had to drop support for nativecamera on Android. It used some undocumented API and never worked stable.
Ничего другого готового найти не удалось, поэтому я пошёл своим путём, собранным по кусочкам.

Общая схема работы

Было решено использовать связку обычного java-кода и C++ кода из NDK. Получить данные с камеры можно без особых проблем фрагментации устройств через android.hardware.Camera, в котором есть метод setPreviewCallback. Его использование требует немного дополнительного кода, но в целом, свою задачу выполняет. Получаемые данные нам нужно передать в нативный код через JNI-вызовы. В java-части описываем JNI функцию:
public native boolean FrameProcessing(int width, int height, byte[] NV21FrameData);
Затем, при получении данных из
onPreviewFrame(byte[] FrameData, Camera camera)
делаем вызов
FrameProcessing(width, height, FrameData);
В java-части я оформил это отдельным классом AndroidCamera, запуск и остановку работы с камерой вынес в публичные методы. Эти методы вызываются из Activity на onPause и оnResume.
Был один неприятный момент. На Android API новее 10 массив FrameData был постоянно пустым. Избежать этого удалось, указав фиктивную текстуру предпросмотра кодом:
if (Build.VERSION.SDK_INT > 10) {
    SurfaceTexture surfaceTexture = new SurfaceTexture(10);
    try {
        mCamera.setPreviewTexture(surfaceTexture);
    } catch (IOException e) {
        e.printStackTrace();
    }   
}
Далее привожу код, который написан в собираемом NDK cpp-файле:
extern "C"
jboolean
Java_org_getid_markersdetector_AndroidCamera_FrameProcessing(
JNIEnv* env, jobject thiz,
jint width, jint height,
jbyteArray NV21FrameData)
{
 
        jbyte* pNV21FrameData = env->GetByteArrayElements(NV21FrameData, NULL);
 
        cv::Mat yuv(height + height/2, width, CV_8UC1, (uchar*)pNV21FrameData);
        cv::Mat bgr(height, width, CV_8UC4);
        cv::cvtColor(yuv, bgr, CV_YUV2BGR_NV21);
    
        frame_mutex.lock();
        MarkersDetector::androidFrame = bgr;
        frame_mutex.unlock();

        env->ReleaseByteArrayElements(NV21FrameData, pNV21FrameData, JNI_ABORT);
 
        return true;
}
Здесь уже используется OpenCV, но это лишь для удобства, так как OpenCV предоставляет контейнер для данных изображения cv::Mat, с помощью которого можно легко преобразовывать форматы изображений.
Кроме того, здесь используется MarkersDetector - это моя библиотека, которая ищет маркеры по контурам, используя полученное изображение. Как видно, передача данных в статическое поле класса обёрнута в mutex. Это нужно потому что данные из этого поля читаются в другом потоке (так же с использованием mutex). Если этого не сделать, то возможны случайные вылеты приложения.

Схематично уровни передачи данных выглядят так:
UE4
^ (событие Tick внутри Blueprint)
MarkersDetector::androidFrame
^
native (Java_org_getid_markersdetector_AndroidCamera_FrameProcessing)
^ (onPreviewFrame)
java (AndroidCamera)

Сборка проекта на Unreal Engine 4 для Android

У вас должны быть установлены SDK и NDK Android. UE4 рекомендует использовать NVIDIA WORKS - пакет, установщик которого поставляется вместе с UE4 и содержит всё необходимое. Даже OpenCV.

Так как у меня была моя библиотека на OpenCV для обработки изображений, то через нее я и получал данные с камеры. Всё как на схеме выше. Вариант без такой библиотеки я не пробовал, поэтому расскажу с ней. Ещё раз ссылка на Github-репозиторий.

Первым делом отдельно собирается с помощью NDK библиотека, в которой описана сама нативная функция FrameProcessing и которая имеет методы для получения данных видео-фрейма. Сборка очень проста. Нужно соблюсти минимальную структуру проекта, описать Android.mk и вызвать ndk-build из пакета NDK.
Собранная so-библиотека подключается как зависимость проекта через build.cs файл вашего проекта на UE4.
Ранее я описывал такую сборку. В примере я собирал под Windows, здесь дополнительно нужно подключить so-файл.

Android.mk

Unreal Engine 4 в текущей версии не скопирует вашу библиотеку и не включит её в apk-файл без правки файлов в каталоге самого движка. Для начала нужно найти, куда установлен UE4 и там отредактировать Android.mk, указав там вашу библиотеку.
В файл
Epic Games/4.9/Engine/Build/Android/Java/jni/Android.mk
добавляется зависимость в виде PREBUILT_SHARED_LIBRARY:
include $(CLEAR_VARS)
LOCAL_MODULE := markersdetector
LOCAL_SRC_FILES := $(TARGET_ARCH_ABI)/libmarkersdetector.so
include $(PREBUILT_SHARED_LIBRARY)
Я собираю под armeabi-v7a, поэтому libmarkersdetector.so у меня расположен рядом в папке armeabi-v7.

GameActivity

Так как я подключаю колбеки к камере из java-кода, тот этот код должен быть вызван из Activity. У Unreal Engine есть свой класс Activity, который находится в файле:
Epic Games/4.9/Engine/Build/Android/Java/src/com/epicgames/ue4/GameActivity.java
Вот в него я и вписываю инициализацию и вызовы своего AndroidCamera в методы onCreate, onPause, onResume, onDestroy. Не забудьте разместить AndroidCamera.java в директории
Epic Games/4.9/Engine/Build/Android/Java/src/org/getid/markersdetector/
Важно: чтобы ue4 загрузил вашу нативную (.so) библиотеку, в GameActivity.java нужно перед
System.loadLibrary("UE4");
дописать строчку с вашей библиотекой
System.loadLibrary("markersdetector"); //для загрузки файла libmarkersdetector.so
Готовый файл GameActivity.java для UE 4.9 можете скачать здесь.

Сборка

После всех этих действий можно собирать проект через редактор Unreal Engine. На выходе будет apk-файл. Для дебага можно узнать, что вошло в apk, исследовав папку Intermediate в директории вашего проекта.

Комментариев нет:

Отправить комментарий