КПДВ

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

Этой статьей я хотел бы продолжить серию о том, как подружить 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

Комментарии (3)


  1. BelBES
    19.10.2015 13:18

    А типы камер и интринсики варировать как-то можно? Или тут только модель сферической камеры в вакууме с идеальными параметрами?


    1. Parilo
      19.10.2015 13:33

      В свойствах самой камеры есть Projection и Field of View, но я пока глубже не копал. Больше параметров есть здесь.


      1. BelBES
        19.10.2015 13:41

        Кстати, если собираетесь экспериментировать с алгоритмами зрения в этом симуляторе, то было бы неплохо помимо самих фреймов с камеры еще и depth map выдавать.