Пандемия потихоньку отступает, вакцинация полным ходом, а мы с апреля снова открыли двери офиса для всех желающих. Для нас это хорошая новость, поэтому решили слегка отметить событие — провести внутренний хакатон с подведением итогов оффлайн. Целей несколько: смена фокуса по задачам, новый опыт и живое общение после самоизоляции. Рабочие моменты на это время можно было отложить.
Задание полностью отличалось от того, чем привыкли заниматься, разрабатывая мобильные приложения — нужно было научить машинку на основе Raspberry Pi 4.0 с камерой объезжать препятствия, искать врага определённого цвета и идти на таран. Кто показал в среднем лучший результат — тот и выиграл.
Задание опубликовали в день старта, выделили пару полноценных дней на разработку и провели финал в офисе. Под катом — подробное описание задачи и решения команд. Гифки с хайлайтами прилагаются.
За несколько дней до хакатона обговорили некоторые условия, чтобы максимально снизить порог входа для всех желающих: использовать готовые библиотеки можно, а со всеми техническими вопросами помогал ментор. Также выложили общедоступные примеры кода для управления машинками.
Хакатон был рассчитан на два полноценных дня плюс вечер дня старта и финальный день подведения итогов. Полные условия озвучили перед самым началом:
Командам выдаются машинки на основе Raspberry Pi 4.0 — с камерой (обзор 170 градусов) и ультразвуковыми дальномерами.
В офисе установлена трасса 4х4 метра с препятствиями для тренировок и финала.
Препятствия и стенки окрашены в оранжевый, сама трасса — чёрная.
«Вражеская» машина будет зелёного цвета и на радиоуправлении.
Команды должны написать алгоритм управления своей машинкой, настроив его на коллизию с машинкой жюри.
Жюри будут управлять машинкой и пытаться избежать столкновения.
В финальный день нужно представить краткую презентацию своего решения, после чего машинки запускаются на трек.
Каждой команде даётся три попытки, чтобы определить среднее время, потраченное на столкновение с машинкой жюри. Во время первой попытки машина жюри будет спрятана за препятствием и не двигаться.
Команда, чья машинка коснётся машинки жюри за наименьшее в среднем время, победит и займёт первое место.
Команды, занявшие три призовых места, получат денежные призы, чтобы каждый сам решил, на что их потратить.
Также рассматривали возможность использовать машинное обучение, для этого в офисе выделили отдельный сервер (за основу взят обычный ПК), мощностями которого можно было пользоваться для анализа данных с машинки. Но в процессе хакатона стало понятно, что 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 снова показал лучшее время, так что победа заслуженная.
Ну и фотографий, конечно, для себя наделали.
anonymous
И ни одна из команд не попробовала погуглить, как на самом деле решаются подобные задачи в беспилотных автомобилях (или хотя бы в мобильной робототехнике)…
Был запрещено?
Хотя бы банальное исправление сферичности изображения и последующая проекция на плоскость пола изображения с камеры решала бы уйму проблем команд
mtop
Да тут все проще, люди из одной технологии начали
гадитькодить в другой технологии.Для них изменилась система координат.
Тех кто писал для пальцетыков и вселенная их продуктов была виртуальная среда с миллионами тестов, написали инструкции для вещей из реального мира, с реальными проблемами и без миллионов тестов
за что и респект
nerumb
У нас не то чтоб времени было на все это много, да и в первую очередь решали более приоритетные проблемы =) На счет исправления сферичности тоже думали, но до нее руки не дошли да и не решало бы это всех проблем.