Пандемия потихоньку отступает, вакцинация полным ходом, а мы с апреля снова открыли двери офиса для всех желающих. Для нас это хорошая новость, поэтому решили слегка отметить событие — провести внутренний хакатон с подведением итогов оффлайн. Целей несколько: смена фокуса по задачам, новый опыт и живое общение после самоизоляции. Рабочие моменты на это время можно было отложить.
Задание полностью отличалось от того, чем привыкли заниматься, разрабатывая мобильные приложения — нужно было научить машинку на основе Raspberry Pi 4.0 с камерой объезжать препятствия, искать врага определённого цвета и идти на таран. Кто показал в среднем лучший результат — тот и выиграл.
Задание опубликовали в день старта, выделили пару полноценных дней на разработку и провели финал в офисе. Под катом — подробное описание задачи и решения команд. Гифки с хайлайтами прилагаются.
За несколько дней до хакатона обговорили некоторые условия, чтобы максимально снизить порог входа для всех желающих: использовать готовые библиотеки можно, а со всеми техническими вопросами помогал ментор. Также выложили общедоступные примеры кода для управления машинками.
Хакатон был рассчитан на два полноценных дня плюс вечер дня старта и финальный день подведения итогов. Полные условия озвучили перед самым началом:
Командам выдаются машинки на основе Raspberry Pi 4.0 — с камерой (обзор 170 градусов) и ультразвуковыми дальномерами.
В офисе установлена трасса 4х4 метра с препятствиями для тренировок и финала.
Препятствия и стенки окрашены в оранжевый, сама трасса — чёрная.
«Вражеская» машина будет зелёного цвета и на радиоуправлении.
Команды должны написать алгоритм управления своей машинкой, настроив его на коллизию с машинкой жюри.
Жюри будут управлять машинкой и пытаться избежать столкновения.
В финальный день нужно представить краткую презентацию своего решения, после чего машинки запускаются на трек.
Каждой команде даётся три попытки, чтобы определить среднее время, потраченное на столкновение с машинкой жюри. Во время первой попытки машина жюри будет спрятана за препятствием и не двигаться.
Команда, чья машинка коснётся машинки жюри за наименьшее в среднем время, победит и займёт первое место.
Команды, занявшие три призовых места, получат денежные призы, чтобы каждый сам решил, на что их потратить.
Также рассматривали возможность использовать машинное обучение, для этого в офисе выделили отдельный сервер (за основу взят обычный ПК), мощностями которого можно было пользоваться для анализа данных с машинки. Но в процессе хакатона стало понятно, что ML за такое короткое время — не самая практичная идея, поэтому все три команды пошли другим путём.
Теперь слово командам и начнём сразу с первого места, так как они подробно описали ход разработки своего решения.
1 место. Команда IDDQD
![Архитектура Архитектура](https://habrastorage.org/getpro/habr/upload_files/294/c94/5ed/294c945edf87a797b55c97f521040ab8.png)
Так как было непонятно, с какой скоростью выполняются все части архитектурного решения, изначально вычисления хотели проводить в том же лупе, в котором снимали картинку с камеры — одновременно определять стены, находить зелёное и так далее.
Но оказалось, что стены находились очень медленно, поэтому пришлось всё быстро переделывать. Решили каждую часть обрабатывать независимо.
То, что снималось с камеры, со своей скоростью писалось в стейт, и из него также независимо два лупа считывали изображение и проводили вычисления. Один из них искал стены, второй — зелёную точку. Это позволило даже при медленном определении стен хоть как-то начать. Потом немного оптимизировали и оно стало работать быстрее.
В итоге получилось, что в стейт писали три лупа одновременно. Также был независимый процесс, который смотрел в этот стейт с определённой скоростью — пробовали 50, 100, 500 мс. Остановились на том, что раз в 100 мс запускается процесс, который смотрит на текущее состояние в стейте и передаёт управление в одно из состояний, в котором мы находимся.
Мы сделали стейт-машину, в которой состояние переходит либо в discovery, либо в hunting. Был еще вариант добавить состояние блокировки, но от него отказались.В зависимости от того, где мы находились, и того, что лежало в стейте, отдавались команды на поворот колёс, мотор, поворачивание камеры. Последнее у нас не работало, мы в самом начале сожгли горизонтальный сервопривод камеры. Потом в новой машинке сожгли его ещё раз и решили, что не судьба.
![Лог работы Лог работы](https://habrastorage.org/getpro/habr/upload_files/0cd/134/dee/0cd134deee347ba57fea94acf6ef1469.png)
Выбранная архитектура позволила в каждый момент времени понимать, где зелёная точка, состояние стен и дистанцию до них. Выводили дополнительно номер итерации для каждого из лупов, чтобы понимать, успевают они за той частотой, с которой мы обрабатываем это состояние, или нет. Выше показан листинг кода при обновлении раз в 100 мс и видно, что каждый из результатов подсчитывался даже быстрее, чем нужно. Мы очень сильно грузили CPU, можно было оптимизировать, но в простейшем виде он справлялся.
Поиск стен и сетка
Расскажу подробнее о нахождении стен и цифрах рядом с grid. Стены находить практически получилось, но до идеала мы это решение не довели, в итоге машинка не уворачивалась, как нужно.
![Поиск оранжевого Поиск оранжевого](https://habrastorage.org/getpro/habr/upload_files/23a/206/d7c/23a206d7c5a6161fc340dd847c147935.png)
Идея была такая. Берем картинку, делам threshold и отделяем цвет стен от всего, что есть вокруг. Получаем картинку, где стены белые, а всё остальное чёрное. Дальше всё белое стараемся объединить в замкнутые контуры.
![Построение контуров Построение контуров](https://habrastorage.org/getpro/habr/upload_files/c31/03c/d7e/c3103cd7e26ae8e525dcfa6f9a8e8e38.png)
![Построение сетки и коллизии. Красное — коллизии есть, зеленое — нет Построение сетки и коллизии. Красное — коллизии есть, зеленое — нет](https://habrastorage.org/getpro/habr/upload_files/514/eec/fa7/514eecfa744475aed1b75d16b4cbd4f5.png)
Затем чертим сетку и ищем пересечение этой сетки с нарисованными контурами. Первоначальный вариант был с мелкой сеткой, но в конечном варианте сделали меньше ячеек, так как было слишком много расчётов.
Со стенами, как и у всех команд, была проблема с освещением.
![Проблемы с освещением Проблемы с освещением](https://habrastorage.org/getpro/habr/upload_files/b00/1b9/1a3/b001b91a3c447c4d2112f9ddc99099f6.png)
В зависимости от того, как ложился свет, и того, с какой стороны подъезжали к кубику, его цвет мог быть абсолютно разным — от белого до чёрного. Часть стен определялась, часть нет.
В конечном варианте сетки по большей части коллизии находились, и можно было построить алгоритм, но часть стен, тем не менее, не видел из-за засветов.
Теперь насчет чисел в скобках после grid на скрине ниже.
![](https://habrastorage.org/getpro/habr/upload_files/273/4b2/3f8/2734b23f83f5fee89bba54383eec4eb6.png)
Первые два — это расстояние до ближайшей стены слева и справа для размеров машинки. Вторые — расстояние до ближайшей стены с кратной шириной машинки. С помощью этих чисел можно было вполне успешно управлять машинкой, но не хватило, как обычно, одного дня, чтобы докрутить.
Поиск зеленого
![Поиск зеленого Поиск зеленого](https://habrastorage.org/getpro/habr/upload_files/cfa/1ec/a3b/cfa1eca3bda47996fe3eaa1e207fd09c.png)
![Координаты Координаты](https://habrastorage.org/getpro/habr/upload_files/930/4a8/12b/9304a812be260046c79a716324107480.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
Непонятно, почему колёса нашей машины двигались вправо и влево на разный угол. Мы подбирали коэффициент, чтобы максимально вариативно можно было крутить колёсами.
Главной проблемой также стало освещение при работе с камерой. Иногда библиотека находила зелёный цвет там, где его не было. Мы решали эту проблему с помощью масок и фильтров для зелёного, плюс пробовали менять площадь. В последний момент опять поменялся свет, опять пробовали крутить маски и начали находить очень много зелёного на потолке и стенах. Приняли решение отрезать верх от картинки. Это помогло, так как на потолке было очень много ложных срабатываний и машинка начинала сходить с ума. Раньше мы предполагали отдельный стейт-паркинг, но итоге его перенесли в процесс поиска. Использовали ультразвуковой датчик и массив, в котором сохраняли историю изменения дистанции. Если новое значение не сильно отличается от среднего по историческим данным, то решали, что застряли и начинали маневр освобождения.
Вечером за день до финала хотели провести больше тестов, но батарея не позволяла — пришлось использовать пилот и усилитель.
![](https://habrastorage.org/getpro/habr/upload_files/ab7/a7d/d6e/ab7a7dd6ec1b9984705e6c0804a8c5c9.png)
Зато первый заезд со спрятанной за укрытием машинкой жюри стал рекордным по скорости.
![](https://habrastorage.org/getpro/habr/upload_files/50d/b8a/860/50db8a86081b8f697538c98f36946c9c.gif)
С убегающей машинкой было посложнее, но в итоге справились.
![](https://habrastorage.org/getpro/habr/upload_files/b77/6ab/e0b/b776abe0ba6ed560c4a1e6a3e450a1f8.gif)
2 место. Команда Aurus Senat
Изначально мы хотели применить машинное обучение, искали готовые решения, например, Donkey Car, который позволяет снять датасет с машинки, обучить на нём что-то и запустить. Но время шло, а нормально поставить его не получалась. Тогда ничего не оставалось, как обратиться к плану Б: использовать OpenCV и написать море условий if-else.
Алгоритм довольно простой: анализируем две основные зоны — ближнюю, где мы находимся, и дальнюю, куда можем ехать.
![](https://habrastorage.org/getpro/habr/upload_files/78c/f83/c80/78cf83c80a059706713036e5881fadf6.png)
Во время анализа ближней зоны смотрим, можем ли поехать вперед. Если впереди препятствие, отъезжаем и пытаемся объехать.
![](https://habrastorage.org/getpro/habr/upload_files/e00/820/077/e00820077f4510058a74788d7f6a9005.png)
Если препятствий нет, проверяем, в какую из трёх зон можем поехать — прямо, влево или вправо, и ищем зеленую точку. Если точку находим — то ускоряемся в эту зону.
![](https://habrastorage.org/getpro/habr/upload_files/208/eb6/f34/208eb6f3453206f229734ebd9a767263.png)
Если машинки ведущего нет в поле видимости, то берём дальнюю зону, разбиваем её на 10 отрезков и смотрим, в каком из этих отрезком меньше всего оранжевых препятствий. На основании этого решаем, куда ехать.
![](https://habrastorage.org/getpro/habr/upload_files/c9e/1f0/4fb/c9e1f04fb88763ecf02753cbc5e56b81.png)
Таким образом мы бродили по карте в поисках машинки, а при нахождении включали анализ ближней зоны.
Ещё нам попалась севшая батарея, из-за этого не сразу смогли понять, почему машинка на втором заезде так странно себя ведёт.
![](https://habrastorage.org/getpro/habr/upload_files/e9f/cfc/9dc/e9fcfc9dc541c3e923be3d29136fcf79.gif)
![Севшая батарейка Севшая батарейка](https://habrastorage.org/getpro/habr/upload_files/9ab/6fe/86c/9ab6fe86cd441d261b6abca4c8da35c0.gif)
Усилили сигнал на мотор и только потом поменяли батарею — машинка стала ездить слишком шустро, но нам понравилось. Правда в следующем же заезде она врезалась на полной скорости в борт и сломалась.
![Когда подкрутили мощность Когда подкрутили мощность](https://habrastorage.org/getpro/habr/upload_files/ee4/cee/f6d/ee4ceef6dfe520e007941e35a19cedef.gif)
3 место. Команда «Команда №1»
Наш алгоритм тоже работал на условиях if-else, главное — найти цель и начать сближение. Для этого нужно распознавать маски, определять расстояние до цели и нужную скорость. Основные состояния — стена, которую нужно объехать; пол, по которому можно проехать и обнаружение цели. Этого оказалось достаточно, чтобы достигать цели.
Самой большой сложностью было хорошо определить объекты, учитывая, что освещение может сильно меняться — можно банально ошибиться с цветом объекта, если что-то зелёное появилось в кадре.
Так это выглядит глазами камеры:
![](https://habrastorage.org/getpro/habr/upload_files/65a/67b/341/65a67b3413ce82c07050c03ffed62be2.gif)
А так выглядят маски:
![](https://habrastorage.org/getpro/habr/upload_files/48a/2ce/5d2/48a2ce5d286c3ff94500f95ccd06c6df.gif)
Бонус
Разумеется, простора для новых решений и доработок осталось ещё много. Зато получили много нового опыта и фана, а под конец хакатона решили поэкспериментировать и испытать алгоритмы по полной — выпустить все машины одновременно без препятствий. Машинки немного сошли с ума, но Гелендваген IDDQD снова показал лучшее время, так что победа заслуженная.
![](https://habrastorage.org/getpro/habr/upload_files/782/134/477/7821344772241124309966e68f804d34.gif)
![Месть жюри Месть жюри](https://habrastorage.org/getpro/habr/upload_files/ee7/a56/bcf/ee7a56bcfa3565240590e85092f81e6e.gif)
Ну и фотографий, конечно, для себя наделали.