Замкнутый контур "поток -> 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>
20 KiB
.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)
Запуск
# 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. Проблемы, которые сейчас есть
Не решены
-
Класс D берётся неустойчиво.
bucket(истинный k = 0.995) не определяется ни одной конфигурацией. На эталонной геометрии меша та же функция даёт 0.934, на нашем облаке 0.66 — разрыв целиком в качестве облака, не в метрике. Габарит ведра выходит вытянутым (333 × 239 при истинных 287 × 287), а вытянутое сечение высокого k дать не может. -
Габариты в движении хуже статичных.
box_300x200x200в потоке дал 513 × 452 против 311 × 218 на статичных кадрах и из-за этого ушёл в C вместо B. Причина видна в логе:detergentпопал на ворота уже на x = −1.708, то есть мимо точки осмотра, а кроп привязан к неподвижной точке (−0.750). Товар в кадре смещён, в кроп попадает соседний. -
Два товара не доехали до ворот.
helmetвстал на x = +4.02,pillowна +1.15. Оба выпускались подряд (2.15 и 2.85 с); при их габаритах (354 и 455 мм) шаг 700 мм оставляет мало зазора, и они, судя по позициям, столкнулись у входа. -
Пушер выбрасывает товар.
detergentзакончил на (+262, +2085) — улетел на километры. Скорость ножа на пределе:PUSHER_MAX_SAFE = 2.5м/с с пометкой «выше ~2.5 м/с кинематический нож сбрасывает товар с линии». -
Шаг 700 мм механически не даёт B/C. Лезвие плуга 0.63 м, на смену угла остаётся 0.07 м (10 % шага). Замерено: при 1.4 м — 9/9, при 0.7 м — 5/9. От скорости ленты не зависит: доля занятости лезвия = 0.63/0.70 = 90 %. Нужен шаг > ~0.95 м либо другой отводящий орган.
-
Очень тонкие товары проходят под лезвием плуга.
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. Ловушки, на которых уже теряли время
Каждая давала правдоподобный, но неверный результат.
-
Узлы OmniGraph надо УДАЛЯТЬ, а не деактивировать.
SetActive(False)убирает прем из обхода, но собранный граф продолжает работать:ConveyorBeltGraphобнулялsurfaceVelocityза 5 шагов послеplay,DiverterAnimGraphостанавливал таймлайн. -
Невидимость не убирает коллайдер.
capture_roi.pyпрятал снятый товар черезMakeInvisible(), и после двух прогонов захвата в точке осмотра стояло 18 невидимых, но твёрдых предметов. Поток вставал на них «посреди ConveyorTrack_02». Лечитсяcell.clear_capture_parks(), вызывается вprepare(). -
BBoxCacheво время прогона врёт. Он читает авторские трансформы из слоя USD, а физика пишет в Fabric. Отчёт показывал, что все товары стоят в точках выпуска, хотя таймлайн отработал 26 с. Положения читать черезRigidPrim.get_world_poses(). -
play()послеstop()перематывает в начало и сбрасывает физику. Обработчик, «возобновляющий» остановившийся таймлайн, обнуляет весь опыт. -
Заданная частота физики не применяется.
timeStepsPerSecond=120не подействовал, фактический шаг 83.33 мс (60 Гц). Скорости выходили ровно вдвое завышенными. Время брать из таймлайна. -
Файл
runtime/worker_readyостаётся от прошлого запуска и даёт ложную готовность. Проверять работника по PID. -
cloud_from_roiвозвращает ПАРУ (облако товара, облако полотна). Складывание кортежа целиком роняетnp.vstackна разнородных формах. -
Меши
items_flow/уже в каталожном масштабе и уже посажены на z = 0, коллайдеры в них уже есть. Домасштабирование и свои коллайдеры ломают спавн — товары не едут. -
ArUco-метки не видны в ИК (на физическом стенде): типографская краска на 850 нм в значительной мере прозрачна. Позу брать из цветного кадра с ЦВЕТНЫМИ интринсиками и переводить в систему ИК заводскими экстринсиками.
7. Что делать дальше — по приоритету
- Привязать кроп к товару, а не к неподвижной точке осмотра. Это лечит проблему 2 — самую вредную из открытых: из-за неё габариты в движении вдвое хуже статичных.
- Снизить скорость ножа пушера — проблема 4, товар улетает.
- Разнести выпуск товаров по времени либо увеличить шаг — проблема 3.
- Доделать вторую половину фильтрации лево-право (заполнение дырок, edge-aware).
- FoundationStereo на GPU: заблокировано внешне —
onnxruntime-gpuтребует CUDA 13, на сервере 12.8; колёс под CUDA 12 для python 3.12 нет;onnx2torchпадает на динамическомClip; TensorRT не установлен. Нужен либо.pthчерез код репозитория, либо CUDA 13 (установка требует прав root).