0d32f32db0
Замкнутый контур "поток -> 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>
494 lines
35 KiB
Markdown
494 lines
35 KiB
Markdown
# 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) — через него в симулятор шлётся код.
|
||
|
||
```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 м использовать для
|
||
замера классификации и габаритов.
|