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

157 lines
6.0 KiB
Python

#!/usr/bin/env python3
"""Lay the discharge out as a fork: C carries straight on, B branches, plow in the corner.
/home/whatevenif/isaacsim/python.sh scripts/build_fork_v2.py
Design, from the sketch:
main run ──────┬─────────────▶ lane C (straight on, same line as the run)
└───▶ lane B (branches away at 45 deg)
plow sits in this corner
Class C needs no action - it runs straight through, and the blade at rest closes the B
mouth, so C is the default route and the blade only leans on it if it wanders. Class B is
the only case that actuates: the blade swings over, the B mouth opens, and the item drives
into its branch instead of being shoved across a belt.
**How the tracks are actually built** - this is what the first attempt got wrong. Each
ConveyorTrack carries `translate + orient(quaternion) + scale`; there is no rotateZ to
write, so clearing the op order and adding one silently produced a different transform. The
`Belt` child then sits at a fixed local offset of +1.0 along the track's local X, scaled by
the track's own X scale. So:
belt centre = track origin + (local +X in world) * 1.0 * scale_x
Placing a branch therefore means: point the track's local X down the branch, and put the
track origin at the fork apex, which lands the belt centre one length-half down the branch.
Verification uses a **fresh** BBoxCache after every write. Reusing one is what made the
first attempt report "nothing moved" while the geometry underneath had in fact been
scattered.
"""
from __future__ import annotations
import math
import sys
from pathlib import Path
from pxr import Gf, Usd, UsdGeom
SCENE = Path(__file__).resolve().parent.parent / "scene" / "plow_cell.usd"
APEX = Gf.Vec3d(-7.00, 0.0, 0.0) # downstream end of the main run
TRAYS = "/World/PlowContainers"
PLOW = "/World/Diverters/DiverterEnd"
LANE_C = "/World/ConveyorTrack_01" # straight on, 0 deg off the run
LANE_B = "/ConveyorTrack_01" # branches 45 deg toward -Y
C_DEG, B_DEG = 0.0, -45.0
TRAY_AT = 2.60 # how far down each branch its tray sits
def quat_z(deg):
h = math.radians(deg) / 2.0
return Gf.Quatd(math.cos(h), Gf.Vec3d(0, 0, math.sin(h)))
def world_dir(deg):
"""travel direction of a branch `deg` off the -X run"""
a = math.radians(180.0 + deg)
return Gf.Vec3d(math.cos(a), math.sin(a), 0.0)
def place_track(stage, path, deg):
"""point the track down its branch and hang its origin on the apex"""
prim = stage.GetPrimAtPath(path)
if not prim.IsValid():
return False
xf = UsdGeom.Xformable(prim)
for op in xf.GetOrderedXformOps():
n = op.GetOpName()
if n.endswith("translate"):
op.Set(APEX)
elif n.endswith("orient"):
op.Set(quat_z(180.0 + deg)) # local +X onto the branch direction
return True
def move_group(stage, prefix, to_xy):
"""shift a tray so its centre lands on `to_xy`, keeping its parts together"""
cache = UsdGeom.BBoxCache(0, ["default"])
parts = [c for c in stage.GetPrimAtPath(TRAYS).GetChildren()
if c.GetName().startswith(prefix)]
if not parts:
return 0
xs, ys = [], []
for c in parts:
r = cache.ComputeWorldBound(c).ComputeAlignedRange()
xs += [r.GetMin()[0], r.GetMax()[0]]
ys += [r.GetMin()[1], r.GetMax()[1]]
dx = to_xy[0] - (min(xs) + max(xs)) / 2.0
dy = to_xy[1] - (min(ys) + max(ys)) / 2.0
n = 0
for c in parts:
for op in UsdGeom.Xformable(c).GetOrderedXformOps():
if op.GetOpName().endswith("translate"):
t = op.Get()
op.Set(type(t)(t[0] + dx, t[1] + dy, t[2]))
n += 1
break
return n
def report(stage, path, label):
"""measure with a FRESH cache - a reused one reports the state before the write"""
r = UsdGeom.BBoxCache(0, ["default"]).ComputeWorldBound(
stage.GetPrimAtPath(path)).ComputeAlignedRange()
if r.IsEmpty():
print(f" {label:22s} (empty)")
return
print(f" {label:22s} x[{r.GetMin()[0]:+.2f},{r.GetMax()[0]:+.2f}] "
f"y[{r.GetMin()[1]:+.2f},{r.GetMax()[1]:+.2f}] top z={r.GetMax()[2]:.3f}")
def main():
if not SCENE.exists():
sys.exit(f"{SCENE} not found")
stage = Usd.Stage.Open(str(SCENE))
for path, deg, tag in ((LANE_C, C_DEG, "C"), (LANE_B, B_DEG, "B")):
if not place_track(stage, path, deg):
print(f" {path} missing"); continue
d = world_dir(deg)
tray = (APEX[0] + d[0] * TRAY_AT, APEX[1] + d[1] * TRAY_AT)
moved = move_group(stage, f"{tag}_", tray)
print(f"branch {tag}: {deg:+.0f} deg, dir ({d[0]:+.3f},{d[1]:+.3f}), "
f"tray -> ({tray[0]:+.2f},{tray[1]:+.2f}) [{moved} parts]")
# the plow sits in the corner between the two branches
pxf = UsdGeom.Xformable(stage.GetPrimAtPath(PLOW))
for op in pxf.GetOrderedXformOps():
if op.GetOpName().endswith("translate"):
t = op.Get()
op.Set(Gf.Vec3d(APEX[0], APEX[1], t[2]))
break
stage.GetRootLayer().Save()
print("\n--- measured after the write (fresh cache each time) ---")
report(stage, "/World/ConveyorTrack_04/Belt", "main run")
report(stage, f"{LANE_C}/Belt", "lane C (straight)")
report(stage, f"{LANE_B}/Belt", "lane B (45 deg)")
report(stage, f"{PLOW}/Arm", "plow arm")
for tag in ("B", "C"):
cache = UsdGeom.BBoxCache(0, ["default"])
xs, ys = [], []
for c in stage.GetPrimAtPath(TRAYS).GetChildren():
if c.GetName().startswith(f"{tag}_"):
r = cache.ComputeWorldBound(c).ComputeAlignedRange()
xs += [r.GetMin()[0], r.GetMax()[0]]; ys += [r.GetMin()[1], r.GetMax()[1]]
if xs:
print(f" tray {tag} centre "
f"({(min(xs)+max(xs))/2:+.2f},{(min(ys)+max(ys))/2:+.2f})")
print(f"\nsaved {SCENE}")
if __name__ == "__main__":
main()