Files
isaac/control_test/.memory.md
T
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

284 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# .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).