Добрый день, 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) или кватернионом. Трансляция же — это просто трехмерный вектор. Поворот и трансляцию можно хранить в отдельных переменных или зашить в расширенную матрицу аффинного преобразования 4x4. Собственно, этот способ и предлагается для удобства использования. Но… “Тоже мне, удобный!”, — скажете вы, — “каждый раз формировать такую матрицу при отрисовке любого объекта!” И я с вами соглашусь, но только если не предоставить удобного средства для создания и манипулирования позами в таком формате. Этим средством является специально написанный класс cv::Affine3d, который кстати, помимо как для визуализации я рекомендую использовать и при разработке алгоритмов одометрии. Да-да, любители кватернионов уже могут бросать в меня камни. Скажу в оправдание, что в будущем планируется и их поддерживать.
Итак, давайте дадим определение. Поза каждого объекта в Viz — это преобразование из евклидовой системы координат, связанной с объектом, в некую глобальную евклидову систему координат. На практике существуют различные соглашения, что такое преобразование и что куда преобразуется. В нашем случае имеется ввиду преобразование точек (point transfer) из системы координат объекта в глобальную. Т.е:
где 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 все мало чем отличается
- git clone github.com/Itseez/opencv.git
- [optional] git checkout -b 2.4 origin/2.4
- mkdir build && cd build
- 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.
Автор: Nerei