Нейросети в фотограмметрии

Oleg Postoev
9 min readJan 21, 2020

--

[EN] English version of this article

Фотограмметрия — дисциплина, по технологиям которой можно создавать 3D-модели из фотографий. То есть, делаем несколько (но лучше, много) снимков объекта, интерьера или экстерьера, загоняем их в программу и получаем 3D-модель.

Упрощенная демонстрация процесса фотограмметрии

Вот так могут выглядеть полученные модели после некоторой обработки:

Гораздо лучшее качество демонстрируют Quixel, они занимаются производством моделей по этой технологии.

Скриншот сайта Quixel

Использование фотограмметрии может быть достаточно широким, вот несколько примеров:

  1. Создание 3D-моделей для кино или компьютерных игр.
    Например, для достижения максимальной реалистичности в последней Call of Duty почти все модели сделаны именно таким образом.
    Что такое фотограмметрия и как ее использовали при разработке перезапуска Modern Warfare
  2. Составление модели ландшафта для строительства.
    Documentation of paintings restoration through photogrammetry and change detection algorithms
  3. Картографирование:
    Photogrammetry on the fly
  4. Автопилоты и робототехника:
    3D reconstruction, shows depth of information a Tesla can collect from just a few seconds of video from the vehicle’s 8 cameras

Проблема и постановка задачи

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

Вот скриншот стандартного конвейера создания 3D-модели из программы Meshroom:

Скриншот из программы Meshroom

За каждым из этих блоков кроется еще несколько вспомогательных под-алгоритмов, десятки параметров влияющих на результат и не всегда очевидно каким именно образом они скажутся на результате.

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

К таким проблемам можно отнести:

  1. Низкую производительность.
    Создание модели занимает от 30 минут до десятков часов на мощном ПК.
  2. Плохая работа с повторяющимися паттернами.
    Текстуры ковров, паркетов, дорожной плитки и т.п. начинают двоиться, а части моделей дублируются и накладываться сами на себя.
  3. Проблемы с глянцевыми и отражающими поверхностями.
    В моделях появляются провалы или выпуклости сильно отличающиеся от реальной формы объекта.
Пример проблем, возникающих, при восстановлении модели по фото. Источник: https://www.agisoft.com/forum/index.php?topic=3594.0
Ошибки при восстановлении глянцевых поверхностей. Источник: https://www.reddit.com/r/photogrammetry/comments/dkii5u/colmap_vs_agisoft_2_low_settings_shiny_cup/

Глядя на проблемы существующих подходов, можно прийти к выводу, что это проблемы алгоритмов, ведь человеку по фото понятно каким должен получиться объект в 3D. Подтверждением тому несчетное многообразие туториалов по созданию модели из нескольких снимков.

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

Пример создания 3D-модели машины по нескольким чертежам. Источник: незаслуженно обделенный вниманием телеграмм-канал

Почему бы не попробовать для решения задачи применить нейросети? Немного погуглив, оказывается, что наработок в этой области много, однако до реального применения они в большинстве своем не дошли.
Подборки с примерами таких решений:
https://github.com/timzhang642/3D-Machine-Learning
https://github.com/natowi/3D-Reconstruction-with-Neural-Network

Для меня это интересная возможность потренироваться и в разработке нейросетей и познакомиться поближе с программной стороной Blender. А знакомиться там есть с чем:

Скриншот окна Blender. Просто восторг, блендер лучший :)

Подход к решению

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

Демонстрация восстановленных позиций камер. Источник: https://thehaskinssociety.wildapricot.org/photogrammetry

В общем эта задача решается так: на паре снимков программа ищет общие точки и, определяя их смещение от снимка к снимку, вычисляет как далеко сделан снимок, и какой поворот совершила камера.

Демонстрация работы алгоритма SIFT для вычисления позиций камер в идеальных условиях. Источник: https://www.researchgate.net/figure/Scale-invariant-feature-transform-SIFT-matching-result-of-a-few-objects-placed-in_fig1_259952213

Если дать нейросети решать такую задачу, она будет предсказывать расстояние между снимками по трем осям XYZ, и поворот относительно другой камеры градусах. Проблема в том, что она это будет делать даже для снимков, между которыми вообще нет общих точек. Например, когда на снимках запечатлены противоположные стены одного помещения. В таком случае, использовать показания нейросети нельзя. Сначала надо научиться понимать — а есть ли между снимками что-то общее, есть ли смысл определять положение камеры.

Вот пример двух кадров из одной сцены, но с противоположных ракурсов. Общего на снимках слишком мало для понимания, как далеко стоит вторая камера относительно первой.

Источник 3D-модели: https://www.youtube.com/watch?v=Crk5btO4WUw

Итого: наша задача — создать нейросеть, определяющую есть ли пересечение между снимками и на сколько оно сильное.

Сборка датасета

Датасет — набор данных, который делится на две части. Одна для обучения нейросети, вторая для проверки, ее нейросеть во время обучения не видит.

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

Пример единицы датасета. Есть первая картинка, вторая, и число, определяющее, как много общего у второй картинки с первой, которое вычисляется по количеству белых пикселей — если вся картинка белая, число равно единице, вся черная — нулю.

Первые две картинки легко получить в Blender классическим подходом — достаточно запустить анимацию камеры по помещению, а для имитации съемки с рук добавить шум в анимацию движения:

Анимация камеры

Нажать «Render» и получить набор картинок:

Теперь надо придумать как получить картинку для расчета степени пересечения двух кадров.
По порядку:

  1. Для упрощения привожу все материалы и текстуры к белому цвету и без отражений:
Сравнение сцены с материалами и без

2. Теперь помещаю камеру в коробку с источником света в центре и с отверстием для камеры. Отверстие достаточно большое, что бы не мешать камере, но не более. В итоге, источник света освещает только ту часть сцены, что камера видит в этом положении:

Помещаю камеру в коробку с источником света в центр и отверстием для камеры

3. Смотрим что получается из этого положения и из другой точки. Видно, что тени размытые и освещена не только та часть, что видна из исходного положения камеры, но и тени:

Сравнение результата из исходного положения камеры (root) и вид со стороны (target)

4. Уменьшаю размер источника света и тени становятся более резкими. А уменьшение числа переотражений позволяет сделать тени полностью черными:

Демонстрация влияния числа переотражений на тени (уменьшение от 4 до 0)

5. Получается набор ч/б кадров характеризующих степень пересечения кадров:

Степень яркости каждого кадра определяет как много в нем общего с исходным

В динамике выглядит несколько яснее:

В этой работе с подготовкой ч/б кадров много нюансов.
Например, для анимации из 800 кадров надо сделать 640 000 таких ч/б картинок для определения соответствий каждого кадра к любому другому. Это не быстро. Скажем, при рендере кадра за 1 секунду (что считается хорошей скоростью) понадобилось бы больше недели непрерывной работы ПК только на рендер под одну анимацию из 800 кадров, а сцен под такие анимации уже заготовлено около 40. Ждать год, пока всё отрендрится я не собирался, так что оптимизировал рендер одной сцены до нескольких часов (около 5), что уже приемлемо.

Описания всего процесса оптимизаций и подобных нюансов хватит еще на пару статей, но здесь не об этом.

Ключевые момент оптимизации рендера для тех, кто в теме

Обработав ч/б картинки парой скриптов получается такой файл с описанием результата:

[
{
"scene": 1,
"root": 100,
"frame": 1,
"value": 0.1138
},
{
"scene": 1,
"root": 100,
"frame": 10,
"value": 0.176
},
{
"scene": 1,
"root": 100,
"frame": 100,
"value": 0.995
},

Здесь:
- scene — номер сцены, идентификатор;
- root — номер исходного кадра;
- frame — номер целевого кадра;
- value — степень пересечения кадра frame с кадром root.

Обучение нейросети

Теперь, когда датасет можно считать готовым, пора переходить к обучению нейросети.

Первая проблема с которой я столкнулся состоит в том, что примеров в датасете с малым value гораздо больше, чем с большим. Если ничего с этим не делать, то нейросети проще всегда предсказывать числа около нуля и иметь точность больше 90 %.

Неравномерность распределения примеров в датасете

Для решения проблемы такого смещения я разделяю датасет на 10 групп по диапазонам. В каждый момент времени обучения нейросеть получает равное число примеров каждой группы.
То есть, когда нейросеть учится на пачках (батчах) из 50 примеров, она получает 5 примеров из каждой группы.
Из этих же групп я получаю данные для проверки нейросети. Каждый пятый пример откладываем для тестовой выборки — нам же надо понимать, как будет работать сеть на данных, которых прежде не видела.

Чтобы убедиться, что нейросеть будет подстроиться под датасет сначала собираю небольшую обучающую выборку (один батч) из пятидесяти примеров и обучаем нейросеть, пока ее точность не приблизится к астрономическим значениям. Например, я допускаю, что погрешности в 1–5 % процентов будет достаточно. Значит, на уменьшенном датасете надо добиться переобучения меньше этих значений, например 0,1–1 %.

Через несколько экспериментов получается подобрать удачные параметры для старта обучения нейросети.

Сравнение графиков функции потерь на обучающей и тестовой выборках, состоящих из одного батча. Видно, что сеть хорошо построилась под данные, как и ожидалось

Теперь этой нейросети можно подать на обучение всю обучающую выборку и добиться погрешности всего около 11–12 %. Это хороший результат за небольшое время. К слову сказать, погрешность около 0, 50 или 100 % была бы плохим знаком. Но в данном случае понятно, что направление верное и можно улучшать полученный результат.

Графики функции потерь, полученные на полной обучающей выборке

Теперь пора добавить дропауты, батч нормализацию и другие data science штуки для предотвращения переобучения нейросети. Получается красиво — погрешность меньше 9 %!

Графики функции потерь, полученные на полной обучающей выборке, еще и с регуляризацией

Прежде, чем описывать что я делал дальше, нужно напомнить про один важный момент: нейросеть учится батчами — набор примеров из обучающей выборки. Количество примеров в батче ограничивается или архитектурой нейросети (иногда бывают нужны батчи из одного или пары примеров) или памятью компьютера, на котором запускается обучение. Я использовал 50 примеров в батче.

К этому моменту мне стало интересно, если средняя ошибка в обучающей выборке около 9 %, то каков разброс. Может быть он в области 5–14 %, а может быть и в диапазоне 0–90 %. По графику погрешности этого не понять, поэтому я запускаю функцию predict() для всех примеров из обучающей выборки.

Гистограмма погрешностей нейросети. Например, на 18 547 примерах погрешность находится в диапазоне 10–15 % и лишь у 7 примеров она более 35 %

Результаты оказались интересными, как и последующее решение.
Выяснилось, что сеть ошибается редко. Лишь на 2 % примеров сеть в своих прогнозах ошибалась более, чем на 10 %.
Сначала я сохранил эти примеры с увеличенной погрешностью отдельно как «супер-сложные» и разбавлял ими каждый батч во время обучения. Общая точность нейросети выросла, но теперь погрешность больше 10 % стала проявляться на других примерах, но в целом таких случаев стало втрое меньше (менее 0,7 %). Это временное решение дало мне понять, что направление верное, хотя реализация пока не очень хороша.

Перед каждой сессией обучения батч формируется рандомно. Однако в Python у функции random можно задать веса — число для каждого из элементов, определяющее вероятность выбора этого элемента.
В качестве такого веса я использовал ранее просчитанную сложность примеров. Чем больше нейросеть ошибалась на том или ином примере, тем чаще этот пример попадает в батч.

Итак, у меня есть нейросеть с приемлемой погрешностью в 9 %. Ею размечаю сложность каждого из обучающих примеров и запускаю обучение подсовывая сложные примеры чаще. Тем чаще, чем сильнее была ошибка. Что б училась, а то че эт она!

Теперь основная фишка процесса обучения — пересчет сложности через каждые 1000 батчей одного процента датасета, такая контрольная работа у нейросети. А каждые 100 000 батчей запускаю полный пересчет сложности всех примеров, экзамен типа. Такая методика показалась наиболее сбалансированной, так как пересчет сложности всего датасета занимает существенное время (около 20–30 минут), а пересчет одного процента достаточно быстрый (лишь 15–20 секунд).

Такого подхода хватило что бы уменьшить погрешность с 9 % до 4,5. Для решения поставленной задачи такой точности более чем достаточно. Я доволен.

Теперь, имея пару кадров и нейросеть можно с точностью выше 95 % сказать, если между ними пересекающиеся области и как много

Заключение

Напомню, что получившаяся нейросеть (я называю ее «Surface Match») лишь промежуточное звено перед созданием нейросети восстанавливающей положение камер в 3D-сцене. Она пригодится и для улучшения датасета и для усиления последующих нейросетей.

Например, с ее помощью можно составить более полный датасет, в котором будут все подходящие кадры для определения положения камер в сцене.

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

🤪 Кабытанибыла, впереди много интересных находок и достижений, чего и вам желаю!

--

--