Сортировочная ячейка 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,132 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Carry the driven surface inboard, to where the plow can actually deliver.
|
||||
|
||||
/home/whatevenif/isaacsim/python.sh scripts/extend_transition_decks.py
|
||||
|
||||
Measured with one item and a full 42 deg sweep: the blade imparts a real push (item picks up
|
||||
0.59 m/s and travels 139 mm sideways) but leaves it at **y = 0.261**, and every driven
|
||||
surface past the plow starts at **|y| = 0.380**:
|
||||
|
||||
belt ConveyorTrack_04 x -7.00..-6.00 y -0.450..+0.450
|
||||
deck PlowTransition_C x -7.00..-6.00 y +0.380..+0.698
|
||||
lane C x -8.12..-6.39 y +0.380..+2.112
|
||||
lane B x -7.03..-6.58 y -2.379..-0.380 (no transition deck at all)
|
||||
|
||||
So there is a 119 mm band where a swept item sits on the very lip of the main belt with
|
||||
nothing driving it toward its lane. That is the gap the goods die in - not a hole they fall
|
||||
through, a strip with no traction, right where the blade lets go of them.
|
||||
|
||||
This closes it from the inside: each transition plate is brought in to |y| = 0.20, well
|
||||
short of where the blade releases, and B gets the plate it never had. `plow_sort.DECK_DIR`
|
||||
already drives both toward their lanes, so an item landing here is carried on instead of
|
||||
stopping.
|
||||
|
||||
Plates are static Cubes with collision, coplanar with the belt at z 1.7805, and are made
|
||||
kinematic + surface-driven at load time like every other deck.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
from pxr import Gf, Sdf, Usd, UsdGeom, UsdPhysics
|
||||
|
||||
SCENE = Path(__file__).resolve().parent.parent / "scene" / "plow_cell.usd"
|
||||
TOP_Z = 1.7805
|
||||
THICK = 0.01
|
||||
INBOARD = 0.20 # how far in the driven surface now reaches
|
||||
|
||||
# Lane B is perpendicular, so a straight strip meets it flush.
|
||||
PLATES_STRAIGHT = {"PlowTransition_B": (-7.03, -6.45, -0.380)}
|
||||
|
||||
# Lane C is laid at 45 deg. Its bounding box, x -8.123..-6.391 by y 0.380..2.112, is the
|
||||
# AABB of a rotated rectangle and describes a footprint the belt does not have: the near
|
||||
# side is a single CORNER at (-7.257, 0.380), and the edge runs away from it at 45 deg,
|
||||
#
|
||||
# y = x + 7.637
|
||||
#
|
||||
# so at x -7.03 the lane really starts at y 0.607, and at x -6.39 at y 1.247 - not at 0.380
|
||||
# anywhere except that one corner. A straight plate ending at y 0.380 therefore leaves a
|
||||
# widening wedge of open air, which is the dark triangle in the viewport and where
|
||||
# `bolts_cluster` fell through after the blade had successfully pushed it to y +0.425.
|
||||
#
|
||||
# The C plate is a trapezoid instead: inboard edge at |y| = INBOARD, outer edge ON the
|
||||
# lane's diagonal.
|
||||
LANE_C_EDGE = lambda x: x + 7.637
|
||||
|
||||
|
||||
def _plate(stage, name, x0, x1, y_in, y_out):
|
||||
path = f"/World/{name}"
|
||||
prim = stage.GetPrimAtPath(path)
|
||||
if prim.IsValid():
|
||||
stage.RemovePrim(path)
|
||||
cube = UsdGeom.Cube.Define(stage, path)
|
||||
cube.CreateSizeAttr().Set(2.0) # size 2 so the scale op IS the half-extent
|
||||
cx, cy = (x0 + x1) / 2.0, (y_in + y_out) / 2.0
|
||||
hx, hy = abs(x1 - x0) / 2.0, abs(y_out - y_in) / 2.0
|
||||
xf = UsdGeom.Xformable(cube.GetPrim())
|
||||
xf.ClearXformOpOrder()
|
||||
xf.AddTranslateOp().Set(Gf.Vec3d(cx, cy, TOP_Z - THICK))
|
||||
xf.AddScaleOp().Set(Gf.Vec3f(hx, hy, THICK))
|
||||
cube.CreateDisplayColorAttr().Set([Gf.Vec3f(0.30, 0.31, 0.33)])
|
||||
UsdPhysics.CollisionAPI.Apply(cube.GetPrim())
|
||||
return path, (round(cx - hx, 3), round(cx + hx, 3), round(cy - hy, 3), round(cy + hy, 3))
|
||||
|
||||
|
||||
def _trapezoid(stage, name, quad):
|
||||
"""a thin prism whose top face is the given 4 corners, coplanar with the belt.
|
||||
|
||||
A Cube cannot do this - the plate has to follow a 45 deg edge, so it is authored as an
|
||||
explicit mesh. Given thickness rather than left as a zero-height quad: a flat sheet is a
|
||||
poor collider and goods catch on its rim.
|
||||
"""
|
||||
path = f"/World/{name}"
|
||||
if stage.GetPrimAtPath(path).IsValid():
|
||||
stage.RemovePrim(path)
|
||||
mesh = UsdGeom.Mesh.Define(stage, path)
|
||||
top = [Gf.Vec3f(x, y, TOP_Z) for x, y in quad]
|
||||
bot = [Gf.Vec3f(x, y, TOP_Z - THICK) for x, y in quad]
|
||||
pts = top + bot
|
||||
mesh.CreatePointsAttr().Set(pts)
|
||||
faces, counts = [], []
|
||||
faces += [0, 1, 2, 3]; counts.append(4) # top
|
||||
faces += [7, 6, 5, 4]; counts.append(4) # bottom
|
||||
for i in range(4): # sides
|
||||
j = (i + 1) % 4
|
||||
faces += [i, 4 + i, 4 + j, j]; counts.append(4)
|
||||
mesh.CreateFaceVertexIndicesAttr().Set(faces)
|
||||
mesh.CreateFaceVertexCountsAttr().Set(counts)
|
||||
xs = [p[0] for p in pts]; ys = [p[1] for p in pts]; zs = [p[2] for p in pts]
|
||||
mesh.CreateExtentAttr().Set([Gf.Vec3f(min(xs), min(ys), min(zs)),
|
||||
Gf.Vec3f(max(xs), max(ys), max(zs))])
|
||||
mesh.CreateDisplayColorAttr().Set([Gf.Vec3f(0.30, 0.31, 0.33)])
|
||||
mesh.CreateSubdivisionSchemeAttr().Set("none")
|
||||
UsdPhysics.CollisionAPI.Apply(mesh.GetPrim())
|
||||
UsdPhysics.MeshCollisionAPI.Apply(mesh.GetPrim()).CreateApproximationAttr().Set("convexHull")
|
||||
return path
|
||||
|
||||
|
||||
def main():
|
||||
if not SCENE.exists():
|
||||
sys.exit(f"{SCENE} not found")
|
||||
stage = Usd.Stage.Open(str(SCENE))
|
||||
for name, (x0, x1, y_out) in PLATES_STRAIGHT.items():
|
||||
y_in = INBOARD if y_out > 0 else -INBOARD
|
||||
path, span = _plate(stage, name, x0, x1, y_in, y_out)
|
||||
print(f" {name:20s} straight x[{span[0]:+.3f},{span[1]:+.3f}] "
|
||||
f"y[{span[2]:+.3f},{span[3]:+.3f}]")
|
||||
|
||||
x0, x1 = -7.03, -6.39
|
||||
quad = [(x0, INBOARD), (x1, INBOARD), (x1, LANE_C_EDGE(x1)), (x0, LANE_C_EDGE(x0))]
|
||||
_trapezoid(stage, "PlowTransition_C", quad)
|
||||
print(f" PlowTransition_C trapezoid corners "
|
||||
+ " ".join(f"({a:+.2f},{b:+.2f})" for a, b in quad))
|
||||
print(f" outer edge follows the lane diagonal y = x + 7.637 "
|
||||
f"({LANE_C_EDGE(x0):+.3f} at x={x0}, {LANE_C_EDGE(x1):+.3f} at x={x1})")
|
||||
stage.GetRootLayer().Save()
|
||||
print(f"saved {SCENE}")
|
||||
print(f"driven surface now reaches |y| = {INBOARD}; the blade releases goods at ~0.26")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user