Files
dasha_f 0d32f32db0 Сортировочная ячейка Isaac Sim: CV-пайплайн и меши товаров
Замкнутый контур "поток -> CV -> механика": товары идут по конвейеру с шагом 700 мм,
класс определяется стереопайплайном во время движения, пушер и плуг реагируют физически.

Состав:
* control_test/ - ячейка и CV. run_sorting_cv.py + cv_worker.py (два процесса, потому что
  torch внутри Isaac роняет сцену), cell.py (физика лент, плуга, пушера), measure_plane.py
  (замер габаритов), README.md и .memory.md с замерами, проблемами и ловушками
* robozon_sorter/ - модули симуляции, scripts/ - утилиты, scene/ - сцены
* assets/ - меши товаров, плуг, объекты Objaverse

Бейзлайн CV: DEFOM-Stereo vitl, вход 480, iters 24, кроп зоны осмотра, без сегментации.
На потоке 700 мм - классы 8/9, габариты MAE 32.8 мм, 469 мс на товар при такте 700 мс.

Веса моделей (4.5 ГБ) и пропсы конвейера NVIDIA (274 МБ) не включены - источники и
команды скачивания в MODELS.md. Выход прогонов (captures/, runtime/) не включён:
воспроизводится.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 13:07:24 +00:00
..

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/<CFG>/                   кадры, 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) — через него в симулятор шлётся код.

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 &

Готовность:

grep -q "app ready" /tmp/isaac.log && ss -ltn | grep 8226   # порт должен слушать

С другой машины порт 8226 пробрасывается ssh-туннелем:

ssh -N -L 8226:127.0.0.1:8226 dasha@46.39.224.77 &

2. Открыть сцену

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. Прогон

cd /home/dasha/robozon-sorter
python3 isaacsim_send.py --context ct --timeout 400 --execution-timeout 390 \
    --file control_test/run_pipeline.py

Только часть объектов / другие параметры:

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:
"my_part": { "zone": "D", "dims_mm": [220, 180, 175], "k": 0.91 }
  1. Прогнать — товар подхватится сам.

Классы берутся из разметки, а не измеряются. Меши обнаруживаются в папке и спавнятся из неё, но 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.pathrun_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 и записана в саму сцену. Применить заново:

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 роняет процесс:

# ЭТАП 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 Запуск

# 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 м использовать для замера классификации и габаритов.