Сортировочная ячейка 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,91 @@
|
||||
"""Through-beams across each lane entry: did the item actually get onto its lane, and at
|
||||
what blade angle and sweep rate.
|
||||
|
||||
The delivery number alone cannot tune the plow. An item that ends on the floor and one that
|
||||
never left the belt both score zero, but they need opposite corrections - the first was
|
||||
pushed too hard, the second not hard enough. A beam at the lane entry separates them: it
|
||||
fires the moment the item crosses onto the lane, so a run yields, per item,
|
||||
|
||||
crossed yes/no - did the push reach the lane at all
|
||||
angle deg - where the blade was at the crossing
|
||||
rate deg/s - how fast it was sweeping at that instant
|
||||
speed m/s - how fast the item was going as it crossed
|
||||
|
||||
which is what the sweep rate is tuned against. A rate that crosses every item but at high
|
||||
speed is throwing them; one that crosses none is too slow.
|
||||
|
||||
The beams are real `raycast_closest` queries, like the gate before the pusher, placed
|
||||
**along the lane entry line** rather than across the belt - the item is travelling sideways
|
||||
here, so the beam has to lie along the direction it is leaving.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
from .. import config as C
|
||||
|
||||
# Beams sit just inside each lane entry, spanning the lane's width in X, so anything pushed
|
||||
# across breaks one. Y is the entry edge after scripts/move_lanes_inboard.py.
|
||||
# Beams sit ON each lane, not at its entry line, so a break means "this item is riding the
|
||||
# lane" rather than "this item touched the boundary". Each is an origin + direction + length,
|
||||
# because lane C is laid at 45 deg and cannot be described by a y value the way B can.
|
||||
#
|
||||
# lane B perpendicular, x -7.03..-6.57, y -2.38..-0.38 -> beam across it at y = -0.80
|
||||
# lane C 45 deg, near edge y = x + 7.637 -> beam across it at y = +0.90,
|
||||
# where the lane occupies roughly x -7.6..-6.7
|
||||
BEAMS = {
|
||||
# lane C: straight run, belt x[-10.00,-8.00] y[-0.45,0.00]; beam across it at x = -8.60
|
||||
"lane_C": dict(o=(-8.60, -0.50, C.BELT_Z + 0.03), d=(0.0, 1.0, 0.0), L=0.55),
|
||||
# lane B: 45 deg band from (-7.84,0.16) to (-9.25,1.57); beam across it 0.7 m in,
|
||||
# so its direction is the lane's perpendicular (0.707, 0.707), not a world axis.
|
||||
"lane_B": dict(o=(-8.53, 0.46, C.BELT_Z + 0.03), d=(0.7071, 0.7071, 0.0), L=0.55),
|
||||
}
|
||||
ITEMS_PREFIX = "/World/Items/"
|
||||
|
||||
|
||||
class LaneBeams:
|
||||
"""crossing detector at each lane entry"""
|
||||
|
||||
def __init__(self, stage, cell, plow=None):
|
||||
self.stage = stage
|
||||
self.cell = cell
|
||||
self.plow = plow
|
||||
self.crossings: dict[str, dict] = {} # item -> first crossing record
|
||||
self._t = 0.0
|
||||
from omni.physx import get_physx_scene_query_interface
|
||||
self._q = get_physx_scene_query_interface()
|
||||
|
||||
def tick(self, dt):
|
||||
self._t += dt
|
||||
|
||||
def _hit(self, b):
|
||||
"""name of whatever breaks this beam, else None"""
|
||||
h = self._q.raycast_closest(list(b["o"]), list(b["d"]), b["L"])
|
||||
if not h or not h.get("hit"):
|
||||
return None
|
||||
path = str(h.get("rigidBody") or h.get("collision") or "")
|
||||
if not path.startswith(ITEMS_PREFIX):
|
||||
return None
|
||||
return path[len(ITEMS_PREFIX):].split("/")[0] or None
|
||||
|
||||
def poll(self, rate=None):
|
||||
"""call each physics step; records the first crossing of each item"""
|
||||
for lane, b in BEAMS.items():
|
||||
name = self._hit(b)
|
||||
if name is None or name in self.crossings:
|
||||
continue
|
||||
try:
|
||||
v = self.cell._rp[name].get_velocities()[0].numpy()[0]
|
||||
speed = float((v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5)
|
||||
except Exception:
|
||||
speed = 0.0
|
||||
self.crossings[name] = dict(
|
||||
item=name, lane=lane, t=round(self._t, 3),
|
||||
angle=round(self.plow.angle, 1) if self.plow else None,
|
||||
commanded=round(self.plow.commanded, 1) if self.plow else None,
|
||||
rate=None if rate is None else round(rate, 1),
|
||||
speed=round(speed, 2))
|
||||
|
||||
def crossed(self, name):
|
||||
return name in self.crossings
|
||||
|
||||
def report(self):
|
||||
return list(self.crossings.values())
|
||||
Reference in New Issue
Block a user