Привет, Хабр. Это пост-отчет-тьюториал про беспилотные автомобили — как (начать) делать свой без расходов на оборудование. Весь код доступен на github, и помимо прочего вы научитесь легко генерить такие класные картинки:
Вкратце
Краткое содержание для знакомых с темой: традиционно для набора обучающей выборки для автопилота на основе машинного обучения нужен был специально оборудованный автомобиль с достаточно информативной CAN шиной и интерфейсом к ней, что дорого. Мы поступим проще и бесплатно — будем набирать такие же по сути данные просто со смартфона на лобовом стекле. Подходит любой авто, никаких модификаций оборудования. В этой серии — вычисляем поворот руля в каждый момент времени по видео. Если в этом абзаце всё понятно, можно перепрыгивать через введение сразу к сути подхода.
Что-зачем-почему более подробно
Итак, ещё пару лет назад без серьёзных ресурсов большой корпорации в тему автопилотов было не сунуться — один только LIDAR сенсор стоил десятки тысяч долларов, но недавняя революция в нейросетях всё изменила. Стартапы из нескольких человек с простейшими наборами сенсоров из пары вебкамер на равных конкурируют по качеству результата со знаменитыми брендами. Почему бы не попробовать и нам, тем более столько качественных компонентов уже в открытом доступе.
Автопилот преобразует данные сенсоров в управляющие воздействия — поворот руля и требуемое ускорение/замедление. В системе с лазерными дальномерами, как у Google, это может выглядеть так:
Простейший же вариант сенсора — видеокамера, "смотрящая" через лобовое стекло. С ним и будем работать, ведь камера на телефоне уже есть у каждого.
Для вычисления управляющих сигналов из "сырого" видео хорошо работают сверточные нейросети, но, как и любой другой подход машинного обучения, предсказывать правильный результат их нужно научить. Для обучения нужно (а) выбрать архитектуру модели и (б) сформировать обучающую выборку, которая будет демонстрировать модели различные входные ситуации и "правильные ответы" (например, угол поворота руля и положение педали газа) на каждую из них. Данные для обучающей выборки обычно записывают с заездов, где машиной управляет человек. То есть водитель демонстрирует роботу, как надо управлять машиной.
Хороших архитектур нейросетей хватает в открытом доступе, а вот с данными ситуация более печальная: во-первых данных просто мало, во-вторых почти все выборки — из США, а у нас на дорогах много от тех мест отличий.
Дефицит открытых данных легко объясним. Во-первых данные — не менее ценный актив, чем экспертиза в алгоритмах и моделях, поэтому делиться никто не торопится:
The rocket engine is the models and the fuel is the data.
Andrew Ng
Во-вторых, процесс сбора данных недёшев, особенно если действовать "в лоб". Хороший пример — Udacity. Они специально подобрали модель автомобиля, где рулевое управление и газ/тормоз завязаны на цифровую шину, сделали интерфейс к шине и считывают оттуда данные напрямую. Плюс подхода — высокое качество данных. Минус — серьезная стоимость, отсекающая подавляющее большинство непрофессионалов. Ведь далеко не каждый даже современный авто пишет в CAN всю нужную нам информацию, да и с интерфейсом придется повозиться.
Мы поступим проще. Записываем "сырые" данные (пока что это будет просто видео) смартфоном на лобовом стекле как видеорегистратором, затем софтом "выжимаем" оттуда нужную информацию — скорость движения и поворотов, на которых уже можно будет обучать автопилот. В результате получаем почти бесплатное решение — если есть держалка для телефона на лобовое стекло, достаточно нажать кнопку, чтобы набирать обучающие данные по дороге на работу.
В этой серии — "выжималка" угла поворота из видео. Все шаги легко повторить своими силами с помощью кода на github.
Задача
Решаем задачу:
- Есть видео с камеры, жестко закрепленной к авто (т.е. камера не болтается).
- Требуется для каждого кадра узнать текущий угол поворота руля.
Ожидаемый результат:
Сразу чуть упростим — вместо угла поворота руля будем вычислять угловую скорость в горизонтальной плоскости. Это примерно эквивалентная информация если знать поступательную скорость, которой мы займемся в следующей серии.
Решение
Решение можно собрать из общедоступных компонент, немного их доработав:
Восстанавливаем траекторию камеры
Первый шаг — восстановление траекториии камеры в трехмерном пространстве с помощью библиотеки SLAM по видео (simultaneous localization and mapping, одновременная локализация и построение карты). На выходе для каждого (почти, см. нюансы) кадра получаем 6 параметров положения: 3D смещение и 3 угла ориентации.
В коде за эту часть отвечает модуль optical_trajectories
Нюансы:
- При записи видео не гонитесь за максимальным разрешением — дальше определенного порога оно только повредит. У меня хорошо работают настройки в окрестностях 720х480.
- Камеру нужно будет откалибровать (инструкции, теория — актуальны части 1 и 2) на тех же настройках, с которыми записывалось видео с заезда.
- Системе SLAM нужна "хорошая" последовательность кадров, за которую можно "зацепиться" как за точку отсчета, поэтому часть видео в начале, пока система не "зацепится" останется не аннотированным. Если на вашем видео локализация не работает совсем, вероятны либо проблемы с калибровкой (попробуйте откалибровать несколько раз и посмотрите на разброс результатов), либо проблемы с качеством видео (слишком высокое разрешание, слишком сильное сжатие и т.д.).
- Возможны срывы отслеживания SLAM системой, если между соседними кадрами потеряется слишком много ключевых точек например, стекло на мгновение залило всплеском из лужи). В этом случае система сбросится в исходное не локализованное состояние и будет локализовываться заново. Поэтому из одного видео можно получить несколько траекторий (не пересекающихся во времени). Системы координат в этих траекториях будут совершенно разными.
- Конкретная библиотека ORB_SLAM2, которой я воспользовался, дает не очень надежные результаты по поступательным перемещениям, поэтому их пока игнорируем, а вот вращения определяет неплохо, их оставляем.
Определяем плоскость дороги
Траектория камеры в трехмерном пространстве — это хорошо, но напрямую еще не дает ответа на конечный вопрос — поворачивать налево или направо, и насколько быстро. Ведь у системы SLAM нет понятий "плоскость дороги", "верх-низ", и т.д. Эту информацию тоже надо добывать из "сырой" 3D траектории.
Здесь поможет простое наблюдение: автомобильные дороги обычно протягиваются гораздо дальше по горизонтали, чем по вертикали. Бывают конечно исключения, ими придется пренебречь. А раз так, можно принять ближайшую плоскость (т.е. плоскость, проекция на которую дает минимальную ошибку реконструкции) нашей траектории за горизонтальную плоскость дороги.
Горизонтальную плоскость выделяем прекрасным методом главных компонент по всем 3D точкам траектории — убираем направление с наименьшим собственным числом, и оставшиеся два дадут оптимальную плоскость.
За логику выделения плоскости также отвечает модуль optical_trajectories
Нюанс:
Из сути главных компонент понятно, что кроме горных дорог выделение главной плоскости будет плохо работать если машина всё время ехала по прямой, — ведь тогда только одно направление настоящей горизонтальной плоскости будет иметь большой диапазон значений, а диапазон по оставшемуся перпендикулярному горизонтальному направлению и по вертикали будут сопоставимы.
Чтобы не загрязнять данные большими погрешностями с таких траекторий, проверяем, что разброс по последнему главному компоненту значительно (в 100 раз) меньше, чем по предпоследнему. Не прошедшие траектории просто выкидываем.
Вычисляем угол поворота
Зная базисные векторы горизонтальной плоскости v1 и v2 (два главных компонента с наибольшими собственными значениями из предыдущей части), проецируем на горизонтальную плоскость оптическую ось камеры:
Таким образом из трехмерной ориентации камеры получаем курсовой угол автомобиля (с точностью до неизвестной константы, т.к. ось камеры и ось автомобиля в общем случае не совпадает). Поскольку нас интересует только интенсивность поворота (т.е. угловая скорость), эта константа и не нужна.
Угол поворота между соседними кадрами дает школьная тригонометрия (первый множитель — абсолютная величина поворота, второй — знак, определяющий направление налево/направо). Здесь под at понимаем вектор проекции ahorizontal в момент времени t:
Эта часть вычислений тоже делается модулем optical_trajectories
. На выходе получаем JSON файл следующего формата:
{
"plane": [
[ 0.35, 0.20, 0.91],
[ 0.94, -0.11, -0.33]
],
"trajectory": [
...,
{
"frame_id": 6710,
"planar_direction": [ 0.91, -0.33 ],
"pose": {
"rotation": {
"w": 0.99,
"x": -0.001,
"y": 0.001,
"z": 0.002
},
"translation": [ -0.005, 0.009, 0.046 ]
},
"time_usec": 223623466,
"turn_angle": 0.0017
},
.....
}
Значения компонент:
plane
— базисные векторы горизонтальной плоскости.trajectory
— список элементов, по одному на каждый успешно отслеженный системой SLAM кадр.
frame_id
— номер кадра в исходном видео (начиная с 0).planar_direction
— проекция отпической оси на горизонтальную плоскостьpose
— положение камеры в 3D пространстве
rotation
— ориентация оптической оси в формате единичного кватерниона.translation
— смещение.
time_use
— время с начала видео в микросекундахturn_angle
— горизонтальное вращение относительно предыдущего кадра в радианах.
Убираем шум
Мы почти у цели, но остается еще проблема. Посмотрим на получившийся (пока что) график угловой скорости:
Визуализируем на видео:
Видно, что в общем направление поворота определяется правильно, но очень много высокочастотного шума. Убираем его Гауссовским размытием, которое является низкочастотным фильтром.
Сглаживание в коде производится модулем smooth_heading_directions
Результат после фильтра:
Это уже можно "скормить" обучаемой модели и рассчитывать на адекватные результаты.
Визуализация
Для наглядности по данным из JSON файлов траекторий можно наложить виртуальный руль на исходное видео, как на демках выше, и проверить, правильно ли он крутится. Этим занимается модуль render_turning
.
Также легко построить покадровый график. Например, в IPython ноутбуке с установленным matplotlib:
import matplotlib
%matplotlib inline
import matplotlib.pyplot as plt
import json
json_raw = json.load(open('path/to/trajectory.json'))
rotations = [x['turn_angle'] for x in json_raw['trajectory']]
plt.plot(rotations, label='Rotations')
plt.show()
На этом пока всё. В следующей серии — определяем поступательную скорость, чтобы обучить еще и управление скоростью, а пока что приветствуются pull-request'ы.