Что быстрее: C++ или Python? Сравниваем скорость на примере стереозрения с Raspberry Pi |
||||||||||||||||||||||||||||||||||||||||||||||
МЕНЮ Искусственный интеллект Поиск Регистрация на сайте Помощь проекту ТЕМЫ Новости ИИ Искусственный интеллект Разработка ИИГолосовой помощник Городские сумасшедшие ИИ в медицине ИИ проекты Искусственные нейросети Слежка за людьми Угроза ИИ ИИ теория Внедрение ИИКомпьютерные науки Машинное обуч. (Ошибки) Машинное обучение Машинный перевод Нейронные сети начинающим Реализация ИИ Реализация нейросетей Создание беспилотных авто Трезво про ИИ Философия ИИ Big data Работа разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика
Генетические алгоритмы Капсульные нейросети Основы нейронных сетей Распознавание лиц Распознавание образов Распознавание речи Техническое зрение Чат-боты Авторизация |
2020-02-07 12:00 Мы часто слышим «Python слишком медленный для компьютерного зрения», особенно когда дело касается одноплатных компьютеров типа Raspberry Pi. Давайте разберемся на примере практической задачи.
Так что же на самом деле у него с производительностью в задачах компьютерного зрения? Где именно он медленнее C++, и насколько? Ответ на этот вопрос не так однозначен. Например, на Raspberry Pi при построении карты глубин с помощью кода на Python «под капотом» используются бинарные библиотеки, написанные на C++. Они отлично оптимизированы именно под малиновый процессор (Neon и всё такое), и этот код очень шустро работает! В этой статье мы решили измерить реальную разницу в скорости и найти «бутылочное горлышко» производительности. Подход очень простой. У нас есть серия небольших программ на Python, позволяющая пройти все шаги от первого запуска стереокамеры и ее калибровки до построения карты глубин по видео в реальном времени (и 2D карты пространства в режиме, эмулирующем работу 2D лидара). Мы перенесли весь этот код на C++, и сравниваем производительность на каждом этапе. В качестве «железа» выступает Raspberry Pi Compute Module 3+ Lite, установленный в плату расширения StereoPi. Перед тем как мы начнем Мы живем в эпоху Twitter, Instagram и прочих сервисов коротких сообщений. Поэтому для простоты восприятия мы разделили статью на две секции. Первая – краткий и сжатый обзор экспериментов и производительности. Вторая – подробный разбор полетов и найденных багов, который будет интересен тем, кто решит повторить эксперименты. Приступим! Итак, у нас есть стереокамера StereoPi на базе raspberry Pi Compute Module 3+, и мы хотим получить карту глубин по видео в реальном времени. Для этого нам надо пройти несколько этапов: - Собрать устройство и проверить, что все работает правильно - Сделать серию снимков для калибровки стереокамеры - Откалибровать стереокамеру, используя сделанные снимки - Настроить параметры карты глубин - Получить карту глубин по видео в реальном времени - Получить 2D карту пространства в реальном времени Для каждого из этих шагов у нас есть готовые скрипты на Питоне и они же, перенесенные на C++. На GitHub питоновые скрипты живут тут, а C++ вот тут. Шаг 1 – тестируем скорость захвата видео На этом этапе мы просто захватываем стереоскопическое видео с камер и отображаем его на экране. В Python мы используем для этого библиотеку PiCamera, а на C++ – передачу через pipe от самого шустрого из нативных приложений, а именно raspividyuv. Подробности можно прочитать ниже в разделе «Детальный разбор полетов». Код на C++ Вот как выглядит процедура компиляции и запуска на экране:
Компилируем пример, и сначала запустим код с разрешением стереопары 640х240, то есть с двумя картинками 320х240. Компилируем: Запускаем: Выходим из скрипта по кнопке Q и видим: Давайте попробуем забрать 90 FPS, указав данный параметр в raspividyuv: Итог – 90.9 FPS. А где же предел? Ставим 150 FPS: Итог: Ну что-же, имеем лимит 90 fps. Неплохо! А теперь попробуем захват на большем разрешении. Для этого делаем два изменения:
Компилируем, а затем запускаем вот с такими параметрами: Итог – 39 FPS. Мы прогнали этот код с разными FPS, и вот что у нас получилось:
Замечено, что если выставлять FPS для raspividyuv существенно выше достижимого, то наблюдается небольшая задержка видео относительно реального времени. Код на Python Вот как выглядит работа этого скрипта:
Итак, пытаемся захватить картинку 768х240, указав желаемый FPS на 30. (почему разрешение именно такое – см. в секции «Детальный разбор полетов») Идем в папку stereopi-fisheye-robot, и запускаем: Мы видим, что при движении в кадре картинка сильно «тормозит», и после остановки скрипта средний FPS равен 9.75 Хм, результат не очень хороший. Запросили 30, а получили 9.7 FPS. Но есть нюанс. В отличие от решения на C++, Питон гораздо более чувствителен к корректной установке FPS. Если вы запрашиваете существенно больше, чем нужно, вы получите не максимально доступное FPS, а его резкое падение. Давайте захватим нашу картинку при 20 FPS. Итог: FPS 20.8211 Другое дело! Мы вдвое увеличили эффективность захвата, указав корректный для данной ситуации FPS. Вот краткая сводка запрошенных и полученных FPS нашего скрипта номер один:
Итог сравнения скорости захвата кадров силами кода на C++ и на Python, с рекомендуемыми установками:
СКРИПТ 2 На втором этапе мы делаем серию снимков с шахматной доской для последующей калибровки. Тут нет необходимости сравнивать производительность, так как сохранение серии снимков через каждые 3-5 секунд не представляет трудности ни для кода на C++, ни для Python. Для этого этапа можно отметить лишь два отличия:
В остальном разницы между скриптами нет. Компиляция и запуск примера на C++:
Запуск кода на Python:
Скрипт 3 – нарезка на пары Этот скрипт имеет очень простую логику, поэтому мы не стали выносить его в отдельный бинарник на С++, а добавили его функции в код предыдущей программы. Поэтому в нашем коде на С++ можно сразу переходить к скрипту 4, а в коде на Питоне нужно запустить 3_pairs_cut.py для нарезки картинок на пары. Процесс резки на пары скриптом Python мы показали в конце предыдущего видео. Для чистоты эксперимента наших дальнейших замеров, мы скопировали все изображения (папку scenes), снятые с помощью кода на C++, в аналогичную папку скриптов питона, а затем запустили скрипт нарезки. Таким образом, оба решения у нас будут работать с одинаковым набором изображений. Скрипт 4 – калибровка Результаты: Python: 17 секунд импорт (и поиск доски), 13 секунд калибровка C++: импорт 19 секунд, калибровка 9,27 Как видим, питон импортирует картинки чуточку быстрее, а код на C++ быстрее производит процедуру калибровки. Но общие показатели примерно равны. Интересно то, что одинаковый код с одинаковыми настройками с разной эффективностью находит на изображениях шахматную доску (Python оказался лучше). Подробности, как мы и договорились, во второй секции статьи. Видео работы кода калибровки на C++:
Видео работы кода калибровки на Python:
Скрипт 5 – настройка параметров карты глубин На данном шаге мы проводим настройку параметров карты глубин, чтобы ее качество нас устраивало. Сразу скажем, что тут сравнение не в пользу Python. Для отображения карты глубин на Python мы использовали библиотеку matplotlib. Она гибкая, удобная, но не предназначена для отображения данных, быстро изменяющихся в реальном времени. Поэтому с момента изменения любого параметра до отображения обновленной карты глубин проходит чуть меньше секунды. И для настроек мы используем лишь одно статичное изображение. А вот на C++ руки у нас развязаны, поэтому мы смогли сделать настройку по видео в реальном времени. Вот как это выглядит на Python:
А вот как на C++:
Скрипт 6 – карта глубин по видео Ну вот мы и дошли до одной из самых интересных частей – скорость работы кода на прикладной задаче. Вот как выглядит работа кода на Python. Чтобы видео не было скучным, мы сначала запустим скрипт с обычными параметрами, покажем частую проблему «прыгающих цветов» на карте глубин, а затем включим автонастройку цвета и посмотрим на карту глубин еще раз.
В процессе работы скрипт выводит среднее время построения каждой карты глубин. Мы видим, что оно варьируется в пределах от 0.05 до 0.1 секунды. В итоге мы имеем примерно 17 FPS. Замечу, что FPS может зависеть от ваших настроек построения карты глубин! А теперь та-же задача, но на C++:
Средний результат – тоже примерно 17 FPS. Пара выводов:
Скрипт 7 – 2D карта пространства в режиме сканирующего лидара Идея этого скрипта очень проста. Мы будем строить карту глубин не по всему изображению, а лишь по его части – это горизонтальная полоса по центру изображения, высотой в треть от всего кадра. В этом случае работа стереокамеры больше похожа на работу сканирующего дальномера. Меняя высоту этой полосы, вы можете регулировать чувствительность системы. После получения карты глубин мы делаем ее проекцию на плоскость, и получаем двумерную карту препятствий для робота. Вот как выглядит работа кода на Python:
И вот вариант на C++:
Вывод по этим тестам. Мы считаем карту глубин лишь по части изображения, что снижает нагрузку и повышает FPS получаемой карты. Но для кода на Python это не дает прироста производительности, так как узким местом является процесс захвата кадров – наш код не может делать это быстрее. А вот код на C++ имеет запас по скорости захвата кадров из видео (до 90FPS), и тут мы имеем неплохой рост. Скажу только, что скорость существенно переваливает за 30 карт в секунду. Точные замеры мы оставляем нашим читателям. ? Итоговые выводы по сравнению производительности Python и C++ В большинстве рассмотренных примеров код на C++ оказывается значительно быстрее, но на ключевой задаче – расчете карты глубин по видео – производительность решений одинаковая. Узким местом является пиковая производительность CPU при расчете карты глубин (мы имеем чуть меньше 20 FPS и в C++, и на Python). Так что для тех, кто начинает изучать компьютерное зрение, Python прекрасно подходит. И второй вывод. Узким местом текущего кода на Python является скорость захвата кадров из видео. На наше счастье, эффективность захвата кадров примерно совпадает с эффективностью расчета карты глубин (порядка 20 кадров в секунду в обоих случаях). Но мы оставили несколько лазеек для тех, кому нужно больше скорости при захвате кадров. Что можно улучшить? На самом деле, в Python можно использовать способ захвата, аналогичный применяемому в коде на C++ – передача видео через pipe шустрой утилитой raspividyuv. Второй подход – решение, предложенное автором PiCamera в одном из ответов на Raspberry.stackexchange. В бинарном коде построения карты глубин не реализована многопоточность, поэтому в работе используется примерно полтора ядра из четырех. Теоретически у нас есть запас на двукратный рост скорости построения карты глубин. Раздел 2 – детальный разбор полетов Подготовка образа Raspbian Ну что, с места в карьер:
Скрипт 1 – захват видео, нюансы C++ и особенности захвата видео
Python и особенности захвата видео Первое, что вам надо знать – PiCamera это очень круто! В отлично написанной документации вы можете найти много уникальной информации об особенностях работы видеосистемы Raspberry Pi (которой больше нет нигде!), и я рекомендую читать ее перед сном. ? Просто взгляните, например, на раздел Advanced recipes! В различных источниках информации можно собрать следующие требования к разрешению картинок, которые не вызывают сложностей при захвате стереоизображения: - высота итоговой картинки должна быть кратна 16 - ширина должна быть кратна 32 - ширина каждого из изображений стереопары должна быть кратна 128 Пугает, да? Вот пример картинки, захваченной с неверно установленными параметрами: Тут мы попытались захватить стереокартинку с разрешением 640x240 (два кадра по 320x240). Но 320 не кратно 128. Видно, что кадр с левой камеры более узкий (256 пикселей), а правый нормальной ширины (320 пикселей). Но пугаться не надо. Есть два пути:
Повторим тут часть из первого раздела статьи. На самом деле, в Python можно использовать способ захвата, аналогичный применяемому в коде на C++ – передача видео через pipe шустрой утилитой raspividyuv. Второй подход – решение, предложенное автором PiCamera в одном из ответов на Raspberry.stackexchange (метод numpy.frombuffer, без лишнего копирования данных в памяти). Скрипт 4 – калибровка, нюансы Лайфхак Сначала пару слов о лайфхаке, который сильно улучшает поиск шахматной доски на изображении. Нам надо откалиброваться для разрешения 320х240, но на картинках такого разрешения шахматная доска детектируется плохо. Поэтому мы снимаем картинки вдаое большего разрешения (640х480), ищем на них координаты углов шахматной доски, а потом уменьшаем найденные координаты в два раза по X и по Y. И только потом отдаем их в механизм калибровки. Если вам нужна еще более высокая точность – можете откалиброваться на 1280х960, и уменьшить найденные координаты в 4 раза. Одинаковое бывает разным Занятное наблюдение – при одинаковых параметрах поиска шахматной доски код на питоне находит ее на большем количестве изображений. Вы заметили на видео, что Python откидывает всего одну пару, где он не нашел шахматную доску, а C++ с десяток. Большинство картинок, где не найдена шахматная доска – это фото, где доска находится на большом расстоянии от камеры. Я не вижу иного объяснения кроме как разницы в алгоритмах OpenCV 4.1.1 и 4.1.0 (первая версия у нас используется в C++, вторая в Python). Баги при калибровке Python. Если запустить калибровку со всеми картинками, имеющимися в примерах, то калибровка выпадает с ошибкой: Это очень неприятная ошибка, и ее подлость в том, что трудно понять логику ее появления. На самом деле, в данном случае ее вызывает картинка с номером 23. Причем эта картинка вполне хорошо выглядит, на ней отлично детектируется шахматная доска, и ничего не предвещает беды. Единственный способ избавиться от ошибки – это вычислить и удалить проблемную картинку. Я для этого пользуюсь методом «половинного деления». Допустим, у нас 50 изображений для калибровки. Я удаляю 25 картинок и смотрю, повторяется ли ошибка. Если исчезла – оставляю эти картинки, и возвращаю половину из удаленных (12 или 13). Если ошибка появляется – снова удаляю, но возвращаю вторую половину. Далее уже работа с половиной от половины – 6 или 7 картинок. Затем повторяю это уже с 3 картинками, и нахожу хулигана. Как нам удалось понять, виновата не сама картинка, а соотношение ее параметров с другими картинками в наборе. В некоторых случаях проблемными могут стать картинки с другим номером – например, если вы начнете калибровку с 1 картинки и будете добавлять другие по одной. Баги при калибровке C++ Наличие в калибровочной серии картинки 47 вызывает такую вот ошибку: Картинка 47 была найдена методом половинного деления, как и в случае с кодом Python (а там, как вы помните, плохо себя вела картинка номер 23). Надо заметить, что в ошибке есть подсказка – проблема в массиве точек No 37. Это большое достижение, так как в прошлых версиях OpenCV вы не получали даже такой информации. Мы видим, что ошибку выдала обработка по флагу CALIB_CHECK_COND, который мы явно не устанавливали. Если попробовать этот флаг явно снять (закомментированная строчка //fisheyeFlags &= cv::fisheye::CALIB_CHECK_COND), то мы получаем другую ошибку: Как и в случае с кодом на Python, причина в одном изображении, удаление которого из списка обрабатываемых решает эту проблему. Ну что можно тут сказать – это очень недружелюбная для пользователя ситуация. Логично было бы добавить, например, игнорирование ошибочных массивов вместо прекращения работы (в качестве дополнительного флага), либо дать пользователям инструмент предварительной проверки корректности массива перед отправкой на расчет. А пока нам остается делать лишь «просев» картинок половинным делением (или править код OpenCV и пересобирать всё целиком). Надеемся, что в следующих релизах OpenCV этот момент с калибровкой будет поправлен. Скрипт 5 – настройка карты глубин, тонкости В нашей реализации интерфейса настройки параметров в C++ есть один «кривой» момент – параметр min_disp может быть отрицательным, а бегунок на нашем интерфейсе начинается от нуля. Поэтому мы вычитаем от значения на бегунке число 40, чтобы захватить отрицательный диапазон. Так что 0 на шкале означает -40 в параметрах. Играясь с параметрами в C++, вы в консоли можете видеть текущий FPS (точнее, DMPS – Depth Maps Per Second). Таким образом можно легко вычислить, какие параметры и как влияют на скорость расчета карты глубин. Скрипт 6 – карта глубин по видео Про скорость. Запустив карту глубин, посмотрите загрузку процессора командой top или htop (вторая покажет загрузку по ядрам). Вы увидите, что загружено, как правило, примерно полтора ядра. Текущая реализация Depth Map не поддерживает многопоточность (возможность которой в коде таки заложена). Это значит, что у нас есть примерно двукратный потенциальный запас производительности, но его достижение требует серьезной правки исходников OpenCV. Скрипт 7 – построение 2D карты пространства в режиме сканирующего лидара Большинство трюков в этом коде связаны с отображением карты на экране. В некоторых случаях возможны «выбросы» на 2D карте в виде точек, которые находятся далеко от камеры. При этом автоматический масштаб меняется так, что карта становится очень мелкой. На этот случай мы оставили возможность отключения автоматического масштаба, и вы можете установить тот, который наиболее подходит для вашего случая. Надо отметить, что при реальном использовании на роботе эта проблема исчезает, так как вам не надо отображать карту, а нужно лишь сделать необходимые вычисления. В зависимости от положения камеры на роботе вы можете менять настройки вырезаемой полосы изображения, перемещая ее выше, ниже или меняя высоту. А если у вас StereoPi будет связана с датчиком положения в пространстве (IMU), то возможности использования полученной карты существенно расширяются. Но тут мы уже выходим за рамки статьи (и касаемся темы ROS). На этом пока всё. Надеюсь, наши эксперименты помогут вам в ваших проектах! Источник: proglib.io Комментарии: |
|||||||||||||||||||||||||||||||||||||||||||||