# control_test — сортировочная ячейка с папкой объектов Автономный стенд: конвейер + пушер + плуг из `plow_cell_90_45_test.usd`, где **набор товаров берётся из папки `items/`**. Положили новый `.usd` — он попадает в следующий прогон. Ничего не зашито под конкретный объект. ``` control_test/ ├── scene/plow_cell_90_45_test.usd сцена (ссылается на ../assets → симлинк на assets проекта) ├── items/ меши товаров: *.usd + textures/ + labels.json ├── classify.py чтение разметки + правила B/C/D (для проверки) ├── cell.py физика ячейки: ленты, плуг, пушер, стыки, свет, пол ├── run_pipeline.py прогон сортировки: спавн из items/ по очереди, отчёт │ │ ── стенд замера габаритов камерами (раздел 9) ── ├── cam_configs.py расстановки камер; DEFAULT = "E60" — рабочая ├── capture_roi.py ЭТАП 1 в Isaac: рендер L/R по всем ригам ├── measure_roi.py ЭТАП 2 отдельным процессом: FastSAM + CREStereo ├── captures// кадры, manifest.json, roi_compare.json │ │ ── замкнутый контур: поток -> CV -> механика (раздел 10) ── ├── run_sorting_cv.py прогон сцены: поток, ворота, пушер, плуг по классу от CV ├── cv_worker.py процесс CV: DEFOM -> габариты -> k -> класс ├── measure_plane.py сам замер; здесь зафиксирован бейзлайн ├── runtime/ обмен заявками и ответами, кадры, результат прогона ├── .memory.md СОСТОЯНИЕ ПАЙПЛАЙНА: замеры, проблемы, ловушки │ └── diag/ одноразовые диагностики, не часть пайплайна ``` **Устаревшие файлы** помечены заголовком `SUPERSEDED` и оставлены только как история: `capture_cfg.py` (тихо снимал пустую ленту), `measure_cfg.py` (мерил ленту вместо товара), `reposition.py` (зашитые 600 мм). Использовать их нельзя. ## 1. Запуск Isaac Sim Isaac Sim 6.0.1 стоит под пользователем `whatevenif`, запускается со стримингом WebRTC и включённым python-сервером (TCP 8226) — через него в симулятор шлётся код. ```bash cd /home/whatevenif/isaacsim nohup ./kit/kit ./apps/isaacsim.exp.full.streaming.kit \ --no-window --no-ros-env \ --enable isaacsim.code_editor.python_server \ --/exts/omni.kit.livestream.app/primaryStream.publicIp=46.39.224.77 \ --/exts/omni.services.livestream.session/quitOnSessionEnded=false \ > /tmp/isaac.log 2>&1 & ``` Готовность: ```bash grep -q "app ready" /tmp/isaac.log && ss -ltn | grep 8226 # порт должен слушать ``` С другой машины порт 8226 пробрасывается ssh-туннелем: ```bash ssh -N -L 8226:127.0.0.1:8226 dasha@46.39.224.77 & ``` ## 2. Открыть сцену ```bash cd /home/dasha/robozon-sorter python3 isaacsim_send.py --timeout 120 \ --file ~/.claude/skills/isaac-sim-remote/scripts/open_stage.py \ --arg action=open \ --arg usd_path=/home/dasha/robozon-sorter/control_test/scene/plow_cell_90_45_test.usd ``` Должно ответить `Stage prims: 366`. Сцену надо открывать **заново перед каждым прогоном** — `run_pipeline.py` меняет состояние сцены (удаляет графы, снимает коллизии, спавнит тела). ## 3. Прогон ```bash cd /home/dasha/robozon-sorter python3 isaacsim_send.py --context ct --timeout 400 --execution-timeout 390 \ --file control_test/run_pipeline.py ``` Только часть объектов / другие параметры: ```bash python3 isaacsim_send.py --context ct --timeout 400 --execution-timeout 390 \ --args-json '{"only": ["bag","lunchbox"], "pitch": 1.4, "plow_angle": 20}' \ --file control_test/run_pipeline.py ``` | аргумент | по умолчанию | что делает | |---|---|---| | `speed` | 1.0 | скорость лент, м/с | | `pitch` | 1.4 | расстояние между товарами, м | | `plow_angle` | 20 | угол плуга, град (B = −угол, C = +угол) | | `swing_margin` | 0.25 | доля T_pitch на поворот; меньше → быстрее плуг | | `plow_hold_max` | 4.0 | сколько плуг держит угол, с | | `only` | все | список имён объектов | | `limit` | 0 | взять первые N | ## 4. Как добавить свой товар 1. Положить `<имя>.usd` в `items/` (текстуры — в `items/textures/`). 2. Дописать строку в `items/labels.json`: ```json "my_part": { "zone": "D", "dims_mm": [220, 180, 175], "k": 0.91 } ``` 3. Прогнать — товар подхватится сам. **Классы берутся из разметки, а не измеряются.** Меши обнаруживаются в папке и спавнятся из неё, но `zone`, `dims_mm` и `k` читаются из `labels.json` — это ground truth, относительно которого проверяется механика. Почему не автозамер: он был реализован и отброшен. Габариты мерились точно (сверено со всем каталогом: `pen` 148.5/13.2/9.0 против 148/13/9, `pouf` 488.9 против 489), но геометрическая оценка круглости систематически занижала тела с ручкой или полостью — `bucket` 0.737 против 0.995, `mug` 0.731 против 0.985, `cylinder` 0.749 против 0.867. Класс D тихо превращался в B, и это выглядело как отказ механики. Совпадение с каталогом было 20/25. Для стенда, где проверяется именно механика, надёжнее читать метку. **Товары без записи в `labels.json` пропускаются** — не угадываются. Прогон печатает их списком; сейчас это 12 мешей (`air_conditioner`, `briefcase_hard`, `carton_large`, `cleat_small`, `clothespin_flat`, `cooler_cube`, `duffel_round`, `nailfile_mini`, `printer_compact`, `safety_pin`, `toaster_compact`, `toaster_oven`). **Разметка проверяется на согласованность.** `classify.verify_labels()` прогоняет каждую запись через правила раздела 5, и прогон печатает `labels consistent with the documented B/C/D rules` либо перечисляет расхождения: ошибка в метке иначе всплыла бы как необъяснимый сбой механики. ## 5. Правила классификации Порядок как в пайплайне — сначала D: * **D** «не подходит без доупаковки» → пушер → BinD. Габариты в норме (как у B), но `k > 0.8` хотя бы в одном сечении. * **C** «не подходит по габаритам» → плуг `+angle` → ConveyorTrack_01 → контейнер C. Хотя бы один размер `< 10 мм` **или** не влезает в `450×320×320 мм`. Форма не важна. * **B** «подходит для сортировки» → плуг `−angle` → ConveyorTrack_06 → контейнер B. Все размеры в диапазоне `10×10×10 … 450×320×320 мм` и `k ≤ 0.8`. Влезаемость проверяется по отсортированным габаритам против отсортированной рамки — товар можно положить любой гранью. ## 6. Ограничения, которые стоит знать * **Шаг 0.7 м из ТЗ: класс D работает, B/C — нет.** Замерено на 9 товарах: | шаг | B | C | D | итого | |---|---|---|---|---| | 1.4 м | 3/3 | 3/3 | 3/3 | **9/9** | | 0.7 м | 1/3 | 1/3 | 3/3 | 5/9 | Причина чисто геометрическая: **лезвие плуга длиной 0.63 м**, и товар «занимает» его 0.63 м пути. При шаге 0.7 м на смену угла остаётся 0.7 − 0.63 = **0.07 м (10 % шага)** — это ниже уровня шума физики (товары подрагивают, интервал плывёт), поэтому регулярно два товара разных классов оказываются на лезвии одновременно и второй получает чужой угол. **Скорость ленты тут не поможет:** доля занятости = длина_лезвия / шаг = 0.63/0.70 = 90 % и от скорости не зависит — время сокращается пропорционально и у товара, и у лезвия. Реально помогает только шаг: нужен **> ~1.5 × длины лезвия ≈ 0.95 м**. При 1.4 м — 100 % по всем классам. Укоротить лезвие нельзя «просто так»: его длина и есть вылет, которым он перекладывает товар через ленту шириной 0.9 м. Чтобы держать 0.7 м, нужен другой отводящий орган (второй плуг в шахматном порядке или толкатель вместо плуга). * **Очень тонкие товары не отрабатывают на плуге.** `watch` (4.2 мм) класса C проехал мимо и остался на линии (x −7.64): лезвие плуга приподнято над полотном, и такой товар проходит под ним. Класс определяется верно, механика — нет. * 12 мешей в `items/` не имеют записи в `labels.json` и потому пропускаются (см. раздел 4). Часть из них ещё и обмерена в других единицах, так что зашивать им класс наугад нельзя — нужна честная разметка. ## 7. Что именно чинит `cell.py` Каждый пункт — отдельная найденная и замеренная проблема, все включены в `prepare()`: | функция | зачем | |---|---| | `_kill_stale_graphs` | авторские `ConveyorBeltGraph` / `DiverterAnimGraph` **удаляются**, а не деактивируются: `SetActive(False)` не останавливает уже собранный OmniGraph, и он обнуляет `surfaceVelocity` каждый тик | | `configure_belts` | все 7 лент + 4 плиты стыка; `ConveyorTrack_05` (вход линии) и ветка пушера `Belt_01` отсутствовали в списке проекта | | `drive_corner_belt` | `ConveyorTrack_06` — **криволинейный** угол (четверть кольца, центр (−8.005, +1.042), радиусы 0.517…1.018). Линейный привод уводил товар в пустую середину кольца, и он проваливался. Направление задано хордой, сохраняющей радиус, и **в мировых координатах** — у ленты неравномерный масштаб, и пересчёт в локальные искажает направление | | `add_transfer_bridge` | между краем `_04` (y = +0.45) и началом `_06` (x = −8.00) не было опоры ровно там, где плуг сталкивает товар | | `add_container_catchers` | лотки толщиной 40 мм пробивались товаром при падении с ленты (~3.4 м/с ≈ 57 мм за шаг) | | `open_junction` | у конвейерной «обшивки» есть коллайдер вместе с бортами — стена ровно там, где товар должен уходить вбок | | `regrip_decks` / `_ensure_grip_material` | `configure_plow` перебивает плиты скользким материалом; сам grip-материал проект создаёт только внутри своей `configure_belts`, которую этот модуль не вызывает | | `resize_pusher_blade` / `grip_pusher_blade` / `seat_pusher_blade` | нож 1200 → 500 мм; свой цепкий материал вместо скользкого «плужного»; посадка на 1 мм над полотном (было 14 мм — тонкие товары проходили под ним) | | `add_ground_and_light` / `add_side_rails` | пол и купольный свет; борта только на прямых участках — на стыках они блокируют штатный сход товара | ## 8. Зависимости `cell.py` и `run_pipeline.py` импортируют `robozon_sorter` (константы `config.py`, класс `Plow`, помощники `sim/scene.py` и `sim/plow_cell.py`), поэтому `/home/dasha/robozon-sorter` должен быть в `sys.path` — `run_pipeline.py` добавляет его сам. `items/` и `scene/` — собственные копии, их правка проект не задевает. `assets` — симлинк на `../assets`, потому что сцена ссылается на ленты и плуг относительным путём. ## 9. Стенд замера габаритов камерами Отдельная задача от сортировки: по стереопарам определить габариты **неизвестного** товара. Классы тут не читаются из разметки — они и есть то, что надо предсказать. ### 9.1 Рабочая расстановка — E60 | параметр | значение | |---|---| | высота над лентой | **700 мм** (все три рига) | | возвышение | **60°** | | рабочая дистанция | **808 мм** | | азимуты | **90° / 208.3° / 330°** (RealSense D435 / Gemini305 / Gemini345) | | точка осмотра | `(-0.750, 0.0, 1.781)` — поверхность `ConveyorTrack_04` | Зафиксирована в `cam_configs.DEFAULT` и записана в саму сцену. Применить заново: ```bash python3 isaacsim_send.py --file /tmp/save_e60.py # apply_config(stage, CC.DEFAULT) ``` Калибровка проверена: поворот левой и правой камеры в паре совпадает точно (отклонение 0.0e+00), база равна паспортной с нулевой поперечной составляющей, Δv между кадрами 0.000 px — то есть `depth = fx·B/disp` применим без ректификации. Цена глубины на 808 мм: Gemini345 8.0 мм/px, D435 13.2, Gemini305 37.2. ### 9.2 Почему именно E60 Развёртка по возвышению при фиксированной высоте 700 мм и, отдельно, попытка дать каждому ригу свою дистанцию под одинаковую цену глубины (EQ15/EQ20). Медиана MAE по 9 товарам, слияние трёх ригов: | конфиг | возвыш. | дистанция | MAE медиана | D435 | Gemini305 | Gemini345 | |---|---|---|---|---|---|---| | A_original | 42° | 1051 мм | 30.0 | 29.3 | **109.5 (1/9)** | 67.4 (5/9) | | E45 | 45° | 991 мм | 25.5 | 29.9 | 50.8 (1/9) | 32.8 | | **E60** | **60°** | **808 мм** | **22.3** | **25.8** | **27.1** | **28.0** | | E75 | 75° | 726 мм | 23.6 | 27.8 | 26.3 | 29.4 | | EQ15 | 45° | 518/863/1146 | 29.8 | 42.2 | 31.3 | 30.9 | | EQ20 | 45° | 598/996/1323 | 24.6 | 33.4 | 28.2 | 32.7 | Два вывода, на которых всё держится: * **Gemini305 не нужна своя короткая дистанция.** На 1051 мм он давал облако один раз из девяти, потому что диспаратность в точке осмотра была всего 17 px. На 808 мм она 21.7 px и риг работает наравне с остальными. Попытка подобрать каждому ригу свою дистанцию (EQ15/EQ20) починила Gemini305, но испортила D435 (25.8 → 42.2) и сделала слияние хуже любой общей дистанции. * **E60 — первая расстановка, где слияние трёх ригов выигрывает у лучшего одиночного** (22.3 против 25.8). На 1051 мм слияние не давало ничего. E75 почти не хуже по точности, но его общая зона ленты на треть меньше (5756 против 7681 см²) — меньше запас на смещение товара. ### 9.3 Как устроен замер Два этапа, потому что torch внутри Isaac роняет процесс: ```bash # ЭТАП 1 — в Isaac, без torch: рендер L/R со всех шести камер python3 isaacsim_send.py --timeout 880 --file control_test/capture_roi.py python3 isaacsim_send.py --timeout 880 --file control_test/capture_roi.py --arg cfg=E75 # ЭТАП 2 — отдельным процессом: FastSAM + CREStereo + обратная проекция /home/whatevenif/isaacsim/python.sh measure_roi.py # E60, все 4 варианта ROI /home/whatevenif/isaacsim/python.sh measure_roi.py E60 objroi # только рабочая схема ``` Схема ROI (`objroi`), она же рабочая: ``` общая зона ленты, видимая всеми 6 камерами ↓ ограничивает, где FastSAM ищет FastSAM segment-everything → маска, содержащая «воротный» пиксель ↓ bbox + 48 px запаса ОДНО окно колонок на левый и правый кадр, левый край расширен на 1.35 × макс. диспаратности ↓ CREStereo → диспаратность → depth = fx·B/disp → обратная проекция по экстринсикам ``` Три правила, каждое подтверждено замером: * **RGB не маскируется до CREStereo.** Матчеру нужен фон вокруг предмета; маска применяется только к глубине, с эрозией 2 × 3×3, иначе в облако попадает кайма фона. * **Левое и правое окно обязаны совпадать.** Контрольный вариант `objroi_bad`, где правый кроп центрируется по своему bbox, разрушил **8 облаков из 9** — сдвиг окна подменяет диспаратность. * **Слияние идёт только по калиброванным экстринсикам, без RANSAC/ICP.** Повторная регистрация ухудшала результат в каждой проверке (11.9 → 40.5 мм): три ракурса видят разные поверхности, и ICP совмещает несоответствующие участки. Общая зона ленты проецируется в 84–92 % кадра, поэтому сама по себе разрешения она не экономит — её роль в том, чтобы ограничить область поиска сегментации. Выигрыш даёт вторая ступень: `objroi` втрое быстрее полного кадра (519 против 1620 мс на товар) при той же точности и не теряет мелкие предметы (`lunchbox` полный кадр терял вовсе). ### 9.4 Эталон габаритов — bbox меша в сцене, не каталог Меши в `items/` **в 2.0–2.8 раза мельче** своих паспортных размеров, и коэффициент у каждого свой (`bag` 2.05, `box_400x400x300` 2.41, `box_300x200x200` 2.79). Для сортировки по меткам это безразлично, но оценивать предсказание масштаба против `labels.json` нельзя. `capture_roi.py` пишет в манифест оба числа: `gt_scene_mm` (реальный bbox в сцене — эталон замера) и `gt_catalogue` (паспорт из `labels.json` — эталон сортировки). ### 9.5 Что не решено * **Систематическое занижение.** На E60: `pillow` 223 → 138, `bucket` 136 → 101, `backpack` 170 → 158. Подушка худшая во всех шести расстановках (66–77 мм) — плоский мягкий силуэт; у ведра, похоже, снимается кромка, а не корпус. От расстановки камер это не зависит. * **Сегментация иногда берёт ленту.** На EQ-расстановках `detergent` дал 157 мм вместо 108. Напрашивается отбраковка ракурса по 3D-центроиду: вид, чей центр дальше 6 см от медианы по трём ригам, выбрасывать до слияния (в прошлом пайплайне это помогало). * Замерено на 9 товарах из 25 — полный набор ещё не прогонялся. ### 9.6 Ловушки спавна, из-за которых стенд полгода мерил пустую ленту Обе тихие, обе дают правдоподобные кадры пустого конвейера: 1. **`ClearXformOpOrder()` на приме, который несёт ссылку**, стирает собственное размещение меша. Замер bbox до очистки и последующий перенос дают промах ровно на этот сдвиг — товары уходили на 0.6 м под полотно. Ссылка должна жить на **дочернем** приме, размещение — на родителе. 2. **Слои товаров прописывают `visibility = invisible` на своём корне**, а `MakeVisible()` на родителе авторское значение потомка не снимает. Нужно пройти `Usd.PrimRange` и выставить `inherited` всем Imageable. Плюс: `BBoxCache.ComputeWorldBound()` на только что созданном родителе возвращает пустой диапазон даже при скомпонованном потомке — мерить надо сам прим со ссылкой; и ссылка не компонуется в том же тике, нужен цикл `await app_utils.update_app_async(steps=2)`. Поэтому `capture_roi.py` печатает **контраст**: разницу средней яркости внутри ожидаемого силуэта и в кольце вокруг него, по каждой камере. Меньше 3 — товар не отрендерился, строка помечается `<-- НЕ ВИДЕН`. Без этой проверки четыре круга «настройки сегментации» были потрачены на кадры, где предмета не было вовсе. --- ## 10. Замкнутый контур: поток → CV → механика Полный прогон, где класс товара приходит **от стереопайплайна во время движения**, а не из `labels.json`. Пушер и плуг реагируют физически на предсказанный класс. Разметка нужна только для подсчёта ошибки в конце. Прежний `run_pipeline.py` (классы из разметки) остаётся рабочим и служит контролем: он разделяет ошибки механики и ошибки распознавания. Подробное состояние пайплайна, все замеры и открытые проблемы — в `.memory.md`. ### 10.1 Два процесса torch внутри Isaac роняет процесс, поэтому CV живёт отдельно, а обмен идёт через каталог `runtime/`: ``` run_sorting_cv.py (в Isaac, без torch) cv_worker.py (отдельный процесс, torch+GPU) поток 700 мм при 1 м/с ворота x = -0.750 -> 6 кадров ──заявка──> runtime/req/<товар>.json DEFOM vitl / вход 480 / iters 24 облако -> габариты -> k -> класс класс <──ответ── runtime/res/<товар>.json пушер: класс D -> Cell.stroke() плуг: класс B/C -> Plow.target(-16 / +16) ``` Времени хватает с запасом: от ворот до пушера товар едет 3.15 с, до плуга 7.10 с, а полная задержка от ворот до класса замерена в **0.70 с** (медиана; инференс 631–1284 мс). ### 10.2 Запуск ```bash # 1. работник CV. Прогрев ~90 с - грузится vitl-энкодер cd /home/dasha/robozon-sorter/control_test nohup /home/whatevenif/isaacsim/python.sh cv_worker.py > /tmp/cvworker.log 2>&1 & # 2. проверить ПО PID, а не по файлу runtime/worker_ready: # файл остаётся от прошлого запуска и даёт ложную готовность pgrep -af "python.*cv_worker.py" # 3. открыть сцену заново (cell.prepare меняет её состояние), затем прогон cd /home/dasha/robozon-sorter python3 isaacsim_send.py --context cvsort --timeout 2580 --execution-timeout 2560 \ --file control_test/run_sorting_cv.py ``` Результат прогона: `runtime/sorting_cv.json`, кадры товаров `runtime/frames/`, обзорный снимок `runtime/shots/overview.png`. ### 10.3 Зафиксированный бейзлайн CV Значения по умолчанию в `measure_plane.py`: | параметр | значение | |---|---| | стереодвижок | **DEFOM-Stereo vitl** (`STEREO=defom`) | | вход сети | **480** px по ширине (`SW=480`) | | итерации | **24** + `scale_iters` 8 | | окно | **кроп зоны осмотра** (`CROP=1`) | | сегментация | **не используется** | Товар отделяется от полотна превышением над плоскостью (20 мм) плюс отсев по плотности: товар даёт сплошную поверхность, полотно — редкие выбросы диспаратности. Сегментация не участвует вовсе — она была источником и раздутых габаритов, и промахов по классу D. Веса: `/home/dasha/isaac_assets/cv/defom-stereo/checkpoints/`. **Замер на статичных кадрах потока 700 мм (9 товаров):** классы 8/9 = 89 %, габариты MAE медиана 32.8 мм, 469 мс на товар при такте 700 мс. **Замер замкнутого контура:** классов получено 7 из 9, верно 5 из 7. Полная таблица сравнения движков и конфигураций — в `.memory.md`, раздел 2. ### 10.4 Кинематика * **Ленты 1.0 м/с.** `surfaceVelocity` задаётся в ЛОКАЛЬНОЙ системе тела; у `_04` и `_06` локальный +X смотрит в мировой −X. `ConveyorTrack_06` — криволинейный угол на 90°, направление задаётся хордой в мировых координатах. Проверено: товар 160 мм идёт 7.93 м ровно на 1.00 м/с. * **Пушер** — нож двигается записью трансформа (`Cell.blade_to`), призматический сустав выключен: иначе сустав и скрипт тянут нож в разные стороны и он дрожит весь прогон. * **Плуг** — лезвие кинематическое, шарнир выключен, угол пишется напрямую (`Plow.target`). Силовой привод не держал: звенел на ±21.4°. Углы `{"B": -16, "C": +16, "D": 0}`, лезвие ставится ЗАРАНЕЕ. Скольжение товара вдоль кромки замерено: 315–350 мм. ### 10.5 Открытые проблемы 1. **Габариты в движении вдвое хуже статичных.** Кроп привязан к неподвижной точке осмотра (−0.750), а товар пересекает её не точно: `detergent` попал на ворота уже на x = −1.708. Товар в кадре смещён, в кроп попадает соседний. Самая вредная из открытых. 2. **Класс D берётся неустойчиво.** `bucket` не определяется ни одной конфигурацией. На эталонной геометрии меша та же функция даёт 0.934, на нашем облаке 0.66 — разрыв целиком в качестве облака. 3. **Товары могут не доехать до ворот.** `helmet` и `pillow`, выпущенные подряд, столкнулись у входа: при габаритах 354 и 455 мм шаг 700 мм оставляет мало зазора. 4. **Пушер выбрасывает товар с линии** при скорости ножа на пределе (`PUSHER_MAX_SAFE` 2.5 м/с). 5. **Шаг 700 мм механически не даёт B/C** — лезвие плуга 0.63 м, на смену угла остаётся 0.07 м. При 1.4 м — 9/9, при 0.7 м — 5/9 (см. раздел 6). Что уже проверено и **не** помогло (сглаживание контура, проверка лево-право, цилиндр RANSAC, отбраковка по центроиду и другое) — перечислено в `.memory.md`, раздел 5. ### 10.6 Сколько товаров доходит до контейнеров Замер последнего прогона замкнутого контура. Зоны лотков берутся из самой сцены (`B_Floor` x −9.26…−8.36 / y +1.07…+1.87; `C_Floor` x −10.90…−10.00 / y −0.63…+0.18; `BinD_Floor` x −6.21…−4.95 / y +1.59…+2.85), допуск 0.35 м — падая с ленты, товар может лечь у стенки, а не над серединой пола. **В контейнеры попало 5 из 9. В СВОЙ контейнер — 3 из 9.** | товар | эталон | CV | конец X, Y | куда попал | |---|---|---|---|---| | `backpack` | C | C | −10.44, −0.47 | контейнер C — **верно** | | `lunchbox` | B | B | −8.95, +1.55 | контейнер B — **верно** | | `bag` | D | D | −8.37, +1.03 | контейнер B — класс верный, доставка нет | | `bucket` | D | C | −10.64, +0.14 | контейнер C — ошибка CV | | `box_300x200x200` | B | C | −10.63, −0.28 | контейнер C — ошибка CV | | `box_400x400x300` | C | C | −9.39, −1.49 | остался на линии, за краем лотка | | `detergent` | B | B | +262.50, +2085.32 | улетел за пределы ячейки | | `helmet` | D | — | +4.02, +0.78 | не доехал до ворот | | `pillow` | C | — | +1.15, +0.91 | не доехал до ворот | ### Разложение потерь по причинам Общая цифра здесь малополезна: причины разные и лечатся по-разному. | причина | сколько | что именно | |---|---|---| | ошибка CV | 2 | `bucket` (D→C), `box_300x200x200` (B→C) | | класс верный, механика не довела | 1 | `bag` — пушер сработал (есть в логе), но товар остался в лотке B вместо BinD | | столкновение на входе | 2 | `helmet` и `pillow` выпущены подряд; при габаритах 354 и 455 мм шаг 700 мм слишком тесен | | бросок пушера | 1 | `detergent` — скорость ножа на пределе `PUSHER_MAX_SAFE` = 2.5 м/с | | плуг сдвинул недостаточно | 1 | `box_400x400x300` закончил на y = −1.49, за краем лотка C | **Из четырёх потерь по механике ни одна не связана с распознаванием.** CV ошибся на двух товарах из семи, кому вообще выдал класс. ### Фоновое ограничение При шаге 700 мм классы B и C механически не отрабатывают в принципе: лезвие плуга 0.63 м, на смену угла остаётся 0.07 м (10 % шага) — см. раздел 6. Прежний прогон с классами из разметки давал при шаге 1.4 м результат **9/9**. То есть значительная часть этих потерь — не про CV и не про настройку, а про геометрию плуга, и на шаге 0.7 м она не устраняется ни скоростью ленты, ни точностью классификации. Проверять сквозную доставку осмысленно **на шаге 1.4 м**, а шаг 0.7 м использовать для замера классификации и габаритов.