Замкнутый контур "поток -> 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>
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. Как добавить свой товар
- Положить
<имя>.usdвitems/(текстуры — вitems/textures/). - Дописать строку в
items/labels.json:
"my_part": { "zone": "D", "dims_mm": [220, 180, 175], "k": 0.91 }
- Прогнать — товар подхватится сам.
Классы берутся из разметки, а не измеряются. Меши обнаруживаются в папке и
спавнятся из неё, но 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 и записана в саму сцену. Применить заново:
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:
pillow223 → 138,bucket136 → 101,backpack170 → 158. Подушка худшая во всех шести расстановках (66–77 мм) — плоский мягкий силуэт; у ведра, похоже, снимается кромка, а не корпус. От расстановки камер это не зависит. - Сегментация иногда берёт ленту. На EQ-расстановках
detergentдал 157 мм вместо 108. Напрашивается отбраковка ракурса по 3D-центроиду: вид, чей центр дальше 6 см от медианы по трём ригам, выбрасывать до слияния (в прошлом пайплайне это помогало). - Замерено на 9 товарах из 25 — полный набор ещё не прогонялся.
9.6 Ловушки спавна, из-за которых стенд полгода мерил пустую ленту
Обе тихие, обе дают правдоподобные кадры пустого конвейера:
ClearXformOpOrder()на приме, который несёт ссылку, стирает собственное размещение меша. Замер bbox до очистки и последующий перенос дают промах ровно на этот сдвиг — товары уходили на 0.6 м под полотно. Ссылка должна жить на дочернем приме, размещение — на родителе.- Слои товаров прописывают
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 Открытые проблемы
- Габариты в движении вдвое хуже статичных. Кроп привязан к неподвижной точке осмотра
(−0.750), а товар пересекает её не точно:
detergentпопал на ворота уже на x = −1.708. Товар в кадре смещён, в кроп попадает соседний. Самая вредная из открытых. - Класс D берётся неустойчиво.
bucketне определяется ни одной конфигурацией. На эталонной геометрии меша та же функция даёт 0.934, на нашем облаке 0.66 — разрыв целиком в качестве облака. - Товары могут не доехать до ворот.
helmetиpillow, выпущенные подряд, столкнулись у входа: при габаритах 354 и 455 мм шаг 700 мм оставляет мало зазора. - Пушер выбрасывает товар с линии при скорости ножа на пределе (
PUSHER_MAX_SAFE2.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 м использовать для замера классификации и габаритов.