Сортировочная ячейка 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,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")
|
||||
Reference in New Issue
Block a user