Files
isaac/scripts/run_plow_cell_vision.py
T
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

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)