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>
159 lines
5.9 KiB
Python
159 lines
5.9 KiB
Python
#!/usr/bin/env python3
|
|
"""Run the plow cell with the full vision stack.
|
|
|
|
./python.sh scripts/run_plow_cell_vision.py # windowed
|
|
./python.sh scripts/run_plow_cell_vision.py --headless
|
|
./python.sh scripts/run_plow_cell_vision.py --no-vision # route on ground truth
|
|
|
|
Items are released on the added infeed belt, measured by CRE-ROI v2b under the camera
|
|
portal, and the ones that come back class D are diverted by the Y-split pusher when they
|
|
break the laser beam. The plow and the authored kinematics are not touched.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def parse_args(argv=None):
|
|
p = argparse.ArgumentParser(description="plow cell + CRE-ROI v2b")
|
|
p.add_argument("--headless", action="store_true")
|
|
p.add_argument("--no-vision", action="store_true",
|
|
help="skip inference and route on ground truth")
|
|
p.add_argument("--speed", type=float, default=None, help="belt speed, m/s")
|
|
p.add_argument("--pitch", type=float, default=None, help="metres between items")
|
|
p.add_argument("--items", default=None, help="comma-separated release order")
|
|
p.add_argument("--log", default=None)
|
|
return p.parse_args(argv)
|
|
|
|
|
|
async def _run(app_utils, args):
|
|
from robozon_sorter import config as C
|
|
from robozon_sorter.sim import plow_vision
|
|
from robozon_sorter.sim.mechanics import Cell
|
|
from robozon_sorter.sim.spawner import AutoFeeder
|
|
|
|
if args.speed:
|
|
C.BELT_SPEED = args.speed
|
|
|
|
stage, info = plow_vision.load(belt_speed=args.speed, script_control=True)
|
|
items = {k: v["zone"] for k, v in info["items"].items()}
|
|
print(f"plow cell ready: {len(items)} items {sorted(set(items.values()))}, "
|
|
f"infeed release at x={info['spawn_x']}")
|
|
|
|
vision = None
|
|
if not args.no_vision:
|
|
from robozon_sorter.cv.pipeline import CreRoiV2b
|
|
vision = CreRoiV2b()
|
|
vision.attach_cameras()
|
|
print("CRE-ROI v2b ready; gate pixels:", vision.gate_px)
|
|
|
|
await app_utils.update_app_async(steps=40)
|
|
cell = Cell(stage, items.keys())
|
|
cell.park_all()
|
|
await app_utils.update_app_async(steps=15)
|
|
|
|
order = [n.strip() for n in args.items.split(",")] if args.items else sorted(items)
|
|
order = [n for n in order if n in items]
|
|
pitch = args.pitch if args.pitch is not None else C.RELEASE_GAP
|
|
|
|
log, seen = [], set()
|
|
route = dict(items) if args.no_vision else {}
|
|
|
|
def on_event(kind, name, payload):
|
|
print(f" {kind:8s} {name:18s} {payload if payload else ''}")
|
|
if kind == "done":
|
|
for rec in log:
|
|
if rec["item"] == name and "outcome" not in rec:
|
|
rec["outcome"] = payload.get("where")
|
|
|
|
feeder = AutoFeeder(cell, order=order, pitch=pitch, route=route,
|
|
on_event=on_event).install()
|
|
|
|
import omni.timeline
|
|
timeline = omni.timeline.get_timeline_interface()
|
|
app_utils.play(commit=True)
|
|
await app_utils.update_app_async(steps=20)
|
|
|
|
print(f"\npitch {pitch} m at {C.BELT_SPEED} m/s\n{'kind':>10} detail")
|
|
for _ in range(400):
|
|
await app_utils.update_app_async(steps=15)
|
|
|
|
# classify each item once, while it sits under the portal
|
|
if vision is not None:
|
|
for name in list(feeder.active):
|
|
if name in seen:
|
|
continue
|
|
x = float(cell.pose(name)[0])
|
|
if abs(x - C.CAM_X) < 0.08:
|
|
was_playing = timeline.is_playing()
|
|
res = vision.measure()
|
|
# Replicator's step stops the timeline; resume or the line freezes
|
|
if was_playing and not timeline.is_playing():
|
|
timeline.play()
|
|
await app_utils.update_app_async(steps=2)
|
|
gt = items[name]
|
|
route[name] = res["cls"]
|
|
seen.add(name)
|
|
print(f" vision {name:18s} pred={res['cls']} gt={gt} "
|
|
f"{'ok' if res['cls'] == gt else 'MISS'} dims={res['dims']} "
|
|
f"K={res['k']:.2f} views={res['views']} cre={res['cre_ms']}ms")
|
|
log.append(dict(item=name, gt=gt, **res))
|
|
|
|
if len(feeder.finished) >= len(order):
|
|
break
|
|
|
|
app_utils.stop()
|
|
await app_utils.update_app_async(steps=15)
|
|
feeder.remove()
|
|
cell.blade_to(C.BLADE_HOME_Y)
|
|
|
|
print(f"\n outcomes: {feeder.finished}")
|
|
expected = {n: ("bin" if items[n] == "D" else "line-end") for n in order}
|
|
wrong = [n for n in expected if feeder.finished.get(n) != expected[n]]
|
|
print(f" expected: {expected}")
|
|
print(" routing matches ground truth" if not wrong else f" differs on: {wrong}")
|
|
graded = [r for r in log if r.get("cls") not in (None, "?")]
|
|
if graded:
|
|
hits = sum(1 for r in graded if r["cls"] == r["gt"])
|
|
cre = [r["cre_ms"] for r in graded if r.get("cre_ms")]
|
|
print(f" vision agreed with ground truth on {hits}/{len(graded)}"
|
|
+ (f", CRE {sum(cre)/len(cre):.0f} ms/item" if cre else ""))
|
|
if args.log:
|
|
Path(args.log).write_text(json.dumps(log, indent=2))
|
|
print(f" log -> {args.log}")
|
|
return log
|
|
|
|
|
|
def main(argv=None):
|
|
args = parse_args(argv)
|
|
try:
|
|
import omni.usd
|
|
inside = omni.usd.get_context().get_stage() is not None
|
|
except Exception:
|
|
inside = False
|
|
|
|
app = None
|
|
if not inside:
|
|
from isaacsim import SimulationApp
|
|
app = SimulationApp({"headless": args.headless, "width": 1600, "height": 900})
|
|
|
|
import asyncio
|
|
import isaacsim.core.experimental.utils.app as app_utils
|
|
loop = asyncio.get_event_loop()
|
|
try:
|
|
return loop.run_until_complete(_run(app_utils, args))
|
|
finally:
|
|
if app is not None:
|
|
app.close()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(0 if main() is not None else 1)
|