Доброго времени суток, дорогие хабравчане!
Этой статьей я хотел бы продолжить серию о том, как подружить 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
BelBES
А типы камер и интринсики варировать как-то можно? Или тут только модель сферической камеры в вакууме с идеальными параметрами?
Parilo
В свойствах самой камеры есть Projection и Field of View, но я пока глубже не копал. Больше параметров есть здесь.
BelBES
Кстати, если собираетесь экспериментировать с алгоритмами зрения в этом симуляторе, то было бы неплохо помимо самих фреймов с камеры еще и depth map выдавать.