Сортировочная ячейка 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
+122
View File
@@ -0,0 +1,122 @@
#!/usr/bin/env python3
"""Build scene/plow_cell.usd from the authored 90_degree.usd.
python scripts/build_plow_cell.py [path/to/90_degree.usd]
The source is the original authored cell - conveyor art, the Y-split pusher, the plow
(``DiverterEnd``) and its OmniGraph drive script exactly as built. This script does not
regenerate geometry; it only makes the file self-contained inside this repo:
1. **References re-pointed.** The source pulls conveyor art straight off the Omniverse S3
bucket and the plow meshes from its own folder. Both are re-pointed at the repo's
``assets/`` tree so the scene composes offline (``scripts/fetch_assets.py`` fills
``assets/conveyors/``).
2. **Baked drive animation stripped.** The pusher's ``PusherSlide`` carries a long
``targetPosition.timeSamples`` track. Time samples outrank the attribute default, so
anything that tries to *control* that drive - the authored script node or Python - is
overwritten every frame while the timeline runs. The track is dropped; the drive keeps
its authored gains and limits. (The plow's own ``ArmHinge`` is already clean in
90_degree.usd; the earlier fixed.usd bakes it too, hence the pattern matches both.)
The authored ``DiverterAnimGraph`` script node is deliberately kept: open the scene, press
Play, and the cell demonstrates itself the way it was built. ``sim/plow_cell.py`` switches
that graph off when you want to drive the plow from code instead.
"""
from __future__ import annotations
import re
import shutil
import subprocess
import sys
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parent.parent
OUT = ROOT / "scene" / "plow_cell.usd"
DEFAULT_SRC = (Path.home() / "Desktop" / "isaac_sim_project" / "test_isassc" / "90_degree.usd")
S3 = ("https://omniverse-content-production.s3-us-west-2.amazonaws.com/"
"Assets/Isaac/6.0/Isaac/Props/Conveyors/")
# asset path in the source -> path relative to scene/
REFS = {
f"{S3}ConveyorBelt_A06.usd": "../assets/conveyors/ConveyorBelt_A06.usd",
f"{S3}ConveyorBelt_A24.usd": "../assets/conveyors/ConveyorBelt_A24.usd",
"./plow_base.usd": "../assets/plow/plow_base.usd",
"./plow_arm.usd": "../assets/plow/plow_arm.usd",
}
# Baked drive tracks fight every attempt to control a diverter. In 90_degree.usd only the
# pusher's linear drive carries one (the plow's angular drive is already clean), but the
# earlier fixed.usd bakes the plow too - match both so either source builds the same way.
BAKED = re.compile(r"drive:(linear|angular):physics:targetPosition\.timeSamples")
def usdcat(src: Path, dst: Path) -> None:
if not shutil.which("usdcat"):
sys.exit("usdcat not found - it ships with USD / Isaac Sim and is needed to "
"convert the binary .usd to text and back")
subprocess.run(["usdcat", str(src), "-o", str(dst)], check=True)
def strip_baked_track(text: str) -> tuple[str, int]:
"""drop `<drive target>.timeSamples = { ... }` blocks"""
out, skipping, dropped = [], False, 0
for line in text.splitlines(keepends=True):
if not skipping and BAKED.search(line) and line.rstrip().endswith("{"):
skipping, dropped = True, dropped + 1
continue
if skipping:
if line.strip() == "}":
skipping = False
continue
out.append(line)
return "".join(out), dropped
def repoint_refs(text: str) -> tuple[str, dict[str, int]]:
counts = {}
for old, new in REFS.items():
n = text.count(f"@{old}@")
if n:
text = text.replace(f"@{old}@", f"@{new}@")
counts[old] = n
return text, counts
def build(src: Path) -> Path:
if not src.exists():
sys.exit(f"source scene not found: {src}")
OUT.parent.mkdir(parents=True, exist_ok=True)
with tempfile.TemporaryDirectory() as tmp:
flat = Path(tmp) / "src.usda"
usdcat(src, flat)
text = flat.read_text()
text, refs = repoint_refs(text)
text, dropped = strip_baked_track(text)
missing = [k for k, v in refs.items() if v == 0]
if missing:
print("warning: reference not found in source (layout may have changed):")
for m in missing:
print(f" {m}")
edited = Path(tmp) / "edited.usda"
edited.write_text(text)
usdcat(edited, OUT)
print(f"built {OUT.relative_to(ROOT)} from {src}")
for old, new in REFS.items():
print(f" ref {refs[old]}x {Path(old).name:24s} -> {new}")
print(f" drop {dropped}x baked drive targetPosition.timeSamples")
left = re.findall(r"@(https?://[^@]+)@", OUT.read_text(errors="ignore")) if OUT.suffix == ".usda" else []
if left:
print(f" warning: {len(left)} remote reference(s) still present")
return OUT
if __name__ == "__main__":
build(Path(sys.argv[1]) if len(sys.argv) > 1 else DEFAULT_SRC)