Сортировочная ячейка 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,438 @@
|
||||
"""Controlled sorting test over the whole item library, with per-item kinematics.
|
||||
|
||||
isaacsim_send.py --context test --file scripts/test_sorting_run.py \
|
||||
--args-json '{"vision": true, "preset": "bright", "repeats": 2}'
|
||||
|
||||
What it records, per dispatched item:
|
||||
|
||||
* **dispatch** when it was released and with what ground-truth class
|
||||
* **detection** predicted class, dimensions, roundness, views, CRE time
|
||||
* **kinematics** at the moment the item is level with the plow: the commanded angle, the
|
||||
angle the arm had actually reached, and the arm's **angular rate** in deg/s. Commanded
|
||||
and reached are different numbers - the drive is compliant - and a blade that is still
|
||||
travelling when the item arrives deflects it differently from one that has settled.
|
||||
* **outcome** where it came to rest: tray B, tray C, the D bin, a lane, the line, or the
|
||||
floor, plus the resting pose and whether it matches the tray its class maps to.
|
||||
|
||||
Two metric blocks are reported separately, because they fail independently: classification
|
||||
(what the vision stack decided) and delivery (where the mechanics actually put it). An item
|
||||
can be classified perfectly and still be left on the line, and the run is only useful if
|
||||
those two are not conflated.
|
||||
|
||||
Lighting preset and the floor come from `sim/staging`, so a run can be repeated under
|
||||
`bright` / `dim` / `harsh` to see how much of the classification error is illumination.
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
from collections import Counter, defaultdict
|
||||
|
||||
REPO = "/home/dasha/robozon-sorter"
|
||||
if REPO not in sys.path:
|
||||
sys.path.insert(0, REPO)
|
||||
import importlib
|
||||
|
||||
# The live Isaac process keeps every module it has ever imported, so an edited
|
||||
# robozon_sorter/ on disk is invisible to a second run. Dropping the package is not enough
|
||||
# on its own: a module file that did not exist when the directory was first scanned stays
|
||||
# invisible until the import finder's cached listing is thrown away too.
|
||||
for _m in [k for k in list(sys.modules) if k.startswith("robozon_sorter")]:
|
||||
del sys.modules[_m]
|
||||
importlib.invalidate_caches()
|
||||
|
||||
import omni.timeline
|
||||
import isaacsim.core.experimental.utils.app as app_utils
|
||||
|
||||
from robozon_sorter import config as C
|
||||
from robozon_sorter.sim import lane_beams, plow_sort, plow_vision, staging
|
||||
from robozon_sorter.sim.mechanics import Cell
|
||||
from robozon_sorter.sim.spawner import AutoFeeder
|
||||
|
||||
USE_VISION = bool(globals().get("vision", True))
|
||||
PRESET = globals().get("preset", "bright")
|
||||
REPEATS = int(globals().get("repeats", 1))
|
||||
PITCH = float(globals().get("pitch", 2.5))
|
||||
SPEED = float(globals().get("speed", 1.0))
|
||||
LIMIT = int(globals().get("limit", 0)) # 0 = whole library
|
||||
CLASSES_ONLY = set(str(globals().get("classes", "")).upper()) or None
|
||||
# Explicit dispatch list, in order, repeats allowed. `limit`/`classes` cannot express
|
||||
# "these exact items, plus an even 10/10/10 of the rest" when a class has fewer than 10
|
||||
# unique members - the only way to balance is to send some of them twice.
|
||||
ONLY = [n for n in str(globals().get("only", "")).split(",") if n.strip()]
|
||||
# Items to score SEPARATELY as well as in the overall figures.
|
||||
FOCUS = [n for n in str(globals().get("focus", "")).split(",") if n.strip()]
|
||||
BUDGET = float(globals().get("max_seconds", 240.0))
|
||||
TRACK_END_X = float(globals().get("track_end_x", -11.5)) # past both trays
|
||||
ITEMS_DIR = globals().get("items_dir", f"{REPO}/assets/items")
|
||||
OUT = globals().get("out", f"{REPO}/runs/test_sorting_{PRESET}.json")
|
||||
|
||||
EXPECT = {"D": "bin", "B": "container_B", "C": "container_C"}
|
||||
CLASSES = ("B", "C", "D")
|
||||
|
||||
# ---------------------------------------------------------------- scene
|
||||
C.BELT_SPEED = SPEED
|
||||
stage, info = plow_vision.load(belt_speed=SPEED, script_control=True,
|
||||
meshes_dir=ITEMS_DIR)
|
||||
staged = staging.stage_cell(stage, preset=PRESET, floor=True)
|
||||
plow_sort.keep_lanes_active(stage)
|
||||
lanes = plow_sort.configure_lanes(stage, SPEED)
|
||||
opened = plow_sort.open_junction(stage)
|
||||
|
||||
items = {k: v["zone"] for k, v in info["items"].items()}
|
||||
gt_dims = {k: v.get("gt_dims_mm") for k, v in info["items"].items()}
|
||||
print(f"library {len(items)} items {dict(Counter(items.values()))} | light={PRESET} "
|
||||
f"| floor={'yes' if staged.get('floor') else 'no'} | lanes={len(lanes)} "
|
||||
f"| junction opened={len(opened)}")
|
||||
|
||||
vision = None
|
||||
if USE_VISION:
|
||||
# The streaming launcher starts Kit WITHOUT the user site-packages, so ultralytics and
|
||||
# torch installed under ~/.local are invisible to the running app even though
|
||||
# `python.sh` imports them fine - it is the same interpreter (3.12.13), just a
|
||||
# different sys.path. Appending (not prepending) leaves Kit's own bundled copies first.
|
||||
for _sp in ("/home/dasha/.local/lib/python3.12/site-packages",):
|
||||
if os.path.isdir(_sp) and _sp not in sys.path:
|
||||
sys.path.append(_sp)
|
||||
from robozon_sorter.cv.pipeline import CreRoiV2b
|
||||
vision = CreRoiV2b()
|
||||
vision.attach_cameras()
|
||||
_w = await vision.warmup()
|
||||
print(f"CRE-ROI v2b attached | прогрев камер: {'ок' if _w['ok'] else 'НЕ УДАЛСЯ'} "
|
||||
f"за {_w['attempts']} подход(а), самый тёмный глаз max={_w['darkest_eye_max']}")
|
||||
if not _w["ok"]:
|
||||
print(" ВНИМАНИЕ: камеры всё ещё отдают чёрное - классификация будет пустой")
|
||||
|
||||
await app_utils.update_app_async(steps=40)
|
||||
# Aim the viewport at the plow before anything else. The default Persp framing tries to
|
||||
# fit the WHOLE stage, and the stage contains the parked queue off at x +37 - so the cell
|
||||
# ends up a few pixels wide and the stream looks black with only the emissive laser stripe
|
||||
# in it. That is what "renders wrong" was: aim, not lighting.
|
||||
try:
|
||||
from isaacsim.core.rendering_manager import ViewportManager
|
||||
ViewportManager.set_camera_view("/OmniverseKit_Persp", eye=[-4.5, -5.0, 5.0],
|
||||
target=[-6.5, 0.0, 1.8])
|
||||
except Exception as _e:
|
||||
print(" (камеру навести не удалось:", _e, ")")
|
||||
|
||||
cell = Cell(stage, items.keys())
|
||||
cell.park_all()
|
||||
await app_utils.update_app_async(steps=15)
|
||||
|
||||
base_order = sorted(items)
|
||||
# Optional class filter, e.g. classes="BC" runs only the B and C items. Without it a small
|
||||
# `limit` just takes the first N alphabetically, which can miss a whole class: limit=8 gave
|
||||
# B=4 D=4 and not one C, so the C route went untested.
|
||||
if ONLY:
|
||||
missing = [n for n in ONLY if n not in items]
|
||||
if missing:
|
||||
print(f" ВНИМАНИЕ: нет в библиотеке: {missing}")
|
||||
base_order = [n.strip() for n in ONLY if n.strip() in items]
|
||||
elif CLASSES_ONLY:
|
||||
base_order = [n for n in base_order if items[n] in CLASSES_ONLY]
|
||||
if LIMIT:
|
||||
base_order = base_order[:LIMIT]
|
||||
order = base_order * max(1, REPEATS) # repeat the library to reach a dispatch count
|
||||
route, classes = ({}, {}) if USE_VISION else (dict(items), dict(items))
|
||||
sorter = plow_sort.PlowSorter(stage, cell, classes, plow_sort.calibrate_mapping())
|
||||
BEAMS = lane_beams.LaneBeams(stage, cell, plow=sorter.plow)
|
||||
print(f"dispatching {len(order)} ({len(base_order)} unique x{max(1, REPEATS)}) | "
|
||||
f"mapping {sorter.mapping} | pitch {PITCH} m @ {SPEED} m/s")
|
||||
|
||||
# ---------------------------------------------------------------- logging
|
||||
def blank(name, pas):
|
||||
return dict(item=name, pass_no=pas, gt=items[name], gt_dims=gt_dims.get(name),
|
||||
released_t=None, pred=None, dims=None, k=None, views=None, cre_ms=None,
|
||||
sensed=False, commanded=None,
|
||||
arm_at_plow=None, rate_at_plow=None, arm_max_rate=0.0,
|
||||
max_speed=0.0, blowup=None, trace=[],
|
||||
contact=[], contact_first=None, contact_last=None,
|
||||
outcome=None, expected=EXPECT.get(items[name]), delivered=None,
|
||||
final=None)
|
||||
|
||||
pas = defaultdict(int)
|
||||
rec = {} # name -> record for the pass currently on the line
|
||||
done_records = []
|
||||
events = []
|
||||
t_sim = 0.0
|
||||
|
||||
DECIMATE = 8
|
||||
BLOWUP_MS = 5.0
|
||||
_tick = 0
|
||||
_prev_angle = 0.0
|
||||
|
||||
|
||||
def on_event(kind, name, payload):
|
||||
events.append(dict(t=round(t_sim, 2), kind=kind, item=name, payload=str(payload)))
|
||||
if kind in ("release", "divert", "error"):
|
||||
print(f" {kind:8s} {name:20s} {payload if payload else ''}")
|
||||
|
||||
|
||||
feeder = AutoFeeder(cell, order=order, pitch=PITCH, route=route, on_event=on_event)
|
||||
|
||||
|
||||
def _speed(name):
|
||||
try:
|
||||
v = cell._rp[name].get_velocities()[0].numpy()[0]
|
||||
return float((v[0] ** 2 + v[1] ** 2 + v[2] ** 2) ** 0.5)
|
||||
except Exception:
|
||||
return 0.0
|
||||
|
||||
|
||||
def _step(dt):
|
||||
"""service the plow and sample kinematics as goods cross it"""
|
||||
global t_sim, _tick, _prev_angle
|
||||
t_sim += dt
|
||||
_tick += 1
|
||||
try:
|
||||
sorter.update(dt)
|
||||
BEAMS.tick(dt)
|
||||
BEAMS.poll(rate=C.PLOW_SWEEP_RATE)
|
||||
arm = sorter.plow.angle
|
||||
rate = (arm - _prev_angle) / dt if dt > 0 else 0.0 # deg/s, measured not commanded
|
||||
_prev_angle = arm
|
||||
for n in list(feeder.active):
|
||||
r = rec.get(n)
|
||||
if r is None:
|
||||
continue
|
||||
p = cell.pose(n)
|
||||
x, y, z = float(p[0]), float(p[1]), float(p[2])
|
||||
if x >= -5.6:
|
||||
continue
|
||||
spd = _speed(n)
|
||||
r["max_speed"] = max(r["max_speed"], round(spd, 2))
|
||||
r["arm_max_rate"] = max(r["arm_max_rate"], round(abs(rate), 1))
|
||||
if spd > BLOWUP_MS and r["blowup"] is None:
|
||||
r["blowup"] = dict(t=round(t_sim, 2), x=round(x, 3), y=round(y, 3),
|
||||
z=round(z, 3), speed=round(spd, 1),
|
||||
arm=round(arm, 1), rate=round(rate, 1))
|
||||
if _tick % DECIMATE == 0 and len(r["trace"]) < 50:
|
||||
r["trace"].append(dict(t=round(t_sim, 2), x=round(x, 3), y=round(y, 3),
|
||||
z=round(z, 3), v=round(spd, 2),
|
||||
cmd=round(sorter.plow.commanded, 1),
|
||||
arm=round(arm, 1), rate=round(rate, 1)))
|
||||
if n in sorter.decided and not r["sensed"]:
|
||||
r["sensed"] = True
|
||||
r["commanded"] = round(sorter.decided[n], 1)
|
||||
# the instant the item is level with the plow: this is the state that decides
|
||||
if abs(x - C.PLOW_POS[0]) < 0.25 and r["arm_at_plow"] is None:
|
||||
r["arm_at_plow"] = round(arm, 1)
|
||||
r["rate_at_plow"] = round(rate, 1)
|
||||
# CONTACT WINDOW: while the item is inside the arm's sweep radius, record how
|
||||
# the blade is actually turning. This is what says whether it leaned the item
|
||||
# over at tip speed or arrived as a hit - a single sample at the plow centre
|
||||
# cannot tell those apart.
|
||||
reach = (x - C.PLOW_POS[0]) ** 2 + (y - C.PLOW_POS[1]) ** 2
|
||||
if reach < (C.PLOW_ARM_LEN + 0.10) ** 2:
|
||||
if r["contact_first"] is None:
|
||||
r["contact_first"] = dict(t=round(t_sim, 2), x=round(x, 3),
|
||||
y=round(y, 3), arm=round(arm, 1),
|
||||
rate=round(rate, 1), v=round(spd, 2))
|
||||
if len(r["contact"]) < 40:
|
||||
r["contact"].append(dict(t=round(t_sim, 2), y=round(y, 3),
|
||||
arm=round(arm, 1), rate=round(rate, 1),
|
||||
v=round(spd, 2)))
|
||||
r["contact_last"] = dict(t=round(t_sim, 2), y=round(y, 3),
|
||||
arm=round(arm, 1), v=round(spd, 2))
|
||||
except Exception as exc:
|
||||
events.append(dict(t=round(t_sim, 2), kind="step-error", item="", payload=repr(exc)))
|
||||
|
||||
|
||||
from omni.physx import get_physx_interface
|
||||
sub = get_physx_interface().subscribe_physics_step_events(_step)
|
||||
feeder.install()
|
||||
|
||||
timeline = omni.timeline.get_timeline_interface()
|
||||
app_utils.play(commit=True)
|
||||
await app_utils.update_app_async(steps=20)
|
||||
|
||||
# ---------------------------------------------------------------- run
|
||||
seen, settled = set(), {}
|
||||
t0 = time.time()
|
||||
while time.time() - t0 < BUDGET:
|
||||
await app_utils.update_app_async(steps=15)
|
||||
|
||||
for n in feeder.active: # open a record when an item is released
|
||||
if n not in rec:
|
||||
pas[n] += 1
|
||||
rec[n] = blank(n, pas[n])
|
||||
rec[n]["released_t"] = round(t_sim, 2)
|
||||
|
||||
if vision is not None:
|
||||
for name in list(feeder.active):
|
||||
if name in seen or name not in rec:
|
||||
continue
|
||||
if abs(float(cell.pose(name)[0]) - C.CAM_X) < 0.10:
|
||||
was = timeline.is_playing()
|
||||
res = vision.measure()
|
||||
if was and not timeline.is_playing():
|
||||
timeline.play()
|
||||
await app_utils.update_app_async(steps=2)
|
||||
seen.add(name)
|
||||
r = rec[name]
|
||||
r.update(pred=res.get("cls"), dims=res.get("dims"),
|
||||
k=round(res.get("k", 0.0), 3), views=res.get("views"),
|
||||
cre_ms=res.get("cre_ms"))
|
||||
route[name] = res.get("cls")
|
||||
sorter.classes[name] = res.get("cls")
|
||||
|
||||
for n in list(rec): # freeze an outcome once the item stops
|
||||
if n in settled:
|
||||
continue
|
||||
where = sorter.lane_of(n)
|
||||
if where == "line" and cell.where(n) == "bin":
|
||||
where = "bin"
|
||||
p = cell.pose(n)
|
||||
resting = where.startswith("container") or where in ("bin", "floor")
|
||||
# Freeze only once the item is genuinely done. The old cutoff was MAIN_X0 + 0.35 =
|
||||
# -7.65, which is the fork apex - every item was declared "line-end" at full 0.80 m/s
|
||||
# the instant it entered its branch, so no B or C delivery could ever be observed.
|
||||
if resting or (where == "line" and float(p[0]) < TRACK_END_X):
|
||||
settled[n] = where if resting else "line-end"
|
||||
r = rec.pop(n)
|
||||
r["outcome"] = settled[n]
|
||||
r["final"] = [round(float(v), 3) for v in p[:3]]
|
||||
r["delivered"] = (r["outcome"] == r["expected"])
|
||||
done_records.append(r)
|
||||
seen.discard(n)
|
||||
settled.pop(n, None)
|
||||
if len(done_records) >= len(order):
|
||||
break
|
||||
|
||||
for n, r in list(rec.items()): # whatever is still on the line at the end
|
||||
p = cell.pose(n)
|
||||
r["outcome"] = sorter.lane_of(n)
|
||||
r["final"] = [round(float(v), 3) for v in p[:3]]
|
||||
r["delivered"] = (r["outcome"] == r["expected"])
|
||||
done_records.append(r)
|
||||
|
||||
app_utils.stop()
|
||||
await app_utils.update_app_async(steps=10)
|
||||
sub = None
|
||||
feeder.remove()
|
||||
|
||||
# ---------------------------------------------------------------- metrics
|
||||
print(f"\n===== DISPATCHED {len(done_records)} =====")
|
||||
print(f"{'item':22s} {'gt':2s} {'pred':4s} {'outcome':13s} {'want':13s} "
|
||||
f"{'arm':>6s} {'rate':>8s} {'vmax':>6s}")
|
||||
for r in done_records:
|
||||
print(f"{r['item']:22s} {r['gt']:2s} {str(r['pred'] or '-'):4s} "
|
||||
f"{str(r['outcome']):13s} {str(r['expected']):13s} "
|
||||
f"{str(r['arm_at_plow']):>6s} {str(r['rate_at_plow']):>8s} "
|
||||
f"{r['max_speed']:>6.1f} {'OK' if r['delivered'] else ''}")
|
||||
|
||||
# --- classification -------------------------------------------------------
|
||||
graded = [r for r in done_records if r["pred"] in CLASSES]
|
||||
print("\n===== CLASSIFICATION (CV) =====")
|
||||
if graded:
|
||||
conf = {a: Counter() for a in CLASSES}
|
||||
for r in graded:
|
||||
conf[r["gt"]][r["pred"]] += 1
|
||||
hits = sum(conf[a][a] for a in CLASSES)
|
||||
print(f" accuracy {hits}/{len(graded)} = {hits / len(graded):.2f}")
|
||||
print(" confusion (rows GT, cols pred): " + " ".join(CLASSES))
|
||||
for a in CLASSES:
|
||||
print(f" {a}: " + " ".join(f"{conf[a][b]:3d}" for b in CLASSES))
|
||||
for a in CLASSES:
|
||||
tp = conf[a][a]
|
||||
fp = sum(conf[g][a] for g in CLASSES) - tp
|
||||
fn = sum(conf[a].values()) - tp
|
||||
pr = tp / (tp + fp) if tp + fp else 0.0
|
||||
rc = tp / (tp + fn) if tp + fn else 0.0
|
||||
f1 = 2 * pr * rc / (pr + rc) if pr + rc else 0.0
|
||||
print(f" {a}: precision {pr:.2f} recall {rc:.2f} F1 {f1:.2f} (n={tp + fn})")
|
||||
cre = [r["cre_ms"] for r in graded if r.get("cre_ms")]
|
||||
if cre:
|
||||
print(f" CRE {sum(cre) / len(cre):.0f} ms/item over {len(cre)}")
|
||||
|
||||
if FOCUS:
|
||||
fset = {n.strip() for n in FOCUS}
|
||||
fg = [r for r in graded if r["item"] in fset]
|
||||
print(f"\n ----- ОТДЕЛЬНО ПО НАЗВАННЫМ ТОВАРАМ ({len(fg)} из {len(fset)}) -----")
|
||||
print(f" {'товар':<20} {'GT':<3} {'пред':<5} {'дim пред, мм':<20} {'GT дим, мм':<20} {'k':<6} верно")
|
||||
okn = 0
|
||||
for r in sorted(fg, key=lambda r: r["item"]):
|
||||
good = r["pred"] == r["gt"]
|
||||
okn += bool(good)
|
||||
dp = "x".join(str(int(x)) for x in (r.get("dims") or [])) or "-"
|
||||
dg = "x".join(str(int(x)) for x in (r.get("gt_dims") or [])) or "-"
|
||||
print(f" {r['item']:<20} {r['gt']:<3} {str(r['pred']):<5} {dp:<20} {dg:<20} "
|
||||
f"{(r.get('k') or 0):<6.3f} {'да' if good else 'НЕТ'}")
|
||||
if fg:
|
||||
print(f" точность по названным: {okn}/{len(fg)} = {okn / len(fg):.2f}")
|
||||
miss = sorted(fset - {r["item"] for r in fg})
|
||||
if miss:
|
||||
print(f" не получили предсказания: {miss}")
|
||||
else:
|
||||
print(" no vision this run")
|
||||
|
||||
# --- delivery -------------------------------------------------------------
|
||||
if sorter.contact is not None:
|
||||
rep = sorter.contact.report()
|
||||
print("\n===== PLOW CONTACT SENSOR =====")
|
||||
print(f" {len(rep['touches'])} items touched the blade")
|
||||
print(f" {'item':22s} {'cls':4s} {'angle@touch':>12s} {'range':>14s} {'dur s':>7s}")
|
||||
for t in rep["touches"]:
|
||||
print(f" {t['item']:22s} {str(t['cls']):4s} {str(t['angle_at_touch']):>12s} "
|
||||
f"{str(t['angle_min']) + '..' + str(t['angle_max']):>14s} "
|
||||
f"{str(t['duration']):>7s}"
|
||||
+ ("" if t["classified"] else " UNCLASSIFIED - not steered"))
|
||||
|
||||
rep = BEAMS.report()
|
||||
cf = sorted(getattr(sorter, "conflicts", set()))
|
||||
print("\n===== КОНФЛИКТЫ ОЧЕРЕДИ ПЛУГА =====")
|
||||
if not cf:
|
||||
print(" нет: в зоне лезвия ни разу не оказалось двух классов одновременно")
|
||||
else:
|
||||
print(f" {len(cf)} товар(ов) делили зону лезвия с товаром ДРУГОГО класса.")
|
||||
print(" Один нож не может держать два угла сразу - это предел подачи, не сбой:")
|
||||
print(" " + ", ".join(cf))
|
||||
|
||||
print("\n===== ЛАЗЕР ПЕРЕД ПЛУГОМ (предустановка угла) =====")
|
||||
gl = getattr(sorter, "gate_log", [])
|
||||
if not gl:
|
||||
print(" створ не сработал ни разу")
|
||||
else:
|
||||
print(f" сработал {len(gl)} раз | створ x={plow_sort.SENSE_X}, лезвие с x=-7.32")
|
||||
print(f" {'товар':<20} {'класс':<6} {'угол':>7} {'x на срабатывании':>18}")
|
||||
for g in gl:
|
||||
print(f" {g['item']:<20} {str(g['cls']):<6} {g['angle']:>+7.1f} {g['x']:>18.2f}")
|
||||
|
||||
print("\n===== ЛАЗЕРНЫЕ ДАТЧИКИ НА ЛЕНТАХ B/C =====")
|
||||
print(f" доехали до ленты: {len(rep)} из {len(done_records)} отправленных")
|
||||
for c in rep:
|
||||
print(f" {c['item']:20s} -> {c['lane']:7s} t={c['t']:6.2f}s угол ножа={c['angle']} v={c['speed']}")
|
||||
if not rep:
|
||||
print(" ни один товар не доехал ни до одной ленты")
|
||||
|
||||
print("\n===== DELIVERY (mechanics) =====")
|
||||
ok = [r for r in done_records if r["delivered"]]
|
||||
print(f" delivered {len(ok)}/{len(done_records)} = {len(ok) / max(len(done_records), 1):.2f}")
|
||||
per_class = defaultdict(lambda: [0, 0])
|
||||
for r in done_records:
|
||||
per_class[r["gt"]][1] += 1
|
||||
per_class[r["gt"]][0] += bool(r["delivered"])
|
||||
for a in CLASSES:
|
||||
got, tot = per_class[a]
|
||||
if tot:
|
||||
print(f" {a}: {got}/{tot} into {EXPECT[a]}")
|
||||
print(" where everything ended up: " +
|
||||
str(dict(Counter(r["outcome"] for r in done_records))))
|
||||
thrown = [r for r in done_records if r["blowup"]]
|
||||
stalled = [r for r in done_records if r["outcome"] in ("line", "lane_B", "lane_C")]
|
||||
print(f" thrown by the mechanics: {len(thrown)} | stalled short of a tray: {len(stalled)}")
|
||||
if thrown:
|
||||
r = thrown[0]
|
||||
print(f" e.g. {r['item']}: {r['blowup']}")
|
||||
if stalled:
|
||||
r = stalled[0]
|
||||
print(f" e.g. {r['item']}: stopped at {r['final']} arm={r['arm_at_plow']}")
|
||||
|
||||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||||
contact_report = sorter.contact.report() if sorter.contact else None
|
||||
json.dump(dict(contact=contact_report, config=dict(preset=PRESET, vision=USE_VISION, pitch=PITCH, speed=SPEED,
|
||||
repeats=REPEATS, mapping=sorter.mapping, expect=EXPECT,
|
||||
staged=staged, items_dir=ITEMS_DIR),
|
||||
records=done_records, events=events[-400:]),
|
||||
open(OUT, "w"), indent=2, ensure_ascii=False)
|
||||
print(f"\nlog -> {OUT}")
|
||||
Reference in New Issue
Block a user