Files
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

92 lines
4.1 KiB
Python

"""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())