Как стать автором
Обновить
0
FUNCORP
Разработка развлекательных сервисов

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

Время на прочтение7 мин
Количество просмотров3.5K

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

Задание полностью отличалось от того, чем привыкли заниматься, разрабатывая мобильные приложения — нужно было научить машинку на основе 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 на скрине ниже.

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

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

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

Для поиска зелёного мы брали фотографию, накладывали фильтр, выделяли цвет, искали его, считали 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

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

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

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

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

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

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

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

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

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

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

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

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

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

Севшая батарейка
Севшая батарейка

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

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

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

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

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

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

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

Бонус

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

Месть жюри
Месть жюри

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

Теги:
Хабы:
Всего голосов 40: ↑40 и ↓0+40
Комментарии8

Публикации

Информация

Сайт
funcorp.dev
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Кипр
Представитель
ulanana

Истории