company_banner

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) или кватернионом. Трансляция же — это просто трехмерный вектор. Поворот и трансляцию можно хранить в отдельных переменных или зашить в расширенную матрицу аффинного преобразования 4x4. Собственно, этот способ и предлагается для удобства использования. Но… “Тоже мне, удобный!”, — скажете вы, — “каждый раз формировать такую матрицу при отрисовке любого объекта!” И я с вами соглашусь, но только если не предоставить удобного средства для создания и манипулирования позами в таком формате. Этим средством является специально написанный класс 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.
    • +23
    • 16,6k
    • 4
    Intel
    185,37
    Компания
    Поделиться публикацией

    Похожие публикации

    Комментарии 4

      +2
      Скажите, этот шаг в сторону 3D означает что OpenCV превращается в конкурента PCL? Как вы вообще видите взаимодействие/пересечение/разграничение сфер применения этих библиотек?
        +1
        Тут уж вам судить. Но я бы не стал мыслить в таком ключе — конкурентов в open source нет, есть совместная польза.

        Взаимодействие, насколько мне известно не планируется в краткосрочной перспективе. Хотя я не могу говорить за все коммунити.
        +1
        VTK 6.1 без проблем собирается для Mavericks 10.9.2. Лично сам собирал.
          +1
          Допускаю что вы правы. У меня было мало времени поработать на маке и проверить все конфигурации. Дьявол здесь в деталях — в версиях qt, в вариантах стандартной библиотеки (OpenCV использует libc++ вместо libstdc++ под MacOS). У меня получилось только с 6.2.

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

        Самое читаемое