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

130 lines
5.2 KiB
Python

#!/usr/bin/env python3
"""Rebuild the discharge as a Y fork with the plow at its apex.
/home/whatevenif/isaacsim/python.sh scripts/build_y_fork.py
The layout so far was a T: lane B perpendicular, lane C at 45 deg, meeting the main run at
different x, with the blade out in the middle of the belt trying to shove goods 400 mm
sideways onto them. Every failure this session came from that - the dead zone, the wedges,
goods stalling on the lip - because a push was being asked to do the job of a route.
A fork does not need the push. Both branches leave one apex, the blade sits in it as a
railway point, and goods **drive** into their branch:
rest (class C) blade closes the B mouth -> everything runs straight on to C
B arrives blade swings over -> the B mouth opens and takes it
Geometry, all from the apex at the downstream end of the main run:
apex x -7.00, y 0
branch C 15 deg up from the run (travel -0.966, +0.259)
branch B 35 deg down from the run (travel -0.819, -0.574)
Each track is placed by measurement, not by assumption: the script reads where the belt slab
currently sits relative to its own prim origin, then sets the transform so the slab's near
end lands on the apex pointing along its branch. The trays follow their branches.
"""
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 = (-7.00, 0.0)
TRAYS = "/World/PlowContainers"
# track prim -> (branch angle from the -X run, tray prefix, tray distance along the branch)
BRANCHES = {
"/World/ConveyorTrack_01": dict(deg=+15.0, tray="C_", tray_at=2.30, name="C"),
"/ConveyorTrack_01": dict(deg=-35.0, tray="B_", tray_at=2.30, name="B"),
}
def _dir(deg):
"""world travel direction of a branch `deg` off the -X run"""
a = math.radians(180.0 - deg) # -X is 180 deg; +deg swings toward +Y
return math.cos(a), math.sin(a)
def _belt_of(stage, track):
for p in (f"{track}/Belt", f"{track}/Belt_01"):
if stage.GetPrimAtPath(p).IsValid():
return p
return None
def _set_xform(stage, path, tx, ty, tz, rot_deg):
xf = UsdGeom.Xformable(stage.GetPrimAtPath(path))
xf.ClearXformOpOrder()
xf.AddTranslateOp().Set(Gf.Vec3d(tx, ty, tz))
xf.AddRotateZOp().Set(float(rot_deg))
def main():
if not SCENE.exists():
sys.exit(f"{SCENE} not found")
stage = Usd.Stage.Open(str(SCENE))
bb = UsdGeom.BBoxCache(0, ["default"])
xc = UsdGeom.XformCache()
for track, b in BRANCHES.items():
prim = stage.GetPrimAtPath(track)
if not prim.IsValid():
print(f" {track} missing"); continue
belt = _belt_of(stage, track)
if belt is None:
print(f" {track} has no Belt"); continue
# where the slab sits now, relative to this track's own origin
org = xc.GetLocalToWorldTransform(prim).ExtractTranslation()
r = bb.ComputeWorldBound(stage.GetPrimAtPath(belt)).ComputeAlignedRange()
span_x, span_y = r.GetMax()[0] - r.GetMin()[0], r.GetMax()[1] - r.GetMin()[1]
length = max(span_x, span_y)
top_z = r.GetMax()[2]
# the slab's centre offset from the origin, in the track's own frame
cx = (r.GetMin()[0] + r.GetMax()[0]) / 2.0 - org[0]
cy = (r.GetMin()[1] + r.GetMax()[1]) / 2.0 - org[1]
off = math.hypot(cx, cy)
dx, dy = _dir(b["deg"])
# put the slab centre half a length down the branch from the apex
tx = APEX[0] + dx * (length / 2.0) - (dx * off - dx * off)
ty = APEX[1] + dy * (length / 2.0)
_set_xform(stage, track, tx - cx, ty - cy, org[2], 180.0 - b["deg"])
r2 = bb.ComputeWorldBound(stage.GetPrimAtPath(belt)).ComputeAlignedRange()
print(f" branch {b['name']} {b['deg']:+.0f} deg dir ({dx:+.3f},{dy:+.3f}) "
f"length {length:.2f} m")
print(f" slab now x[{r2.GetMin()[0]:+.2f},{r2.GetMax()[0]:+.2f}] "
f"y[{r2.GetMin()[1]:+.2f},{r2.GetMax()[1]:+.2f}] top z={r2.GetMax()[2]:.3f}")
# the tray rides to the end of its branch
tex, tey = APEX[0] + dx * b["tray_at"], APEX[1] + dy * b["tray_at"]
moved = 0
for c in stage.GetPrimAtPath(TRAYS).GetChildren():
if not c.GetName().startswith(b["tray"]):
continue
cr = bb.ComputeWorldBound(c).ComputeAlignedRange()
ccx = (cr.GetMin()[0] + cr.GetMax()[0]) / 2.0
ccy = (cr.GetMin()[1] + cr.GetMax()[1]) / 2.0
cxf = UsdGeom.Xformable(c)
for op in cxf.GetOrderedXformOps():
if op.GetOpType() == UsdGeom.XformOp.TypeTranslate:
t = op.Get()
op.Set(type(t)(t[0] + (tex - ccx), t[1] + (tey - ccy), t[2]))
moved += 1
break
print(f" tray {b['name']} -> ({tex:+.2f},{tey:+.2f}) {moved} parts moved")
stage.GetRootLayer().Save()
print(f"saved {SCENE}")
print("NOTE: the blade's rest position is now 'B closed', not 0 - re-measure which sign")
print(" closes B before running a sort.")
if __name__ == "__main__":
main()