Сортировочная ячейка 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,126 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Bring the plow arm to a 600 mm sweep width.
|
||||
|
||||
python scripts/narrow_plow.py [--width 0.60]
|
||||
|
||||
The authored arm is 730 mm along its own long axis. That is wider than the 450 mm belt by
|
||||
enough that it overhangs both rails: it clips goods it should have passed and shoulders
|
||||
others off the lane instead of steering them. 600 mm still spans the belt with margin while
|
||||
leaving the lane edges clear.
|
||||
|
||||
Only the scale on `DiverterEnd/Arm/Geom` changes. The hinge, its drive, the limits and the
|
||||
arm's rigid body are untouched, so the kinematics are exactly as authored - the arm is
|
||||
simply shorter.
|
||||
|
||||
Which local axis carries the length is not obvious: Geom is rotated -90 deg about Z and its
|
||||
parent another 180 deg, so the mesh's own X and Y do not map to the world axes you would
|
||||
guess. The script measures instead of assuming.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import shutil
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import numpy as np
|
||||
from pxr import Gf, Usd, UsdGeom
|
||||
|
||||
ROOT = Path(__file__).resolve().parent.parent
|
||||
CELL = ROOT / "scene" / "plow_cell.usd"
|
||||
ARM_GEOM = "/World/Diverters/DiverterEnd/Arm/Geom"
|
||||
ARM = "/World/Diverters/DiverterEnd/Arm"
|
||||
|
||||
|
||||
def arm_length(stage):
|
||||
"""longest principal extent of the arm's mesh points, in world metres"""
|
||||
cache = UsdGeom.XformCache()
|
||||
pts = []
|
||||
for prim in Usd.PrimRange(stage.GetPrimAtPath(ARM)):
|
||||
mesh = UsdGeom.Mesh(prim)
|
||||
if not mesh:
|
||||
continue
|
||||
p = mesh.GetPointsAttr().Get()
|
||||
if not p:
|
||||
continue
|
||||
M = cache.GetLocalToWorldTransform(prim)
|
||||
pts.append(np.array([M.Transform(Gf.Vec3d(*q)) for q in p]))
|
||||
if not pts:
|
||||
return None
|
||||
P = np.vstack(pts)
|
||||
Q = P - P.mean(0)
|
||||
_, _, vt = np.linalg.svd(Q, full_matrices=False)
|
||||
return float(np.ptp(Q @ vt[0]))
|
||||
|
||||
|
||||
def main():
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--width", type=float, default=0.60, help="target sweep width, metres")
|
||||
args = ap.parse_args()
|
||||
|
||||
if not CELL.exists():
|
||||
print(f"{CELL} not found")
|
||||
return 1
|
||||
|
||||
stage = Usd.Stage.Open(str(CELL))
|
||||
geom = stage.GetPrimAtPath(ARM_GEOM)
|
||||
if not geom.IsValid():
|
||||
print(f"{ARM_GEOM} missing - is this plow_cell.usd?")
|
||||
return 1
|
||||
|
||||
before = arm_length(stage)
|
||||
if not before:
|
||||
print("arm carries no mesh points - is assets/plow/ populated?")
|
||||
return 1
|
||||
print(f"arm length now {before*1000:.0f} mm, target {args.width*1000:.0f} mm")
|
||||
|
||||
xf = UsdGeom.Xformable(geom)
|
||||
scale_op = None
|
||||
for op in xf.GetOrderedXformOps():
|
||||
if op.GetOpType() == UsdGeom.XformOp.TypeScale:
|
||||
scale_op = op
|
||||
if scale_op is None:
|
||||
scale_op = xf.AddScaleOp()
|
||||
scale_op.Set(Gf.Vec3f(1, 1, 1))
|
||||
base = Gf.Vec3f(scale_op.Get() or Gf.Vec3f(1, 1, 1))
|
||||
|
||||
# find which local axis the length rides on, by testing rather than reasoning about
|
||||
# the two stacked rotations
|
||||
factor = args.width / before
|
||||
best = None
|
||||
for axis in (0, 1, 2):
|
||||
trial = Gf.Vec3f(base)
|
||||
trial[axis] = base[axis] * factor
|
||||
scale_op.Set(trial)
|
||||
got = arm_length(stage)
|
||||
print(f" scale on local {'XYZ'[axis]} -> {got*1000:.0f} mm")
|
||||
if best is None or abs(got - args.width) < abs(best[1] - args.width):
|
||||
best = (axis, got, trial)
|
||||
axis, got, trial = best
|
||||
scale_op.Set(trial)
|
||||
|
||||
if abs(got - args.width) > 0.005:
|
||||
print(f"closest achievable was {got*1000:.0f} mm on local {'XYZ'[axis]} - "
|
||||
"the arm's length may not lie on a single local axis")
|
||||
return 1
|
||||
|
||||
backup = CELL.with_suffix(".usd.prewidth")
|
||||
if not backup.exists():
|
||||
shutil.copy(CELL, backup)
|
||||
print(f"backup -> {backup.name}")
|
||||
stage.GetRootLayer().Save()
|
||||
|
||||
check = Usd.Stage.Open(str(CELL))
|
||||
final = arm_length(check)
|
||||
cache = UsdGeom.BBoxCache(0, ["default", "render"], useExtentsHint=True)
|
||||
r = cache.ComputeWorldBound(check.GetPrimAtPath(ARM)).ComputeAlignedRange()
|
||||
mn, mx = r.GetMin(), r.GetMax()
|
||||
print(f"\nsaved. arm is now {final*1000:.0f} mm "
|
||||
f"(scale {tuple(round(v,4) for v in trial)} on local {'XYZ'[axis]})")
|
||||
print(f" world AABB x[{mn[0]:.3f}..{mx[0]:.3f}] y[{mn[1]:.3f}..{mx[1]:.3f}] "
|
||||
f"z[{mn[2]:.3f}..{mx[2]:.3f}]")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user