Сортировочная ячейка 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:
@@ -0,0 +1,207 @@
|
||||
"""Loads the real sorting cell (scene/sorter.usd) and applies the runtime configuration
|
||||
it needs to actually run.
|
||||
|
||||
The scene file is the original build - conveyor art, diverters, camera portal, camera
|
||||
bodies, laser gate and collection bin exactly as authored. Nothing here rebuilds geometry.
|
||||
What this module does is re-apply the handful of runtime settings that USD does not carry
|
||||
and that the cell does not work without; each one is documented where it is applied,
|
||||
because every one of them was a silent failure at some point.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from pxr import Gf, PhysxSchema, UsdGeom, UsdPhysics, UsdShade
|
||||
|
||||
from .. import config as C
|
||||
|
||||
SCENE = C.ROOT / "scene" / "sorter.usd"
|
||||
|
||||
# --- prim paths in the authored scene -------------------------------------------------
|
||||
BELTS = ["/World/ConveyorTrack/Belt", "/World/ConveyorTrack_02/Belt",
|
||||
"/World/ConveyorTrack_03/Belt", "/World/ConveyorTrack_04/Belt",
|
||||
"/World/ConveyorTrack_01/Belt"]
|
||||
SPAWN_BELT = "/World/SortingRig/SpawnBelt"
|
||||
BRANCH = "/World/ConveyorTrack_03/Belt_01" # the branch the pusher feeds
|
||||
BLADE = "/World/Diverters/DiverterY_Split/Pusher"
|
||||
PUSHER_JOINT = "/World/Diverters/DiverterY_Split/PusherSlide"
|
||||
ANIM_GRAPH = "/World/Diverters/DiverterAnimGraph"
|
||||
CAMERA_BODIES = "/World/CameraBodies"
|
||||
ITEMS_ROOT = "/World/Items"
|
||||
RIG = "/RigRS"
|
||||
|
||||
# The blade's parent carries this offset; world_y = PARENT_Y + local_y.
|
||||
BLADE_PARENT_Y = -0.35
|
||||
|
||||
|
||||
def open_scene(usd_path: str | Path | None = None):
|
||||
"""open sorter.usd into the current context"""
|
||||
import omni.usd
|
||||
path = str(usd_path or SCENE)
|
||||
if not Path(path).exists():
|
||||
raise FileNotFoundError(
|
||||
f"{path} not found. The conveyor art it references lives in assets/conveyors/ - "
|
||||
"run scripts/fetch_assets.py if that folder is empty."
|
||||
)
|
||||
omni.usd.get_context().open_stage(path)
|
||||
return omni.usd.get_context().get_stage()
|
||||
|
||||
|
||||
# --------------------------------------------------------------------- runtime config
|
||||
def configure_physics(stage):
|
||||
scene = stage.GetPrimAtPath("/World/PhysicsScene")
|
||||
if not scene.IsValid():
|
||||
scene = UsdPhysics.Scene.Define(stage, "/World/PhysicsScene").GetPrim()
|
||||
UsdPhysics.Scene(scene).CreateGravityMagnitudeAttr().Set(9.81)
|
||||
px = PhysxSchema.PhysxSceneAPI.Apply(scene)
|
||||
# 120 Hz is what the cell was tuned and validated at, together with the 2.5 m/s blade.
|
||||
# Raising it changes the contact response and the pushed item stops landing in the bin,
|
||||
# so treat this number and PUSHER_SPEED as a matched pair.
|
||||
px.CreateTimeStepsPerSecondAttr().Set(120)
|
||||
px.CreateEnableCCDAttr().Set(True)
|
||||
px.CreateSolverTypeAttr().Set("TGS")
|
||||
|
||||
|
||||
def configure_belts(stage, speed=None, grip_path="/World/SortingRig/M_beltPhysics"):
|
||||
"""explicit surface velocities; the authored ConveyorBeltGraphs carry no speed and
|
||||
would only fight these, so they are switched off.
|
||||
|
||||
`grip_path` is where the belt friction material is authored. It defaults to a prim
|
||||
under the sorter's rig; plow_cell.usd has no SortingRig and passes its own path so the
|
||||
scene does not grow an empty one.
|
||||
"""
|
||||
speed = speed if speed is not None else C.BELT_SPEED
|
||||
|
||||
grip = stage.GetPrimAtPath(grip_path)
|
||||
if not grip.IsValid():
|
||||
grip = stage.DefinePrim(grip_path, "Material")
|
||||
pm = UsdPhysics.MaterialAPI.Apply(grip)
|
||||
pm.CreateStaticFrictionAttr().Set(1.1)
|
||||
pm.CreateDynamicFrictionAttr().Set(0.95)
|
||||
pm.CreateRestitutionAttr().Set(0.02)
|
||||
grip_mat = UsdShade.Material(grip)
|
||||
|
||||
def drive(path, vel):
|
||||
prim = stage.GetPrimAtPath(path)
|
||||
if not prim.IsValid():
|
||||
return False
|
||||
if not prim.HasAPI(UsdPhysics.RigidBodyAPI):
|
||||
UsdPhysics.RigidBodyAPI.Apply(prim)
|
||||
UsdPhysics.RigidBodyAPI(prim).CreateKinematicEnabledAttr().Set(True)
|
||||
PhysxSchema.PhysxSurfaceVelocityAPI.Apply(prim)
|
||||
PhysxSchema.PhysxSurfaceVelocityAPI(prim).CreateSurfaceVelocityAttr().Set(Gf.Vec3f(*vel))
|
||||
api = UsdShade.MaterialBindingAPI.Apply(prim)
|
||||
api.Bind(grip_mat, bindingStrength=UsdShade.Tokens.strongerThanDescendants,
|
||||
materialPurpose="physics")
|
||||
return True
|
||||
|
||||
for path in BELTS + [SPAWN_BELT]:
|
||||
drive(path, (-speed, 0, 0))
|
||||
|
||||
# The branch is rotated: its LOCAL X points along world -Y. surfaceVelocity is given
|
||||
# in the body's local frame, so carrying goods toward the bin (+Y) needs (-speed,0,0).
|
||||
# Setting the "obvious" (0,+speed,0) drags them sideways and they sit there.
|
||||
drive(BRANCH, (-speed, 0, 0))
|
||||
|
||||
for track in ["ConveyorTrack", "ConveyorTrack_02", "ConveyorTrack_03",
|
||||
"ConveyorTrack_04", "ConveyorTrack_01"]:
|
||||
for graph in (f"/World/{track}/ConveyorBeltGraph", f"/World/{track}/ConveyorBeltGraph_01"):
|
||||
g = stage.GetPrimAtPath(graph)
|
||||
if g.IsValid():
|
||||
g.SetActive(False)
|
||||
|
||||
|
||||
def configure_pusher(stage):
|
||||
"""the blade is driven kinematically from script.
|
||||
|
||||
Its authored PhysicsPrismaticJoint is unusable at runtime: USD drive-target writes
|
||||
reach PhysX about a second late, so the blade never completes its stroke while the
|
||||
item is still in reach. The joint is disabled and the blade is moved directly.
|
||||
"""
|
||||
blade = stage.GetPrimAtPath(BLADE)
|
||||
if not blade.IsValid():
|
||||
raise RuntimeError(f"{BLADE} missing - is this the right scene?")
|
||||
UsdPhysics.RigidBodyAPI(blade).CreateKinematicEnabledAttr().Set(True)
|
||||
|
||||
joint = stage.GetPrimAtPath(PUSHER_JOINT)
|
||||
if joint.IsValid():
|
||||
joint.GetAttribute("physics:jointEnabled").Set(False)
|
||||
|
||||
graph = stage.GetPrimAtPath(ANIM_GRAPH)
|
||||
if graph.IsValid():
|
||||
graph.SetActive(False) # otherwise it rewrites the diverter targets every tick
|
||||
|
||||
# the blade must sweep through the conveyor rails rather than grind on them
|
||||
filt = UsdPhysics.FilteredPairsAPI.Apply(blade)
|
||||
rel = filt.CreateFilteredPairsRel()
|
||||
have = {str(t) for t in rel.GetTargets()}
|
||||
for path in BELTS + [SPAWN_BELT, BRANCH, "/World/Diverters/DiverterY_Split/Base"]:
|
||||
if stage.GetPrimAtPath(path).IsValid() and path not in have:
|
||||
rel.AddTarget(path)
|
||||
|
||||
# seat the blade just over the belt so flat items cannot slip underneath
|
||||
for op in UsdGeom.Xformable(blade).GetOrderedXformOps():
|
||||
if op.GetOpType() == UsdGeom.XformOp.TypeTranslate:
|
||||
v = op.Get()
|
||||
op.Set(Gf.Vec3d(v[0], C.BLADE_HOME_Y - BLADE_PARENT_Y, -0.135))
|
||||
break
|
||||
|
||||
|
||||
def hide_aim_markers(stage):
|
||||
"""the camera bodies carry cosmetic aim-ray cones that sit right over the inspection
|
||||
point; left visible they dominate the frame and segmentation locks onto them."""
|
||||
n = 0
|
||||
for rig in ["RealSense_D435", "Orbbec_Gemini305", "Orbbec_Gemini345"]:
|
||||
p = stage.GetPrimAtPath(f"{CAMERA_BODIES}/{rig}/AimRay")
|
||||
if p.IsValid():
|
||||
UsdGeom.Imageable(p).MakeInvisible()
|
||||
n += 1
|
||||
return n
|
||||
|
||||
|
||||
def load_test_items(stage, meshes_dir=None):
|
||||
"""add the bundled per-class test meshes as dynamic rigid bodies"""
|
||||
meshes_dir = Path(meshes_dir or C.MESHES)
|
||||
manifest = json.loads((meshes_dir / "manifest.json").read_text())
|
||||
UsdGeom.Xform.Define(stage, ITEMS_ROOT)
|
||||
items = {}
|
||||
for i, (name, meta) in enumerate(sorted(manifest.items())):
|
||||
usd = meshes_dir / f"{name}.usd"
|
||||
if not usd.exists():
|
||||
continue
|
||||
prim = UsdGeom.Xform.Define(stage, f"{ITEMS_ROOT}/{name}").GetPrim()
|
||||
refs = prim.GetReferences()
|
||||
refs.ClearReferences() # idempotent: prepare() may run more than once
|
||||
refs.AddReference(str(usd))
|
||||
xf = UsdGeom.Xformable(prim)
|
||||
xf.ClearXformOpOrder()
|
||||
xf.AddTranslateOp().Set(Gf.Vec3d(9.0 + 1.2 * i, 5.0, 0.4))
|
||||
UsdPhysics.RigidBodyAPI.Apply(prim)
|
||||
# meshes exported from a streaming scene arrive kinematic and hidden - both make
|
||||
# them inert: kinematic ignores gravity and belt friction, hidden shows nothing
|
||||
UsdPhysics.RigidBodyAPI(prim).CreateKinematicEnabledAttr().Set(False)
|
||||
UsdPhysics.MassAPI.Apply(prim).CreateMassAttr().Set(0.6)
|
||||
px = PhysxSchema.PhysxRigidBodyAPI.Apply(prim)
|
||||
px.CreateEnableCCDAttr().Set(True)
|
||||
px.CreateSolverPositionIterationCountAttr().Set(24)
|
||||
px.CreateSleepThresholdAttr().Set(0.0) # a settled item must still be draggable
|
||||
UsdGeom.Imageable(prim).MakeVisible()
|
||||
items[name] = meta
|
||||
return items
|
||||
|
||||
|
||||
def prepare(stage, belt_speed=None, meshes_dir=None):
|
||||
"""everything the authored scene needs before it will run"""
|
||||
configure_physics(stage)
|
||||
configure_belts(stage, belt_speed)
|
||||
configure_pusher(stage)
|
||||
hidden = hide_aim_markers(stage)
|
||||
items = load_test_items(stage, meshes_dir)
|
||||
calib = json.loads((C.CONFIG / "calib.json").read_text())
|
||||
return dict(items=items, calib=calib, aim_markers_hidden=hidden)
|
||||
|
||||
|
||||
def load(usd_path=None, belt_speed=None, meshes_dir=None):
|
||||
stage = open_scene(usd_path)
|
||||
return stage, prepare(stage, belt_speed, meshes_dir)
|
||||
Reference in New Issue
Block a user