Гражданин куст, уступите дорогу

«Проезд перекрыт, пожалуйста, уступите дорогу». На камере — куст. Проверили, откуда приходит категория пешехода, и нашли ещё два дефекта: земля возвращалась в препятствия, а настоящую коробку удаляла маска дороги.
Я Лев Ларин, инженер Unibot. Мы подключаем Alfa Robotics SpeedyBot к своей платформе UNIBOT Fleet, сохраняя штатную навигацию робота-доставщика. Это пятая часть серии.
Начало серии · ← Часть 4: CRISPR для ARM: чиним ДНК робота · Часть 6: За пивом за угол — через Магадан →

Два тракта восприятия
На RK3588 работают ROS1, лидар RoboSense Airy Lite и детектор PointPillars. Камерная модель выполняется через RKNN. Её 2D-детекции сопоставляются с 3D-рамками в lidar_camera_fusion_node.py, затем проходят трекер. Геометрия камеры глубины обрабатывается отдельно:
LiDAR → PointPillars → 3D boxes ─┐
RGB → RKNN → 2D detections ───┴→ fusion → tracker → объекты
RGB-D → GroundFilter → Remember → obstacle_cloud_classifier
↓
static/dynamic → costmap → навигация
Обзорные IP-камеры из статьи про видео не следует автоматически считать источником кадров этой модели. В контрольной записи камера не дала детекций, а лидарная ветка продолжала выдавать пешеходов и велосипедистов. Установить физический источник каждой старой голосовой реплики по записям нельзя; путь категорий прослеживается.
Категория меняется, препятствие остаётся
Неподтверждённые камерой human-like объекты переводим в существующий класс Unknown, сохраняя геометрию. На одинаковом записанном входе у оставленных рамок сохранились число, порядок, положение, размеры, поворот и confidence.

Классы LiDAR/tracker: 0: car, 1: pedestrian, 2: cyclist, 3: truck, 4: forklift, 5: Unknown. Камерный publisher передаёт текстовые метки car, person, rider: порядковые номера этих списков не совпадают.
Сначала отбираем камерные рамки по классу и confidence, затем выбираем максимальный IoU — отношение площади пересечения рамок к площади объединения:
camera_class = {
0: 'car', 1: 'person', 2: 'rider',
3: 'truck', 4: 'forklift',
}.get(cls_id)
for j in range(len(camera_boxes)):
if (camera_class is None
or camera_boxes[j]['class'] != camera_class
or not camera_boxes[j]['conf'] > self.camera_conf_threshold):
continue
# Дальше исходный код сравнивает IoU с проекцией LiDAR-box.
Если выбирать IoU до проверки класса, рамка машины может подтвердить пешехода или перекрыть подходящее совпадение с человеком. Пороги остаются штатными.
После существующего решения keep_object нормализуем категорию:
if keep_object:
if cls_id in (1, 2) and not (
i in lidar_matches
and lidar_matches[i]['iou'] > self.iou_threshold
):
obj['cls_id'] = 5
obj['score'] = new_score
Confidence сохраняется. Для Unknown трекер использует порог 0,2, как для human-like классов; для остальных остаётся 0,6. При публикации берём текущую категорию, чтобы прошлое состояние ID трека не вернуло «пешехода».
Голосовой запрос уступить дорогу теперь требует действительной блокировки и свежего подтверждения остановки по телеметрии шасси. Человеческой рамки на будущем участке пути недостаточно. Пауза между репликами сохраняется при кратком исчезновении кандидата.
Фильтрация земли
GroundFilter ищет плоскость поверхности методом PCL SAC segmentation/RANSAC: a·x+b·y+c·z+d=0. Согласованные точки, inliers, относятся к земле. Дополнительная ветка возвращала часть inliers в препятствия для сохранения ступеней, но заодно возвращала покрытие дорожки.
Просто убрать возврат не получилось: пропала контрольная коробка высотой 27 см. Допуск поиска плоскости оказался 50 см, и коробка попадала в полосу земли. Поэтому одновременно установили предусмотренный кодом допуск 5 см и удалили возврат inliers. Допуск — расстояние от найденной плоскости, не высота обнаруживаемого препятствия относительно корпуса.
В дальней области нижняя граница использовала фиксированный Z и ошибалась на наклонной дорожке. Заменили её высотой той же найденной SAC-плоскости, не выполняя второй fit. Ядро filter_existing_plane:
double lower_z = -(a * point.x + b * point.y + d) / c + clearance;
if (point.x >= x_min && point.x <= 10.f &&
point.y >= -5.f && point.y <= 5.f &&
point.z <= 0.f && point.z >= lower_z) {
far_cloud->begin[retained++] = point;
}
clearance=0,45 м — прежний отступ дальней ветки над плоскостью. XY-границы и верхний Z остались штатными. Если плоскость или её inliers некорректны, вызывается прежний crop. Основной ближний вход имеет границу 2,6 м, предварительный диапазон — 4,5 м; дальний отступ 45 см на ближние коробки не переносится.
Маска дороги удаляла коробку
Перед неподвижным роботом поставили коробку высотой 27 см и сохранили данные примерно с одного и двух метров.

После Ground её точки сохранялись. После маски проезжей области часть исчезала; в близком контроле исчезала вся проверенная область коробки.
Проверили проекцию RGB-D-точек в изображение: u=fx·X/Z+cx, v=fy·Y/Z+cy после преобразования координат и компенсации движения. Нашли рассогласование проекций. Но диагностический пересчёт с правильной проекцией тоже не спас коробку: её центральная часть была размечена как дорога. Ошибка геометрической проекции и ошибка семантической маски требовали разных исправлений.
Удаляющий mask-stage убрали из пространственного тракта: Ground → Remember → classifier. Камерные детекции остались в ветке категорий. Этот опыт относится к камере глубины, а не к потере коробки всеми сенсорами робота.
Проверка результата
Настоящий ARM64-код обрабатывал одинаковые сохранённые входы. Проверяли точки в одной 3D-области предмета после каждого этапа. На реальных сценах использовали коробку, кусты и бетон; на искусственных облаках — низкую и высокую преграды на заданной поверхности.
В проверенных областях лишние точки покрытия удалялись, коробка на последующих сообщениях и ближние кусты сохранялись. В некоторых контролях первое сообщение после запуска оставалось пустым; это поведение включено в результат. Проверка доходила до классификатора, чтобы не пропустить удаление полезных точек следующим узлом.
Нативный стенд: 33 условия, 132 callbacks с разными поверхностями и контрольными коробками на разных расстояниях. На установленном неподвижном роботе Ground и Remember выдавали около 10 Гц. Локальная costmap — 480×480 клеток по 5 см, то есть 24×24 м.
Владелец встал перед камерой: она подтвердила человека, категория пешехода сохранилась дальше по цепочке.

В другом коротком наблюдении без камерных подтверждений оставались Unknown-объекты без человеческих категорий.
После установки комплекта, включая исправление порядка TF, робот выполнил Площадка → Пруд → док. Три команды завершились успешно, шасси подтвердило контакт зарядки. За проверенное окно не было TF sync failed и вызовов просьбы уступить дорогу в журналах озвучки; акустическую запись не делали. Контроль блокировки оставался включён.
Поездка проверяет весь установленный комплект. Результат одной фильтрации отдельно от исправления категорий и TF ею не измерен. Погодная фильтрация разобрана в истории про туман.
Все 10 историй SpeedyBot
Робот доставщик дороже автомобиля, мокрая трава и открытый JDWP
Гражданин куст, уступите дорогу — вы здесь.
KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.