Сортировочная ячейка 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>
This commit is contained in:
dasha_f
2026-08-01 13:07:24 +00:00
parent 6ce460378a
commit 0d32f32db0
342 changed files with 18000 additions and 0 deletions
+283
View File
@@ -0,0 +1,283 @@
# .memory — состояние пайплайна control_test
Живая справка по замкнутому контуру «поток → CV → механика». Обновлять при изменении
конфигурации или при появлении нового замеренного факта. Здесь только то, что **измерено**;
предположения помечены отдельно.
Последнее обновление: 2026-08-01.
---
## 1. Что сейчас работает
**Полный прогон с кинематикой и CV — `run_sorting_cv.py` в паре с `cv_worker.py`.**
Класс товара приходит от стереопайплайна **во время движения**, а не из разметки. Пушер и
плуг реагируют физически на предсказанный класс. Разметка используется только для подсчёта
ошибки в конце.
Прежний `run_pipeline.py` (классы из `labels.json`, без CV) остаётся рабочим и нужен как
контроль: он разделяет ошибки механики и ошибки распознавания.
### Почему два процесса
torch внутри Isaac роняет процесс. Поэтому CV живёт отдельно, обмен через каталог:
```
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)
```
### Запуск
```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
```
---
## 2. Зафиксированный бейзлайн CV
В `measure_plane.py` значения по умолчанию:
| параметр | значение |
|---|---|
| стереодвижок | **DEFOM-Stereo vitl** (`STEREO=defom`) |
| вход сети | **480** px по ширине (`SW=480`) |
| итерации | **24** + scale_iters 8 |
| окно | **кроп зоны осмотра** (`CROP=1`) |
| сегментация | **не используется** |
Товар отделяется от полотна превышением над плоскостью (порог 20 мм) плюс отсев по
плотности; сегментация не участвует вовсе.
**Замер на статичных кадрах потока 700 мм (9 товаров):** классы 8/9 = 89 %, габариты MAE
медиана 32.8 мм, 469 мс на товар при такте 700 мс.
Веса: `/home/dasha/isaac_assets/cv/defom-stereo/checkpoints/`
`defomstereo_vitl_sceneflow.pth` (1.53 ГБ), `defomstereo_vits_sceneflow.pth` (173 МБ),
энкодер `depth_anything_v2_vitl.pth` (1.34 ГБ).
### Сравнение движков на том же потоке
| конфигурация | MAE медиана | классы | время | в такт 700 мс |
|---|---|---|---|---|
| **DEFOM vitl, вход 480, iters 24** | 32.8 мм | **8/9 = 89 %** | **469 мс** | да, +231 мс |
| DEFOM vits, вход 640, iters 24 | 33.9 мм | 7/9 | 502 мс | да |
| DEFOM vitl, вход 640, iters 12 | 37.5 мм | 6/9 | 627 мс | да |
| DEFOM vitl, вход 640, iters 24 | 29.9 мм | 7/9 | 735 мс | нет, 35 мс |
| CRE, кроп, вход 640 | **23.5 мм** | 7/9 | 551 мс | да |
| CRE, кроп, исходный вход | 25.6 мм | 7/9 | 1366 мс | нет |
| CRE, вся лента, исходный вход | 32.4 мм | 7/9 | 1701 мс | нет |
| FastSAM + CRE | 40.0 мм | 5/9 | 753 мс | нет |
| yolo26n-seg + CRE | 41.2 мм | 2/6 | 238 мс | да |
| FoundationStereo (только CPU) | 152.8 мм | 4/9 | 7159 мс | нет |
**CRE точнее по габаритам (23.5 против 32.8), DEFOM лучше по классам (8/9 против 7/9).**
---
## 3. Кинематика
### Ленты — 1.0 м/с
Семь дорожек, привод через `PhysxSurfaceVelocityAPI`. Скорость задаётся **в локальной
системе тела**, а дорожки уложены по-разному: у `_04` и `_06` локальный +X смотрит в
мировой −X. Направление выводится из мировой цели, величина делится на то, сколько
мирового стоит одна локальная единица (у `_06` масштаб 0.5).
`ConveyorTrack_06`**криволинейный** угол на 90°, четверть кольца с центром
(8.005, +1.042), радиусы 0.517…1.018. Линейный привод уводил товар в пустую середину
кольца; направление задаётся хордой, сохраняющей радиус, и в мировых координатах.
Проверено: товар 160 мм проходит **7.93 м за 7.9 с ровно на 1.00 м/с** без замедлений;
поток из 9 товаров проходит всю линию 10.9 м.
### Пушер — класс D
Нож двигается **записью трансформа** (`Cell.blade_to`), его призматический сустав
**выключен**: иначе сустав тянет нож к своей цели, пока скрипт пишет его в другое место,
и нож дрожит весь прогон. Ход 720 мм, срабатывание у `PUSH_X = -3.900`.
### Плуг — классы B и C
Лезвие **кинематическое**, шарнир **выключен** (`physics:jointEnabled=False`), угол
пишется напрямую (`Plow.target`). Силовой привод перенастраивали трижды и он не держал:
звенел на ±21.4° быстрее, чем его успевала вести команда.
Углы: `PLOW_PRESET = {"B": -16.0, "C": +16.0, "D": 0.0}`. Положительный поворот отклоняет
в −Y. Лезвие ставится **заранее**, до подхода товара. Скольжение товара вдоль кромки
измерено: 315–350 мм, то есть товар ведётся, а не отбрасывается ударом.
### Камеры — E60
Шесть камер, три стереопары, все на 700 мм над лентой, возвышение 60°, дистанция 808 мм,
азимуты 90° / 208.3° / 330°. Точка осмотра (0.750, 0.0, 1.781).
Базы: D435 73.5 мм, Gemini305 26.5 мм, Gemini345 129.4 мм. Цена глубины соответственно
13.2 / 37.2 / 8.1 мм на пиксель диспаратности.
**Расхождение с реальным стендом:** у настоящего D435 база **50.0 мм** (прочитано из
прошивки), а в симуляции 73.5 — в 1.47 раза больше. Значит результаты симуляции для этого
рига оптимистичнее реальности примерно в полтора раза. Не исправлено сознательно.
---
## 4. Замер последнего прогона замкнутого контура
**Классов получено 7 из 9, верно 5 из 7.** Верно: `bag` (D), `backpack` (C), `lunchbox` (B),
`detergent` (B), `box_400x400x300` (C).
**Задержка от ворот до класса — медиана 0.70 с** при 3.15 с до пушера и 7.10 с до плуга.
Инференс 631–1284 мс. Запас четырёх- и десятикратный.
Пушер сработал по классу D от камер. Плуг предпозиционировался шесть раз.
**Сквозная доставка: в контейнеры попало 5 из 9, в СВОЙ контейнер - 3 из 9.**
| причина потери | сколько | что именно |
|---|---|---|
| ошибка CV | 2 | bucket (D->C), box_300x200x200 (B->C) |
| класс верный, механика не довела | 1 | bag - пушер сработал, товар остался в лотке B вместо BinD |
| столкновение на входе | 2 | helmet и pillow, выпущены подряд |
| бросок пушера | 1 | detergent улетел на (+262, +2085) |
| плуг сдвинул недостаточно | 1 | box_400x400x300 на y = -1.49, за краем лотка C |
Из четырёх потерь по механике ни одна не связана с распознаванием. Подробная таблица с
координатами и зонами лотков - в README, раздел 10.6.
Сквозную доставку осмысленно мерить на шаге 1.4 м (там прежний прогон давал 9/9), а шаг
0.7 м использовать для замера классификации и габаритов.
---
## 5. Проблемы, которые сейчас есть
### Не решены
1. **Класс D берётся неустойчиво.** `bucket` (истинный k = 0.995) не определяется ни одной
конфигурацией. На **эталонной геометрии меша** та же функция даёт 0.934, на нашем облаке
0.66 — разрыв целиком в качестве облака, не в метрике. Габарит ведра выходит
вытянутым (333 × 239 при истинных 287 × 287), а вытянутое сечение высокого k дать не может.
2. **Габариты в движении хуже статичных.** `box_300x200x200` в потоке дал 513 × 452 против
311 × 218 на статичных кадрах и из-за этого ушёл в C вместо B. Причина видна в логе:
`detergent` попал на ворота уже на x = −1.708, то есть **мимо точки осмотра**, а кроп
привязан к неподвижной точке (−0.750). Товар в кадре смещён, в кроп попадает соседний.
3. **Два товара не доехали до ворот.** `helmet` встал на x = +4.02, `pillow` на +1.15. Оба
выпускались подряд (2.15 и 2.85 с); при их габаритах (354 и 455 мм) шаг 700 мм оставляет
мало зазора, и они, судя по позициям, столкнулись у входа.
4. **Пушер выбрасывает товар.** `detergent` закончил на (+262, +2085) — улетел на километры.
Скорость ножа на пределе: `PUSHER_MAX_SAFE = 2.5` м/с с пометкой «выше ~2.5 м/с
кинематический нож сбрасывает товар с линии».
5. **Шаг 700 мм механически не даёт B/C.** Лезвие плуга 0.63 м, на смену угла остаётся
0.07 м (10 % шага). Замерено: при 1.4 м — 9/9, при 0.7 м — 5/9. От скорости ленты не
зависит: доля занятости лезвия = 0.63/0.70 = 90 %. Нужен шаг > ~0.95 м либо другой
отводящий орган.
6. **Очень тонкие товары проходят под лезвием плуга.** `watch` (4.2 мм) класса C проехал
мимо: класс определяется верно, механика — нет.
### Проверено и НЕ помогло
Каждый пункт — отдельный замер, все ухудшили результат:
| попытка | результат |
|---|---|
| выбор маски по плоскости ленты вместо воротного пикселя | MAE 39.9 (было 40.0), классы 4/9 (было 5/9) |
| k подгонкой окружности P10/P90 | подняло k без разбора формы: коробки пошли в D |
| k по трём **мировым** сечениям | классы 6/9 (было 7/9) |
| сглаживание контура по угловым секторам | k макс 0.67 (было 0.85), классы 6/9 |
| проверка лево-право 0.5 / 1.0 / 1.5 px | отсеивает 28–52 % пикселей, MAE 34.6 (было 32.4), время ×2 |
| подгонка цилиндра RANSAC | не сработала ни на одном товаре: доля точек в допуске < 60 % |
| отбраковка ракурса по центроиду, порог 60 мм | MAE 65.4 (было 49.5) — откидывала два вида из трёх |
| ICP/RANSAC-совмещение облаков | 11.9 → 40.5 мм, три ракурса видят разные поверхности |
**Общий вывод из этой серии:** у нас не выбросы, а **дырки в диспаратности**. Любая правка,
которая *вычитает* точки (сглаживание, лево-право, отбраковка), делает хуже. Помогает то,
что *повышает плотность* или *уменьшает область поиска*: кроп зоны осмотра (MAE 32.4 → 25.6)
и понижение входа сети (25.6 → 23.5).
Недоделанная половина рецепта лево-право: заполнение мелких внутренних дырок и edge-aware
фильтр с запретом интерполяции через границу. Сейчас реализовано только удаление.
### Установлено, что НЕ виновато
- **Калибровка камер.** Восстановленное полотно садится на эталонную плоскость со смещением
0.59 / 0.56 / 1.13 мм и наклоном 0.56° / 0.35° / 1.72° по трём ригам. Ни интринсики, ни
боковое расположение, ни положение виртуальных камер не при чём.
- **Стереодвижок как таковой.** CRE проверен против штатной глубины RealSense на физическом
стенде: отношение 0.998 и 1.000 на 249 тыс. пикселей.
- **Покрытие ракурсами.** Дуга сечения у ведра покрыта на 295–360°.
---
## 6. Ловушки, на которых уже теряли время
Каждая давала правдоподобный, но неверный результат.
1. **Узлы OmniGraph надо УДАЛЯТЬ, а не деактивировать.** `SetActive(False)` убирает прем из
обхода, но собранный граф продолжает работать: `ConveyorBeltGraph` обнулял
`surfaceVelocity` за 5 шагов после `play`, `DiverterAnimGraph` останавливал таймлайн.
2. **Невидимость не убирает коллайдер.** `capture_roi.py` прятал снятый товар через
`MakeInvisible()`, и после двух прогонов захвата в точке осмотра стояло **18 невидимых,
но твёрдых предметов**. Поток вставал на них «посреди ConveyorTrack_02». Лечится
`cell.clear_capture_parks()`, вызывается в `prepare()`.
3. **`BBoxCache` во время прогона врёт.** Он читает авторские трансформы из слоя USD, а
физика пишет в Fabric. Отчёт показывал, что все товары стоят в точках выпуска, хотя
таймлайн отработал 26 с. Положения читать через `RigidPrim.get_world_poses()`.
4. **`play()` после `stop()` перематывает в начало** и сбрасывает физику. Обработчик,
«возобновляющий» остановившийся таймлайн, обнуляет весь опыт.
5. **Заданная частота физики не применяется.** `timeStepsPerSecond=120` не подействовал,
фактический шаг 83.33 мс (60 Гц). Скорости выходили ровно вдвое завышенными. Время
брать из таймлайна.
6. **Файл `runtime/worker_ready` остаётся от прошлого запуска** и даёт ложную готовность.
Проверять работника по PID.
7. **`cloud_from_roi` возвращает ПАРУ** (облако товара, облако полотна). Складывание
кортежа целиком роняет `np.vstack` на разнородных формах.
8. **Меши `items_flow/` уже в каталожном масштабе и уже посажены на z = 0**, коллайдеры в
них уже есть. Домасштабирование и свои коллайдеры ломают спавн — товары не едут.
9. **ArUco-метки не видны в ИК** (на физическом стенде): типографская краска на 850 нм
в значительной мере прозрачна. Позу брать из цветного кадра с ЦВЕТНЫМИ интринсиками и
переводить в систему ИК заводскими экстринсиками.
---
## 7. Что делать дальше — по приоритету
1. **Привязать кроп к товару, а не к неподвижной точке осмотра.** Это лечит проблему 2 —
самую вредную из открытых: из-за неё габариты в движении вдвое хуже статичных.
2. **Снизить скорость ножа пушера** — проблема 4, товар улетает.
3. **Разнести выпуск товаров по времени** либо увеличить шаг — проблема 3.
4. Доделать вторую половину фильтрации лево-право (заполнение дырок, edge-aware).
5. FoundationStereo на GPU: заблокировано внешне — `onnxruntime-gpu` требует CUDA 13, на
сервере 12.8; колёс под CUDA 12 для python 3.12 нет; `onnx2torch` падает на динамическом
`Clip`; TensorRT не установлен. Нужен либо `.pth` через код репозитория, либо CUDA 13
(установка требует прав root).