- PVSM.RU - https://www.pvsm.ru -
Нынче все технологии «дроностроительства» активно дешевеют. Кроме одной: получение карты окружающего пространства. Тут есть две крайности: либо дорогие лидары (тысячи долларов) и оптические решения для построения карты глубин (много сотен долларов), либо совсем копеечные решения типа ультразвуковых дальномеров.
Поэтому возникла идея на базе недорогой Raspberry Pi с одной камерой сделать решение, которое окажется в пустующей нише и позволит получать карту глубин «за недорого». Причем сделать это на простом языке программирования типа Python, чтобы это было доступно новичкам для экспериментов. Собственно, о своих результатах я и хотел рассказать. Получившиеся скрипты с примерами фоток можно запускать и на десктопе.
Сначала пару слов об оптической части. Для построения карты глубин всегда используются две картинки – с левой и правой камер. А у нас в наличии малина с одной камерой. Поэтому был разработан оптический разделитель, который в результате отдает на камеру уже стереопару.
Простым языком – если глянуть на фото, то из чёрной коробочки на вас смотрят два глазка камеры. А на самом деле камера одна. Просто немного оптической магии.
На фото представлена уже двенадцатая итерация устройства. Потребовалось немало времени, чтобы получить надежную стабильную конструкцию, которая при этом еще и обходилась бы недорого. Самой непростой частью являются внутренние зеркала, которые делались под заказ вакуумным напылением алюминия. Если использовать типовые зеркала, у которых отражающий слой находится под стеклом, а не над ним, то в месте их стыка образуется зазор, который радикально портит всю картину.
За базу был взят малиновый образ Raspbian Wheezy, поставлен Python 2.7 и OpenCV 2.4, ну и требуемые пакеты по мелочи — matplotlib, numpy и прочие. Все сорсы и ссылка на готовый образ карточки выложены в конце статьи.
Так как решение у нас не из металла и без сверхточной оптики, то в результате сборки возможны небольшие отклонения от идеальной геометрии. Плюс еще камера у нас к устройству крепится винтиками, поэтому её положение может быть не идеальным. Вопрос с расположением камеры решается вручную, а компенсация «кривизны» сборки конструкции у нас будет делаться программно.
Стык зеркал на картинке в идеале должен быть вертикальным и находиться по центру. «На глазок» это сделать тяжеловато, поэтому был сделан первый скрипт. Он просто захватывает картинку с камеры в режиме live preview, выводит её на экран, а по центру в overlay рисует белую полоску, по которой и идет выравнивание. После того как камера правильно сориентирована – затягиваем винтики посильнее, и сборка закончена.
Наша левая и правая картинки стыкуются в центре изображения. Стык на фото имеет ненулевую ширину – от нее можно избавиться только удаляя зеркала от устройства, что увеличивает размер конструкции. Камера малины имеет настроенный на бесконечность фокус, и близко расположенные объекты (в нашем случае это стык) просто «размываются». Поэтому нужно просто указать скрипту, что по нашему мнению является «плохой» зоной, чтобы стереопара резалась чистые на картинки. Был сделан второй скрипт, который выводит картинку и позволяет клавишами указать ту зону, которая будет вырезана.
Вот как выглядит процесс:
loadImagePath = ""
# loadImagePath = "./src/scene_1280x720_1.png"
Фундаментальная наука говорит, что для успешного построения карты глубин стереопара должна быть откалибрована. А именно, все ключевые точки с левой картинки должны находиться на той-же высоте и на правой картинке. При таком раскладе функция StereoBM, которая у нас единственная real-time, может успешно делать свое дело.
Для калибровки нам нужно напечатать эталонную картинку, сделать серию фотографий и отдать её алгоритму калибровки, который высчитает все искажения и сохранит параметры для приведения картинок в норму.
Итак, печатаем «шахматную доску [3]» и приклеиваем на твердую плоскую поверхность. Для простоты серийного фото был сделан скрипт с таймером обратного отсчета, который выводится поверх видео.
Вот как выглядит скрипт серийного фотографирования в работе:
Надо особо отметить, что «правильность» сделанной серии критична для результатов калибровки. Чуть позже мы посмотрим на результат, который получается при неверно сделанных фотографиях.
После того как серия фото сделана, сделаем еще один сервисный скриптик, который берет всю сделанную серию фото и режет её на пары картинок – левые и правые, и сохраняет пары в папку ./pairs Просмотр нарезанных пар позволяет оценить, насколько хорошо мы настроили удаление расфокусированной зоны по центру картинки. Скрипт достаточно зауряден, поэтому видео я спрятал под спойлер.
Скрипт калибровки скармливает все стереопары из папки ./src в функцию калибровки и погружается в раздумье. После своей непростой работы (для 15 картинок 1280х720 на первой малине это около 5 минут) он берет последнюю стереопару, «исправляет» картинки (ректифицирует) и показывает уже исправленные версии, по которым можно строить карту глубин.
Вот как выглядит скрипт в работе:
calibrator.add_corners((imgLeft, imgRight), True)
замените True на False – картинки показываться не будут, кроме последней откалиброванной пары.
Бывают случаи, когда результаты калибровки оказываются неожиданными.
Вот парочка ярких примеров:
На самом деле калибровка – определяющий момент. От его качества напрямую зависит то, что мы получим на этапе построения карты глубин. После большого количества экспериментов вырисовался такой список требований к съёмке:
Вот как выглядит «исправленная» стереопара при хороших результатах калибровки:
Типа все готово – можно уже и карту глубин построить. Загружаем результаты калибровки, делаем фото и смело строим карту глубин с помощью cv2.StereoBM
Получаем примерно следующее:
Результат не очень впечатляет, явно надо что-то подкручивать. Ну что-же, приступим к более тонкой настройке в следующем, 7-м скрипте. Там мы будем использовать для построения не 2 параметра, как в StereoBM(), а почти 10, что гораздо интереснее.
Вот исходники 6-го скрипта [7]
Когда параметров не 2 а 10, то перебирать их варианты с постоянным перезапуском скриптов занятие неправильное. Поэтому был сделан скрипт для удобной интерактивной настройки карты глубин. Задача стояла не усложнять код работой с интерфейсом, поэтому все было сделано на matplotlib. Прорисовка интерфейса в matplotlib на малине происходит достаточно медленно, поэтому я обычно переношу рабочую папку с малины на ноутбук и подбираю параметры там. Вот как выглядит работа скрипта:
После того, как вы подобрали параметры, скрипт по кнопке Save сохраняет результат в файлик 3dmap_set.txt в формате JSON.
Практическая работа с картой глубин показала, что в первую очередь нужно подбирать параметр minDisparity, и в связке с ним numOfDisparities. Ну и помним, что numOfDisparities меняется на самом деле дискретно, с шагом 16.
После настройки этой пары можно поиграться с другими параметрами.
Особенности настройки карты – это уже дело вкуса пользователя, и зависит от решаемой задачи. Можно привести карту к большому количеству мелких деталей либо выводить укрупненные зоны. Для простого обхода препятствия роботами второе подходит больше. Для облака точек первое, но тут всплывают вопросы производительности (мы к ним еще вернемся).
Ну и пожалуй один из самых важных моментов — это юстировка «дальнозоркости» нашего девайса. При настройке карты глубин я обычно располагаю один объект на расстоянии около 30 см, второй — в метре, и остальные в двух метрах и далее. И при настройке в 7-м скрипте первых двух параметров (minDisparity и numOfDisparities) добиваюсь следующего:
В итоге получаем систему, настроенную на распознавание «ближней» зоны препятствий в радиусе до 5-10 метров.
Ну что, теперь у нас имеется готовая настроенная система, и надо бы получить практический результат. Пробуем строить карту глубин в реальном времени по видео с нашей камеры, и показывать её в реальном времени по мере обновления.
disparity_color = cv2.applyColorMap(disparity_grayscale, cv2.COLORMAP_JET)
Итак, первый замер делался на первой Raspberry с одноядерным процессором.
— 4 секунды — построение карты по изображению 1280х720 Это много.
— 2,5 секунды — на Raspberry Pi 2, уже лучше.
Анализ показал, что при этом на второй малине используется всего одно ядро. Непорядок! Я пересобрал OpenCV с использованием библиотеки распараллеливания TBB.
— 1,5 секунды – запуск на второй малине с использованием многоядерности. По факту оказалось, что используются всего 2 ядра – с этим предстоит еще повозиться. Оказалось, что на эту проблему наткнулся не только я [9], значит еще есть куда двигаться.
Судя по алгоритму, скорость работы должна линейно зависеть от размера обрабатываемых данных. Поэтому, если снизить разрешение в 2 раза, то теоретически все должно работать в 4 раза быстрее.
— 0,3 секунды, или примерно 3-4 FPS – при сниженном вдвое разрешении 640х360. Теория подтвердилась.
В первую очередь я хотел выжать максимум из многоядерности второй малины. Посмотрю подробнее исходники функции StereoBM и попробую понять, почему работа идет не на всю катушку.
Следующий этап обещает гораздо больше приключений — это использование GPU малины для ускорения расчетов.
Тут рисуются три возможных пути:
Если у вас был опыт по работе с TBB под малиновое OpenCV, или вы имели дело с кодингом под GPU малинки — буду благодарен за дополнительные подсказки-наводки. Готовых наработок мне удалось найти достаточно мало по одной простой причине — малина с двумя камерами явление редкое. Если цеплять две вебки по USB то наступают большие тормоза, а с двумя родными камерами умеет работать только Raspberry Pi Compute, к которой нужна еще здоровенная devboard со шнурками и переходниками.
Рабочие скрипты:
Настройка OpenCV и Python на малине:
Библиотека StereoVision:
Работа с GPU
Ну и интересная статья на хабре про «Чтобы распознавать картинки, не нужно распознавать картинки [20]»
Автор: Realizator
Источник [21]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/python/106866
Ссылки в тексте:
[1] ссылка: https://github.com/realizator/3dberry-turorial/blob/master/1_show_image.py
[2] исходники второго скрипта: https://github.com/realizator/3dberry-turorial/blob/master/2_pair_fit.py
[3] шахматную доску: https://github.com/realizator/3dberry-turorial/blob/master/pattern.png
[4] третьего скрипта: https://github.com/realizator/3dberry-turorial/blob/master/3_chess_cycle.py
[5] 4-го скрипта: https://github.com/realizator/3dberry-turorial/blob/master/4_pairs_cut.py
[6] пятого скрипта: https://github.com/realizator/3dberry-turorial/blob/master/5_calibration.py
[7] 6-го скрипта: https://github.com/realizator/3dberry-turorial/blob/master/6_depth_map.py
[8] 7-го скрипта: https://github.com/realizator/3dberry-turorial/blob/master/7_dm_tune.py
[9] не только я: http://stackoverflow.com/questions/33111618/multithreading-not-effective-with-stereobm-for-small-images
[10] успешный пример: https://www.raspberrypi.org/blog/real-time-depth-perception-with-the-compute-module/
[11] наработки на гитхабе: https://github.com/nineties/py-videocore
[12] Programming AudioVideo on the Raspberry Pi GPU: https://jan.newmarch.name/RPi/
[13] Исходники: https://github.com/realizator/3dberry-turorial
[14] 2,6Гб в архиве: https://yadi.sk/d/5GOmRxwBmNXEo
[15] по настройке Python и OpenCV на малине: http://www.pyimagesearch.com/2015/02/23/install-opencv-and-python-on-your-raspberry-pi-2-and-b/
[16] форк на гитхабе: https://github.com/realizator/StereoVision
[17] на гитхабе: https://github.com/erget/StereoVision
[18] его блоге: https://erget.wordpress.com/2014/02/28/calibrating-a-stereo-pair-with-python/
[19] документация: http://picamera.readthedocs.org/en/latest/index.html
[20] Чтобы распознавать картинки, не нужно распознавать картинки: http://habrahabr.ru/post/249661/
[21] Источник: http://geektimes.ru/post/268000/
Нажмите здесь для печати.