Виртуальный квадрокоптер на Unity + OpenCV (Часть 2)

    КПДВ

    Доброго времени суток, дорогие хабравчане!

    Этой статьей я хотел бы продолжить серию о том, как подружить Unity, C++ и OpenCV. А также, как получить виртуальную среду для тестирования алгоритмов компьютерного зрения и навигации дронов на основе Unity. В предыдущей статье я рассказывал о том, как сделать виртуальный квадрокоптер в Unity. В этой статье речь пойдет о том, как подключить C++ плагин, передать туда изображение с виртуальной камеры и обработать его посредством OpenCV.

    Идея


    Идея состоит в том, чтобы рендерить изображение с камеры в текстуру. Далее эта текстура загружается в память видеокарты в Unity, откуда посредством OpenGL извлекается уже внутри C++ и загружается в матрицу OpenCV. Далее можно анализировать/обрабатывать ее OpenCV. И в конце можно передать модифицированную текстуру назад в Unity, загрузив ее на место соответствующей текстуры в память видеокарты. Итак приступим.

    Рендерим камеры в текстуры


    Для начала нам надо создать текстуры, в которые будут помещаться изображения с камер. Для этого в папке assets жмем правую кнопку, затем Create -> Render Texture. Далее нам нужно добавить камеру в наш квадрик. Это делается в Hierarchy, с помощью Create -> Camera. Добавим нашу камеру в Quadrocopter -> Frame, чтобы она прикрепилась к раме. В свойстве камеры Target Texture указываем Render Texture, который мы создали. Также нам хорошо бы видеть текстуру камеры в Unity. Для этого нам потребуется Create -> UI -> Canvas в нашей сцене. Этот объект служит для отображения элементов управления (как правило, двухмерных) на экране. Я назвал его CamerasTextures. В канвас нам надо добавить UI -> RawImage. В параметре Texture картинки указываем нашу текстуру. С помощью Rect Transform внутри RawImage позиционируем картинку в нужное место на экране, это проще всего сделать во вкладке Game. Запускаем и видим как меняется картинка с камеры, пока квадрокоптер летит.
    Виртуальный квадрокоптер с камерой

    Передача текстур в C++


    Официальную документацию по плагинам можно найти вот тут. Также могут быть полезны официальные примеры: этот и этот. Передавать и получать из C++ текстуры я буду в отдельном скрипте. Назовем его GameManager. Чтобы он работал надо добавить на сцену пустой объект и добавить в него этот скрипт. Ниже я, сначала, опишу отдельные части скрипта, потом приведу скрипт целиком.
    Функция, передающая идентификатор текстуры в C++
    	//указатель на текстуру, которая будет передана в плагин.
    	//указатель нам нужен чтобы иметь доступ в других частях скрипта
    	private Texture2D cam1Tex;
    	//эта функция вызывается один раз в начале работы программы
    	private void PassCamerasTexturesToPlugin () {
    		// Создаем текстуру
    		cam1Tex = new Texture2D(256,256,TextureFormat.ARGB32,false);
    		// Убираем фильтрацию
    		cam1Tex.filterMode = FilterMode.Point;
    		// Вызов Apply() загружает текстуру в GPU
    		cam1Tex.Apply();
    		// Помещаем вновь созданную текстуру на наш канвас.
    		// Да, мы меняем текстуру, заданную в предидущем параграфе
    		GameObject.Find ("/CamerasTextures/Camera1RawImage").GetComponent<RawImage>().texture = cam1Tex;
    		// Функции, которые непосредственно передают идентификатор текстуры в плагин
    #if UNITY_GLES_RENDERER
    		SetTextureOfCam1 (cam1Tex.GetNativeTexturePtr(), cam1Tex.width, cam1Tex.height);
    #else
    		SetTextureOfCam1 (cam1Tex.GetNativeTexturePtr());
    #endif
    	}
    	// Так выглядит объявление функции передачи идентификатора текстуры
    	// Определение ее будет внутри плагина
    #if UNITY_IPHONE && !UNITY_EDITOR
    	[DllImport ("__Internal")]
    #else
    	// Здесь указывается имя подгружаемой динамической библиотеки, в которой лежит плагин
    	[DllImport ("QuadrocopterBrain")]
    #endif
    #if UNITY_GLES_RENDERER
    	private static extern void SetTextureOfCam1(System.IntPtr texture, int w, int h);
    #else
    	private static extern void SetTextureOfCam1(System.IntPtr texture);
    #endif
    


    Функция, которая будет обновлять текстуру cam1Tex данными с камеры.
    	private IEnumerator CallPluginAtEndOfFrames () {
    		while (true) {
    			// Подождать пока выполнится рендеринг кадра
    			yield return new WaitForEndOfFrame();
    			RenderTexture cam1RT = GameObject.Find ("/Quadrocopter/Frame/Camera1").GetComponent<Camera>().targetTexture;
    			// Активный Render Texture - это тот, с которого будут прочитаны пиксели
    			RenderTexture.active = cam1RT;
    			cam1Tex.ReadPixels(new Rect(0, 0, cam1RT.width, cam1RT.height), 0, 0);
    			// Помещаем текстуру в GPU
    			cam1Tex.Apply ();
    			RenderTexture.active = null;
    
    			// Передаем управление в плагин
    			// Передающийся int можно использовать например для
    			// определения какое действие надо совершить в плагине.
    			// Я его использую как счетчик кадров в debug целях
    			GL.IssuePluginEvent(GetRenderEventFunc(), frameIndex++);
    		}
    	}
    
    // Объявление функции, которая будет возвращать функцию,
    // куда будет передано управление в C++
    #if UNITY_IPHONE && !UNITY_EDITOR
    	[DllImport ("__Internal")]
    #else
    	[DllImport("QuadrocopterBrain")]
    #endif
    	private static extern IntPtr GetRenderEventFunc();
    }
    


    Весь код скрипта должен выглядеть так
    using UnityEngine;
    using UnityEngine.UI;
    using System;
    using System.Collections;
    using System.Runtime.InteropServices;
    
    public class GameManager : MonoBehaviour {
    	
    	private Texture2D cam1Tex;
    	private int frameIndex = 0;
    	
    	IEnumerator Start () {
    		PassCamerasTexturesToPlugin ();
    		yield return StartCoroutine ("CallPluginAtEndOfFrames");
    	}
    	
    	private IEnumerator CallPluginAtEndOfFrames () {
    		while (true) {
    			// Подождать пока выполнится рендеринг кадра
    			yield return new WaitForEndOfFrame();
    			RenderTexture cam1RT = GameObject.Find ("/Quadrocopter/Frame/Camera1").GetComponent<Camera>().targetTexture;
    			// Активный Render Texture - это тот, с которого будут прочитаны пиксели
    			RenderTexture.active = cam1RT;
    			cam1Tex.ReadPixels(new Rect(0, 0, cam1RT.width, cam1RT.height), 0, 0);
    			// Помещаем текстуру в GPU
    			cam1Tex.Apply ();
    			RenderTexture.active = null;
    			
    			// Передаем управление в плагин
    			// Передающийся int можно использовать например для
    			// определения какое действие надо совершить в плагине.
    			// Я его использую как счетчик кадров в debug целях
    			GL.IssuePluginEvent(GetRenderEventFunc(), frameIndex++);
    		}
    	}
    
    	private void PassCamerasTexturesToPlugin () {
    		// Создаем текстуру
    		cam1Tex = new Texture2D(256,256,TextureFormat.ARGB32,false);
    		// Убираем фильтрацию
    		cam1Tex.filterMode = FilterMode.Point;
    		// Вызов Apply() загружает текстуру в GPU
    		cam1Tex.Apply();
    		// Помещаем вновь созданную текстуру на наш канвас.
    		// Да, мы меняем текстуру, заданную в предидущем параграфе
    		GameObject.Find ("/CamerasTextures/Camera1RawImage").GetComponent<RawImage>().texture = cam1Tex;
    		// Функции, которые непосредственно передают идентификатор текстуры в плагин
    		#if UNITY_GLES_RENDERER
    		SetTextureOfCam1 (cam1Tex.GetNativeTexturePtr(), cam1Tex.width, cam1Tex.height);
    		#else
    		SetTextureOfCam1 (cam1Tex.GetNativeTexturePtr());
    		#endif
    	}
    
    // Так выглядит объявление функции передачи идентификатора текстуры
    // Определение ее будет внутри плагина
    #if UNITY_IPHONE && !UNITY_EDITOR
    [DllImport ("__Internal")]
    #else
    // Здесь указывается имя подгружаемой динамической библиотеки, в которой лежит плагин
    [DllImport ("QuadrocopterBrain")]
    #endif
    #if UNITY_GLES_RENDERER
    private static extern void SetTextureOfCam1(System.IntPtr texture, int w, int h);
    #else
    private static extern void SetTextureOfCam1(System.IntPtr texture);
    #endif
    
    // Объявление функции, которая будет возвращать функцию,
    // куда будет передано управление в C++
    #if UNITY_IPHONE && !UNITY_EDITOR
    [DllImport ("__Internal")]
    #else
    [DllImport("QuadrocopterBrain")]
    #endif
    private static extern IntPtr GetRenderEventFunc();
    
    }
    


    OpenCV


    Так как мы собираемся использовать OpenCV для обработки изображений вам нужно будет установить его себе. Подойдет обычный ванильный OpenCV, взятый с официального сайта. Ставил я его себе по гайду. Если вы пытаетесь повторять написанное в статье, то советую для начала сделать и скомпилировать пустой проект динамической библиотеки, которая бы как-либо использовала OpenCV. Вышеназванный гайд должен вам помочь. Также будут нужны функции работы с OpenGL.
    Пример пустой библиотеки
    #include <opencv2/opencv.hpp>
    void someFunc () {
            cv::Mat img (256, 256, CV_8UC4);
            return 0;
    }
    


    Получение и обработка текстур в плагине


    Здесь я тоже для начала приведу отдельные фрагменты программы, а потом уже весь код целиком.
    Функция передачи идентификатора текстуры
    //переменные для хранения идентификатора текстуры
    static void* g_Cam1TexturePointer = NULL;
    #ifdef SUPPORT_OPENGLES
    static int   g_TexWidth  = 0;
    static int   g_TexHeight = 0;
    #endif
    
    //Определение функции передачи идентификатора текстуры.
    //Объявление находится в C# скрипте
    #ifdef SUPPORT_OPENGLES
    extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTextureOfCam1(void* texturePtr, int w, int h)
    #else
    extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTextureOfCam1(void* texturePtr)
    #endif
    {
        g_Cam1TexturePointer = texturePtr;
    #ifdef SUPPORT_OPENGLES
        g_TexWidth = w;
        g_TexHeight = h;
    #endif
    }
    


    Функция обработки текстуры
    static void UNITY_INTERFACE_API OnRenderEvent(int eventID) {
    //если вы посмотрите в пример RenderingPluginExampleXX.zip,
    //то увидите здесь большее количество кода для разных платформ, я оставил здесь только OpenGL.
    //Передача текстур на разных платформах осуществляется по разному, если у вас другая платформа
    //обратитесь к коду примера
    #if SUPPORT_OPENGL
        if (g_Cam1TexturePointer) {
            GLuint gltex = (GLuint)(size_t)(g_Cam1TexturePointer);
            glBindTexture (GL_TEXTURE_2D, gltex);
            int texWidth, texHeight;
            glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &texWidth);
            glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &texHeight);
            // Матрица OpenCV, в которую будет считана текстура
            cv::Mat img (texHeight, texWidth, CV_8UC4);
            // Считывание текстуры в матрицу
            glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.data);
            // В качестве тестовой обработки просто напишем на текстуре Cam1
            cv::putText(img, "Cam1", cv::Point2f(10,50), cv::FONT_HERSHEY_SIMPLEX, 2,  cv::Scalar(0, 0, 0, 255), 3);
    
            // Загружаем назад в память текстуру, чтобы отобразить ее в Unity.
            // Unity не позволяет обращаться к GUI функциям OpenCV, так что никакие imshow работать не будут
            // Поэтому весь вывод графической информации надо осуществлять передачей ее в Unity
            glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, texWidth, texHeight, GL_RGBA, GL_UNSIGNED_BYTE, img.data);
        }
    #endif
    }
    // см GameManager.cs
    extern "C" UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderEventFunc() {
        return OnRenderEvent;
    }
    


    У меня плагин состоит из 2х файлов:
    Main.h
    #ifndef __QuadrocopterBrain__Main__
    #define __QuadrocopterBrain__Main__
    
    // Which platform we are on?
    #if _MSC_VER
    #define UNITY_WIN 1
    #elif defined(__APPLE__)
    	#if defined(__arm__)
    		#define UNITY_IPHONE 1
    	#else
    		#define UNITY_OSX 1
    	#endif
    #elif defined(__linux__)
    #define UNITY_LINUX 1
    #elif defined(UNITY_METRO) || defined(UNITY_ANDROID)
    // these are defined externally
    #else
    #error "Unknown platform!"
    #endif
    
    // Which graphics device APIs we possibly support?
    #if UNITY_METRO
    	#define SUPPORT_D3D11 1
    #elif UNITY_WIN
    	#define SUPPORT_D3D9 1
    	#define SUPPORT_D3D11 1 // comment this out if you don't have D3D11 header/library files
    	#ifdef _MSC_VER
    	  #if _MSC_VER >= 1900
    	    #define SUPPORT_D3D12 1
    	  #endif
    	#endif
    	#define SUPPORT_OPENGL 1
    #elif UNITY_IPHONE || UNITY_ANDROID
    	#define SUPPORT_OPENGLES 1
    #elif UNITY_OSX || UNITY_LINUX
    	#define SUPPORT_OPENGL 1
    #endif
    #endif /* defined(__QuadrocopterBrain__Main__) */
    


    Main.cpp
    #include <math.h>
    #include <stdio.h>
    #include <vector>
    #include <string>
    
    #include "Unity/IUnityGraphics.h"
    
    #include <opencv2/opencv.hpp>
    
    #include "Main.h"
    
    // --------------------------------------------------------------------------
    // Include headers for the graphics APIs we support
    
    #if SUPPORT_D3D9
    	#include <d3d9.h>
    	#include "Unity/IUnityGraphicsD3D9.h"
    #endif
    #if SUPPORT_D3D11
    	#include <d3d11.h>
    	#include "Unity/IUnityGraphicsD3D11.h"
    #endif
    #if SUPPORT_D3D12
    	#include <d3d12.h>
    	#include "Unity/IUnityGraphicsD3D12.h"
    #endif
    
    #if SUPPORT_OPENGLES
    	#if UNITY_IPHONE
    		#include <OpenGLES/ES2/gl.h>
    	#elif UNITY_ANDROID
    		#include <GLES2/gl2.h>
    	#endif
    #elif SUPPORT_OPENGL
    	#if UNITY_WIN || UNITY_LINUX
    		#include <GL/gl.h>
    	#else
    		#include <OpenGL/gl.h>
    	#endif
    #endif
    
    // Prints a string
    static void DebugLog (const char* str)
    {
    #if UNITY_WIN
        OutputDebugStringA (str);
    #else
        fprintf(stderr, "%s", str);
    #endif
    }
    
    
    
    //переменные для хранения идентификатора текстуры
    static void* g_Cam1TexturePointer = NULL;
    #ifdef SUPPORT_OPENGLES
    static int   g_TexWidth  = 0;
    static int   g_TexHeight = 0;
    #endif
    
    //Определение функции передачи идентификатора текстуры.
    //Объявление находится в C# скрипте
    #ifdef SUPPORT_OPENGLES
    extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTextureOfCam1(void* texturePtr, int w, int h)
    #else
    extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API SetTextureOfCam1(void* texturePtr)
    #endif
    {
        g_Cam1TexturePointer = texturePtr;
    #ifdef SUPPORT_OPENGLES
        g_TexWidth = w;
        g_TexHeight = h;
    #endif
    }
    
    static void UNITY_INTERFACE_API OnRenderEvent(int eventID) {
    //если вы посмотрите в пример RenderingPluginExampleXX.zip,
    //то увидите здесь большее количество кода для разных платформ, я оставил здесь только OpenGL.
    //Передача текстур на разных платформах осуществляется по разному, если у вас другая платформа
    //обратитесь к коду примера
    #if SUPPORT_OPENGL
        if (g_Cam1TexturePointer) {
    	
            GLuint gltex = (GLuint)(size_t)(g_Cam1TexturePointer);
            glBindTexture (GL_TEXTURE_2D, gltex);
            int texWidth, texHeight;
            glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &texWidth);
            glGetTexLevelParameteriv (GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &texHeight);
    		
            // Матрица OpenCV, в которую будет считана текстура
            cv::Mat img (texHeight, texWidth, CV_8UC4);
    		
            // Считывание текстуры в матрицу
            glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_UNSIGNED_BYTE, img.data);
    		
            // В качестве тестовой обработки просто напишем на текстуре Cam1
            cv::putText(img, "Cam1", cv::Point2f(10,50), cv::FONT_HERSHEY_SIMPLEX, 2,  cv::Scalar(0, 0, 0, 255), 3);
    
            // Загружаем назад в память текстуру, чтобы отобразить ее в Unity.
            // Unity не позволяет обращаться к GUI функциям OpenCV, так что никакие imshow работать не будут
            // Поэтому весь вывод графической информации надо осуществлять передачей ее в Unity
            glTexSubImage2D (GL_TEXTURE_2D, 0, 0, 0, texWidth, texHeight, GL_RGBA, GL_UNSIGNED_BYTE, img.data);
        }
    #endif
    }
    
    
    
    // --------------------------------------------------------------------------
    // UnitySetInterfaces
    
    static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType);
    
    static IUnityInterfaces* s_UnityInterfaces = NULL;
    static IUnityGraphics* s_Graphics = NULL;
    static UnityGfxRenderer s_DeviceType = kUnityGfxRendererNull;
    
    extern "C" void	UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginLoad(IUnityInterfaces* unityInterfaces)
    {
        DebugLog("--- UnityPluginLoad");
        s_UnityInterfaces = unityInterfaces;
        s_Graphics = s_UnityInterfaces->Get<IUnityGraphics>();
        s_Graphics->RegisterDeviceEventCallback(OnGraphicsDeviceEvent);
        
        // Run OnGraphicsDeviceEvent(initialize) manually on plugin load
        OnGraphicsDeviceEvent(kUnityGfxDeviceEventInitialize);
    }
    
    extern "C" void UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API UnityPluginUnload()
    {
        s_Graphics->UnregisterDeviceEventCallback(OnGraphicsDeviceEvent);
    }
    
    // --------------------------------------------------------------------------
    // GraphicsDeviceEvent
    
    // Actual setup/teardown functions defined below
    #if SUPPORT_D3D9
    static void DoEventGraphicsDeviceD3D9(UnityGfxDeviceEventType eventType);
    #endif
    #if SUPPORT_D3D11
    static void DoEventGraphicsDeviceD3D11(UnityGfxDeviceEventType eventType);
    #endif
    #if SUPPORT_D3D12
    static void DoEventGraphicsDeviceD3D12(UnityGfxDeviceEventType eventType);
    #endif
    #if SUPPORT_OPENGLES
    static void DoEventGraphicsDeviceGLES(UnityGfxDeviceEventType eventType);
    #endif
    
    static void UNITY_INTERFACE_API OnGraphicsDeviceEvent(UnityGfxDeviceEventType eventType)
    {
        UnityGfxRenderer currentDeviceType = s_DeviceType;
        
        switch (eventType)
        {
            case kUnityGfxDeviceEventInitialize:
            {
                DebugLog("OnGraphicsDeviceEvent(Initialize).\n");
                s_DeviceType = s_Graphics->GetRenderer();
                currentDeviceType = s_DeviceType;
                break;
            }
                
            case kUnityGfxDeviceEventShutdown:
            {
                DebugLog("OnGraphicsDeviceEvent(Shutdown).\n");
                s_DeviceType = kUnityGfxRendererNull;
                g_Cam1TexturePointer = NULL;
                break;
            }
                
            case kUnityGfxDeviceEventBeforeReset:
            {
                DebugLog("OnGraphicsDeviceEvent(BeforeReset).\n");
                break;
            }
                
            case kUnityGfxDeviceEventAfterReset:
            {
                DebugLog("OnGraphicsDeviceEvent(AfterReset).\n");
                break;
            }
        };
        
    #if SUPPORT_D3D9
        if (currentDeviceType == kUnityGfxRendererD3D9)
            DoEventGraphicsDeviceD3D9(eventType);
    #endif
        
    #if SUPPORT_D3D11
        if (currentDeviceType == kUnityGfxRendererD3D11)
            DoEventGraphicsDeviceD3D11(eventType);
    #endif
        
    #if SUPPORT_D3D12
        if (currentDeviceType == kUnityGfxRendererD3D12)
            DoEventGraphicsDeviceD3D12(eventType);
    #endif
        
    #if SUPPORT_OPENGLES
        if (currentDeviceType == kUnityGfxRendererOpenGLES20 ||
            currentDeviceType == kUnityGfxRendererOpenGLES30)
            DoEventGraphicsDeviceGLES(eventType);
    #endif
    }
    
    // --------------------------------------------------------------------------
    // GetRenderEventFunc, an example function we export which is used to get a rendering event callback function.
    extern "C" UnityRenderingEvent UNITY_INTERFACE_EXPORT UNITY_INTERFACE_API GetRenderEventFunc()
    {
        return OnRenderEvent;
    }
    


    Как собрать все воедино


    Создаем в папке Assets папку Plugins. В нее помещаем наш плагин. Просто воспользовавшись вашей любимой утилитой для работы с файловой системой. Unity загружает динамическую библиотеку только один раз, поэтому если вы будете вносить изменения в нее, то необходимо будет перезапускать Unity. Запускаем и видим зеркально отображенную надпись «Cam1» на картинке с камеры. Так происходит из-за различия представления текстуры в OpenGL и OpenCV.

    Код доступен на гитхабе в ветке habr_part2
    Метки:
    • +10
    • 14,8k
    • 3
    Поделиться публикацией
    Комментарии 3
    • 0
      А типы камер и интринсики варировать как-то можно? Или тут только модель сферической камеры в вакууме с идеальными параметрами?
      • 0
        В свойствах самой камеры есть Projection и Field of View, но я пока глубже не копал. Больше параметров есть здесь.
        • 0
          Кстати, если собираетесь экспериментировать с алгоритмами зрения в этом симуляторе, то было бы неплохо помимо самих фреймов с камеры еще и depth map выдавать.

      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.