# Эксперименты Все числа в документе получены скриптами из `tools/` на предоставленных данных и воспроизводятся командами, указанными в каждом разделе. Там, где результат оказался хуже ожидаемого, он приведён как есть. Машина разработки: Ryzen 5 7600X, 32 ГБ, RTX 5070 Ti. Стенд жюри слабее по CPU (i7-9700E, 8 ядер, 2.6 ГГц), поэтому замеры задержки приведены с запасом и обсуждаются отдельно в разделе 7. --- ## 1. Что на самом деле в данных ```bash python tools/inspect_bags.py --root data/for_hackathon ``` | Бэг | Кадров | Топик | Точек в кадре | Валидных лучей | |---|---|---|---|---| | `doubleT_obstacle` | 201 | `/sensing/lidar/hesai128/pointcloud` | 921 600 | 37.6 % | | `doubleT_platform` | 345 | `/lidar_points` | 307 200 | 60.5 % | | `roundT_doubleT` | 252 | `/lidar_points` | 307 200 | 61.7 % | | `roundT_pressureGate_roundT` | 268 | `/lidar_points` | 307 200 | 60.6 % | | `roundT_squareT_pressureGate_squareT` | 545 | `/lidar_points` | 307 200 | 61.6 % | | `squareT_platform_squareT_switch` | 877 | `/lidar_points` | 307 200 | 59.4 % | | `new_data` (221 шард) | 11 271 | `/lidar_points` | 307 200 | — | Три вещи, о которых README датасета молчит и которые ломают наивную обработку: 1. **Архивы — несжатый tar**, а не `.zst`. 2. **Раскладка скана различается**: 3600 азимутов на 360° против 1200 на 120°. Ничего нельзя захардкодить. 3. **У каналов постоянный азимутальный сдвиг до 15.6°** (±78 столбцов при шаге 0.1°). Без выпрямления «столбец» не является направлением, и все пространственные фильтры считают мусор. Руководство Pandar128E3X это подтверждает прямо: «Each laser channel has an intrinsic azimuth offset». Различаются и условия съёмки: высота установки сенсора над головкой рельса составляет 1.31–1.33 м в пяти бэгах и 1.70 м в `doubleT_obstacle`. Решение калибруется по данным и работает с обоими без правок. --- ## 2. Проверка калибровки по паспорту ```bash python tools/extract_channel_table.py --pdf <руководство> --out .../pandar128_channels.csv python tools/validate_calibration.py ``` Решётка лучей восстанавливается **из самих облаков точек**, паспортные углы нигде не используются. Поэтому таблица каналов из Приложения A руководства служит независимой проверкой: | Величина | Измерено по данным | Паспорт | Расхождение | |---|---|---|---| | Разброс азимутального сдвига каналов | ±7.80° (размах 15.60°) | −7.811°…+7.804° (15.615°) | 0.015° | | Диапазон углов места | −25.1°…+14.4° | −25.016°…+14.436° | < 0.1° | | Угол места, поканально | — | — | медиана 0.065°, макс 0.123° | | Азимутальный сдвиг, поканально | — | — | медиана 0.044°, макс 0.152° | Расхождение одинаково на всех шести бэгах. Остаток объясняется тем, что в руководстве приведены **проектные** значения, а каждый экземпляр прибора поставляется с собственным файлом угловой коррекции — именно её и восстанавливает калибровка по данным. Остаточная угловая ошибка после выпрямления образа: **p50 = 0.0000°, p99 = 0.028°, макс 0.040°** — меньше половины шага решётки (0.05°). --- ## 3. Сколько тоннель вообще позволяет увидеть ```bash python tools/analyze_corridor.py --frames 50 ``` | Бэг | Радиус кривой | Прямая видимость (p99.9) | |---|---|---| | `doubleT_obstacle` | 1690 м | 167 м | | `doubleT_platform` | 7310 м | 143 м | | `roundT_doubleT` | 1310 м | 126 м | | `roundT_pressureGate_roundT` | 2970 м | 121 м | | `roundT_squareT_pressureGate_squareT` | 8660 м | 127 м | | `squareT_platform_squareT_switch` | 2300 м | 132 м | Максимальное эхо во всём датасете — **208.8 м**, что совпадает с паспортными 0.3…200 м. Но **реальная прямая видимость 121–167 м**: тоннели кривые, и линия взгляда упирается в стену раньше, чем кончается дальнобойность прибора. Это измерение, а не оправдание: заявленные в ТЗ «300 м → отлично» на предоставленных участках недостижимы **никаким** алгоритмом, потому что от объекта за поворотом до лидара не доходит ни одного фотона. Проверяется прямо: при вставке синтетического предмета на 84 м в кадр, где фон в том же направлении стоит на 76 м, ни один луч до предмета не доходит — он физически закрыт стеной. --- ## 4. Реальное препятствие Единственный бэг с настоящим посторонним объектом — `doubleT_obstacle`. Поезд стоит, объект **0.67 × 1.35 м на 54.7…56.9 м**, 47–69 лучей, медленно смещается поперёк пути (с +1.16 м до −1.98 м и обратно за 20 с, примерно 0.2 м/с). ```bash python tools/check_obstacle.py --memory artifacts/mushroom_body.npz ``` | Метрика | Значение | |---|---| | Попал в кандидаты | 100 % кадров | | Подтверждён треком | 98.9 % кадров | | Новизна (ответ MBON) | 0.62 при фоне 0.13 | --- ## 5. Обобщаемость: leave-one-bag-out ```bash python tools/evaluate.py --device cuda ``` Память тоннеля обучается на всех данных **кроме проверяемого бэга** — иначе цифры лгут: подавлять конструкции, которые сам же и запомнил, умеет кто угодно, а на приватном тесте будет новый участок. | Бэг | Путь | Кадров с тревогой | Разных ложных треков | На километр | |---|---|---|---|---| | `doubleT_platform` | 201 м | 18.8 % | 2 | 10.0 | | `roundT_doubleT` | 201 м | 1.3 % | 1 | 5.0 | | `roundT_pressureGate_roundT` | 247 м | 0.0 % | 0 | 0.0 | | `roundT_squareT_pressureGate_squareT` | 274 м | 13.4 % | 1 | 3.7 | | `squareT_platform_squareT_switch` | 271 м | 13.4 % | 2 | 7.4 | | **Итого** | **1193 м** | **9.4 %** | **6** | **5.2** (медиана 5.0) | Реальный объект в `doubleT_obstacle` при этом обнаруживается в **98.9 %** кадров. Две метрики отличаются принципиально. «Кадров с тревогой» завышает картину: одна и та же конструкция, попавшая в треки, видна сотню кадров подряд. Для эксплуатации важно другое — сколько **разных** ложных объектов возникло, потому что именно столько раз поезд затормозил бы напрасно. ### 5.1. Что дал разбор худшего бэга `roundT_doubleT` давал 39.8 ложных трека на километр — втрое хуже любого другого. Разбор (`tools/diagnose_fp.py`) показал, что пять из восьми треков — один и тот же тип объекта: полоса шириной 0.6 м, ростом ровно с габарит, тёмная (интенсивность 9 из 255), с разрывом дальности до фона 18–25 м, повторяющаяся вдоль тоннеля каждые 13–20 м. Снятие ограничений по высоте показало, что это **колонна, идущая от полотна до свода**: кластер тянется от −0.33 м до 4.45 м, и в габарит 0.28…2.30 попадает лишь **15 %** его лучей. Признак «наполненность» этого не видел: контекст кластеризации обрывался на `h_hi + 1.2 = 3.5` м — ровно в талии между колонной и сводом, — поэтому срез конструкции выглядел целым предметом. Контекст поднят до `h_hi + 4.0 = 6.3` м (параметр `ctx_up`). Результат: | | контекст до 3.5 м | контекст до 6.3 м | |---|---|---| | `roundT_doubleT`, кадров с тревогой | 39.7 % | **1.3 %** | | `roundT_doubleT`, ложных треков | 8 | **1** | | Всего ложных треков на км | 12.4 | **5.2** | | Реальный объект на 55 м | 98.9 % | **98.9 %** | Значения 2.5, 4.0 и 12.0 дают одинаковый результат: контекст просто дотягивается до свода и дальше упирается в пустоту. Взято 4.0 — с запасом на более высокий тоннель. --- ## 6. Абляция: что именно работает ```bash python tools/ablation.py --device cuda ``` Каждый вариант отличается от полного ровно одним отключённым механизмом; память во всех вариантах обучена без проверяемого бэга. | Вариант | Кадров с ложной тревогой | Разных ложных треков | Объект на 55 м | |---|---|---|---| | полная система | 10.7 % | 6 | 98.9 % | | − память тоннеля | 24.1 % | 16 | 98.9 % | | − ось пути (прямой коридор) | 4.4 % | 3 | 98.9 % | | − признаки формы | 30.5 % | 21 | 98.9 % | | − накопление улик | 37.4 % | **81** | 100.0 % | Что из этого следует. **Накопление улик в центральном комплексе — самый весомый механизм.** Без него число разных ложных объектов растёт в 13.5 раза (6 → 81): каждое случайное пятно немедленно становится «обнаружением». Это прямое подтверждение того, что подтверждение по нескольким кадрам должно быть не эвристическим фильтром, а накопителем. **Грибовидное тело снижает ложные тревоги вдвое** (24.1 % → 10.7 %) и при этом **никак не влияет на обнаружение реального объекта** (98.9 % в обоих случаях). Именно этого от памяти и ждали: она гасит знакомое, не трогая незнакомое. **Признаки формы** (целостность, компактность вдоль пути, опора снизу) дают почти такой же вклад, как память, — втрое меньше ложных треков (21 → 6). **Ось пути — единственный механизм, который сейчас стоит дороже, чем даёт.** Её отключение снижает ложные тревоги вдвое (10.7 % → 4.4 %, 6 → 3 трека) и не трогает обнаружение реального объекта. Так вышло потому, что в двухпутном тоннеле центр свода смещён относительно пути: ось оценивается со сдвигом, и кривой габарит заводит в зону поиска куски стены. Держим её ради кривых участков, где без неё объект уезжает из габарита, — но это осознанная плата, а не выигрыш. Из этого сделан вывод и изменено решение: габарит теперь **объединение** прямого и кривого коридоров, а не замена одного другим. Система безопасности не имеет права сужать зону поиска по неуверенной оценке. Итог замены на объединение: | | ось заменяет прямой коридор | ось **дополняет** прямой | |---|---|---| | Реальный объект на 55 м | 75.3 % | **98.9 %** | | Кадров с ложной тревогой | 10.6 % | 17.5 % | | Разных ложных треков на км | 7.5 | 12.4 | Размен сознательный: +23.6 п.п. обнаружения за +6.9 п.п. ложных тревог. Пропустить человека на пути существенно хуже, чем лишний раз затормозить. Таблица выше пересчитана уже на объединённом коридоре, поэтому «полная система» показывает 98.9 % обнаружения. --- ## 7. Скорость ```bash python tools/run_pipeline.py --all --memory artifacts/mushroom_body.npz --verbose ``` Медиана по стадиям на кадре 128 × 600 (сектор ±30°), машина разработки: | Стадия | мс | |---|---| | retina (оконная проекция) | 7.0 | | ламина | 6.4 | | оценка движения (LPTC) | 5.5 | | ось пути | 4.7 | | лобула (кандидаты) | 4.5 | | стабилизация | 3.5 | | грибовидное тело | 1.2 | | центральный комплекс | 0.2 | | решение | 0.02 | | **итого** | **p50 ≈ 35, p95 ≈ 45** | Бюджет по ТЗ — 100 мс на кадр. Запас примерно двукратный, что важно: стенд жюри по CPU слабее машины разработки. Если запаса не хватит, первыми кандидатами на перенос в numba являются ламина и оценка движения — вместе это 12 мс почти чистой арифметики. Отдельно измерена оптимизация ретины: на круговом скане (921 600 точек) оконная проекция сократила стадию с **29 до 7 мс**, причём результат совпадает с полной проекцией **побитово** — проверено сравнением массивов. ## 7.3. Почему далёкий предмет теряется — и что нужно для 200 м ТЗ просит 300 м как «отлично» и 200 м как «очень хорошо». Разберём честно, чего не хватает, потому что причина не та, которая кажется. **Фотоны есть.** Вставленный человек на оси пути, по замерам полигона: | Полоса | Лучей на кадр | Есть эхо | Кадров в полосе | Накоплено лучей | Обнаружено сейчас | |---|---|---|---|---|---| | 160–190 м | 5 | 100 % | 17 | ~84 | **0 %** | | 135–160 м | 7 | 100 % | 17 | ~120 | **0 %** | | 110–135 м | 10 | 94 % | 18 | ~176 | **0 %** | | 90–110 м | 16 | 92 % | 14 | ~227 | 25 % | | 70–90 м | 26 | 87 % | 15 | ~400 | 58 % | На 170 м предмет освещён в **каждом** кадре и за проход набирает под сотню попаданий в одну и ту же точку мира. Информация есть — мы её выбрасываем, решая покадрово. **Кандидат при этом формируется.** Покадровый разбор (`doubleT_platform`, человек от 200 м) показывает кандидата в большинстве кадров на 140–185 м: 4–9 лучей, наполненность до 1.00, размер 0.4 × 1.5 м — верный. Но улика трека остаётся 0.00, и виноват один множитель: **`gap` = 0.0 во всех кадрах без исключения**. **Почему.** Кольцо окружения ламины берётся ±6 столбцов, то есть ±0.6°. На 170 м этот угол отвечает боковому смещению 1.8 м, а стена тоннеля на таком смещении находится на 172 м — там же, где предмет. Центр-окружение перестаёт работать, когда собственный градиент тоннеля по глубине сравним с шагом от предмета: предмет не «ближе окружения», он «на той же дальности, что окружение». В `_quality` это даёт `contrast = clip(0/3, 0.2, 1) = 0.2`, и улика не набирается ни за 17 кадров, ни за сто. Это не настройка порога. Локальный контраст на больших дальностях в тоннеле физически не несёт сигнала, и никакая подстройка ламины этого не изменит. ### Что сделано: накопление в координатах пути Предмет неподвижен в мире, а тоннель проплывает мимо. Собственное движение мы уже оцениваем, поэтому лучи из габарита складываются не в кадре, а в сетке, привязанной к пройденному пути (`fan_body.py`, шаг 2 м вдоль пути × 0.2 м поперёк × 0.25 м по высоте, забывание с полураспадом 23 кадра). Так устроено веерное тело центрального комплекса мухи: оно копит вектор к цели в координатах мира, а не текущего кадра. **Первая версия порождала собственных кандидатов — и это оказалось тупиком.** Ложных треков стало 39 на километр вместо 5.2. Разбор показал, почему: по геометрии накопленное скопление предмета и накопленный кусок конструкции тоннеля **неразличимы**. Медианы (предмет / ложные), 145 против 761 скопления: | признак | дальность | \|u\| | высота | h_min | ширина | протяжённость | опора | кадров | |---|---|---|---|---|---|---|---|---| | предмет | 88 | 1.16 | 1.28 | 0.28 | 0.80 | 4.0 | 1.31 | 22.1 | | ложные | 100 | 1.10 | 1.27 | 0.28 | 0.80 | 4.0 | 1.44 | 18.5 | Совпадает всё. Разделяет их только память тоннеля, а ей нужны признаки кадра — контраст, интенсивность, тень, — которых у скопления нет по построению. **Рабочая версия.** Накопитель не порождает кандидатов вовсе. Он отвечает на один вопрос про **уже найденного покадрового кандидата** — возвращались ли лучи из этой точки мира кадр за кадром — и эта опора подставляется в вес улики вместо недоступного контраста (`contrast = max(по gap, по накоплению)`). Кандидат при этом остаётся под судом грибовидного тела со всеми своими признаками, и штатные конструкции по-прежнему подавляются. Измеренный результат на вставленном человеке (доля кадров с обнаружением): | Бэг | 55–75 м | 75–100 | 100–130 | 130–170 | |---|---|---|---|---| | `roundT_squareT_pressureGate_squareT` | 1.00 | 0.38 → **1.00** | 0.00 → **1.00** | 0.00 → **0.55** | | `squareT_platform_squareT_switch` | 1.00 | 1.00 | 0.33 → **1.00** | 0.00 → 0.14 | | `doubleT_platform` | 1.00 | 0.82 → **0.95** | 0.00 → 0.09 | 0.00 | | `roundT_doubleT` | 0.00 | 0.00 | 0.00 | 0.00 | | `roundT_pressureGate_roundT` | 0.00 | 0.00 | 0.00 | 0.00 | Рабочая дальность там, где кандидат вообще формируется, **выросла вдвое**: с 75–100 до 130–170 м. Два круглых тоннеля накопление не спасает — там предмет слипается со стеной в одну связную компоненту, кандидата нет, и поддерживать нечего (см. п. 9.3). На полном полигоне (90 сценариев, 14 004 наблюдения): | | без накопления | с накоплением | |---|---|---| | Рабочая дальность, человек стоя | 62 м | **100 м** | | P@100 м, человек стоя | 0.24 | **0.57** | | P@150 м, человек стоя | 0.00 | **0.10** | | P@100 м, человек сидя | 0.19 | **0.33** | | Ложных треков на км (leave-one-bag-out) | **5.2** | 9.1 | | Кадров с тревогой | **9.4 %** | 17.2 % | | Посторонних тревог на кадр (полигон) | **0.055** | 0.171 | | Реальный объект на 55 м | 98.9 % | 98.9 % | | Задержка, медиана | 32 мс | 33 мс | **Включено по умолчанию.** Размен здесь принципиально лучше, чем у разделения фигуры и фона (п. 9.4): там было вчетверо больше ложных за +29 % дальности, здесь — в 1.75 раза больше за +61 % рабочей дальности и рост обнаружения на 100 м в 2.4 раза. ТЗ прямо оценивает дальность (100 м → «хорошо»), а «важно найти баланс между дальностью, надёжностью и количеством ложных тревог» — этот баланс мы и выбираем осознанно. Выключается одним параметром: `enable_accumulator: false` возвращает 5.2 ложных трека на километр при рабочей дальности 62 м. ### Что ещё нужно **Геометрическая карта линии.** Метро — неизменная среда: за несколько проездов строится ожидаемый дальностный образ, привязанный к положению вдоль линии. Тогда «препятствие» = «луч вернулся ближе, чем говорит карта», и это единственный способ получить **и** дальность, **и** околонулевые ложные тревоги: всё постоянное в карте, всё остальное — предмет. Грибовидное тело делает то же самое в пространстве признаков; карта делает это в пространстве геометрии, где на 170 м ещё есть сигнал. ### Чего не будет никогда * **На предоставленных участках 200 м недостижимы геометрически**: прямая видимость 121–167 м, дальше линия взгляда упирается в стену кривой. Это не свойство алгоритма. * **Мелкие предметы на 200 м невозможны с этим сенсором**: ведро (0.1 м²) на 160–190 м даёт 1 луч при видимости 2 %, каска и бутылка — ноль. Накопление не поможет там, где фотонов нет. * Реалистичная планка для предмета размером с человека на прямом участке — **около 200 м**, и путь к ней измерен выше: накопление плюс карта. --- ## 8. Что не сработало Раздел намеренно подробный: ТЗ п. 8.7 просит именно этого. **Поиск рельсов по интенсивности.** Идея была привязать ось пути к колее 1520 мм. Не вышло: медианная интенсивность на уровне головок рельсов равна 8 из 255, рельсы ничем не выделяются на фоне полотна, и пара пиков на расстоянии 1.52 м находится где попало — оценки прыгали от −1.18 до +1.37 м в соседних срезах одного кадра. Отказались, ось пути оценивается по дрейфу центра свода. **Обычная связность при кластеризации.** Соседние лучи объединялись без учёта глубины, и предмет на 55 м слипался со стеной на 150 м в одно пятно размером 5 × 4 м. Реальный объект обнаруживался в 16 кадрах из 20. После введения допуска по глубине, растущего с расстоянием, — 20 из 20 и правильные габариты 0.67 × 1.35 м. **Грибовидное тело с мушиными параметрами.** 2000 клеток Кеньона и 5 % активных насыщаются после нескольких тысяч примеров: подавлено 98 % синапсов, новизна реального препятствия падает до 0.001, и оно перестаёт обнаруживаться совсем. Потребовалось увеличить популяцию до 50 000 и снизить разрежённость до 0.1 %, а темп депрессии согласовать с размером обучающей выборки. **Корреляция продольного профиля «в лоб».** Первая версия оценки скорости залипала на нулевом сдвиге: в профиль входила ближняя зона, где на метр пути приходятся тысячи лучей, и её вклад подавлял всё остальное. Помогло исключение ближней зоны и вычитание скользящего среднего. Отдельно обнаружилась ошибка в перепроекции — использовалась высота над рельсом вместо z сенсора, из-за чего согласие держалось на 0.12 вместо 0.9. **Взвешивание срезов по числу точек при оценке оси пути.** У ближних срезов точек в сотни раз больше, и подгонка полностью игнорировала дальние, где как раз содержится кривизна. Кривые расходились от +10 до −10 м на 200 м в соседних кадрах. Равные веса по срезам и линейная (а не квадратичная) экстраполяция за горизонт видимости решили проблему. **Оценка оси пути на станции.** Платформа делает сечение резко несимметричным, центр свода «уезжает», и габарит заезжает прямо на платформу: радиус кривой падал с 999 до 225 м за 4.5 с. Это давало 54 % кадров с ложной тревогой на бэге с платформой и стрелкой. Помогли два физических ограничения — минимальный радиус 300 м и предел скорости изменения оси. Стало 5.4 %. **Срыв оценки скорости на смене типа тоннеля.** На переходе круглого тоннеля в двухпутный сопоставление кадров теряло опору и выдавало попеременно 0 и 70 км/ч. Помог фильтр с физическим пределом ускорения 3 м/с²: поезд за 0.1 с так не разгоняется. **Полигон без учёта кривизны.** Первая версия синтетических сценариев ставила предмет в поперечных координатах сенсора, а не на ось пути. В кривой это уносило его в стену, и «рабочая дальность» выходила 32 м вместо реальных 55+. Исправлено привязкой к оси. --- ## 9. Размеченный полигон: дальность обнаружения ```bash python tools/make_benchmark.py --memory artifacts/mushroom_body.npz python tools/plot_benchmark.py ``` Разметки в датасете нет, а организаторы предупредили, что приватный тест собран добавлением синтезированных препятствий. Полигон строится тем же способом: в реальные кадры пустого тоннеля трассировкой лучей вставляется предмет, стоящий **на оси пути** в фиксированной точке тоннеля, поезд к нему подъезжает, и на каждом кадре известна истинная дистанция. Модель сенсора опирается на руководство: поканальная дальность из Приложения A (каналы 34–65 берут 200 м, каналы 98–128 смотрят в землю и рассчитаны только на ближнее поле), вероятность обнаружения на паспортной дальности PoD = 70 %, шум дальности ±2 см, заполнение пятна луча для мелких предметов. Проверка модели: настоящий объект 0.67 × 1.35 м на 55 м даёт 47–69 лучей; синтетический человек 0.44 × 1.71 м на 60 м даёт 50 лучей. Совпадает. Результаты приводятся в трёх разрезах, потому что смешивать их нельзя: 1. **видимость** — доля кадров, в которых до предмета дошёл хотя бы один луч; за поворотом она падает до нуля независимо от алгоритма; 2. **обнаружение при условии видимости** — собственно качество алгоритма; 3. **обнаружение как есть** — произведение первых двух, эксплуатационная величина. ### 9.1. Результат 90 сценариев: 9 предметов × 2 поперечных смещения × 5 бэгов, 14 004 наблюдения с известной истинной дистанцией. | Предмет | Площадь | Рабочая дальность | P@50 м | P@100 м | P@150 м | Видимость | |---|---|---|---|---|---|---| | человек стоя | 0.75 м² | **100 м** | **0.70** | 0.53 | 0.12 | 92.5 % | | человек сидя | 0.42 м² | 20 м | 0.62 | 0.34 | 0.00 | 89.3 % | | ящик | 0.36 м² | 20 м | 0.49 | 0.00 | 0.00 | 88.1 % | | чемодан | 0.25 м² | 62 м | 0.53 | 0.00 | 0.00 | 81.9 % | | ведро | 0.10 м² | 8 м | 0.00 | 0.00 | 0.00 | 61.2 % | | каска | 0.07 м² | — | 0.00 | 0.00 | 0.00 | 51.1 % | | камень | 0.05 м² | — | 0.00 | 0.00 | 0.00 | 45.1 % | | бутылка | 0.03 м² | — | 0.00 | 0.00 | 0.00 | 41.2 % | | кабель | ~0 м² | — | 0.00 | 0.00 | 0.00 | 25.7 % | Предыдущие замеры для сравнения. Без накопления в координатах пути (п. 7.3): человек — рабочая дальность 62 м, P@50 = 0.56, P@100 = 0.24. С накоплением, но без разреза по контрасту (п. 9.5): 100 м, P@50 = 0.56, P@100 = 0.57. **«Рабочая дальность» у предметов около порога неустойчива** и её не надо читать как физическую величину: метрика идёт от ближнего пояса и обрывается на первом, где доля падает ниже 0.5, усредняя при этом два поперечных положения — на оси и со смещением 0.9 м к краю габарита. У сидящего человека и ящика пояс 25–40 м даёт 0.48 против порога 0.50, и число падает с 62 до 20 м, хотя P@50 при этом не ухудшилось. Содержательны таблицы по поясам (п. 9.2 и 9.5), а не это одно число. Граница проходит по числу лучей: на 40–55 м человек даёт 68 лучей, чемодан 25, ведро 11, каска 6, бутылка 3. Ниже примерно **десяти лучей предмет перестаёт отличаться от шума** решётки, и никакая обработка этого не исправит — нужен либо более плотный сенсор, либо подъезд ближе. ### 9.2. Почему рабочая дальность 62 м, а реальный объект виден на 98.9 % Разброс по бэгам огромный, и он объясняет расхождение: | Бэг (человек стоя, на оси) | 25–40 м | 40–55 м | 55–70 м | 70–90 м | лучей на 50 м | |---|---|---|---|---|---| | `doubleT_platform` | 1.00 | 1.00 | 1.00 | 1.00 | 68 | | `roundT_squareT_pressureGate_squareT` | 1.00 | 1.00 | 1.00 | 0.67 | 68 | | `squareT_platform_squareT_switch` | 0.00 | 0.18 | 1.00 | 1.00 | 56 | | `roundT_pressureGate_roundT` | 0.73 | 0.00 | 0.00 | 0.00 | 65 | | `roundT_doubleT` | 0.27 | 0.00 | 0.00 | 0.00 | 32 | Не «плохо везде понемногу», а **идеально на одних участках и слепо на других**, причём при 65 лучах на предмете. То есть дело не в видимости и не в размере. > Таблица снята до накопления в координатах пути (п. 7.3) и до разреза по контрасту > (п. 9.5). Актуальные цифры по тем же бэгам — в п. 9.5: `roundT_pressureGate_roundT` > из полностью слепого за 40 м стал 0.45 / 1.00 / 0.57 на 40–90 м, > `roundT_doubleT` за 40 м слепым остался. ### 9.3. Найденная причина слепоты: связная компонента течёт вдоль стены Покадровый разбор провала (`roundT_pressureGate_roundT`, человек на 50 м, 65 лучей вставлено) показал: кандидата нет вообще. В кадре всего 5 компонент, и одна из них — **42 059 лучей, протянувшиеся по дальности от 4 до 99 м**, наполненность 0.05. Кластеризация с разрывом по глубине объединяет соседние лучи, если их дальности отличаются меньше чем на `0.06·R + 0.35` м. Вдоль гладкой стены тоннеля соседние лучи отличаются на сантиметры, поэтому стена связна от ближнего поля до горизонта. Предмет, стоящий у такой стены, попадает в ту же компоненту и **вместе с ней отбрасывается** правилом «ни один предмет не тянется на 15 м вдоль пути». Это не регрессия: мерж одинаков при любой верхней границе контекста, включая исходную. **Попытка первая: разрезать по дальности.** Переглубокая компонента не выбрасывается, а пересобирается с более строгим допуском. Предмет при этом действительно выделяется — 57 лучей, наполненность 1.00. Измеренный размен на `roundT_pressureGate_roundT` (человек на 25…90 м) и глобально: | Допуск разреза | Обнаружение | Ложных кадров (бэг) | Ложных треков (бэг) | |---|---|---|---| | выключен | 28.2 % | 0.0 % | 0 | | 0.045 | 28.2 % | 27.2 % | 1 | | 0.040 | 61.5 % | 21.5 % | 2 | | **0.030** | **94.9 %** | 57.5 % | 5 | | 0.015 | 94.9 % | 73.7 % | 16 | | 0.006 | 94.9 % | 96.5 % | 47 | Глобально при 0.030: ложных треков **25.8 на км против 5.2**, кадров с тревогой **52 % против 9.4 %**. Половина кадров с тревогой — это непрерывное торможение, поэтому разрез по умолчанию **выключен**. Проверялось и то, можно ли отделить осколок стены от предмета по признакам: ни один не разделяет их. Медианы (предмет / стена): ширина 0.21 / 0.20 м, наполненность 1.00 / 1.00, лучей 12 / 8, новизна 0.50 / 0.41. Строгий разрез делает стену геометрически неотличимой от предметов — потому и цена такая. Этот вариант убран. ### 9.4. Разделение фигуры и фона по движению Разрезать по дальности нельзя: предмет и стена рядом с ним стоят на одной дальности. Зато они по-разному **приближаются**, и это чистая геометрия. Вдоль фиксированного луча стена, параллельная движению, не приближается вовсе: поезд едет, точка пересечения скользит по стене, дальность не меняется. Предмет, обращённый к поезду, приближается ровно на пройденный путь. Отсюда признак: `advance = (r_прошлый − r_текущий) / ds`. Ноль у фона, единица у фигуры. Ничего перепроецировать не нужно — столбец решётки отвечает фиксированному азимуту, а рысканье в кривой за кадр (0.06° при радиусе 1300 м) меньше шага решётки. Это тот самый канал T4/T5 → LPTC: широкопольный поток задаёт ожидание, а что движется иначе — фигура. Замер на вставленном человеке подтверждает физику: у предмета `advance` = 0.99…1.01, у стены на той же дальности — 0.39…0.82 и падает по мере приближения. Разрез по этому признаку (`Params.split_adv`, `lobula.split_by_figure`) не крошит стену: из склеенной компоненты в 42 000 лучей остаётся 2.6–5.5 тысяч и **6–15 кандидатов** вместо 1259 у разреза по дальности. **Важно для честности замера.** Первая проверка дала 70 % ложных кадров, но она была некорректной: память тоннеля обучена на кандидатах **старого** генератора, а разрез порождает формы, которых она никогда не видела, — всё выглядит новым. После пересбора кэша и переобучения памяти на тех же настройках (`tune_candidates_adv.npz`, `new_data_candidates_adv.npz`, 70 273 кандидата) картина такая: **Что это даёт — полигон:** | | без разделения | с разделением | |---|---|---| | Рабочая дальность, человек стоя | 62 м | **80 м** | | P@50 м | 0.56 | **0.71** | | P@100 м | 0.24 | **0.36** | | Чемодан, P@50 м | 0.57 | 0.67 | По бэгам (человек на оси, доля кадров с обнаружением): | Бэг | 25–40 м | 40–55 | 55–70 | 70–90 | 90–110 | |---|---|---|---|---|---| | `squareT_platform_squareT_switch` | 0.00 → **1.00** | 0.18 → **1.00** | 1.00 → 1.00 | 1.00 | 1.00 | | `roundT_doubleT` | 0.27 → **0.64** | 0.00 | 0.00 | 0.00 | 0.00 | | `roundT_pressureGate_roundT` | 0.73 → **0.80** | 0.00 | 0.00 | 0.00 | 0.00 | | `roundT_squareT_pressureGate_squareT` | 1.00 | 1.00 | 1.00 | 0.67 → **0.89** | 0.00 | | `doubleT_platform` | 1.00 | 1.00 | 1.00 | 1.00 | 0.25 | **Что это стоит:** | | без разделения | с разделением | |---|---|---| | Кадров с ложной тревогой | 9.4 % | 30.3 % | | Разных ложных треков на км | **5.2** | **21.5** | | Ложных тревог на полигоне, на кадр | 0.055 | 0.348 | | Задержка, медиана | 31 мс | 39 мс | | Реальный объект на 55 м | 98.9 % | 98.9 % | **Порогом это не лечится.** Проверены значения 0.6 / 0.8 / 0.9: ложных треков 21.5 / 21.6 / 21.6 на километр, а доля кадров с тревогой только растёт (30 → 35 → 40 %). Причина в том, что ложные срабатывания здесь — **не шум, а настоящие поверхности, обращённые к поезду**: рамы гермозатворов, торцы, порталы. По одному признаку движения они от предмета неотличимы, потому что физически ведут себя так же. **Решение: разделение по умолчанию выключено** (`split_adv: 0.0`). 21.5 ложного трека на километр — это напрасное торможение каждые 47 метров, система в таком виде неработоспособна, и +18 м рабочей дальности этого не окупают. Два бэга из пяти разделение к тому же не спасает: за 40 м они остаются слепыми. **Чем это лечится по-настоящему.** Не порогом, а памятью: приближающиеся конструкции тоннеля постоянны и повторяются от проезда к проезду. Здесь память обучена на пяти бэгах и 20 минутах записи; на реальной линии с многими проездами грибовидное тело подавило бы их так же, как подавляет всё остальное штатное. Это проверяемое предсказание, а не надежда: переобучение памяти уже снизило ложные кадры с 70 % до 30 % — просто за счёт того, что она увидела эти формы один раз. --- ## 9.5. Разделение фигуры и фона по контрасту — третья попытка, и она работает Разрез по допуску (п. 9.3) и разрез по движению (п. 9.4) вернули зрение и оба оказались слишком дороги. Оставался третий признак фигуры, и он всё это время уже вычислялся — просто не участвовал в сегментации. **Физика.** Гладкая стена даёт **нулевой центр-окружение по построению**. Как бы сильно дальность ни менялась вдоль стены, она меняется плавно: центр равен своему окружению, и контраст равен нулю. Предмет на стене — это ступенька, и она видна. Именно этим занята ламина, и её выход `gap` («насколько ближе окружения», м) наш конвейер считал с самого начала — но использовал только как **признак кандидата**. Сегментация же шла по связности с допуском по глубине, и переглубокая компонента отбрасывалась целиком ещё до того, как признак кому-то пригождался. **Замер разделимости** (вставленный человек, два круглых тоннеля, 55 кадров): | Порог `gap` | Лучей предмета | Лучей фона | Компонент фона в кадре | |---|---|---|---| | 2 м | 90.8 % / 87.5 % | 4.15 % / 3.10 % | 24.6 / 16.3 | | 4 м | 89.2 % / 83.4 % | 0.68 % / 0.62 % | 13.7 / 13.5 | | **6 м** | **85.7 % / 75.1 %** | **0.13 % / 0.22 %** | **5.0 / 8.9** | | 9 м | 80.7 % / 61.7 % | 0.02 % / 0.05 % | 0.7 / 2.6 | Для сравнения: разрез по допуску давал 1259 кандидатов в кадре. Здесь фон распадается на 5–9 компонент — на два порядка меньше, и это **настоящие выступы**, которые память способна выучить, а не произвольные осколки гладкой стены. Разделение держится до 75 м и разваливается к 90 м: доля лучей предмета, прошедших порог 6 м, по полосам — 88 / 96 / 97 / 89 / 61 / 14 / 0 % на 15 / 30 / 45 / 60 / 75 / 90 / 105 м. Дальше 90 м кольцо окружения снова упирается в стену на той же дальности (п. 7.3), и контраста нет. ### Почему первая версия всё равно была дорогой Без ограничений разрез дал **9.4 ложных трека на км против 7.4** — и причина нашлась замером, а не рассуждением. Медианы кандидатов на `roundT_doubleT`: | признак | без разреза | с разрезом | |---|---|---| | протяжённость вдоль пути | 2.82 м | **1.00 м** | | то же, дальше 55 м | 6.87 м | **0.81 м** | | наполненность | 0.47 | 0.57 | | контраст `gap` | 1.24 м | 3.29 м | Разрез **отрезает фон**, и вместе с фоном исчезает протяжённость. В весе улики (`central_complex._quality`) за неё отвечает множитель компактности `clip(1.5 − depth/(3·span))`: при depth 6.9 м и размере 0.65 м он равен 0.1, при depth 0.8 м — 1.0. То есть каждое наблюдение вырезанной фигуры стало весить **вдесятеро больше**, и подавление протяжённых конструкций, работавшее до разреза, выключилось. Порог этого не лечит — проверено сквозным замером: | Порог разреза | Ложных треков на км | |---|---| | выключен | 7.4 | | 6 м | 9.4 | | 9 м | 10.4 | | 12 м | 9.4 | ### Что решило: одна фигура на компоненту Раз каждая лишняя фигура стоит вдесятеро дороже прежнего, их число надо ограничить. Из переглубокой компоненты выносится только **сильнейшая** фигура по сумме превышения порога. Это тот же приём глобального торможения, которым APL оставляет активными считанные проценты клеток Кеньона, а широкопольное торможение — считанные колонки LC11. | Фигур на компоненту | Ложных треков на км | Кадров с тревогой | |---|---|---| | без ограничения | 9.4 | 16.9 % | | 2 | 7.4 | 15.5 % | | **1** | **6.4** | **15.0 %** | Дальность обнаружения при этом **не меняется вовсе**: и при одной фигуре, и при двух, и без ограничения все три конфигурации дают одинаковые доли по полосам. Лишние фигуры не добавляли зрения — только ложные тревоги. Разрез вдобавок не применяется ближе 55 м (`split_near`): там покадровый тракт видит предмет и без него (100 % на всех пяти бэгах до 70 м), а обстановки, дающей контраст, в ближнем поле на порядок больше. Без этого ограничения 75 % добавленных ложных треков приходили именно оттуда. ### Итог Цифры ниже — leave-one-bag-out с пересобранным кэшем кандидатов и переобученной памятью: без этого замер лжёт (п. 9.4). | Конфигурация | Ложных треков на км | Кадров с тревогой | Объект 55 м | |---|---|---|---| | как было | 9.1 | 17.2 % | 98.9 % | | **с разрезом по контрасту** | **7.5** | **16.7 %** | **98.9 %** | То есть разрез не только вернул зрение там, где его не было, но и **снизил** ложные тревоги — за счёт того, что переглубокая компонента перестала выбрасываться целиком и вместо неё в память попадает одна осмысленная фигура, которую есть чему выучить. Обнаружение вставленного человека на оси, доля кадров. Протокол полигона: предмет стоит в точке тоннеля, до которой от начала записи 200 м, поезд подъезжает. Память обучена и на этом бэге — как и во всём полигоне. | Бэг | 15–25 м | 25–40 | 40–55 | 55–70 | 70–90 | |---|---|---|---|---|---| | `roundT_pressureGate_roundT` | 1.00 | 0.73 | 0.00 → **0.45** | 0.00 → **1.00** | 0.00 → **0.57** | | `squareT_platform_squareT_switch` | 0.00 | 0.00 | 0.18 → **0.45** | 1.00 | 1.00 | | `roundT_doubleT` | 1.00 | 0.27 | 0.00 | 0.00 | 0.00 | **Это ровно тот бэг, который разбирался в п. 9.3.** `roundT_pressureGate_roundT` — запись, где компонента из 42 000 лучей течёт вдоль стены от 4 до 99 м и уносит предмет с собой. Разрез по контрасту превращает полностью слепую полосу 40–90 м в 0.45 / 1.00 / 0.57. Разрез по допуску (п. 9.3) и по движению (п. 9.4) добивались там же 0.94 и 0.80 ценой 25.8 и 21.5 ложного трека на километр; здесь ложных треков стало **меньше**, чем было до разреза. **Второй слепой бэг разрез не спасает**, и причины там две, обе разобраны покадрово в п. 9.6: за 50 м до предмета не доходит линия взгляда (98 % лучей упираются в преграду ближе него), а на 35–52 м мешает уже наш собственный порог `split_near`, снижать который оказалось слишком дорого. **Осторожно с «рабочей дальностью».** Метрика в `plot_benchmark.py` идёт от ближнего пояса и останавливается на первом, где доля падает ниже 0.5, а усредняет она два поперечных положения сразу — на оси и со смещением 0.9 м к краю габарита. У предметов, стоящих около порога, она поэтому скачет: у сидящего человека пояс 25–40 м даёт 0.48 против 0.50, и «рабочая дальность» падает с 62 до 20 м при том, что P@50 м выросло с 0.58 до 0.62. Смотреть надо на таблицу по поясам, а не на одно число. **Где разрез стоит денег — 1: холодный старт.** Всё сказанное верно, когда память обучена. На совершенно новой линии, где памяти нет вовсе, разрез, наоборот, дорог: 21.8 → **31.8** ложных трека на километр. Это логично — он порождает кандидатов из настоящих выступов тоннеля, и без памяти отличить их не от чего. Реальный сценарий — именно с обученной памятью, она поставляется в образе; но если участок настолько новый, что память к нему неприменима, `split_gap: 0.0` возвращает прежнее поведение. **Где разрез стоит денег — 2: стоянка.** На записи со **стоящим** поездом (`doubleT_obstacle`) он добавляет один устойчивый трек на 76.6 м, и доля кадров с посторонней тревогой растёт с 63 % до 96 %. Это не случайность: при нулевом пройденном пути не работают ни накопитель, ни привыкание — оба живут в координатах пути. Число разных ложных треков на этом бэге растёт всего с 4 до 5; раздувается именно доля кадров, потому что на стоянке ничто не уходит из поля зрения. --- --- ## 9.6. Почему `roundT_doubleT` остаётся слепым за 40 м Разрез по контрасту (п. 9.5) открыл `roundT_pressureGate_roundT`, но второй слепой бэг не тронул. Покадровый разбор цепочки «лучи → кандидат → улика → тревога» на штатной постановке полигона (предмет в точке, до которой от начала записи 200 м) показал **две разные причины в двух разных полосах дальности**. Одна физическая, вторая — наша. ### Дальше 50 м: смотреть не на что Считаем, сколько лучей доходит до предмета, по этапам: | Дальность до предмета | Геометрически попали | Пережили модель сенсора | Не заслонены | |---|---|---|---| | 55–105 м | 29 | **29** | **1** | | 30–55 м | 109 | 109 | 86 | Модель сенсора не теряет **ни одного** луча: предмет крупный, дальность паспортная. Но за 55 м **98 % лучей упираются в эхо, которое ближе предмета**. Дело не в отражательной способности и не в числе каналов — до предмета просто не доходит линия взгляда. Это не артефакт постановки. Предмет пробовали ставить четырьмя способами — на оценённую ось коридора, прямо по оси сенсора, на половину смещения и со сдвигом +0.8 м; заслонение одинаково во всех четырёх, и дальность преграды тоже одна и та же: | Дальность до предмета | 104 | 94 | 84 | 74 | 64 | 54 | 45 | |---|---|---|---|---|---|---|---| | Преграда, м | 90 | 84 | 76 | 66 | 58 | 52 | **49** | | Лучей видно (ось коридора) | 3/17 | 1/19 | 0/25 | 0/37 | 1/41 | 8/61 | **63/89** | Преграда приближается вместе с поездом и в какой-то момент **обгоняет** предмет: на 45 м обзор доходит до 49 м, предмет оказывается ближе преграды — и 63 луча из 89 приходят разом. Ровно с этого места и начинается обнаружение. Для сравнения, на том же замере `roundT_pressureGate_roundT` преграда всегда **за** предметом (предмет на 104 м — преграда на 113 м; предмет на 56 м — преграда на 70 м), поэтому там предмет виден, и разрез по контрасту может ему помочь. **Почему видимость на этом перегоне такая короткая.** По бэгу в целом вдоль оси пути видно на 106 м (медиана), но полигон ставит предмет в точку за 200 м от начала записи, а это самое начало проезда — как раз худший его участок. Плюс общий для всех бэгов эффект: луч, идущий вдоль пути, **скользит по полотну**, и с какой-то дальности прирельсовая зона перестаёт наблюдаться вовсе. Нижняя наблюдаемая точка у оси пути (5-й процентиль высоты над головкой рельса): | Дальность | 20 м | 40 | 60 | 80 | 90 | 100 | |---|---|---|---|---|---|---| | `roundT_doubleT` | −0.35 | −0.32 | −0.21 | −0.07 | **+0.36** | **+0.64** | | `roundT_pressureGate_roundT` | −0.32 | −0.32 | −0.06 | +0.18 | +0.10 | −0.11 | | `doubleT_platform` | −0.35 | −0.36 | −0.34 | −0.06 | +0.08 | +0.35 | За 80–90 м самое низкое, что видно у оси, находится уже **выше рельса** на 0.1–0.6 м. Это и есть предел скользящего луча, и он объясняет, почему у мелких лежащих предметов видимость в полигоне 25–61 %: их просто нечем осветить. Никакой обработкой это не лечится. Лечится геометрической картой линии (п. 7.3) — или вторым сенсором, поднятым выше. ### 35–52 м: лучи есть, кандидата нет — и это наш порог Здесь картина обратная: лучей 83…146, а кандидата нет. | Дальность | 41.6 | 38.7 | 35.9 | 33.3 | 30.7 | 28.3 | |---|---|---|---|---|---|---| | Лучей на предмете | 83 | 101 | 126 | 146 | 168 | 200 | | Кандидат при `split_near = 55` | нет | нет | нет | нет | есть | есть | | Кандидат при `split_near = 20` | есть | есть | есть | есть | есть | есть | Причина — тот же мерж со стеной, что в п. 9.3, и наш собственный порог: разрез по контрасту по умолчанию не применяется ближе 55 м. Порог был введён из соображения «ближе покадровый тракт видит предмет и сам» — на этом бэге это неверно. Со сниженным порогом первая тревога наступает на **37.3 м вместо 28.3 м**, а доля кадров без кандидата в окне 20–42 м падает с 44 % до 6 %. **Порог всё равно оставлен 55 м.** Цена снижения измерена честно — с пересбором кэша кандидатов при `split_near = 20` и переобучением памяти на нём: | `split_near` | Ложных треков на км | Кадров с тревогой | |---|---|---| | **55 м** | **7.4** | **16.7 %** | | 30 м | 14.1 | 25.6 % | | 20 м (память не пересобрана) | 18.1 | 28.2 % | | 20 м (**честно**, память переобучена) | 17.1 | 27.9 % | Переобучение на этот раз почти ничего не вернуло (18.1 → 17.1): ближние вырезанные фигуры от препятствий действительно неотличимы. А выигрыш — девять метров на дальности, где он ничего не меняет: ТЗ оценивает 100 / 200 / 300 м, и поезд на 50 км/ч не останавливается ни за 28, ни за 37 м. Платить за это 2.3-кратным ростом ложных тревог нельзя. `split_near: 20.0` в конфиге доступен для линии, где обстановка беднее и ложные тревоги дешевле. --- ## 10. Новый участок: привыкание внутри проезда — отрицательный результат Обученная память отвечает на вопрос «этот тоннель я уже видел». На новом участке она бесполезна по определению, и это измеренная величина, а не абстрактный риск: | | Ложных треков на км | Кадров с тревогой | |---|---|---| | память обучена на других бэгах (leave-one-bag-out) | 9.1 | 17.2 % | | **памяти нет вовсе (новая линия)** | **21.8** | **33.1 %** | Приватный тест — это как раз новый участок, поэтому вопрос важный. Механизм был сделан, доведён до работы и **отвергнут по результатам замера**. Ниже — почему, потому что отрицательный результат здесь содержательнее положительного. ### Замысел **Тоннельная обстановка повторяется вдоль пути, а посторонний предмет — нет.** Кабельный кронштейн, рама крепи, стык тюбингов встречаются каждые несколько метров в одном и том же виде; разбор худшего бэга (п. 5.1) прямо это показал — пять из восьми ложных треков были колоннами, повторяющимися каждые 13–20 м. Упавший же предмет лежит в одном месте. Отсюда привыкание, считающее не по времени и не по числу кадров, а по **числу разных точек пути**, где встретилась эта форма. Настоящий предмет виден сто кадров подряд, но всё это время стоит в одной точке мира: его код депрессируется один раз и остаётся новым до конца подъезда. Биологически это familiarity suppression MBON-α′3, а момент депрессии разрешает координата пути из центрального комплекса — роль, которую у мухи играют дофаминергические PPL1/PAM. ### Что пришлось починить, чтобы механизм вообще заработал Обе ошибки найдены замером и сами по себе поучительны. **Полный набор признаков не годится.** Первая версия кодировала кандидата тем же дескриптором, что и долговременная память. За двенадцать разных мест новизна упала с 1.000 до 0.981 — то есть ни на что. В дескрипторе есть дальность, число лучей, угловой размер, интенсивность: код одного и того же кронштейна с 90 и с 40 м разъезжается, повторы не узнаются, а подъезжающий предмет каждый кадр выглядит новой формой и привыкает сам к себе. Переведено на десять признаков формы, не зависящих от дальности. **Нормировка обязана замирать.** Пока среднее и разброс пересчитываются, вместе с ними плывёт код. У неподвижного предмета, чьи признаки формы не меняются вовсе, набор активных клеток обновлялся настолько, что предмет засчитывался как **двадцать шесть разных мест** и гасил сам себя. При пороге заморозки 4000 кандидатов эффект оставался живуч: в `roundT_doubleT` (около 570 кандидатов за проезд) нормировка не замирала никогда, привыкание стояло на 0.20, медианная новизна кандидата 1.00, и ложных треков было ровно столько же, сколько без него. Порог снижен до 300. ### Почему всё равно отвергнуто После починок механизм начал давать цифры: leave-one-bag-out 7.5 → **5.9** ложных трека на км, новая линия 31.8 → **26.8**. Выглядело как успех — до проверки обратной стороны. **Привыкание глушило сам предмет.** Замер на вставленных предметах (`doubleT_platform`, подъезд с 200 м): | | Новизна кандидата, 20–60 м | Обнаружение, 70–90 м | |---|---|---| | привыкание выключено | 0.75 | 95 % | | привыкание включено | **0.24** | **74 %** | И это не «предмет заодно с обстановкой»: медианная новизна обычного кандидата в тех же прогонах — 0.43…0.71, то есть **предмет подавлялся сильнее, чем тоннельная обстановка**. Избирательность не просто мала, она отрицательна. **Причина — насыщение популяции, а не узнавание повторов.** За проезд набирается около 270 событий депрессии по 80 клеток каждое; при популяции 4000 это 5.4 попадания на клетку, то есть подавлено всё. Ровно та же ошибка, что описана в п. 8 для долговременной памяти с мушиными параметрами, — и проверяется она так же, поднятием ёмкости: | Ёмкость короткой памяти | Ложных треков на км (LOO) | Новизна предмета | Обнаружение 70–90 м | |---|---|---|---| | привыкание выключено | 7.5 | 0.75 | 95 % | | 4 000 клеток | **5.9** | **0.24** | **74 %** | | 20 000 клеток | 7.5 | 0.47 | 95 % | | 50 000 клеток | 7.5 | 0.64 | 95 % | При ёмкости, достаточной чтобы не насыщаться, привыкание не меняет **ничего**: 7.5 против 7.5 на обученной памяти и 31.8 против 31.8 на новой линии. Весь видимый выигрыш был глобальным глушением — а его и так даёт порог решения, который у нас уже есть отдельным параметром. Пробовались два способа сделать отсчёт избирательным, оба измерены и оба ничего не дали: * **резкий отсчёт MBON**: вместо среднего по активным клеткам — квантиль 0.75 или 0.90, то есть «знакомо, только если подавлено не меньше трёх четвертей кода». Новизну предмета это вернуло (0.75), но и весь эффект тоже (7.5 → 7.5); * **огрубление признаков** (coarse coding) до половины и до целого разброса, чтобы два похожих кронштейна давали буквально один и тот же код: 7.5 / 7.5 / 7.5. ### Что это на самом деле означает Коды двух разных экземпляров одной и той же конструкции не перекрываются настолько, чтобы второй узнавал первого, даже после огрубления до целого стандартного отклонения. Проще говоря: **в этих данных штатная обстановка повторяется не так буквально, как предполагалось**. Разброс между экземплярами одной конструкции — по ракурсу, по числу лучей, по тому, какая часть попала в габарит — больше, чем разрешение любого разумного кода формы. Поэтому на новом участке работает то, что и работало: долговременная память, обученная на других участках, снижает ложные тревоги с 21.8 до 9.1 трека на километр. Признаки в дескрипторе намеренно смешаны — форма и угловой размер переносятся на любой тоннель, положение в сечении запоминает конкретную обстановку, и именно первая половина даёт этот перенос. **Механизм оставлен в коде и выключен** (`enable_habituation: false`) со всеми параметрами: ёмкость, квантиль отсчёта, огрубление, шаг «разных мест», путь полузабывания. На линии, где обстановка стандартизована сильнее, чем в этих записях, он может заработать — но проверять это надо замером на той линии, а не надеждой. --- ## 11. Обученное считывание MBON: метки из физики До сих пор грибовидное тело училось **без учителя**: оно запоминало частоту обстановки и отвечало «знакомо/ново». Вставка предметов трассировкой лучей даёт то, чего в датасете нет, — **метку**. `synth.inject` возвращает индексы лучей, которые он переписал, поэтому кандидат размечается точным пересечением: предмет, если не меньше половины его лучей пришли от вставки (`tools/make_training_set.py`, `MIN_OVERLAP = 0.5`). Слои остаются те же — разрежённый код клеток Кеньона, торможение APL, один выход MBON, — меняется только учитель: вместо частоты обстановки различение «предмет / тоннель» (`flyguard/mbon_readout.py`). ### 11.1. Сначала — найденный артефакт: интенсивность выдавала вставку Первое же обучение дало подозрительно красивые цифры: leave-one-bag-out AUC **0.9935**, по полосам дальности от 0.998 вблизи до 0.941 на 160–230 м. Разбор важности перестановкой поставил наверх не форму, а **интенсивность**: ``` inten +0.0187 ← втрое больше следующего elongation +0.0051 fill +0.0048 log_rays +0.0030 ``` Проверка показала, откуда это взялось. Интенсивность вставки считалась по ламбертовой модели ρ·cosθ/r², и произведение `intensity · r²` у вставленных предметов держалось в пределах **1.8×** между p10 и p90, тогда как у настоящих кандидатов разброс **64×**. Иначе говоря, вставка — и только она — точно подчинялась учебниковому закону обратных квадратов, и это распознаётся надёжнее любой формы. Хуже того, у нижнего среза шкалы признак переворачивается. Разделяющая способность **одной интенсивности** по полосам: | полоса | intensity вставки | intensity обстановки | AUC по одной интенсивности | |---|---:|---:|---:| | 0–30 м | 37.2 | 5.0 | 0.956 | | 30–55 м | 4.6 | 4.5 | 0.513 | | 55–80 м | 1.7 | 4.0 | 0.287 | | 80–110 м | 1.0 | 3.0 | 0.197 | | 110–160 м | 1.0 | 5.0 | **0.049** | | 160–230 м | 1.0 | 7.5 | **0.010** | За 110 м расчётная яркость вставки падала ниже единицы, упиралась в срез шкалы — и «тускло» становилось почти безошибочным признаком предмета. AUC 0.010 означает почти идеальное разделение с обратным знаком. **В реальных данных всё наоборот.** Медиана интенсивности по всему облаку, 20 кадров на бэг: | бэг | 5–15 м | 15–30 | 30–50 | 50–80 | 80–120 | 120–170 | 170–230 | |---|---:|---:|---:|---:|---:|---:|---:| | `doubleT_obstacle` | 11 | 9 | 8 | 9 | 6 | 6 | 6 | | `roundT_doubleT` | 10 | 8 | 8 | 4 | 2 | 3 | 4 | | `squareT_platform_squareT_switch` | 8 | 8 | 7 | 3 | 2 | 2 | 4 | Никакого 1/r². Прибор отдаёт не принятую энергию, а **отражательную способность с компенсацией дальности** — то, что в руководстве Hesai называется reflectivity. Ламбертова модель описывала физику фотона, а не то число, которое лежит в поле `intensity`. Настоящий предмет даёт точку привязки. В `doubleT_obstacle` он стоит на 55.9 м, 57 лучей, и его медианная интенсивность — **34.5** (p10…p90 = 31…39) при медиане окружающих кандидатов 23.5. Старая формула на этой дальности дала бы **1.45**, то есть в 24 раза тусклее настоящего. **Первая попытка починки была неправильной.** Раз прибор компенсирует дальность, яркость вставки сделали постоянной: `100 · ρ · √cosθ` со шкалой, привязанной к настоящему предмету (ρ = 0.35 → около 34). Выборка пересобрана, модель переобучена — и артефакт просто перевернулся: | полоса | вставка | обстановка | AUC по одной интенсивности | |---|---:|---:|---:| | 0–30 м | 35.0 | 5.0 | 0.966 | | 30–55 м | 35.2 | 4.5 | 0.960 | | 55–80 м | 34.9 | 4.0 | 0.959 | | 80–110 м | 35.1 | 3.0 | 0.970 | | 110–160 м | 34.8 | 5.0 | 0.832 | | 160–230 м | 34.2 | 7.5 | 0.798 | Важность интенсивности при этом выросла втрое (+0.0533 против +0.0187), то есть модель стала опираться на подсказку ещё сильнее. **Настоящая причина: абсолютной шкалы в этих данных нет.** Медиана яркости кандидатов обстановки по бэгам — 3.0, 4.0, 4.0, 4.0, 7.0, а в `doubleT_obstacle`, где лежит настоящий предмет, 23.5 при 34.5 у самого предмета. Разброс между записями впятеро больше, чем контраст предмета к фону внутри записи. Любое абсолютное число, назначенное вставке, оказывается подарком детектору — в ту или в другую сторону. **Как сделано в итоге** (`synth.local_intensity` и `synth.IntensityEnv`): вставка берёт яркость **реальных возвратов с тех же самых лучей** — того, что предмет заслонил. Лучи, ушедшие в пустоту (за 100 м таких большинство), получают образец из реальных возвратов **той же полосы дальности** в этом же кадре; разложение кадра по дальности считается один раз и переиспользуется всеми 135 сценариями. Ни отражательной способности, ни закона яркости от дальности в функции нет; ρ осталась там, где ей место, — в вероятности, что эхо вообще вернётся. Проверка вставками в реальные бэги: | дальность | 30 м | 60 м | 100 м | 150 м | 200 м | |---|---:|---:|---:|---:|---:| | вставка | 8.8 | 6.9 | 4.3 | 4.3 | 10.1 | | реальные возвраты рядом | 10.0 | 4.0 | 2.0 | 3.0 | 3.0 | Это **заниженная** оценка: настоящий предмет в записи был в 1.47 раза ярче окружения, и этот запас мы себе не засчитываем. На линии с откалиброванной яркостью его можно вернуть — замером, а не верой. Тест `test_injected_intensity_copies_the_surroundings` держит свойство: среди аргументов нет отражения, а без разложения окружения дальность ничего не меняет. Замечание, важное для оценки ущерба: складка, не видевшая `doubleT_obstacle`, и с артефактом ставила настоящему предмету **1.000** (p10 = 0.998) против 0.393 у обстановки — на ближней дальности модель опиралась не только на подсказку. Но полигонная дальность, измеренная на таких вставках, мерила бы признак, которого в реальности нет. Поэтому все цифры ниже получены на выборке, где яркость взята из самой записи. Отдельный урок про метрику: таблицы выше потребовали починить подсчёт AUC. Ранги раздавались по порядку в массиве, а не усреднялись на совпадениях, и признак, у которого все значения равны, получал 0 или 1 вместо 0.5. Первым ложным сигналом такого рода оказался контраст к фону за 160 м — там он структурно равен нулю у всех кандидатов сразу. Теперь ранги средние, а сторож «что делит выборку само по себе» встроен в `tools/train_mbon.py` и печатается перед каждым обучением. ### 11.2. Что даёт обученное считывание Обучение — полнопакетный логистический спуск по разрежённому коду: 4 000 клеток Кеньона, 100 активных после торможения APL, 23 признака кандидата, 185 252 примера, 19 445 из них предметы. Ёмкость выбрана развёрткой: 8 000 и 20 000 клеток дают ту же AUC (0.9859 и 0.9850 против 0.9860), а в худшем кадре стоят 2.8 и 7.3 мс вместо 1.5 — платить не за что. Проверка — leave-one-bag-out, отдельная модель на каждую складку. Без этого сквозные цифры были бы ложными: считывание увидело бы проверяемый бэг ровно так же, как память тоннеля, обученная на всём подряд. | бэг | AUC | обстановки при 95 % предметов | бустинг по сырым признакам | |---|---:|---:|---:| | `doubleT_platform` | 0.9923 | 3.54 % | 0.9951 | | `roundT_doubleT` | 0.9939 | 2.79 % | 0.9987 | | `roundT_pressureGate_roundT` | 0.9759 | 16.30 % | 0.9866 | | `roundT_squareT_pressureGate_squareT` | 0.9828 | 11.65 % | 0.9930 | | `squareT_platform_squareT_switch` | 0.9851 | 5.87 % | 0.9902 | | **среднее** | **0.9860** | **8.03 %** | 0.9927 | AUC по полосам дальности: 0.995 / 0.996 / 0.992 / 0.973 / 0.955 / 0.738 — качество честно падает с дальностью, как и должно быть, когда от предмета остаётся четыре луча. Градиентный бустинг по тем же сырым признакам стабильно лучше на 0.005…0.01 AUC. Это цена разрежённого кода, и мы её знаем: взамен получаем схему, которая считается за 1.5 мс на худшем кадре и держит непрерывное дообучение. Наверху важности — форма, а не положение и не яркость: ``` fill +0.0079 abs_lat +0.0051 elongation +0.0076 acc_support +0.0046 h +0.0055 lat +0.0044 ``` Положение в сечении (`abs_lat`, `lat`) стоит на 4–6 месте. Формально это законный признак — опасны именно предметы в габарите, — но границу «положительной» области задали наши же расстановки (0, ±0.6, ±1.2 м от оси), и на защите это надо называть вслух, а не прятать. **Сквозной результат, leave-one-bag-out:** | | без считывания | со считыванием | |---|---:|---:| | разных ложных треков на километр | 7.5 | **3.3** | | кадров с ложной тревогой | 16.7 % | **4.4 %** | | реальный объект на 55 м | 98.9 % | **98.9 %** | | задержка кадра, медиана | 33 мс | 35–41 мс | **На размеченном полигоне** (модель по складкам, то есть для каждого бэга та, что его не видела): | человек стоя | без считывания | со считыванием | |---|---:|---:| | рабочая дальность «как есть» | 62 м | **80 м** | | P@50 м | 0.70 | 0.70 | | P@100 м | 0.19 | **0.31** | | посторонних тревог на кадр | 0.169 | **0.030** | Прибавка не только на человеке: ящик P@100 0.02 → 0.24, человек сидя 0.25 → 0.39, чемодан P@50 0.55 → 0.60, ведро впервые поднялось над нулём (рабочая дальность 8 → 20 м). Отдельно: прежние заявленные 100 м рабочей дальности и P@100 = 0.53 получены на полигоне с артефактом яркости и **завышены**. Честные числа без считывания — 80 м и 0.19, со считыванием — 80 м и 0.31. ### 11.3. Сколько дальности покупается за ложные тревоги Смысл обученного считывания не в том, чтобы просто показать меньшее число ложных. Оно даёт **запас**, а запас надо тратить: поезд движется, и дальность обнаружения — это время на торможение. Поэтому пороги решения вынесены в `Params` и развёрнуты замером. Все строки — leave-one-bag-out по ложным и полигон по дальности, модель по складкам: | конфигурация | ложных треков/км | кадров с тревогой | реальный объект | P@50 | P@100 | тревог на кадр, полигон | |---|---:|---:|---:|---:|---:|---:| | без считывания | 7.4 | 16.65 % | 98.9 % | 0.70 | 0.19 | 0.169 | | считывание, три подтверждения | 3.3 | 4.44 % | 98.9 % | 0.70 | 0.31 | 0.030 | | **по умолчанию: считывание, два подтверждения** | **3.3** | **4.52 %** | **99.5 %** | **0.70** | **0.31** | **0.030** | | тише: смешивание 0.7 | 2.5 | 4.85 % | 98.9 % | 0.70 | 0.23 | 0.016 | | дальше: порог улики 0.40 | 5.3 | 6.19 % | 98.9 % | 0.71 | 0.34 | 0.039 | | смешивание 0.7 + порог 0.32 | 4.3 | 7.28 % | 98.9 % | 0.71 | 0.31 | 0.055 | | порог 0.30 | 7.8 | 8.37 % | — | — | — | — | | порог 0.22 | 9.8 | 10.04 % | — | — | — | — | Три вывода, каждый замером. **Смешивание с ручной формулой — не способ купить дальность.** Строка «смешивание 0.7 + порог 0.32» даёт ровно ту же P@100 = 0.31, что и чистая модель при штатном пороге, но стоит 4.3 ложных трека вместо 3.3 и вдвое больше посторонних тревог на полигоне. Смешивание полезно в другом качестве — как **тихий режим**: при штатном пороге оно опускает ложные до 2.5 на км и 0.016 на кадр, отдавая дальность (P@100 0.31 → 0.23). Оно же — страховка на случай, когда предмет не похож ни на что из каталога: ручная формула продолжает работать по геометрии. **Понижение порога работает, но дорого.** 0.50 → 0.40 добавляет 0.03 к P@100 человека и 0.07 ящику, а платит 3.3 → 5.3 ложных трека. Дальше кривая только круче: 0.30 стоит 7.8, 0.22 — 9.8, то есть хуже, чем было без считывания вообще. Оставлено пресетом, а не умолчанием. **Два подтверждения вместо трёх оказались бесплатными.** Ложные не изменились (3.3 на км, 4.44 → 4.52 % кадров), посторонние тревоги на полигоне не изменились тоже (0.030 на кадр), а обнаружение выросло: реальный объект 98.9 → **99.5 %**, рабочая дальность человека сидя 20 → **80 м**, чемодана «как есть» 20 → **62 м**. Причина видна из данных: на 60…100 м предмет среднего размера виден через кадр, и третьего подтверждения приходится ждать дольше, чем он успевает приблизиться. Это стало умолчанием. Строки пресетов измерены при трёх подтверждениях — прежнем умолчании. В базовой точке переход на два ложных не изменил, но для пресетов это отдельно не перемерялось. **Чего порогами не изменить.** P@150 равна нулю в любой конфигурации. Это не алгоритм: прямая видимость в этих тоннелях 121–167 м, а прирельсовая зона за 80–90 м не наблюдается вовсе — луч скользит по полотну (п. 3 и п. 7.3). Потолок здесь геометрический.