Карта 2×2 км на роботе: тайлы 512² вместо растра на 1,6 ГБ

· ·

Карта 2×2 км на роботе: тайлы 512² вместо растра на 1,6 ГБ

PrimeCoder · 2026 · формат: tech

Разбор новинки простыми словами: что это, кому нужно и брать ли.

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

Связанные услуги: каталог, AI Boost Team, стать клиентом.

Робот упирается не в батарейку, а в гигабайт

Территория 2×2 км с клеткой 5 см — это 40 000 клеток по стороне. Перемножьте: 1,6 млрд клеток. Один байтовый слой — 1,6 ГБ, примерно 1,49 ГиБ. RGBA — 6,4 ГБ. И это только массивы одного прямоугольника, без копий, других слоёв и 3D-данных. При этом обследованы узкие дорожки, а большая часть прямоугольника — пустота.

В седьмой части своей серии инженер Unibot Лев Ларин показал, как SpeedyBot от Alfa Robotics на платформе UNIBOT Fleet доезжает к адресу за краем прежней карты. Детализацию 5 см оставили. Цельный растр на всю территорию — нет.

Что именно сделали

Карта лежит на диске PNG-тайлами 512×512 клеток. В метрах это 25,6×25,6. Скрипт native_map_atlas.py собран на Python, NumPy, Pillow и PyYAML: метаданные задают общую геометрию, штатные PCD и BlockMap остаются у native SLAM. Один тайл выглядит как файл вида atlas/tiles/map/-1_0.png.

Адресация клетки — место, где обычно и ломается. Из мировой координаты получают клетку через math.floor, а не через усечение int. При x = −0,05 м и resolution 0,05 клетка равна −1, тайл −1, локальная колонка 511. Усечение в ноль сдвинуло бы всю карту на клетку влево от нуля. Дальше divmod на 512 даёт номер тайла и локальный индекс, NumPy индексирует как [ly, lx], а при записи PNG строки переворачиваются по Y — картинка идёт сверху вниз.

Три уровня вместо одного массива

Ключ ко всему кейсу: единица хранения, единица кеша и единица работы — разные вещи.

  • Хранение — разреженный атлас тайлов по 25,6 м. Пустые квадраты никто не пишет.
  • Кеш — восемь декодированных тайлов с вытеснением по LRU через OrderedDict. Полезные массивы: 2 МиБ для одноканальных 512², 8 МиБ для RGBA.
  • Работа — скользящее окно 250×250 м: ядро 200 м плюс нахлёст 25 м с каждой стороны. Это 5000² клеток, или 25 млн байт на слой. В клетках 4000 + 2×500 = 5000, что ниже native-предела 8192.

Полный массив территории не создаётся ни на одном шаге. Соседние окна перекрываются на 50 м, поэтому на стыке не рвётся проходимость. Тайл 512² — единица хранения, ядро 4000² — единица навигации. Это и есть тот самый паттерн, который переносится на любой продукт с большими пространственными данными.

Что это меняет в поведении робота

Наивная нарезка растра дала бы красивый диск и мёртвую навигацию: на границе окна пришлось бы строить новую задачу. Здесь planning не перезапускается. При смене ядра планировщик запрашивает новый растр через типизированный ROS-сервис AtlasPlan у существующего native adapter. Ответ содержит bounded PGM, родной loadMapFromFile читает его со своими порогами, StaticLayer принимает растр под тем же именем карты. PGM передаётся локально на роботе, не через мобильную сеть. Поездка остаётся одной задачей с прежними целью, yaw и действиями.

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

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

Досъёмка не теряется. В фотослое обновляются наблюдаемые пиксели (RGB и alpha 255), прозрачные участки новой съёмки сохраняют прежнее покрытие. Для лидарного растра работает np.minimum в байтовом формате native-карты. Изменённый PNG пишется во временный файл и переименовывается, а облако, атлас, разметка и рабочий растр публикуются вместе как candidate-каталог. Атлас не восстанавливают из последнего PGM — рабочее окно производное.

Кому пригодится паттерн

Логика «тайл на диске + LRU-кеш + ограниченное рабочее окно» не привязана к доставке. Склад с сотнями стеллажных проходов, агро-поле под обход дроном, стройплощадка с ежемесячной досъёмкой, ГИС-сервис с ортофотопланом региона — везде один и тот же вопрос: сколько памяти съест прямоугольник и какая его доля реально заполнена. Если пустоты больше половины, разреженный атлас почти наверняка дешевле цельного растра.

Второй переносимый урок — разделение ответственности. Нарезка на тайлы отвечает за хранение. Окно отвечает за вычисления. Интерфейс между ними один: «дай мне растр под этот прямоугольник». Такой контракт можно поставить и над не-ROS бэкендом.

Где подвох

Стенд синтетический. Территория 2×2 км с чередующимися проходами между стенами дала маршрут 19,8 км через сто окон. Цифры пика памяти — 87,27 МиБ на построении и досъёмке и 67,34 МиБ на исполнении — это два отдельных процесса на ПК, а не потребление SLAM на бортовом RK3588.

Физический переход через двухсотметровую границу этим опытом не проверялся.

Что проверили по-настоящему: 59 базовых тайлов (4 у Z75 и 55 у Z77) сверили на роботе и во Fleet — координаты, размеры и декодированные пиксели совпали. При сохранении Z77 после возобновления съёмки остались 69 018 прежних занятых клеток и объекты десяти карт, граница выросла влево на 13 клеток, то есть на 65 см. Поездка на роботе — около 0,85 м от старта с возвратом на док, 3962 заданных положения.

Ещё два ограничения, которые легко проглядеть. Память трёхмерной локализации этим способом не ограничена. Память индексов, графа и пользовательских объектов считается отдельно от растра. Экономия касается карты, а не всего стека.

Факт → что это значит

ФактЧто это значит для вашего продукта
2×2 км при клетке 5 см = 1,6 ГБ на слой, 6,4 ГБ на RGBAЦельный растр упирается в память задолго до того, как упрётся в процессор
Тайл 512×512 клеток = 25,6×25,6 мРазмер тайла выбирается под паттерн доступа, а не под удобство файловой системы
Кеш на 8 тайлов: 2 МиБ / 8 МиБПотолок памяти фиксирован и не зависит от площади территории
Окно 250×250 м = 25 млн байт на слойРабочий бюджет памяти задаётся заранее и проверяется на стенде
floor вместо int для отрицательных координатКлассическая ошибка, из-за которой карта сдвигается влево от нуля
Пик 67,34 МиБ на маршруте 19,8 кмСинтетика, а не борт — но верхняя граница уже видна

Брать или подождать

Брать — если у вас продукт с большими пространственными данными и известной долей пустоты в прямоугольнике. Паттерн «тайл + LRU + скользящее окно» проверяется на своём стенде за неделю, а не за квартал.

Подождать — если вам нужны гарантии по памяти всего навигационного стека, включая 3D-локализацию. Здесь кейс отвечает только за растр. И если переход между окнами критичен для физического робота: на железе эту границу ещё не переезжали.

FAQ

Почему не сжатый растр целиком?

PNG на диске сжимает пустоту хорошо. Проблема не в файле, а в декодированном массиве в памяти. Тайлы ограничивают именно его.

Можно ли обойтись без ROS?

Сервис AtlasPlan и StaticLayer — часть конкретной интеграции. Сама схема хранения и окна от ROS не зависит.

Досъёмка затирает старое?

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

Что сделать на этой неделе

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

Читайте также