Привет, Хабр! Разработка систем содействия водителю (ADAS) и автопилотов для автомобильного транспорта сейчас у всех на слуху. Но в сфере рельсового транспорта — и особенно в метрополитене — стоят свои, гораздо более специфические задачи.

Недавно я участвовал в хакатоне «Лидеры Цифровой Трансформации», где перед командами стояла безумно интересная Mission-Critical задача от Дептранса Москвы: Разработка алгоритма контроля пространства по ходу движения беспилотного поезда метро. Суть в том, чтобы по данным 3D-лидара Hesai 128 надежно обнаруживать препятствия в створе путей на максимальной дальности (до 150+ метров) в полной темноте тоннеля, сохраняя нулевой или минимальный уровень ложно-положительных срабатываний.

Моё решение не вошло в топ-10 победителей. Подвела классическая ловушка хакатонов — автоматический судейский скрипт проверяющей платформы капризен к структуре выходных данных, и потеря всего 5–6 кадров на ROS-шине из-за блокирующих логов и принтов в stdout, а также мелкая рассинхронизация знаков в экстраполяции, привели к жесткому срезанию баллов.

Однако архитектурный каркас нашего пайплайна (одометрия, концепция «Трубы из Прошлого» и интерактивный 3D-кокпит на Three.js для машиниста) получился монументальным. Мы сознательно отказались от «школьного» покадрового вычисления полиномов путей (polyfit). Вместо этого мы построили Цифровой Двойник (Digital Twin) инфраструктуры на базе пространственной памяти.

Цель этой статьи — не просто провести работу над ошибками, а найти единомышленников. Я выложил ядро проекта в Open Source под лицензией MIT. И если вы тоже участвовали в этом треке, ковыряли этот сложнейший датасет, не прошли, но чувствуете спортивный азарт — давайте объединимся, переложим критические узлы на C++ и добурим это решение до идеального промышленного робототехнического продукта.

Ниже — подробный разбор того, как перемалывать около 1 000 000 точек на кадр на частоте 10 Гц и как заставить DBSCAN видеть преграды на горизонте видимости по двум лучам.

🚇 В чём боль: Специфика тоннелей метро

Большинство готовых библиотечных алгоритмов 3D Computer Vision заточены под открытые пространства (беспилотные авто на дорогах). Попытка запустить их в метрополитен приводит к моментальному отказу:

  1. Геометрическая вырожденность (Geometric Degeneracy): Тоннель метро представляет собой идеальный, гладкий бетонный или чугунный цилиндр на протяжении сотен метров. На прямых перегонах классическим алгоритмам локализации (вроде Open3D ICP Point-to-Plane) банально не за что зацепиться по оси движения (Z). Стены везде одинаковы. Система начинает «проскальзывать», занижая реальную скорость поезда.

  2. Взрыв полиномов: Попытка аппроксимировать рельсы кривыми второго или третьего порядка покадрово разбивается о геометрию лидара. На дистанциях свыше 40 метров лучи редеют, шаг сетки точек увеличивается. Любой случайный лазерный шум или вспышка пыли заставляют полином «взрываться» и улетать в потолок тоннеля, ослепляя детектор.

  3. Облака взвеси: Тоннель — это замкнутая экосистема с высокой концентрацией пыли, мелкодисперсного пара и колесных искр. На частоте 10 Гц лидар Hesai 128 генерирует сотни случайных микро-кластеров по 3–5 точек. Если выставить алгоритму кластеризации высокую чувствительность, поезд будет экстренно тормозить перед каждым облаком пара.

🏗️ Архитектура Mission-Critical конвейера

Чтобы система была устойчива к сбоям и не теряла точки, мы разделили вычислительные контуры. Контур лазерной одометрии (Локализация) полностью изолирован в оперативной памяти от Контура детекции препятствий (Безопасность).

Пайплайн спроектирован по принципу Producer/Consumer через ROS 2 Humble. Сетевой поток FastDDS перехватывает сообщения PointCloud2 в ОЗУ, буферизирует их в асинхронную очередь Queue(), защищая систему от джиттера ОС, пока параллельный поток последовательно крутит тяжелую математику:

```text
       [ СЫРОЕ ОБЛАКО ТОЧЕК: ROS 2 HESAI 128 (10 Гц) ]
                                |
                                v
     Приведение к единому мосту осей [X:Ширина, Y:Высота, Z:Дальность (-abs)]
                                |
      ┌─────────────────────────┴─────────────────────────┐
      ▼                                                   ▼
[ КОНТУР ОДОМЕТРИИ (Локализация) ]          [ КОНТУР ДЕТЕКЦИИ (Безопасность) ]
      │                                                   │
      ├─► Воксельное сжатие стен (15см)                   ├─► Смарт-Щит Сжатия Фона (Защита колеи)
      │                                                   │
      ├─► Сплошной DBSCAN по скелету стен                 ├─► Секторный RANSAC пола (Локальный ноль)
      │                                                   │
      ├─► Паспортизация «ДНК» Ориентиров                  ├─► ВЕКТОРНЫЙ ПУТЬ-ОБЪЕКТ (TrackLock)
      │   (Тензор инерции + Интенсивность)                │   (Одометрический сдвиг + Маска «Трубы»)
      │                                                   │
      ├─► Двухконтурный ИИ-Матчинг (TTL Карта)            ├─► Адаптивный DBSCAN аномалий в конусе
      │                                                   │
      ├─► Расчет ΔZ (Open3D ICP Point-to-Plane)           ├─► Dynamic NMS (Схлопывание осколков)
      │                                                   │
      └─────────────────────────┬─────────────────────────┘
                                |
                                v
            [ ИТОГОВЫЕ 3D-БОКСЫ ВЕРИФИЦИРОВАННЫХ УГРОЗ ]
                                |
             ┌──────────────────┴──────────────────┐
             ▼                                     ▼
   [ Сабмит: SUBMISSION.CSV ]           [ Стрим: WEBSOCKETS (WebGL Three.js) ]
```

🧬 Модуль 1: Двухконтурная одометрия со стабилизацией по блеску шпал

Чтобы победить проблему скольжения в «гладкой трубе», мы внедрили параллельный двухэшелонный контур локализации, использующий разные физические свойства облака точек.

1. Быстрый относительный контур (Стены тоннеля)

Мы берем точки, находящиеся за пределами рельсовой колеи:

outside_tracks_mask = (x < config.TRACK_GAUGE_LEFT) | (x > config.TRACK_GAUGE_RIGHT)

Пропускаем их через DBSCAN для выделения крупных кластеров элементов обделки туннеля, кабельных муфт и пикетных знаков. Для каждого кластера через тензор ковариации вычисляются инварианты формы: Linearity (вытянутость кабелей), Planarity (плоскостность стен) и Sphericity (объемность конструкций).

Эти объекты заносятся в Единую Хроно-Карту памяти с TTL-логикой деградации. В нормальном режиме между текущим кадром и картой якорей запускается высокоточный алгоритм Open3D ICP Point-to-Plane по нормалям плоскостей, вычисляя смещение кадра calculated_shift_z.

2. Абсолютный калибровочный контур (Рельсошпальная решетка)

Каждые 5 секунд параллельно включается модуль абсолютного квантования путей. Рельсы со шпалами имеют фиксированный периодический шаг укладки и высокий коэффициент отражения лазера (блеск) по сравнению с бетоном.

Алгоритм жестко вырезает ближний створ путей:

rail_mask = (z >= -15.0) & (z <= -3.5) & (x >= -0.75) & (x <= 0.75) & (y >= -1.85) & (y <= -1.05)

И производит плотностное квантование оси Z вокселями по 35 см. Внутри каждого плотного вокселя вычисляется уникальный многофакторный паспорт: центроид, масса точек, средняя лазерная интенсивность отражения (intensity) и геометрическое соотношение стандартных отклонений shape = std_x / std_z.

Сверяя паспорта гребенки шпал текущего и прошлых кадров через весовые дельты признаков, этот контур вычисляет абсолютно чистую физическую скорость состава, независимую от истории ICP:

feature_dist = (mass_diff * 0.1) + (intensity_diff * 0.45) + (shape_diff * 0.45)

3. Орган ИИ-арбитража

Если мгновенный ICP-контур стен уходит в кому в гладкой трубе перегона (скорость падает около нуля, хотя поезд физически несется вперед), интеллектуальный арбитраж фиксирует критический разрыв пропорций (velocity_ratio):

cond_coma = (self.prev_velocity_kmh <= config.ARBITER_COMA_THRESHOLD_KMH and stab_speed > config.ARBITER_WAKEUP_SPEED_KMH)

Принудительно срабатывает ИИ-Перехват: скорость, Хроно-Карта ориентиров и состояние трекера путей из абсолютной «песочницы» рельс инжектируются в основной контур, полностью сбрасывая ошибку дрифта.

🏗️ Глава 4: Модуль TrackVectorLock вместо Polyfit

Как я уже говорил, классический покадровый np.polyfit сходит с ума на дистанциях 50+ метров, когда лучи лидара редеют, и любая случайная взвесь заставляет параболу искривляться, обрезая реальные объекты.

Мы решили эту проблему через структуру пространственной памяти — модуль TrackVectorLock.

1. Инерциальный перенос пути

Мы инициализируем монолитную цифровую трассу — массив Z-нод с шагом 5 метров от бампера поезда до горизонта видимости (150м). Поскольку поезд едет вперед, рельсы физически набегают на него. Мы переносим (интерполируем) накопленные профили X (ширина) и Y (высота) назад навстречу кабине с помощью высокоскоростной NumPy-интерполяции на основе честного шага поезда train_step_z из ядра одометрии:

if abs(train_step_z) > 0.001:
    old_z_shifted = self.track_vector[:, 2] + abs(train_step_z)
    self.track_vector[:, 0] = np.interp(self.track_vector[:, 2], old_z_shifted, self.track_vector[:, 0])
    self.track_vector[:, 1] = np.interp(self.track_vector[:, 2], old_z_shifted, self.track_vector[:, 1])

Если лидар на каком-то кадре «ослеп» из-за пыли, трасса перед поездом не исчезает и не взрывается в космос. Она плавно инерциально «втекает» под бампер из накопленной истории. Система помнит форму туннеля, которую она видела 40 метров назад.

2. Разделение зон ответственности

  • Ближняя зона (до 30 метров): Профиль X и Y непрерывно обновляется «живыми» медианами точек пола, полученными от секторного RANSAC. Здесь плотность лучей максимальна, и система постоянно адаптирует «голову» вектора под реальные стыки и микро-повороты.

  • Дальняя зона (за 30 метров): Код содержит жесткий брейк: if z_val > 30.0: break. Дальний профиль вообще не трогается лазером текущего кадра! Он формируется исключительно за счет того, что дальние ноды — это бывшие ближние ноды, которые поезд записал ранее и сместил по одометрии. Это прецизионная защита от шума темноты. Чтобы профиль высоты Y не улетал в космос на пустых участках и станциях, мы зажали его в жесткие рамки лотка метро: np.clip(..., -1.80, -1.35).

3. Векторизованный 3D-коридор без циклов

Генерация маски безопасности выполняется в один NumPy-шаг для всего миллиона точек:

grid_x_center = np.interp(z_pts, self.track_vector[:, 2], self.track_vector[:, 0])
grid_y_floor  = np.interp(z_pts, self.track_vector[:, 2], self.track_vector[:, 1])

Функция мгновенно находит точные координаты центра колеи X и пола Y индивидуально для каждой из N точек облака.

В туннеле ширина жестко зажимается до габаритов состава (TRAIN_HALF_WIDTH + abs_z * 0.001), а потолок плавно опускается конусом с 3.3м до 3.0м вдалеке, чтобы срезать набегающий круглый свод тюбингов. На станциях шлюз раскрывается конусом вширь (до 3.5м) и поднимается до 3.8м вверх для заглядывания в кривые.

🔬 Глава 5: Контур детекции — как заставить DBSCAN видеть на 150 метров

Когда маска безопасности отсекла 95% точек стен и потолка тоннеля, оставшиеся аномалии внутри колеи нужно сгруппировать в объекты. Главная проблема — на расстоянии 100+ метров от 3D-лидара Hesai 128 на препятствие попадает всего 2–3 луча. Обычный DBSCAN разорвал бы единый дальний объект на десятки мелких шумов. Мы применили несколько мощных ЦОС-хаков:

1. Селективный смарт-щит сжатия фона

Чтобы защитить CPU от перегрузки, интеллектуальный балансировщик разделяет сцену. Точки, которые летят с расстояния свыше 40 метров строго внутри габаритов поезда, защищены на 100% — разреженные дальние лучи доходят до кластеризации в исходном качестве. А вот избыточные миллионы точек ближних стен туннеля эффективно прореживаются с динамическим шагом skip_step.

2. Плотoperatorное масштабирование конуса перспективы

Мы применили математический хак сжатия пространства. Алгоритм вычисляет падение плотности точек пола через гистограмму путей. Дальше критического рубежа (dynamic_start_z) координаты X и Y точек принудительно сжимаются конусом к оси поезда:

scaling_factors = np.where(z_coords < -dynamic_start_z, -dynamic_start_z / z_coords, 1.0)
scaled_points[:, 0] *= scaling_factors
scaled_points[:, 1] *= scaling_factors

Для DBSCAN далекое облако точек искусственно становится плотным и монолитным, что позволило выставить min_samples=2 и ловить препятствия на горизонтах видимости всего по паре лучей.

3. Математический аппарат динамической «Дышащей воронки»

Выделенные кластеры проходят через ИИ-валидатор геометрических боксов. Вместо жестких порогов объема и массы, критерии физики лазера непрерывно угасают по гиперболе вслед за дальностью:

  • Требуемое количество точек: dynamic_min_points = 2.0 + (15.0 / (1.0 + 0.05 * distance)) — на 10 метрах система требует 12 точек, а на 100 метрах пропускает объект всего по 2–3 точкам.

  • Объем бокса: volume_decay = np.exp(-distance / 45.0) — порог объема экспоненциально падает вдали, позволяя «зацепить» коробку, потерявшую объемную форму в облаке разреженных лучей.

🛰️ Глава 6: Промышленный MOT-трекер на векторных NumPy-операциях

После того как адаптивная «дышащая воронка» отфильтровала лазерный шум и одобрила геометрические макро-боксы, они поступают на сборочную линию Multi-Object Tracking (MOT) трекера. Наша задача — присвоить объектам сквозные ID (Препятствие #01, Препятствие #02) и полностью подавить сиюминутные шумовые вспышки станционных конструкций.

1. Дышащие скоростные ворота ассоциации

Классический подход с жестким радиусом поиска соответствия (например, 2.5 метра) ломается на высоких скоростях поезда или на сверхдальней дистанции, где облако точек редеет. Происходит хаотичная смена ID (ID Switches).

Мы внедрили динамический адаптивный радиус поиска, который эластично расширяется вслед за шагом поезда (train_step_z) и удалением объекта (distance):

abs_step = abs(train_step_z)
allowed_gate = 2.5 + (abs_step * 3.5) + (distance * 0.02)
allowed_gate = np.clip(allowed_gate, 2.5, 7.5)

На расстоянии 120 метров при ходовых скоростях ворота раскрываются почти до 6.5 метров, намертво удерживая сквозную траекторию объекта и не давая треку разорваться.

2. Курсовое ведение в слепоте (Coasting) и Хронологический карантин

Если кадр частично дропнулся на ROS-шине или объект на мгновение скрылся из видимости лучей, трекер не уничтожает его ID. Включается режим инерционного сопровождения: цель удерживается в памяти до 15 тактов (1.5 секунды полной слепоты), при этом её центроид покадрово смещается навстречу бамперу на основе поступающей одометрии.

Чтобы полностью гарантировать чистоту финальной выгрузки submission.csv, мы ввели жесткий карантин. Объект признается реальной угрозой и выводится в систему принятия решений только в том случае, если он стабильно верифицируется трекером на протяжении минимум 8 кадров подряд.

📺 Глава 7: Интерактивный 3D-Кокпит машиниста (Three.js & WebSockets)

Технические отчеты и CSV-таблицы — это хорошо, но Mission-Critical системы должны обладать прецизионным интерфейсом оператора. Мы развернули полноценный веб-сервер на FastAPI, который по WebSockets стримит обработанные ИИ-ядром данные на фронтенд.

  [ ИИ-Бэкенд (NumPy / Open3D) ] ➔ (WebSockets / JSON) ➔ [ Three.js WebGL UI ]

На фронтенде библиотека Three.js отрисовывает интерактивный 3D-кокпит машиниста. Чтобы не перегружать сетевой стек и WebGL-рендер миллионами сырых строк, бэкенд прореживает облако точек внутри подтвержденного куба препятствия с шагом [::5].

В темноте виртуального тоннеля Three.js подсвечивает истинную лазерную форму преграды (будь то силуэт человека, коробка или настил). При входе объекта в критическую зону интерфейс мгновенно переходит в режим тревоги (EMERGENCY BRAKE TRIPPED), выводя на HUD-панель карточку цели с точной дистанцией до бампера поезда.

🎬 Видео демонстрация работы системы

Чтобы оценить, как пайплайн детекции «Трубы из Прошлого» работает в связке с WebGL-визуализацией в реальном времени, посмотрите короткую видеодемонстрацию работы нашего 3D-Кокпита:

📺 Посмотреть видео работы 3D ADAS системы на YouTube https://youtu.be/o_xoYm8RtW8

🚀 Open Source Вызов: Давайте дожмем это решение вместе

Наш проект доказал, что концепция Цифрового Двойника инфраструктуры и модуль векторной фиксации пути TrackVectorLock («Труба из Прошлого») архитектурно превосходят покадровые геометрические параболы и способны держать габарит на дистанциях до 150+ метров.

Да, бездушный автоматический скрипт хакатонной платформы отбраковал наш финальный сабмит из-за потери нескольких кадров на ROS-шине при питоновских принтах и тонкостей полярности осей. Но мы не планируем бросать эту разработку. Это готовый прототип промышленного уровня TRL 4–5 для реального железнодорожного ADAS.

И сейчас я хочу превратить его в полноценный, ультимативный Open Source проект. Код полностью открыт и опубликован под свободной лицензией MIT.

Кого я ищу в команду:

  • Участников этого же хакатона, которые «в теме», ковыряли эти же датасеты Hesai 128, не вошли в топ-10, но чувствуют спортивный азарт и хотят довести дело до конца.

  • C++ / PCL разработчиков, готовых помочь переложить тяжелую математику фильтрации, RANSAC пола и DBSCAN с Python на нативный, высокоскоростной код (Zero-Drop Policy кадров).

  • Robotics / ROS 2 инженеров, желающих отполировать сетевой буфер FastDDS и упаковать проект в идеальный Mission-Critical контейнер.

С меня — прозрачная архитектура, двухконтурная одометрия со стабилизацией по блеску шпал, векторный лок путей и рабочий 3D-кокпит на Three.js.

Буду рад вашим звездам в репозитории, пулл-реквестам и комментариям под статьей.

репозиторий был некоторое время закрыт случайно, я открыл.