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

Мы решили сделать навигацию нагляднее — с помощью AR прямо через камеру смартфона. Сейчас технология доступна в столичных торговых центрах «Авиапарк», «Афимолл», «Европейский» и в «Галерее» в Петербурге.

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

Навигация без GPS

Снаружи всё просто — есть GPS, есть карта, можно строить маршрут. Внутри зданий всё иначе:

  • GPS не работает или даёт большую погрешность.

  • ARKit не знает, где он находится, пока не получит локализацию.

  • Пользователь может повернуться или пройти несколько шагов, пока идёт локализация.

А ещё в некоторых районах работают РЭБ-системы, которые дополнительно мешают навигации.

Решение — визуальная локализация. 

Как мы научились определять, где находится пользователь

Итак, перед нами стояла задача: понять, куда смотрит пользователь. То есть по одной фотографии, сделанной пользователем, определить позу камеры, а точнее — её центр. Это шесть чисел: три координаты в пространстве (x, y, z) и три угла поворота. Это нужно, чтобы ARKit мог правильно отрисовать стрелку навигации в нужном месте.

Начали с  самого ядрышка трёхмерного компьютерного зрения — сопоставления ключевых точек и алгоритма Perspective-n-Point (PnP).

Сопоставление изображений: как найти одинаковое на разном

Представим, что у нас есть две фотографии. Мы хотим понять, как они расположены относительно друг друга. Для этого нужно найти на них одни и те же объекты и, по тому, как они сдвинулись, прикинуть, куда сдвинулись камеры.

Это называется image matching. Базовый подход: находим ключевые точки на изображении, считаем для них дескрипторы, и сопоставляем между двумя изображениями.

Классика — это SIFT. Он ищет стабильные участки на изображении (blobs — например, углы, переходы в яркости), и для каждого участка считает дескриптор — вектор, описывающий эту точку. Дескрипторы устойчивы к повороту и масштабированию. Алгоритм — эвристика на эвристике, но для не очень далёких кадров, снятых в одно время, работает отлично.

Затем мы сравниваем дескрипторы между двумя изображениями и получаем пары точек. Какие-то из них правильные, какие-то — нет. Поэтому сверху накладываем фильтрации, чтобы убрать шум. Получаются такие «точечки и чёрточки» — линии между совпавшими точками.

 Сырые сопоставления ключевых точек SIFT
 Сырые сопоставления ключевых точек SIFT

Лучи, которые не пересекаются

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

Для этого мы пускаем лучи из центра камеры через ключевые точки. В теории нам надо знать точные параметры камеры, чтобы это провернуть: где линза кривая, куда она вогнута или выпукла, и самое главное — фокусное расстояние. Обычно на старте хватает знания фокусного расстояния.

Машина может занимать одинаковую часть кадра, но быть на разных расстояниях, а лучи будут исходить из камеры под разными углами
Машина может занимать одинаковую часть кадра, но быть на разных расстояниях, а лучи будут исходить из камеры под разными углами

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

Это и есть задача восстановления относительного положения камер. Строго говоря, это не совсем PnP, а 5-point algorithm, но мы здесь не за формальной духотой, а за сутью.

Лучи проходят один над другим, где-то на них есть точка наибольшего сближения
Лучи проходят один над другим, где-то на них есть точка наибольшего сближения
Вот тут кратенько формулы для самых любопытных:

Дано

Калибровки ( K_1, K_2 ) и пары пиксельных точек ( p_{1i}, p_{2i}). Нормализуем:

x_{1i} = K_1^{-1} \hat{p}{1i}, \quad x{2i} = K_2^{-1} \hat{p}_{2i}, \quad \hat{p} = (u, v, 1)^\top.

Эпиполярное ограничение: x_{2i}^\top E x_{1i} = 0, \quad E = [t]_\times R, где [t]_\times — кососимметричная матрица вектора ( t ) .

1) Минимальный решатель «5-point» внутри RANSAC

Выбираем 5 соответствий, получаем 5 линейных уравнений по элементам ( E ):

A \, \text{vec}(E) = 0.

У A ранг 5, поэтому \text{null}(A) четырёхмерна:

E(\alpha) = \alpha_1 E_1 + \alpha_2 E_2 + \alpha_3 E_3 + \alpha_4 E_4.

Навязываем ограничения сущностной матрицы:

\det(E) = 0, \quad 2EE^\top E - \text{tr}(EE^\top)E = 0,

что даёт 10-й степени полином по  \alpha; его корни → до 10 кандидатов E.

2) Оценка согласованности (RANSAC)

Для каждого кандидата E считаем ошибку Сампсона (классическая аппроксимация репроекционной):

r_i(E) = \frac{(x_{2i}^\top E x_{1i})^2}{(Ex_{1i})1^2 + (Ex{1i})2^2 + (E^\top x{2i})_2^2}.

Точки с ( r_i(E) < \tau ) — инлайеры. Храним модель с максимумом инлайеров. Число итераций:

N = \frac{\log(1 - p)}{\log(1 - w^s)},

где p — желаемая вероятность успеха, w — доля инлайеров, s = 5.

На практике — cv::findEssentialMat(..., RANSAC, ...), который делает ровно это с 5-точечным решателем.

3) Восстановление позы ( (R, t) ) из лучшего ( E )

SVD:

E = U \, \text{diag}(1, 1, 0) \, V^\top.

W = \begin{bmatrix} 0 & -1 & 0 \ 1 & 0 & 0 \ 0 & 0 & 1 \end{bmatrix}

(двумерная матрица размера 3×3)

Кандидаты:

R_1 = UWV^\top, \quad R_2 = UW^\top V^\top, \quad t = \pm U_{:,3}.

Выбираем пару (R, t) по cheirality test: все (или большинство) триангулированных точек должны иметь положительную глубину в обеих камерах.

Псевдокод

normalize points -> x1, x2
best_inliers = ∅
for k = 1..N:
    S = random 5 matches
    {E_j} = five_point_solver(S)
    for each E_j:
        score = {i | r_i(E_j) < τ}
        if |score| > |best_inliers|:
            best_E = E_j; best_inliers = score
[R, t] = recoverPose(best_E, x1[best_inliers], x2[best_inliers])  // + cheirality

Триангуляция

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

Очень похожую операцию можно сделать, когда есть сопоставления точек в 3D с точками в 2D. Для этого нужно добавить к этим двум фотографиям ещё одну, третью. Сопоставим её с ключевыми точками первых двух фотографий, и получается, что мы не просто сопоставили точки на фотографиях, а с некоторыми точками, которые смогли посадить в 3D. Теперь задача похожая: хотим повернуть лучики так, чтобы они проходили сквозь наши точки в пространстве.

Вот так выглядит нахождение позы по облаку точек, выставляем камеру так, чтобы лучи попали на точки
Вот так выглядит нахождение позы по облаку точек, выставляем камеру так, чтобы лучи попали на точки

Bundle Adjustment

Итак, про камеры мы знаем только фокусное расстояние, и то неточно. Искажения на линзах, рыбьи глаза и прочее сильно портят точность. Чтобы всё выровнять, мы минимизируем ошибку репроекции — разницу между тем, где точка должна быть на изображении, и где она оказалась после триангуляции. Если её свести к нулю, получится идеальная реконструкция. Это задача гладкой оптимизации, решаемая градиентным спуском. Называется это Bundle Adjustment.

Промежуточный итог

Итак, чему мы научились? Мы можем брать пачку картинок, сопоставлять их и понимать их взаимное расположение в пространстве. Допустим, мы знаем настоящие GPS-координаты этих фотографий. Тогда мы можем относительное расположение растянуть, сдвинуть и повернуть, натянув на метры земного шара. Не будем душнить про разные системы координат на Земле:)

Теперь мы можем сделать безумный трюк: взять новую фотографию, сопоставить её со всеми старыми и получить матчи. Затем, имея сопоставление 2D-точек с 3D-точками, найти позицию новой фотографии относительно камер в сцене и, следовательно, её позицию в мире. Вот так мы научились делать визуальную локализацию. Но у нас появилась целая куча проблем…

Проблема 1: Масштаб и сложность маппинга

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

Чтобы покрыть весь ТЦ, нужно сделать огромное количество фотографий с каждого ракурса, с интервалом в пару метров. Этот сизифов труд можно описать так: «Сделать 3 шага, затем сделать 6 фото вокруг себя, повторить».

Вот так мы ходим по ТЦ и фотографируем всё вокруг себя
Вот так мы ходим по ТЦ и фотографируем всё вокруг себя

Процедуру можно немного упростить, если прикрутить, допустим, 6 телефонов к одной палке. Тогда процедура сведётся к «Сделать 3 шага, сделать 6 фото разом».

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

Следует учесть, что у нас 6 кадров снимаются с одного штатива, и нам нужно вычислить только позу штатива (и 6 относительных сдвигов). Это называют мультикамерной системой. 

Слева — так размотало кусочек этажа из-за ложных матчей, справа — как должно быть
Слева — так размотало кусочек этажа из-за ложных матчей, справа — как должно быть

Спасение — взять 360-камеру. Это по сути та же мультикамерная система, но уже откалиброванная и умеющая сшивать изображения в панораму. Каждый пиксель на такой фотографии имеет фиксированное место на сфере. Нам не нужно знать параметры оптики — всё уже учтено при сшивке.

Наши 360-фото
Наши 360-фото

Главный плюс (помимо того, что не нужно приматывать изолентой 6 телефонов): не требуется выполнять никакой «калибровки» для этой камеры.

Что такое калибровка в данном контексте? Самое базовое – это вычисление параметров камеры: фокусного расстояния, коэффициентов дисторсии и т.п. В нашей мультикамерной системе ещё сдвиги и повороты относительно центра.

Калибровка происходит либо в процессе построения реконструкции (уменьшение ошибки репроекции, наш bundle adjustment), либо по фотографиям с шахматной доской, либо всё вместе.

Делаем на каждую камеру 100 таких фото с разных углов
Делаем на каждую камеру 100 таких фото с разных углов

Метод с шахматной доской и калибровкой по кнопке в OpenCV может показаться простым и привлекательным, но это ловушка. Помимо того, что метод очень чувствителен к качеству и количеству фотографий с досками, он ещё и не подбирает за вас модель камеры (т.е. какие именно параметры будем подбирать, как именно будет искривляться изображение – фишай, радиальная и т.д.).

Калибровка в процессе bundle adjustment усложняет и так непростую задачу маппинга и будет только вставлять палки в колёса. 360-фотография уже полностью откалибрована, у неё нет никаких параметров.

Итак, эта проблема решена: снимать торговый центр (сравнимый по площади с небольшим европейским городом) решили на 360-камеру, экономя силы на съёмке и маппинге.

Проблема 2: Быстрый поиск по базе

На одну фотографию у нас приходится примерно 3 тысячи ключевых точек. Таких фотографий у нас около 10 тысяч. А значит — впереди нас ждёт очень долгое и утомительное сопоставление снимков друг с другом. И это ещё полбеды: мы натыкаемся на ложные сопоставления — когда две фотографии из разных мест вдруг «сходятся», потому что локации чем-то похожи.

Это портит нам жизнь сразу в двух местах.

Во-первых, на этапе маппинга помещения. У нас будет куча фотографий, которые неприятно долго сопоставлять между собой. В огромном торговом центре SIFT местами начинает путаться и склеивает не те снимки, даже несмотря на 360-градусный обзор. На этапе реконструкции такой мусор можно отфильтровать, но с ростом масштаба (и количества фотографий) этот мусор начинает ломать всю сцену. Жизнь была бы куда проще, если бы мы заранее знали, какие кадры вообще стоит сопоставлять. Не кидайтесь помидорами — да, я в курсе, что последовательные кадры надо сравнивать, но как быть с пересечениями траекторий и просто соседними по пространству фото?

Нам нужно понимать, какие фото в съёмке рядом, а какие — далеко
Нам нужно понимать, какие фото в съёмке рядом, а какие — далеко

Во-вторых, на этапе локализации. К нам с устройства пользователя всегда приходит фотография, по которой мы определяем его местоположение. Без глобальных дескрипторов нам придётся прикладывать его снимок ко всем 10 000 фотографий нашего ТЦ и терпеливо перебирать, пока не найдём совпадение.

Можно ли получить глобальный дескриптор из локальных? Просто склеить все локальные дескрипторы в один вектор не получится: он будет гигантским, размерность будет прыгать, и к тому же — никакой инвариантности к сдвигу и повороту, которая нам так нужна.

Но тут на сцену выходят мудрые седовласые мужи (так я называю всех, кто придумывает что-то умнее, чем «накинуть ещё слоёв»). Они предложили метод агрегации локальных дескрипторов, который решает все эти проблемы. Суть простая: вместо того чтобы просто склеивать дескрипторы, мы считаем расстояния до заранее определённых кластеров. Допустим, выбрали 32 кластера. Сначала считаем локальные дескрипторы по пачке характерных фотографий (например, прогоняем кучу снимков из ТЦ), потом кластеризуем их — и получаем 32 центра. У SIFT размерность дескриптора — 128, значит, итоговый глобальный дескриптор будет размером 128 × 32.

Но для качественного поиска похожих фото по большой базе этого уже маловато. Нужно заменить простенькие SIFT-дескрипторы на что-то посерьёзнее. Например, можно использовать нейросетевые ключевые точки и дескрипторы (о них поговорим чуть позже), но и они не всегда достаточно выразительны. Лучше всего с этим справляются огромные foundation-модели, обученные на тоннах данных в self-supervised режиме — например, DinoV2. Эта модель выдаёт отличные эмбеддинги (вектора с последнего слоя), которые можно быстро дообучить: всего один линейный слой на небольшом наборе примеров — и готово, можно решать конкретную задачу, например, сегментацию котиков.

Эти вектора — огонь! Смотрите, как красиво всё кластеризуется
Эти вектора — огонь! Смотрите, как красиво всё кластеризуется

Мы же возьмём чудо-вектор, затолкнём его во VLAD (это подход AnyLoc) — и вуаля, у нас есть компактный и мощный глобальный дескриптор.

Проблема 3: Точность сопоставления

Маппинг помещения мы делаем за один заход: аккуратно, шаг за шагом, обходим торговый центр и снимаем всё, что видим. Но пользователи приходят потом — в другое время, с другими телефонами, и направляют камеры с совершенно других ракурсов. Обычный SIFT к такому повороту событий не готов: он не находит достаточно сопоставленных ключевых точек, чтобы точно определить, где находится пользователь. И вот тут в бой вступают нейросети.

Во-первых, можно просто заменить SIFT на нейросетевую альтернативу. Берём сеть, которая выдаёт ключевые точки и их дескрипторы, и обучаем её в self-supervised стиле: крутим, обрезаем, двигаем, сжимаем картинки — и добиваемся, чтобы ключевые точки на оригинале и искажённой версии совпадали, а дескрипторы были максимально похожи.

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

Для обучения такой сети self-supervised подход уже не прокатит — нужна разметка. Поэтому собираем датасеты с помощью фотограмметрии: пары фото + 3D-модель. Это позволяет точно знать, какие пиксели на разных изображениях должны соответствовать друг другу.

Пара SuperPoint + SuperGlue — это уже тяжёлая артиллерия. Для реконструкции всего ТЦ она избыточна (там и SIFT справляется, если заранее подобрать пары фото через AnyLoc). А вот для локализации пользователя — это просто must-have.

Матчи в сложных случаях на простых ключевых точка, просто SuperPoint, даже не SIFT
Матчи в сложных случаях на простых ключевых точка, просто SuperPoint, даже не SIFT
А вот SuperPoint + SuperGlue
А вот SuperPoint + SuperGlue

С помощью этих методов компьютерного зрения мы можем достаточно точно локализовать пользователя в огромном магазине — и всё это за ~100 мс.

Передаю слово своему коллеге —@Cr0sSS, дальше будет часть интеграции в мобильное приложение.

Как мы используем CV и ARKit вместе

Андрей Кузнецов

iOS-разработчик

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

Когда располагаем точки маршрута на сцене, важно учесть угол этого поворота. Иначе маршрут будет чётко и красиво вести куда-нибудь в стену
Когда располагаем точки маршрута на сцене, важно учесть угол этого поворота. Иначе маршрут будет чётко и красиво вести куда-нибудь в стену

Третий важный параметр —  этаж, на котором находится пользователь. Один и тот же lat-lon может быть и на первом, и на третьем этаже. Поэтому без него мы не сможем понять, где именно находится пользователь. 

Зная эти три параметра, мы можем:

  1. Построить маршрут от текущей позиции

  2. Пересчитать координаты точек маршрута относительно позиции и ориентации пользователя

  3. Отобразить маршрут в ARKit и на карте

Кажется, что всё готово, можно идти. Но по ходу мы тоже встретили проблемы.

1. Погрешность ARKit

ARKit умеет отслеживать движение пользователя внутри сцены. Но со временем он начинает накапливать ошибку. Особенно заметна ошибка в угле взгляда — объекты начинают «уезжать» и могут оказаться в стене.

Для решения этой проблемы мы пересоздаём сцену по одному из двух условий:

  • Прошло определённое время с последней локализации

  • Пройдено определённое расстояние

Это позволяет регулярно «обнулять» ошибку и поддерживать точность отображения маршрута.

2. Смена этажа — враг ARKit

Казалось бы, какая проблема? Поднялся на этаж выше — локализовался заново, и всё. Но нет.

ARKit не замечает изменение высоты. Вернее, он может распознать подъём по лестнице, но кто в ТЦ ходит по лестницам? Все едут на эскалаторе или в лифте.

  • Эскалаторы — ступени движутся вместе с вами, и для камеры они статичны.

  • Лифты — кабина вообще не меняется, и ARKit думает, что вы стоите на месте.

В итоге:

  • В худшем случае — метки маршрута дёргаются вверх-вниз

  • В лучшем — поднимаются вместе с вами, как будто вы тянете сцену за собой

Поэтому мы решили определять, что пользователь находится в зоне смены этажа (например, рядом с лифтом или эскалатором), и временно скрывать маршрут. Показываем только подсказку: «поднимитесь на этаж выше». После выхода — повторная локализация, и маршрут продолжается уже на новом уровне.

Движение между локализациями

CV даёт координаты только в момент локализации. Но пользователь может двигаться между этими моментами. Что делать?

На помощь приходит ARKit. Он умеет отслеживать перемещения внутри сцены с достаточно высокой точностью. Внутри ARKit одна единица по любой оси — это один метр. Мы берём:

  • начальную координату, полученную от CV,

  • смещение по сцене (по осям X и Z),

  • пересчитываем всё это в новую координату.

Делая это регулярно, мы можем:

  • обновлять позицию пользователя на карте,

  • поддерживать точность даже без GPS,

  • вести пользователя не только в AR, но и на карте.

Как всё работает вместе

CV даёт нам точку старта: координаты, азимут, этаж. ARKit берёт эту точку и отслеживает движение. Мы пересчитываем маршрут, учитываем повороты, пересоздаём сцену при необходимости и даже умеем корректно обрабатывать смену этажей.

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

Комментарии (0)