Привет, Хабр! Я Руслан Рахимов, занимаюсь воплощенным ИИ в отделе исследований Т-Технологий: на стыке восприятия, управления и симуляции. Сейчас я в основном работаю с роботами-манипуляторами, а до этого плотно занимался 3D-компьютерным зрением и защитил по нему кандидатскую. Например, в работе GSplatLoc мы с коллегами улучшали визуальную локализацию с помощью 3D Gaussian Splatting — чтобы робот по камере точно понимал, где находится.
В статье рассказываю о чемпионате True Tech Champ 2026 и о том, как я готовил своего робота к участию. Делюсь опытом, как устроить цикл «из реальности в симулятор и обратно» (real2sim2real) и как решать, каким цифрам двойника верить, когда робота нет под рукой.
Как был устроен чемпионат
True Tech Champ 2026 — четвертый сезон технологического чемпионата МТС. В нем два трека: один по алгоритмам, а второй по программированию роботов. Участвовать бесплатно мог любой желающий от 14 лет. В отборочном этапе нужно было запрограммировать виртуального робота, чтобы он прошел лабиринт в симуляторе Webots, а в полуфинал отбирали команды с лучшими результатами. Отбор и полуфинал проходили онлайн, финал обещают провести очно в Москве.
О чемпионате я узнал из чата друзей. Алгоритмических задач я в свое время нарешался в школе, поэтому выбрал трек роботов. На чемпионате этот опыт пригодился: большую часть версий я проверял не на роботе, а в цифровом двойнике, виртуальной копии робота и зала внутри физического симулятора, который собрал сам по записям робота.
Этой осенью я написал программу для робота, которого ни разу не потрогал. Серийный робот-собака Unitree Go2 Pro стоял в зале организатора, и подключиться к нему можно было только по сети. В зачетном заезде полуфинала робот должен был сам, без внешнего телеуправления или доступа в интернет, пройти полосу препятствий и вернуться на старт.
Так был устроен полуфинал в треке «Программирование роботов».В этом треке:
125 команд отобрали в полуфинал;
38 вышли на зачетные заезды;
12 получили засчитанный результат;
8 прошли в финал — и я среди них.

Зачетная попытка № 1 на камере организаторов (ускорено вдвое). Мой робот сам подходит к A-раме, выравнивается перед ней, поднимается и спускается
Робот, которого нельзя потрогать
Полуфинал проходил онлайн, и роботы всех команд стояли в зале МТС. Я мог подключаться по сети к компьютеру на роботе, смотреть камеру робота и видеть его на камерах зала, но все только удаленно. На тренировках по трассе вместе с моей собакой ходили собаки других команд.

За весь полуфинал у меня набралось около 13 часов тренировок, в основном по часу. Но робот двигался не все 13 часов. Первые тренировки ушли на то, чтобы просто разобраться, что за компьютер на роботе, что на нем находится, и поставить все нужное программное окружение. Организаторы хорошо сделали, что между тренировками ничего с твоего компьютера не стирается и при следующем подключении можно просто продолжить работу. В итоге первые самостоятельные проходы препятствий получились только на восьмой тренировке.
К роботу я подключался с готовым планом. Команды, сценарии и сбор данных готовил заранее и каждый заезд проверял одну конкретную гипотезу. Все, что робот видел и делал, я сохранял в программе на роботе в ROS2 bag в формате MCAP: карту лидара, одометрию, IMU, кадры камеры и команды. По этим записям уже позже я собирал двойника в симуляторе, в котором уже мог отлаживать алгоритмы и решения.
Дано: штатная походка, карта лидара и Raspberry Pi
Командам предоставлялся серийный робот Unitree Go2 Pro с заводской прошивкой. По умолчанию он может сам ходить и держать равновесие, а участникам необходимо было только давать команды, куда и с какой скоростью идти. У робота есть лидар Unitree L1, камера спереди, инерциальный датчик и одометрия, которая оценивает пройденный путь по движениям ног. Программа работает на Raspberry Pi 5 (8 ГБ памяти), закрепленном у робота на спине, и по правилам это единственный компьютер для кода участника.
Во время зачетных попыток ни у участников нет доступа к роботу, ни у робота — доступа в интернет. Программа должна сама находить и проходить препятствия, а в конце вернуть робота в стартовую зону всеми четырьмя лапами.

Главная трудность в том, что не все известно заранее. Порядок, типы и размеры препятствий известны, стены зала тоже, а направление прохода показывают стрелки на опубликованной карте трассы. Координаты препятствий никто не дает, и поэтому каждое из препятствий робот должен найти датчиками прямо во время заезда. Максимум, что разрешалось, — использовать примерное положение препятствий как подсказку для программы. В зале разместили пять препятствий: A-раму, змейку из конусов, качели, платформы со ступенькой и подвесной мост.
Зачетных попыток три, считается лучшая. На заезд дают 8 минут, и итог в конце считают как время плюс штрафы. За каждое пропущенное препятствие добавляют несколько минут. Хуже всего — не вернуться в зону старта, потому что по правилам в таком случае время считается как 8 минут, а все непройденное — пропущенным.

Пропущенная змейка добавляет 2 минуты, A-рама и мост — по 3, качели и платформы — по 4. Мелкие ошибки стоят секунды — 15 за каждое касание конуса или лапу на полу или мате сбоку от препятствия и 30 за падение, после которого робот поднялся сам. Если робот 30 секунд лежит или стоит на месте, его переносят в зону старта и запускают программу заново, но время идет и штрафы остаются.
Строже всего наказывают нарушения. Езда по заранее заданным координатам, без поиска препятствий датчиками, стоит 5 минут, а запрещенные команды вроде прыжков или прямого управления моторами, превышение скорости или выход в сеть лишают попытку результата. Финиш засчитывают, только если робот дошел хотя бы до одного препятствия.

Как я построил заезд: найти, встать на ось, пройти, проверить
В зачетном заезде я уже ни на что не влиял, поэтому все, что должно случиться на трассе, закладывал в программу заранее. От робота я получал карту, которую он сам собирает по лидару на несколько метров вокруг себя. Положение робота я находил, прикладывая эту карту к заранее известным стенам зала. Для препятствий я сделал простой цикл. Сначала робот ищет следующее препятствие, потом подходит и встает на его ось, то есть на линию, вдоль которой его проходят. Дальше он проходит препятствие маневром, заранее подобранным под него, и в конце проверяет, получилось ли. Проверку я сделал по наклону корпуса. На A-раме корпус должен сначала задраться на подъеме, а потом клюнуть носом на спуске. После пятого препятствия робот идет обратно в зону старта.
Отдельно я продумал, что делать, если проход не получился. Повторный подход робот делал, только если время на него было меньше штрафа за пропуск, умноженного на шанс пройти. Если препятствие не находится за отведенное на поиск время, робот пропускает его и идет к следующему. Время на дорогу домой я оставлял в запасе всегда, просто потому, что заезд без возвращения дает худший исход.

Что видит лидар: стены хорошо, препятствия плохо
На схеме логика заезда выглядит просто. Первые же записи показали, что главным ограничением оказался лидар. У всех команд робот отдавал не сырые точки, а готовую воксельную карту, которую собирал сам: квадрат 6,4 на 6,4 метра вокруг собаки из вокселей — кубиков с ребром 5 см. Это окно карты обновлялось примерно семь раз в секунду. Оно едет вместе с роботом, и воксели, оставшиеся за его краем, робот забывает. Хотя лидар смотрит во все стороны, почти все обновления карты приходятся на 2,5—3 метра перед роботом, а позади него почти ничего не появляется.

Препятствия на карте видны плохо. Например, у A-рамы гребень поднимается примерно на полметра, а на карте большинство ее вокселей были не выше 0,3 м. Если посмотреть, как далеко по такой карте можно различать препятствия, то дальше 3—4 м различить уже сложно. Плюс сильно мешали другие собаки, которых лидар тоже принимает за препятствия. У одометрии тоже были ошибки. Случалось так, что за заезд робот начинал ошибаться в том, куда он смотрит, примерно на 4°, а настоящий путь выходил на 5—16% длиннее, чем показывала одометрия.

Привычные решения в таких условиях не годились. У участников полуфинала не было ни доступа к моторам, ни сырых сканов, ни нейроускорителя — только штатная походка, готовая карта лидара и Raspberry Pi.
Для сравнения возьму другое соревнование, ICRA 2024 Quadruped Robot Challenge. Его выиграла команда fuRo из Chiba Institute of Technology тоже на Go2. Их робот ходил на собственном контроллере, обученном с подкреплением, определял положение по сырым сканам, а еще на него поставили другой лидар, Livox Mid-360, и вычислитель Jetson. Поэтому свой контроллер ходьбы и SLAM по облаку точек, где робот одновременно строит карту и ищет себя на ней (например, FAST-LIO2), я отложил сразу.
Три вопроса к разреженной карте
Нам важно понимать по карте, где робот, что перед ним и как через это пройти. Одной карты для этого мало, поэтому я завел в программе три.
Первая карта — последнее окно от робота, которое показывает, что видно сейчас в малой окрестности около него, то есть какие клетки заняты. Вторая карта — накопленная за весь заезд путем складывания окон, то есть для каждой клетки хранил, как часто та была занята и на какой высоте. Так препятствие, мелькнувшее в одном окне, со временем проступает, а ушедшая чужая собака стирается. Третья карта — план зала, который мы заложили заранее.
Положения стен оценил по записям лидара на тренировках, а зону старта и стрелки взял с карты трассы. В плане нет информации о том, где стоят препятствия. Карты прошлых тренировок я в зачете не использовал. В первой версии робот искал препятствия вообще без подсказок, а со второй я брал из своей разметки опубликованной карты трассы лишь примерное место, где искать. Но проходил препятствие робот там, где нашел его датчиками.

Стены зала правила разрешали заложить в программу заранее, поэтому полноценный SLAM мне не понадобился. Одометрия со временем копит ошибку, и я поправлял ее по стенам. Для этого я брал из окна воксели на высоте стен, сдвигал и поворачивал их, как кальку на чертеже, пока они не лягут на стены плана. Первый раз это происходит в зоне старта, за одну-две секунды, а потом снова и снова по ходу всего заезда.
Обычно для этого берут фильтр частиц, например AMCL. Он держит множество гипотез о положении робота, частиц, и оставляет те, что лучше согласуются с картой. Я его проверил. На записи первой зачетной попытки AMCL терял робота: положение 17 раз скакало, а моя перенастроенная реализация фильтра на части записей уверенно находила его не там, где он был. А вдоль стены ошибалась в 2—3 раза сильнее подгонки. Это результат на моих записях и такой разреженной карте, а не приговор фильтру частиц.
В карте за заезд я собирал приподнятые клетки в группы и для каждой считал длину, высоту и положение самой высокой точки. A-рама — доска с пиком посередине и ровными концами, качели — доска, у которой один конец выше другого, платформы — длинная и высокая группа, мост — ряд столбиков, а змейка — цепочка тонких одиночных столбиков на одной линии. Если следующего препятствия пока было не видно, робот поворачивался на месте, а потом шел туда, откуда открывалось больше не просмотренной части зала.
Свой маневр вместо планировщика. Стандартный планировщик Nav2 из ROS 2 прокладывает путь в обход препятствий, а здесь каждое нужно пройти насквозь, поэтому маневры подхода и прохода я написал сам. Перед проходом робот встает у входа и заново уточняет положение препятствия по свежему окну. Змейку он проходит дугами вокруг конусов, чередуя стороны, и достраивает маршрут, когда видит следующие конусы. К доске робот подходит сбоку, разворачивается на ее ось, отходит назад для разбега и шагом вбок встает точно на линию входа. Для этого рядом с доской нужно свободное место.
Как я учил нейросеть узнавать препятствия
Лидар замечает препятствие поздно, поэтому к третьей попытке я добавил камеру: она узнает его за 2,5—6 м, когда в карте лидара его еще нет. Расстояние по камере выходит ненадежным, и поэтому я разделил логику между ними. Камера говорит, что впереди, а лидар — где именно находится, и ближе полутора метров робот решает, что перед ним находится, по лидару. Для конусов я сначала попробовал классический поиск по цветовой маске, но так находилась только четверть размеченных конусов на расстоянии 0,5—4 м. Тогда я обучил маленькую нейросеть, тут еще приходилось учитывать, что у Raspberry Pi 5 нет нейроускорителя и при этом еще процессор в это же время строит карту и ведет робота.
Я разметил около тысячи кадров с камеры робота. Сначала рамки-черновики предлагала модель SAM 3, далее я их проверял, правил и ставил точки входа и выхода с препятствия.

Роботу мало узнать качели, ему нужно понять, с какой стороны на них заходить. Эти точки сеть училась ставить вместе с рамками, но в зачет они не попали. По камере плохо видно расстояние, а для подхода нужна ось с точностью в несколько градусов и центр с точностью до десятков сантиметров. Проверить, что точки дают такую точность, я до третьей попытки не успел и рисковать заездом не стал. Камера подсказывала, где искать и что впереди, а ось и сторону входа робот брал из карты лидара и стрелок схемы.

Real2sim2real: трасса в симуляторе и обратно
Локализацию, поиск, камеру и маневры нужно было где-то проверять, а настоящий робот был доступен только на коротких тренировках. Поэтому основным местом проверки стал цифровой двойник, копия робота и трассы в физическом симуляторе MuJoCo.
Сам подход называется real2sim2real, то есть переносим реальность в симулятор, отлаживаем там программу и ставим ее на настоящего робота. MuJoCo я выбрал, так как в нем работает заводская походка Go2 и на нем легко запускать сотни виртуальных заездов параллельно.
В двойник я перенес:
Походку — в симуляторе ходит заводская политика Go2, нейросеть, которая переводит команду «иди туда» в движения ног.
Карту лидара — симулятор пускает лучи в виртуальную сцену и приблизительно воспроизводит воксельную карту робота; ее параметры я подогнал по четырехминутной записи настоящего зала.
Сам зал — размеры и положения препятствий я брал из карт лидара, своей разметки зачетной схемы и камер зала, а где источники расходились, каждый виртуальный заезд выбирал случайное значение из всего разброса.

Расхождение симуляции с реальностью называют sim-to-real gap, и первая версия двойника показала его сразу. Я провел виртуального робота тем же маршрутом, которым настоящий обходил зал, и сравнил карты. В двойнике программа узнавала змейку в 95% близких проходов, а на настоящей карте всего лишь в 12%: например, от конусов там почти ничего не оставалось.

Сначала я пробовал просто подкрутить параметры лидара в симуляторе, но это не помогало: препятствия в двойнике все равно оставались в 2—4 раза заметнее, чем на настоящей карте. Причину я нашел, когда посмотрел на обновления карты отдельно для разных случаев: когда собака стояла, шла и поворачивала. Настоящая карта за одно обновление добавляла 125 вокселей пола, пока собака стояла, 481 на ходу и 538 на повороте, а двойник — около 235 в любом случае.
Как оказалось, ошибка зависит от движения. Лидар делает оборот за какое-то время, и в это же самое время робот тоже успевает сдвинуться. Но при этом полный оборот лидара записывался так, будто робот стоял в одной точке, еще и с задержкой, поэтому на ходу карта смазывалась. Этот дефект я перенес в симулятор. Совпадать с реальностью стало лучше, но не идеально, поэтому результаты двойника я все равно сверял с настоящими записями.
Я не стал обучать в двойнике нейросетевую политику, которая берет на вход данные датчиков, а на выходе дает команды. Обучать ее получилось бы только в двойнике, а с учетом разрыва между симуляцией и реальностью было бы сложно ожидать хорошего качества. Да и разбираться с правилами мне удобнее. Если заезд не задался, я беру запись, иду по ней шаг за шагом и нахожу место, где программа свернула не туда.
79 спусков без падений в симуляторе и одно настоящее падение
Сам симулятор тоже нужно проверять, и лучше всего это видно на спуске с платформы. На одной тренировке я вел собаку с геймпада. Подъем она прошла нормально, а на спуске поскользнулась, ее накренило на 27°, и она завалилась на правый бок.
В тот же день она падала с этих платформ еще дважды, оба раза правые лапы уходили за край доски. Я повторил такой спуск во втором симуляторе, Isaac Sim от NVIDIA, на разных скоростях и со смещением от оси. Я прогнал этот спуск 79 раз, и виртуальная собака ни разу не упала: задняя лапа соскальзывала за край, а она выравнивалась и шла дальше. Настоящая в тот день падала три раза. С тех пор ноль падений в симуляторе меня не успокаивал.

В MuJoCo тоже хватало странностей. Даю команду 0,2 м/с, а виртуальный робот почти топчется на месте, хотя настоящий на той же команде идет 0,15 м/с. В какой-то момент я сравнил оба симулятора с записями настоящего робота и для каждого записал, где ему можно верить, а где нет. Падения не угадывал ни тот ни другой.
Однажды правка змейки прошла в симуляторе все проверки, а на настоящей карте ее маршрут шел прямо через ряд конусов. Меня это здорово удивило. После этого я проверял каждое изменение в четыре захода.
Первым шел прогон по старым записям с заездов и тренировок. Программа каждые 2 секунды решала, какое перед ней препятствие, где у него ось и стоит ли подходить, а я смотрел, совпадают ли ответы с прошлой версией и с тем, что я знал заранее. Только действие так не проверишь: запись остается той же, что бы программа ни решила. В симуляторе иначе: робот повернул, и вокруг уже другая картинка.
Дальше шли двойник с одним препятствием, сотня виртуальных стартов и полный маршрут, где новая и текущая версии стартовали из одних и тех же условий, как в парном A/B-тесте. Иначе одной могли просто достаться залы полегче. Эта проверка и зарубила новый маневр змейки: со старта у препятствия он смотрелся лучше, а на полном маршруте проходил змейку в 45 заездах из 200 вместо 59. На робота он так и не поехал.
Трасса и обратно в симулятор: почему двойник переоценил новые версии
Проверки хорошо отсеивали плохие правки, но не гарантировали, что новая версия на трассе окажется лучше старой. На каждый зачетный заезд я отправлял новую версию программы, и лучше всех сработала первая, только с лидаром. Она бралась за первые три препятствия, прошла A-раму и вернулась в зону старта, а платформы и мост пропустила по плану, потому что ступенька платформ выше той, что штатная походка берет уверенно. Ее итог, 17:53.9, — это 4 минуты 9 секунд хода и почти 14 минут штрафов за касания конусов и пропуски. Законченный заезд на этой трассе многого стоил — засчитанный результат получили 12 команд из 38.

Вторая версия бралась за все пять препятствий, застревала на змейке, качелях и платформах и в зону старта не вернулась. Третья, уже с камерой, впервые прошла качели, которые первая не нашла, но тоже уступила ей. Обе новые версии отказались от A-рамы еще до прохода, потому что не нашли рядом с доской места для разворота. Программа сделала то, что я в нее заложил. Она посчитала, что новый подход не окупит потраченного времени, и пошла дальше. А в двойнике версия третьей попытки проходила A-раму примерно в трех заездах из четырех и побивала итог первой в 83% заездов.

После каждой зачетной попытки я продолжал вносить правки в двойник. Например, передвинул препятствия туда, где они стояли на самом деле. А камеры зала показали, что зал примерно на метр длиннее, чем думал робот. Двойник стал точнее, а ошибаться стал сильнее. Робот в нем и так ездил быстрее, чем в зале, а после одной из правок стал быстрее еще на минуту.
Время первой версии он угадал, две следующие перехвалил. Две попытки еще не статистика, но ошибся он оба раза в одну сторону. Думаю, дело в досках и конусах: новые версии дольше с ними возились, а в двойнике лапы касаются всего слишком гладко, да и карты лидара, по которым я рисовал препятствия, были неполными.
И все равно двойник окупился. Сломанную правку он показывал сразу, и на робота я с ней уже не шел. А вот сколько правка выиграет, он не знал.
В следующий раз я бы перенес в двойник еще и то, как лапы цепляются за доски и конусы. И каждую новую версию сразу гонял бы по всей трассе.
Вместо заключения
В Сколтехе у меня был отличный курс Perception in Robotics от профессора Гонзало Феррера, но там мы в основном занимались SLAM. С настоящими роботами я работал и раньше, но это были манипуляторы. А вот навигацию робота-собаки на живом железе я делал впервые. Мне это интересно прежде всего с точки зрения мобильной манипуляции: когда робот сам доходит до нужного места и уже там работает руками.
Финал пройдет весной 2027 года. Спасибо команде МТС True Tech Champ за задачу, трассу и удаленный доступ к роботам. Спасибо помощникам в зале, которые ставили роботов на старт и поднимали после падений, и всем, кто вел трансляции и отвечал в чате участников. Других полуфиналистов приглашаю рассказать в комментариях, как они готовились к трассе.
Напоследок: тот же зал в симуляторе, другой робот. Гуманоид Unitree G1 катается по трассе на скейте.

Комментарии 0