Сортировочная ячейка 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:
dasha_f
2026-08-01 13:07:24 +00:00
parent 6ce460378a
commit 0d32f32db0
342 changed files with 18000 additions and 0 deletions
+168
View File
@@ -0,0 +1,168 @@
"""Fast FoundationStereo вместо CREStereo, на том же прогоне потока 700 мм.
Модель взята ONNX-экспортом (23_36_37, iters 4, 320x736) и гоняется через onnxruntime-gpu:
это снимает зависимость от окружения репозитория (torch 2.6 + xformers), которое с
питоном Isaac не совпадает. Веса лежат в fast-foundationstereo/weights.
ВХОД У МОДЕЛИ ФИКСИРОВАННЫЙ - 320x736. Кроп приводится к этому размеру, а полученная
диспаратность масштабируется ОБРАТНО по горизонтали: диспаратность измеряется в пикселях,
и при сжатии кадра в k раз по ширине она сжимается во столько же. Без этого множителя
глубина уехала бы ровно в k раз - самая частая ошибка при подмене стереосети.
Сравнение честное: сегментация, отделение полотна, слияние и метрики те же, что в
варианте без сегментации (лучшем на сегодня). Меняется ТОЛЬКО стереодвижок.
"""
import os, sys, json, time
import numpy as np
import cv2
CV = "/home/dasha/isaac_assets/cv"
CT = "/home/dasha/robozon-sorter/control_test"
FFS = f"{CV}/fast-foundationstereo/weights/ffs_23_36_37_iters4_320x736.onnx"
OUT = f"{CT}/diag/ffs"
os.makedirs(OUT, exist_ok=True)
sys.path.insert(0, CT)
os.environ.setdefault("MASK_MODE", "gate")
import measure_flow as MF
import classify as CL
import onnxruntime as ort
so = ort.SessionOptions()
so.graph_optimization_level = ort.GraphOptimizationLevel.ORT_ENABLE_ALL
sess = ort.InferenceSession(FFS, so, providers=["CUDAExecutionProvider", "CPUExecutionProvider"])
IN_H, IN_W = 320, 736
print(f"Fast FoundationStereo: {os.path.basename(FFS)}, вход {IN_W}x{IN_H}, "
f"провайдер {sess.get_providers()[0]}")
man, calib = MF.man, MF.calib
TARGET, BELT_Z, RIGS = MF.TARGET, MF.BELT_Z, MF.RIGS
FLOOR, GATE_R = 0.020, 0.28
DENS_CELL, DENS_MIN = 0.010, 12
PITCH_S = 0.70
def ffs_disp(L, R):
"""диспаратность одной пары. Кроп -> 320x736 -> обратный масштаб по горизонтали."""
# cv2.imread даёт BGR, а модель обучена на RGB (run_demo.py читает через imageio).
# Нормализацию (ImageNet mean/std после деления на 255) модель делает ВНУТРИ forward,
# см. core/foundation_stereo.py:32 - значит она экспортирована в ONNX, и подавать надо
# именно СЫРЫЕ 0-255. Ошибкой был только порядок каналов.
h, w = L.shape[:2]
sx = IN_W / float(w)
lr = cv2.resize(cv2.cvtColor(L, cv2.COLOR_BGR2RGB), (IN_W, IN_H)).astype(np.float32)
rr = cv2.resize(cv2.cvtColor(R, cv2.COLOR_BGR2RGB), (IN_W, IN_H)).astype(np.float32)
a = lr.transpose(2, 0, 1)[None]
b = rr.transpose(2, 0, 1)[None]
d = sess.run(["disparity"], {"left_image": a, "right_image": b})[0][0, 0]
d = cv2.resize(d, (w, h), interpolation=cv2.INTER_LINEAR)
return np.abs(d) / sx # пиксели исходного кропа, а не приведённого
def dense_only(P, cell=DENS_CELL, need=DENS_MIN):
if len(P) < 60:
return np.ones(len(P), bool)
key = np.floor(P[:, :2] / cell).astype(np.int64)
uniq, inv, cnt = np.unique(key, axis=0, return_inverse=True, return_counts=True)
return cnt[inv] >= need
def biggest_blob_idx(P, grid=0.012):
if len(P) < 40:
return np.ones(len(P), bool)
cx = np.round(P[:, 0] / grid).astype(int); cy = np.round(P[:, 1] / grid).astype(int)
im = np.zeros((cy.max() - cy.min() + 3, cx.max() - cx.min() + 3), np.uint8)
im[cy - cy.min() + 1, cx - cx.min() + 1] = 255
im = cv2.morphologyEx(im, cv2.MORPH_CLOSE, np.ones((3, 3), np.uint8))
n, lab = cv2.connectedComponents(im)
li = lab[cy - cy.min() + 1, cx - cx.min() + 1]
best, bn = None, 0
for k in range(1, n):
m = li == k
if m.sum() > bn:
best, bn = m, m.sum()
return np.ones(len(P), bool) if best is None else best
def cloud_from_roi(disp, win, cam):
x0, y0, x1, y1 = win
depth = np.where(disp > 0.5, cam["fx"] * cam["baseline"] / np.maximum(disp, 1e-6), np.nan)
vs, us = np.mgrid[y0:y0 + depth.shape[0], x0:x0 + depth.shape[1]]
ok = np.isfinite(depth) & (depth > 1e-3)
u, v, z = us[ok], vs[ok], depth[ok]
P = np.stack([(u - cam["cx"]) * z / cam["fx"],
-(v - cam["cy"]) * z / cam["fy"], -z, np.ones_like(z)], 1)
P = (P @ np.array(cam["M"]))[:, :3]
near = np.hypot(P[:, 0] - TARGET[0], P[:, 1] - TARGET[1]) < GATE_R
return P[near & (P[:, 2] > BELT_Z + FLOOR) & (P[:, 2] < BELT_Z + 0.60)]
for _ in range(2): # прогрев
ffs_disp(np.zeros((240, 480, 3), np.uint8), np.zeros((240, 480, 3), np.uint8))
print("подача: RGB 0-255, нормализация внутри графа")
print(f"\n {'товар':18s} {'эталон, мм':>18s} {'предсказано, мм':>20s} {'MAE':>6s} "
f"{'k':>6s} {'класс':>12s} {'точек':>7s} {'мс':>6s}")
print(" " + "-" * 100)
rows, times = [], []
for name, e in man["items"].items():
t0 = time.time()
clouds = []
for rig in RIGS:
cam = calib[f"{rig}_Left"]
IL = cv2.imread(e["files"][f"{rig}_Left"])
IR = cv2.imread(e["files"][f"{rig}_Right"])
if IL is None or IR is None:
continue
x0, y0, x1, y1 = MF.belt_roi_px(cam, MF.POLY3)
maxd = int(np.ceil(MF.DPAD * cam["fx"] * cam["baseline"] / MF.ZMIN))
x0 = max(0, x0 - maxd)
d = ffs_disp(IL[y0:y1, x0:x1], IR[y0:y1, x0:x1])
P = cloud_from_roi(d, (x0, y0, x1, y1), cam)
if len(P):
clouds.append(P)
if name in ("bag", "bucket", "box_300x200x200"):
dn = cv2.normalize(d, None, 0, 255, cv2.NORM_MINMAX).astype(np.uint8)
cv2.imwrite(f"{OUT}/{name}_{rig}_disp.png", cv2.applyColorMap(dn, cv2.COLORMAP_TURBO))
dt = (time.time() - t0) * 1000
times.append(dt)
gt = e["gt"]; gtd = sorted(gt["dims_mm"], reverse=True)
if not clouds:
print(f" {name:18s} {str([round(v) for v in gtd]):>18s} облако пустое"); continue
P = np.vstack(clouds)
s1 = dense_only(P)
if s1.sum() >= 60:
P = P[s1]
P = P[biggest_blob_idx(P)]
out = MF.dims_and_k(P)
if out is None:
print(f" {name:18s} габариты не взялись"); continue
dims, k = out
mae = float(np.mean(np.abs(np.array(dims) - np.array(gtd))))
cls = CL.classify(dims, 0.0 if np.isnan(k) else k)
ok = "верно" if cls == gt["zone_scene"] else f"ОШ({gt['zone_scene']})"
print(f" {name:18s} {str([round(v) for v in gtd]):>18s} "
f"{str([round(v) for v in dims]):>20s} {mae:6.1f} {k:6.2f} {cls+' '+ok:>12s} "
f"{len(P):7d} {dt:6.0f}")
rows.append(dict(name=name, gt=gtd, gt_cls=gt["zone_scene"], pred=[round(v, 1) for v in dims],
pred_k=round(float(k), 3), pred_cls=cls, mae=round(mae, 1),
n=len(P), ms=round(dt)))
print("\n === МЕТРИКИ (Fast FoundationStereo, без сегментации) ===")
if rows:
maes = [r["mae"] for r in rows]
acc = sum(1 for r in rows if r["pred_cls"] == r["gt_cls"])
print(f" габариты: MAE медиана {np.median(maes):.1f} мм, среднее {np.mean(maes):.1f}, "
f"худший {max(maes):.1f} ({max(rows, key=lambda r: r['mae'])['name']})")
print(f" классы: {acc}/{len(rows)} = {100.0*acc/len(rows):.0f}%")
print(f" k макс {max(r['pred_k'] for r in rows):.2f}")
lab = ["B", "C", "D"]
print(" матрица (строки истина, столбцы предсказание):")
print(" " + "".join(f"{c:>5s}" for c in lab))
for a in lab:
print(f" {a:3s} " + "".join(
f"{sum(1 for r in rows if r['gt_cls']==a and r['pred_cls']==b):5d}" for b in lab))
print(f" время: медиана {np.median(times):.0f} мс, такт {PITCH_S*1000:.0f} мс -> "
f"{'УКЛАДЫВАЕТСЯ' if np.median(times) < PITCH_S*1000 else 'НЕ УКЛАДЫВАЕТСЯ'} "
f"(запас {PITCH_S*1000 - np.median(times):+.0f} мс)")
json.dump(rows, open(f"{OUT}/metrics.json", "w"), indent=1, ensure_ascii=False)
print(f"\n -> {OUT}/metrics.json")