Files
isaac/scripts/add_vision_to_plow_cell.py
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

162 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Give scene/plow_cell.usd an infeed belt, the camera portal and the laser gate.
python scripts/add_vision_to_plow_cell.py
Purely additive: the conveyors, the Y-split pusher and the plow are left exactly as
authored, including their drives and the DiverterAnimGraph. Nothing existing is edited,
so the cell's kinematics are untouched.
What gets added
ConveyorTrack_05 a fourth infeed belt, upstream of ConveyorTrack_02. It references the
SAME ConveyorBelt_A06.usd with the same scale as the existing tracks,
so it is the same conveyor with the same textures, not a lookalike.
Goods run -X, so "behind" ConveyorTrack_02 (which spans x -2..0) means
x 0..+2.
CameraMountFrame the portal, camera bodies and the three rectified stereo pairs, copied
CameraBodies verbatim from sorter.usd. They already sit over x=-0.75, which is
/RigRS inside ConveyorTrack_02's belt, so no repositioning is needed.
SortingRig/LaserGate the through-beam gate at the Y-split pusher (x=-3.74).
Re-running is safe: existing prims are replaced rather than duplicated.
"""
from __future__ import annotations
import shutil
import sys
from pathlib import Path
from pxr import Gf, Sdf, Usd, UsdGeom
ROOT = Path(__file__).resolve().parent.parent
CELL = ROOT / "scene" / "plow_cell.usd"
SORTER = ROOT / "scene" / "sorter.usd"
# copied from sorter.usd unchanged - they are already aligned with this belt
FROM_SORTER = [
"/World/CameraMountFrame",
"/World/CameraBodies",
"/RigRS",
"/World/SortingRig", # carries LaserGate (and its materials)
]
# sorter.usd's SortingRig carries a plain box SpawnBelt at x 0..2.6. The real conveyor
# added below occupies the same lane, so the box and its rails/legs are dropped after the
# copy - otherwise two belts sit inside each other. LaserGate, the bin and the materials stay.
DROP_AFTER_COPY = [
"/World/SortingRig/SpawnBelt",
"/World/SortingRig/SpawnRail_p", "/World/SortingRig/SpawnRail_n",
"/World/SortingRig/Spawn_Leg0", "/World/SortingRig/Spawn_Leg1",
"/World/SortingRig/Spawn_Leg2", "/World/SortingRig/Spawn_Leg3",
]
INFEED_PRIM = "/World/ConveyorTrack_05"
INFEED_ASSET = "../assets/conveyors/ConveyorBelt_A06.usd"
# ConveyorTrack_02 sits at translate x=-2 and spans x -2..0, so the asset occupies
# [tx, tx+2]. Upstream of it is therefore tx=0 -> x 0..+2.
INFEED_TRANSLATE = Gf.Vec3d(0.0, 0.0, 0.0)
INFEED_SCALE = Gf.Vec3d(1.0, 0.5, 1.0) # identical to the other tracks
def _drop(layer: Sdf.Layer, path: str):
"""remove a prim spec if present, so the script is idempotent"""
spec = layer.GetPrimAtPath(path)
if not spec:
return False
parent = layer.GetPrimAtPath(str(Sdf.Path(path).GetParentPath())) or layer.pseudoRoot
name = Sdf.Path(path).name
if name in parent.nameChildren:
del parent.nameChildren[name]
return True
return False
def add_infeed(layer: Sdf.Layer):
"""a fourth conveyor upstream of ConveyorTrack_02, same asset and scale"""
_drop(layer, INFEED_PRIM)
spec = Sdf.CreatePrimInLayer(layer, INFEED_PRIM)
spec.specifier = Sdf.SpecifierDef
spec.typeName = "Xform"
spec.referenceList.prependedItems.append(Sdf.Reference(INFEED_ASSET))
for name, value, vtype in (
("xformOp:translate", INFEED_TRANSLATE, Sdf.ValueTypeNames.Double3),
("xformOp:scale", INFEED_SCALE, Sdf.ValueTypeNames.Double3)):
attr = Sdf.AttributeSpec(spec, name, vtype)
attr.default = value
order = Sdf.AttributeSpec(spec, "xformOpOrder", Sdf.ValueTypeNames.TokenArray)
order.default = ["xformOp:translate", "xformOp:scale"]
return INFEED_PRIM
def copy_from_sorter(layer: Sdf.Layer, src: Sdf.Layer):
copied = []
for path in FROM_SORTER:
if not src.GetPrimAtPath(path):
print(f" skip {path} - not in sorter.usd")
continue
_drop(layer, path)
if Sdf.CopySpec(src, Sdf.Path(path), layer, Sdf.Path(path)):
copied.append(path)
return copied
def main():
if not CELL.exists():
print(f"{CELL} not found - build it with scripts/build_plow_cell.py")
return 1
if not SORTER.exists():
print(f"{SORTER} not found - the camera stand is copied from it")
return 1
backup = CELL.with_suffix(".usd.bak")
if backup.exists():
print(f"backup {backup.name} already exists - keeping the pre-vision copy")
else:
shutil.copy(CELL, backup)
print(f"backup -> {backup.name}")
layer = Sdf.Layer.FindOrOpen(str(CELL))
src = Sdf.Layer.FindOrOpen(str(SORTER))
infeed = add_infeed(layer)
print(f"added {infeed} (references {INFEED_ASSET}, translate {tuple(INFEED_TRANSLATE)})")
for path in copy_from_sorter(layer, src):
print(f"copied {path}")
for path in DROP_AFTER_COPY:
if _drop(layer, path):
print(f"dropped {path} (superseded by the real conveyor)")
layer.Save()
print(f"saved {CELL}")
# verify by composing the result
stage = Usd.Stage.Open(str(CELL))
cache = UsdGeom.BBoxCache(0, ["default", "render"], useExtentsHint=True)
print("\nverification:")
ok = True
for path in [f"{INFEED_PRIM}/Belt", "/World/ConveyorTrack_02/Belt",
"/World/CameraMountFrame", "/World/SortingRig/LaserGate"]:
prim = stage.GetPrimAtPath(path)
if not prim.IsValid():
print(f" MISSING {path}")
ok = False
continue
r = cache.ComputeWorldBound(prim).ComputeAlignedRange()
if r.IsEmpty():
print(f" EMPTY {path}")
ok = False
continue
mn, mx = r.GetMin(), r.GetMax()
print(f" ok {path}: x[{mn[0]:7.3f}..{mx[0]:7.3f}] y[{mn[1]:6.3f}..{mx[1]:6.3f}] "
f"top_z={mx[2]:.3f}")
cams = stage.GetPrimAtPath("/RigRS")
n = len(cams.GetChildren()) if cams.IsValid() else 0
print(f" {'ok ' if n == 6 else 'PROBLEM'} /RigRS: {n} cameras")
return 0 if ok and n == 6 else 1
if __name__ == "__main__":
sys.exit(main())