Viz — Новый модуль 3D визуализации в библиотеке OpenCV

от автора

Добрый день, cегодняшний блогпост я хочу посвятить обзору нового модуля для 3D визуализации Viz в библиотеке OpenCV, в проектировании и реализации которого я участвовал. Наверное тут мне стоит представиться, меня зовут Анатолий Бакшеев, я работаю в компании Itseez, использую библиотеку OpenCV вот уже 7 лет, и вместе с коллегами разрабатываю и развиваю ее.

Какое же отношение имеет 3D визуализация к компьютерному зрению, спросите вы, и зачем нам вообще потребовался подобный модуль? И будете правы, если смотреть на компьютерное зрение как на область, работающую с изображениями. Но мы живем в 21-м веке, и область применения компьютерного зрения вышла далеко за пределы просто обработки изображений, выделения границ объектов или распознавания лиц. Наука и техника уже научились в более или менее приемлемом качестве измерять наш трехмерный мир. Этому многим поспособствовало и появление несколько лет назад на рынке дешевых сенсоров типа Kinect, позволивших на то время с хорошей точностью и скоростью получать представление сцены в виде трехмерного цветного облака точек, и прогресс в области реконструкции 3D мира данных по серии изображений, и даже уход в мобильные технологии, где интегрированный гироскоп и акселерометр значительно упрощает задачу оценки передвижения камеры мобильного устройства в 3D мире, а значит и точность реконструкции сцены.

Все это подтолкнуло к развитию различных методов и алгоритмов, работающих с 3D данными. 3D сегментация, 3D фильтрация шумов, 3D распозвание объектов по форме, 3D распознавание лица, 3D слежение за позой тела, или руки для распознавания жестов. Вы наверное знаете, что когда Kinect для XBox вышел в продажу, Microsoft предоставила разработчикам игр SDK по определению позиции человеческого тела, что привело к появлению большого количества игр с интересным интерфейсом — когда, например, игровой персонаж повторяет движения игрока, стоящего перед Kinect’ом. Результаты таких 3D алгоритмов надо как-то визуализировать. Ими являются трехмерные траектории, восстановленная геометрия, или, например, вычисленная позиция человеческой руки в 3D. Также подобные алгоритмы надо отлаживать, зачастую визуализируя промежуточные данные в процессе сходимости разрабатываемого алгоритма.


Различные способы отображения траекторий камеры в OpenCV Viz

Таким образом, раз вектор разработок смещается в 3D область, в OpenCV будет все больше и больше появляться алгоритмов, работающих с 3D данными. И раз наблюдается такой тренд, спешим создать удобную инфраструктуру для этого. Модуль Viz — это первый шаг в данном направлении. OpenCV всегда была библиотекой, содержащей очень удобную базу, на основе которой разрабатывались алгоритмы и приложения компьютерного зрения. Удобную как из-за функциональности, так как она включает практически все наиболее часто используемые операции для манипуляции с изображениями и данными, так и из-за тщательно выработанного и годами проверенно API (контейнеры, базовые типы и операции с ними), позволяющего очень компактно реализовывать методы компьютерного зрения, экономя время разработчика. Надеемся, что Viz удовлетворяет всем этим требованиям.

Для нетерпеливых привожу вот это видео с демонстрацией возможностей модуля.

Философия Viz

Идея создания такого модуля появилась у меня, когда мне как-то пришлось отлаживать один алгоритм визуальной одометрии (vslam), в условиях ограниченного времени, когда я на собственной шкуре почувствовал, как помог бы мне такой модуль и какую функциональность я хотел бы в нем видеть. Да и коллеги заявляли, что здорого было бы иметь такой модуль. Все привело к началу его разработки, а затем доведение до более или менее зрелого состояния вместе с Озаном Тонкалом, нашим Google Summer Of Code студентом. Работа над совершенствованием Viz’а ведется и сейчас.

Дизайн идея в том, что неплохо бы иметь систему трехмерных виджетов, каждый из которых можно было бы отрисовывать в 3D визуализаторе, просто передав позицию и ориентацию этого виджета. Например, облако точек, приходящее с Kinect, часто хранится в системе координат, связанной с положением камеры, и для визуализации зачастую приходится преобразовывать все облака точек, снятые с разных позиций камеры, в какую-то глобальную систему координат. И удобно было бы не пересчитывать данные каждый раз в глобальную систему, а просто задать позицию этого облака точек. Таким образом, в OpenCV Viz каждый поддерживаемый объект-виджет формируется в собственной системе координат, а затем сдвигается и ориентируется уже в процессе отрисовки.

Но ни одна хорошая идея не приходит в голову только одному человеку. Как выяснилось, библиотека VTK для манипулирования и визуализации научных данных тоже реализует такой же подход. Поэтому, задача свелась к написанию грамотного враппера над подмножеством VTK, с интерфейсом и структурами данных в стиле OpenCV и написанию какого-то набора базовых виджетов с возможностью в будущем расширить это множество. Кроме описанного, VTK удовлетворяет требованию кроссплатформенности, поэтому решение использовать ее было выбрано практически сразу. Я думаю, небольшое неудобство из-за зависимости от VTK с лихвой компенсируется удобством и расширяемостью в будущем.

Представление позиции объектов в Viz

Позиция в евклидовом пространстве задается поворотом и трансляцией. Поворот может представляться в виде матрицы поворота, в виде вектора поворота (Rodrigues’ vector) или кватернионом. Трансляция же — это просто трехмерный вектор. Поворот и трансляцию можно хранить в отдельных переменных или зашить в расширенную матрицу аффинного преобразования 4×4. Собственно, этот способ и предлагается для удобства использования. Но… “Тоже мне, удобный!”, — скажете вы, — “каждый раз формировать такую матрицу при отрисовке любого объекта!” И я с вами соглашусь, но только если не предоставить удобного средства для создания и манипулирования позами в таком формате. Этим средством является специально написанный класс cv::Affine3d, который кстати, помимо как для визуализации я рекомендую использовать и при разработке алгоритмов одометрии. Да-да, любители кватернионов уже могут бросать в меня камни. Скажу в оправдание, что в будущем планируется и их поддерживать.

Итак, давайте дадим определение. Поза каждого объекта в Viz — это преобразование из евклидовой системы координат, связанной с объектом, в некую глобальную евклидову систему координат. На практике существуют различные соглашения, что такое преобразование и что куда преобразуется. В нашем случае имеется ввиду преобразование точек (point transfer) из системы координат объекта в глобальную. Т.е:

image

где PG, PO — координаты точки в глобальной системе координат и в системе координат объекта, M — матрица преобразования или поза объекта. Давайте рассмотрим как можно сформировать позу объекта.

// Если известна система координат связанная с объетом cv::Vec3d x_axis, y_axis, z_axis, origin; cv::Affine3d pose = cv::makeTransformToGlobal(x_axis, y_axis, z_axis, origin);  // Если же необходимо вычислить позу камеры cv::Vec3d position, view_direction, y_direction; Affine3d pose = makeCameraPose(position, view_direction, y_direction);  // Единичные преобразования, поза объекта совпадает с глобальной системой Affine3d pose1;   Affine3d pose2 = Affine3d::Identity();  // Из матрицы поворота и трансляции cv::Matx33d R; cv::Vec3d t; Affine3d pose = Affine3d(R, t);  // Если вы сторонник жесткой оптимизации и храните матрицы как массивы на стеке double rotation[9]; double translation[3]; Affine3d pose = Affine3d(cv::Matx33d(rotation), cv::Vec3d(translation)); 

А может быть, вы уже разрабатывали алгоритмы визуальной одометрии, и в вашей программе уже есть эти матрицы преобразования, хранящиеся внутри cv::Mat? Тогда позу в новом формате можно легко получить:

// Для матриц 4x4 или 4х3 cv::Mat pose_in_old_format; Affine3d pose = Affine3d(pose_in_old_format);  // Для матрицы 3х3 и трансляцией отдельно cv::Mat R, t; Affine3d pose = Affine3d(R, translation);  // Для вектора Родригеса и трансляции cv::Vec3d rotation_vector: Affine3d pose = Affine3d(rotation_vector, translation); 

Кроме конструирования данный класс позволяет еще и манипулировать позами и применять их к трехмерным векторам и точкам. Примеры:

// Поворот на 90 градусов вокруг Oy затем перемещение на 5 вдоль Ox. Affine3d pose = Affine3d().rotate(Vec3d(0, CV_PI/2, 0,)).translate(Vec3d(5, 0, 0));  // Применение позы cv::Vec3d a_vector; cv::Point3d a_point; cv::Vec3d transformed_vector = pose * a_vector; cv::Vec3d transformed_point  = pose * a_point;  // Комбинация двух поз Affine3d camera1_to_global, camera2_to_global; Affine3d camera1_to_camera2 = camera2_to_global.inv() * camera1_to_global 

Читать это надо так: если домножить справа на точку в системе координат камеры 1, то после первого (справа) преобразования получим точку в глобальной системе, а затем инвертированным преобразованием из глобальной системы переведем ее в систему координат камеры 2. Т.е. мы получим позу камеры 1 относительно системы координат камеры 2.

// Расстояние между двумя позами можно вычислить так double distance = cv::norm((cam2_to_global.inv() * cam1_to_global).translation()); double rotation_angle = cv::norm((cam2_to_global.inv() * cam1_to_global).rvec()); 

На этом, наверное, надо завершить наш экскурс в возможности данного класса. Кому понравилось, предлагаю использовать его в ваших алгоритмах, т.к. код с ним компактен и легко читаем. А то, что экземпляры cv::Affine3d выделяются на стеке, а все методы являются inline методами, открывает возможности для оптимизации производительности вашего приложения.

Визуализация с помощью Viz

Самый главный класс, отвечающий за визуализацию, называется cv::viz::Viz3d. Этот класс отвечает за создание окна, его инициализацию, отображение виджетов и управление и обработку ввода от пользователя. Воспользоваться им можно следующим образом:

Viz3d viz1(“mywindow”); // подготавливаем окно с именем mywindow ... добавляем содержимое ... viz1.spin();    // отображаем; исполнение блокируется, пока окно не будет закрыто 

Как и почти вся высокоуровневая функциональность в OpenCV, этот класс является по сути умным указателем с подсчетом ссылок на его внутреннюю реализацию, поэтому его свободно можно копировать, или получать по имени из внутренней базы данных.

Viz3d viz2 = viz1; Viz3d viz3 = cv::viz::getWindowByName(“mywindow”): Viz3d viz4(“mywindow”);  

Если окно с запрашиваемым именем уже существует, получаемый экземпляр Viz3d будет указывать на него, иначе новое окно с таким именем будет создано и зарегистрировано. Сделано это для упрощения отладки алгоритмов — вам теперь не нужно передавать окно вглубь стека вызовов каждый раз, когда где-то что-то надо отобразить. Достаточно в начале функции main() завести окно, и затем получать доступ к нему по имени из любого места в коде. Эта идея унаследована от зарекомендовавшей себя в OpenCV функции cv::imshow(window_name, image), также позволяющей отобразить картинку в именованное окно в любом месте кода.

Система Виджетов

Как уже упоминалось раньше, для отрисовки различных данных используется система виджетов. Каждый виджет имеет несколько конструкторов и иногда методов для управления его внутренними данными. Каждый виджет формируется в своей координатной системе. Например:

// задаем линию двумя точками WLine line(Point3d(0.0, 0.0, 0.0), Point3d(1.0, 1.0, 1.0), Color::apricot());   // задаем куб двумя углами с гранями паралельно осям координат  WCube cube(Point3d(-1.0, -1.0, -1.0), Point3d(1.0, 1.0, 1.0), true, Color::pink()); 

Как видим, мы можем указать произвольную линию, однако для куба возможно выставлять только позицию, но не ориентацию относительно осей координат. Однако, это не есть ограничение, а скорее даже фича, приучающая мыслить в стиле Viz. Как мы уже обсуждали ранее, при отрисовке можно задать любую позу виджета в глобальной системе координат. Таким образом, мы простым конструктором создаем виджет в его системе координат, например, задаем таким образом размеры куба. А затем позиционируем и ориентируем его в глобальной при отрисовке.

// Вектор Родригеса определяющий поворот вокруг (1.0, 1.0, 1.0) на 3 радиана Vec3d rvec = Vec3d(1.0, 1.0, 1.0) * (3.0/cv::norm(Vec3d(1.0, 1.0, 1.0));  Viz3d viz(“test1”); viz.showWidget(“coo”, WCoordinateSystem()); viz.showWidget(“cube”, cube,  Affine3d(rvec, Vec3d::all(0))); viz.spin(); 

И вот результат:

Как мы видим, отрисовка происходит через вызов метода Viz3d::showWidget() с передачей ему строкового имени объекта, экземпляра созданного виджета и его позиции в глобальной системе координат. Строковое имя необходимо для того, чтобы можно было добавлять, удалять и обновлять виджеты в 3D сцене по имени. Если виджет с таким именем уже присутствует, то он удаляется и заменяется на новый.

Помимо куба и линии, в Viz реализованы сфера, цилиндр, плоскость, 2D окружность, картинки и текст в 3D и 2D, различные типы траекторий, положения камеры, ну и, конечно, облака точек и виджет для работы с мешем (бецветным, раскрашенным или текстурированным). Это множество виджетов не является финальным, и будет расширяться. Более того, есть возможность создания пользовательских вижетов, но об этом как-нибудь в другой раз. Если вас заинтересовала эта возможность, читайте вот этот туториал. А сейчас давайте рассмотрим еще пример, как отрисовывать облака точек:

// читаем облако точек с диска. возвращается матрица с типом CV_32FC3 cv::Mat cloud = cv::viz::readCloud(“dragon.ply”);   // создаем массив цветов для облака и заполняем его случайными данными cv::Mat colors(cloud.size(), CV_8UC3); theRNG().fill(colors, RNG::UNIFORM, 50, 255);  // копируем облако точек и выставляем часть точек в NAN - такие точки будут проигнорированы float qnan = std::numeric_limits<float>::quiet_NaN(); cv::Mat masked_cloud = cloud.clone(); for(int i = 0; i < cloud.total(); ++i)     if ( i % 16 != 0)         masked_cloud.at<Vec3f>(i) = Vec3f(qnan, qnan, qnan);     Viz3d viz(“dragons”); viz.showWidget(“coo”, WCoordinateSystem());  // Красный дракон viz.showWidget(“red”, WCloud(cloud, Color::red()),  Affine3d().translate(Vec3d(-1.0, 0.0, 0.0)));  // Дракон со случайными цветами viz.showWidget(“colored”, WCloud(cloud, colors),  Affine3d().translate(Vec3d(+1.0, 0.0, 0.0)));  // Дракон со случайными цветами и отфильтрованными точками с единичной позой viz.showWidget(“masked”, WCloud(masked_cloud, colors), Affine3d::Identity());  // Aвтоматическая раскраска, полезно если у нас нет цветов viz.showWidget(“painted”, WPaintedCloud(cloud),  Affine3d().translate(Vec3d(+2.0, 0.0, 0.0))); viz.spin(); 

Результат работы это кода:

Для более подробной информации о доступных виджетах читайте нашу документацию.

Динамически меняющаяся сцена

Зачастую недостаточно просто отобразить объекты, чтобы пользователь мог их рассмотреть, а необходимо предоставить некоторую динамику. Объекты могут двигаться, менять свои атрибуты. Если у нас есть видеопоток с Kinect, то можно проигрывать так называемое point cloud videо. Для этого можно сделать следующее:

cv::VideoCapture capture(CV_CAP_OPENNI) Viz3d viz(“dynamic”); //... добавляем содержимое...  // выставляем положение камеры чуть сбоку viz.setViewerPose(Affine3d().translate(1.0, 0.0, 0.0));  while(!viz.wasStopped()) {     //... обновляем содержимое...     //если надо, меняем позы у добавленных виджетов     //если надо, заменяем облака новыми полученными с Kinect     //если надо, меняем положение камеры      capture.grab();     capture.retrieve(color, CV_CAP_OPENNI_BGR_IMAGE);     capture.retrieve(depth, CV_CAP_OPENNI_DEPTH_MAP);     Mat cloud = computeCloud(depth);     Mat display = normalizeDepth(depth);          viz.showWidget("cloud", WCloud(cloud, color));     viz.showWidget("image", WImageOverlay(display, Rect(0, 0, 240, 160)));      // отрисовываем и обрабатываем пользовательский ввод в течении 30 мс     viz.spinOnce(30 /*ms*/,  true /*force_redraw*/)); } 

Данный цикл будет выполняться пока пользователь не закроет окно. При этом, на каждой итерации цикла виджет со старым облаком будет заменяться на новый с новым облаком.

Интерфейс управления

На данный момент управление камерой сделано в так называемом стиле trackball camera, удобном для рассматривая различных 3D объектов. Представьте себе, что перед камерой есть некоторая точка в 3D, вокруг которой эта камера и вращается с помощью мышки. Скроллер на мышке приближает/удаляет к и от этой точки. Используя кнопки shift/ctrl и мышку, можно перемещать эту точку вращения в 3D мире. В будущем планируется реализовать free-fly режим для навигации по большим пространствам. Я также рекомендую нажать горячую кнопку ‘H’ во время работы Viz, чтобы прочитать распечатанную в консоль информацию о прочих горячих клавишах и возможностях, от сохранения скришнотов и до включения анаглифического стерео режима.

Как построить OpenCV Viz модуль

Ну и наконец, для тех, кто после прочтения этого текста загорелся желанием начать использовать этот модуль, предназначен этот раздел. Viz можно использовать на всех трех доминирующих PC платформах — Windows, Linux, и Mac. Вам потребуется установить VTK и скомпилировать OpenCV с поддержкой VTK. Саму OpenCV c модулем Viz можно скачать только из нашего репозитория на GitHub’е https://github.com/Itseez/opencv в ветках 2.4 и master. Итак, инструкция:

1. Установка VTK

Под Linux наиболее простым решением является установка VTK из apt репозитория через команду apt-get install libvtk5-dev. Под Windows вам необходимо скачать VTK с сайта разработчика, лучше всего версию 5.10, сгенерировать CMake-ом проект для Visual Studio и скомпилировать в Release и Debug конфигурациях. Я рекомендую снять галочку в CMake BUILD_SHARED_LIBS, что приведет к компиляции статических библиотек VTK. В этом случае после компиляции размер OpenCV Viz модуля без каких-либо зависимостей составит всего около 10Мб.

Под Mac для версий OSX 10.8 и ранее подойдет любая версия VTK, под 10.9 Mavericks удастся скомпилировать VTK 6.2 из официально репозитория github.com/Kitware/VTK.git. Релизов 6.2 на момент написание данного блогпоста еще не было. Под Mac также рекомендуется сгенерировать с помощью CMake проект под XCode и построить статические библиотеки в Release и Debug конфигурациях.

2. Компиляция OpenCV c VTK

Этот шаг проще и быстрее. Я привожу команды для Linux, под Windows все мало чем отличается

  1. git clone github.com/Itseez/opencv.git
  2. [optional] git checkout -b 2.4 origin/2.4
  3. mkdir build && cd build
  4. cmake -DWITH_VTK=ON -DVTK_DIR=<путь к билд каталогу VTK> ../opencv

Если вы ставили VTK через apt-get install, то путь к ней указывать не надо — она будет найдена CMake’ом автоматически. Далее нужно удостовериться в консольном логе CMake, что он нашел и подключил VTK. И не отрапортовал о каких-либо несовместимостях. Например, если вы компилируете OpenCV с поддержкой Qt5, а VTK собрана с Qt4, линковка с VTK приведет к падению приложения еще на этапе инициализации до входа в функцию main(). Решение — выбирать что-то одно. Либо скомпилировать VTK без Qt4, сняв соответствующую галочку в CMake для VTK. Либо взять VTK 6.1 и выше и собрать ее с поддержкой Qt5. Ну и наконец, для сборки OpenCV запускаем make -j 6

3. Запуск текстов (опционально)

Я также рекомендую скачать вот этот репозиторий: github.com/Itseez/opencv_extra.git, прописать в переменную окружения OPENCV_TEST_DATA_PATH путь к opencv_extra/testdata. И запустить файл opencv_test_viz из build каталога OpenCV. На данном приложении можно ознакомиться со всеми текущими возможностями данного модуля, а его исходник можно использовать для изучения API.

Заключение

Ну что ж, вот я добрался и до заключения. Надеюсь, было интересно. Этим постом мне хотелось показать, какой основной тренд, c моей точки зрения, сейчас наблюдается в компьютерном зрении, и что библиотека OpenCV движется в ногу со временем. И что в OpenCV будут появляться алгоритмы для работы с 3D миром. Потому что мы сами будем их разрабатывать или с помощью Google Summer of Code студентов, или благодарные пользователи использующие нашу базу, будут участвовать и в создании и развитии подобных алгоритмов в OpenCV.

А также хотелось заинтересовать вас этим разработанным инструментом, или, может быть, даже этой областью для исследований. Кстати, если у вас появилось желание вести подобную разработку для OpenCV — You are welcome! Мы принимаем pull request’ы через GitHub. Инструкция выложена здесь. Будем рады видеть новый хорошо работающий подход 🙂

И хотя основная необходимая сейчас база создана, я думаю, в будущем в Viz будут добавляться новые возможности. Например, модель скелета человеческой руки и ее визуализация. Или карты 3D мира из таких алгоритмов, как PTAM. А может быть, и сетевой клиент, чтобы возможно было пересылать данные для визуализации с мобильного устройства при отладке алгоритмов на нем 🙂 Но это пока безумные идеи :-). Если интересно, в следующем блогпосте я мог бы рассказать о каком-нибудь алгоритме, например, ICP или Kinect Fusion, и как использовался Viz для его отладки и визуализации.

А для тех кто дочитал до конца — бонус. Здесь лежит мой оптимизированный и легковесный remake моей же реализации Kinect Fusion в библиотеке PCL.

ссылка на оригинал статьи http://habrahabr.ru/company/itseez/blog/217021/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *