Files
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

120 lines
5.1 KiB
Python

"""Рабочий процесс замкнутого контура: слушает заявки от Isaac, отвечает классом товара.
Зачем отдельный процесс. torch внутри Isaac роняет процесс, поэтому CV не может жить в
одном процессе со сценой. Обмен идёт через каталог: Isaac кладёт заявку с путями к шести
кадрам, работник отвечает файлом с габаритами, k и классом.
Времени на это хватает с запасом. От ворот осмотра (x = -0.750) до пушера (x = -3.900)
товар при 1 м/с едет 3.15 с, до плуга (x = -7.85) - 7.1 с, а инференс на зафиксированном
бейзлайне занимает 469 мс. То есть класс успевает к обоим механизмам, и контур замыкается
по-настоящему, а не постфактум.
БЕЙЗЛАЙН ЗАФИКСИРОВАН: DEFOM-Stereo vitl, вход сети 480, iters 24, кроп зоны осмотра,
без сегментации (товар отделяется превышением над плоскостью полотна + отсев по плотности).
На потоке 700 мм он дал классы 8/9 = 89 % и габариты MAE 32.8 мм.
"""
import os, sys, json, time, glob, traceback
os.environ["IMPORT_ONLY"] = "1" # measure_plane импортируется как библиотека
CT = "/home/dasha/robozon-sorter/control_test"
sys.path.insert(0, CT)
import numpy as np
import cv2
import measure_plane as MP
import measure_flow as MF
import classify as CL
RT = f"{CT}/runtime"
REQ, RES = f"{RT}/req", f"{RT}/res"
for d in (REQ, RES):
os.makedirs(d, exist_ok=True)
calib = MF.calib
TARGET, BELT_Z, RIGS = MF.TARGET, MF.BELT_Z, MF.RIGS
def measure(files):
"""шесть кадров -> габариты, k, класс. Тот же тракт, что в зафиксированном бейзлайне."""
pairs, metas = [], []
for rig in RIGS:
cam = calib[f"{rig}_Left"]
IL = cv2.imread(files[f"{rig}_Left"])
IR = cv2.imread(files[f"{rig}_Right"])
if IL is None or IR is None:
continue
x0, y0, x1, y1 = MP.gate_crop_px(cam)
maxd = int(np.ceil(MF.DPAD * cam["fx"] * cam["baseline"] / MF.ZMIN))
x0 = max(0, x0 - maxd)
pairs.append((IL[y0:y1, x0:x1].astype(np.float32), IR[y0:y1, x0:x1].astype(np.float32)))
metas.append(((x0, y0, x1, y1), cam))
if not pairs:
return None
disps = MP.cre_scaled(pairs, MP.SW) # движок выбирается внутри (бейзлайн: DEFOM)
clouds = []
for d, (win, cam) in zip(disps, metas):
# cloud_from_roi отдаёт ПАРУ: облако товара и облако полотна. В первой версии я
# складывал кортеж целиком, и vstack падал на разнородных формах.
obj, _belt = MP.cloud_from_roi(d, win, cam)
if len(obj):
clouds.append(obj)
if not clouds:
return None
P = np.vstack(clouds)
sel = MP.dense_only(P)
if sel.sum() >= 60:
P = P[sel]
P = P[MP.biggest_blob_idx(P)]
out = MF.dims_and_k(P)
if out is None:
return None
dims, _ = out
k, is_round, sec = MP.circular_section_K(P)
cls = CL.classify(dims, 0.0 if not k else k)
return dict(dims=[round(v, 1) for v in dims], k=round(float(k), 3),
cls=cls, n=len(P))
print(f"работник запущен: движок {MP.STEREO} {MP.DEFOM_CKPT}, вход {MP.SW}, "
f"iters {MP.DEFOM_ITERS}, кроп {MP.CROP}")
print(f"заявки: {REQ} ответы: {RES}")
# прогрев, чтобы первый настоящий товар не ждал загрузку энкодера
try:
z = np.zeros((240, 480, 3), np.float32)
MP.cre_scaled([(z, z)], MP.SW)
print("прогрев выполнен")
except Exception as ex:
print("прогрев не удался:", ex)
open(f"{RT}/worker_ready", "w").write(str(time.time()))
seen = set()
idle = 0.0
while True:
reqs = sorted(glob.glob(f"{REQ}/*.json"))
if not reqs:
time.sleep(0.05); idle += 0.05
if idle > 1800:
print("1800 с без заявок - выхожу"); break
continue
idle = 0.0
for rq in reqs:
try:
j = json.load(open(rq))
except Exception:
continue
name = j["name"]
t0 = time.time()
try:
r = measure(j["files"])
except Exception:
traceback.print_exc(); r = None
dt = (time.time() - t0) * 1000
ans = dict(name=name, ms=round(dt), ok=r is not None)
if r:
ans.update(r)
json.dump(ans, open(f"{RES}/{name}.json", "w"), ensure_ascii=False)
os.remove(rq)
print(f" {name}: {ans.get('cls','-')} dims={ans.get('dims')} k={ans.get('k')} "
f"за {dt:.0f} мс", flush=True)