0d32f32db0
Замкнутый контур "поток -> 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>
92 lines
4.1 KiB
Python
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())
|