Провели внутренний хакатон впервые после карантина: как мы обучали машинки устраивать в офисе ДТП

420f78f5672291fa1018237909162232.JPG

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

Задание полностью отличалось от того, чем привыкли заниматься, разрабатывая мобильные приложения — нужно было научить машинку на основе Raspberry Pi 4.0 с камерой объезжать препятствия, искать врага определённого цвета и идти на таран. Кто показал в среднем лучший результат — тот и выиграл.

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

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

Хакатон был рассчитан на два полноценных дня плюс вечер дня старта и финальный день подведения итогов. Полные условия озвучили перед самым началом:

  1. Командам выдаются машинки на основе Raspberry Pi 4.0 — с камерой (обзор 170 градусов) и ультразвуковыми дальномерами.

  2. В офисе установлена трасса 4×4 метра с препятствиями для тренировок и финала.

  3. Препятствия и стенки окрашены в оранжевый, сама трасса — чёрная.

  4. «Вражеская» машина будет зелёного цвета и на радиоуправлении.

  5. Команды должны написать алгоритм управления своей машинкой, настроив его на коллизию с машинкой жюри.

  6. Жюри будут управлять машинкой и пытаться избежать столкновения.

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

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

  9. Команда, чья машинка коснётся машинки жюри за наименьшее в среднем время, победит и займёт первое место.

  10. Команды, занявшие три призовых места, получат денежные призы, чтобы каждый сам решил, на что их потратить.

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

Теперь слово командам и начнём сразу с первого места, так как они подробно описали ход разработки своего решения.

1 место. Команда IDDQD

АрхитектураАрхитектура

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

Но оказалось, что стены находились очень медленно, поэтому пришлось всё быстро переделывать. Решили каждую часть обрабатывать независимо.

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

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

Мы сделали стейт-машину, в которой состояние переходит либо в discovery, либо в hunting. Был еще вариант добавить состояние блокировки, но от него отказались.В зависимости от того, где мы находились, и того, что лежало в стейте, отдавались команды на поворот колёс, мотор, поворачивание камеры. Последнее у нас не работало, мы в самом начале сожгли горизонтальный сервопривод камеры. Потом в новой машинке сожгли его ещё раз и решили, что не судьба.

Лог работыЛог работы

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

Поиск стен и сетка

Расскажу подробнее о нахождении стен и цифрах рядом с grid. Стены находить практически получилось, но до идеала мы это решение не довели, в итоге машинка не уворачивалась, как нужно.

Поиск оранжевогоПоиск оранжевого

Идея была такая. Берем картинку, делам threshold и отделяем цвет стен от всего, что есть вокруг. Получаем картинку, где стены белые, а всё остальное чёрное. Дальше всё белое стараемся объединить в замкнутые контуры.

Построение контуровПостроение контуровПостроение сетки и коллизии. Красное — коллизии есть, зеленое — нетПостроение сетки и коллизии. Красное — коллизии есть, зеленое — нет

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

Со стенами, как и у всех команд, была проблема с освещением.

Проблемы с освещениемПроблемы с освещением

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

В конечном варианте сетки по большей части коллизии находились, и можно было построить алгоритм, но часть стен, тем не менее, не видел из-за засветов.

Теперь насчет чисел в скобках после grid на скрине ниже.

2734b23f83f5fee89bba54383eec4eb6.png

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

Поиск зеленого

Поиск зеленогоПоиск зеленогоКоординатыКоординаты

Для поиска зелёного мы брали фотографию, накладывали фильтр, выделяли цвет, искали его, считали x-координату. Она нужна, чтобы понимать расположение.

Это решалось достаточно просто. Вычислили моменты через стандартную библиотеку OpenCV, взяли значение по х-координате, написали небольшую функцию gree_angle_prod, которая на вход получала картинку, и на выходе передавала угол на который нужно повернуть колёса.

def green_angle_prod(img):
    crop_img = img[60:240, 0:320]
    # преобразуем RGB картинку в HSV модель
    hsv = cv2.cvtColor(crop_img, cv2.COLOR_BGR2HSV)
    # применяем цветовой фильтр
    thresh1 = cv2.inRange(hsv, hsv_min, hsv_max)
    thresh2 = cv2.inRange(hsv, hsv_min2, hsv_max2)
    thresh = thresh1 + thresh2

    moments = cv2.moments(thresh, 1)
    dM10 = moments['m10']
    dArea = moments['m00']
    
    wheel_angle = not_find_angle
    
    if dArea > area:
        x = int(dM10 / dArea)
        if x > 160:
            wheel_angle = round(((x - 160) / 160) * 100) * 1.85
        elif x < 160:
            wheel_angle = -round((160 - x) / 160 * 100) * 1.45
            
        # print(f"wheel={wheel_angle} x={x}")
        
    return wheel_angle

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

Главной проблемой также стало освещение при работе с камерой. Иногда библиотека находила зелёный цвет там, где его не было. Мы решали эту проблему с помощью масок и фильтров для зелёного, плюс пробовали менять площадь. В последний момент опять поменялся свет, опять пробовали крутить маски и начали находить очень много зелёного на потолке и стенах. Приняли решение отрезать верх от картинки. Это помогло, так как на потолке было очень много ложных срабатываний и машинка начинала сходить с ума. Раньше мы предполагали отдельный стейт-паркинг, но итоге его перенесли в процесс поиска. Использовали ультразвуковой датчик и массив, в котором сохраняли историю изменения дистанции. Если новое значение не сильно отличается от среднего по историческим данным, то решали, что застряли и начинали маневр освобождения.

Вечером за день до финала хотели провести больше тестов, но батарея не позволяла — пришлось использовать пилот и усилитель. 

ab7a7dd6ec1b9984705e6c0804a8c5c9.png

Зато первый заезд со спрятанной за укрытием машинкой жюри стал рекордным по скорости.

50db8a86081b8f697538c98f36946c9c.gif

С убегающей машинкой было посложнее, но в итоге справились.

b776abe0ba6ed560c4a1e6a3e450a1f8.gif

2 место. Команда Aurus Senat

Изначально мы хотели применить машинное обучение, искали готовые решения, например, Donkey Car, который позволяет снять датасет с машинки, обучить на нём что-то и запустить. Но время шло, а нормально поставить его не получалась. Тогда ничего не оставалось, как обратиться к плану Б: использовать OpenCV и написать море условий if-else.

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

78cf83c80a059706713036e5881fadf6.png

Во время анализа ближней зоны смотрим, можем ли поехать вперед. Если впереди препятствие, отъезжаем и пытаемся объехать. 

Если препятствий нет, проверяем, в какую из трёх зон можем поехать — прямо, влево или вправо, и ищем зеленую точку. Если точку находим — то ускоряемся в эту зону.

208eb6f3453206f229734ebd9a767263.png

Если машинки ведущего нет в поле видимости, то берём дальнюю зону, разбиваем её на 10 отрезков и смотрим, в каком из этих отрезком меньше всего оранжевых препятствий. На основании этого решаем, куда ехать. 

c9e1f04fb88763ecf02753cbc5e56b81.png

Таким образом мы бродили по карте в поисках машинки, а при нахождении включали анализ ближней зоны.

Ещё нам попалась севшая батарея, из-за этого не сразу смогли понять, почему машинка на втором заезде так странно себя ведёт.

e9fcfc9dc541c3e923be3d29136fcf79.gifСевшая батарейкаСевшая батарейка

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

Когда подкрутили мощностьКогда подкрутили мощность

3 место. Команда «Команда №1»

Наш алгоритм тоже работал на условиях if-else, главное — найти цель и начать сближение. Для этого нужно распознавать маски, определять расстояние до цели и нужную скорость. Основные состояния — стена, которую нужно объехать; пол, по которому можно проехать и обнаружение цели. Этого оказалось достаточно, чтобы достигать цели.

Самой большой сложностью было хорошо определить объекты, учитывая, что освещение может сильно меняться — можно банально ошибиться с цветом объекта, если что-то зелёное появилось в кадре.

Так это выглядит глазами камеры:

65a67b3413ce82c07050c03ffed62be2.gif

А так выглядят маски:

48a2ce5d286c3ff94500f95ccd06c6df.gif

Бонус

Разумеется, простора для новых решений и доработок осталось ещё много. Зато получили много нового опыта и фана, а под конец хакатона решили поэкспериментировать и испытать алгоритмы по полной — выпустить все машины одновременно без препятствий. Машинки немного сошли с ума, но Гелендваген IDDQD снова показал лучшее время, так что победа заслуженная.

7821344772241124309966e68f804d34.gifМесть жюриМесть жюри

Ну и фотографий, конечно, для себя наделали.

a2809e3ff63b44e9085e679337622d80.JPGc8fd9ea73f354d6dc7622454a1968d94.JPG56e223670e4fd3c9dbae5763ea6faf51.JPG

© Habrahabr.ru