0d32f32db0
Замкнутый контур "поток -> 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>
162 lines
6.1 KiB
Python
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())
|