Skip to content

MLDovakin/robozone

Repository files navigation

RoboZone · Трек 3 — Интеллектуальная роботизированная система сортировки

Демо работы обученной политики

Открыть видео MP4

Статус: сквозной контур работает в симуляции — товар определяется, классифицируется, захватывается вакуумным захватом и доставляется в целевую зону. Есть Gymnasium-среда для дообучения RL и ROS2-мост для переноса на реальный робот.


8. Обучение RL — среда, вход/выход, агент (актёр + критик) и обучение

Gymnasium-среда SortingPickEnv (src/robozone/rl_env.py). Один эпизод = один товар из накопителя в целевую зону. Управление 20 Гц (25 подшагов физики на шаг), до 300 шагов. Целевая зона подаётся средой как условие (one-hot) — RL-часть учит физическую маршрутизацию, CV-часть подключается независимо.

Кратко о потоке: наблюдение (46)актёр выдаёт действие (7) → среда шагает физику и возвращает награду и следующее наблюдение → переход (s,a,r,s′) кладётся в replay buffer → на выборках из буфера учатся критик (оценивает качество действий) и актёр (улучшается по оценке критика).

Как устроена среда

Обёртка над RobozoneSim: на reset спавнит товар с доменной рандомизацией и выбирает сценарий корнер-кейса (см. ниже); на step интегрирует целевые позиции суставов в позиционные сервоприводы и обновляет вакуум. Кинематика реальная — политика переносима на UR10e через ROS2-мост (раздел 7).

Действие — выход актёра (Box, 7)

Индексы Смысл
0..5 приращения целевых позиций 6 суставов, ∈[−1,1] × ACT_SCALE(0.05 рад)
6 вакуум: >0 включить, иначе выключить

Наблюдение — вход актёра и критика (Box, 46)

q(6), q̇(6) — состояние руки; ee(3) — TCP присоски; obj_pos(3), obj_quat(4) — поза товара; ee→obj(3), ee→drop(3); held(1) (реальный захват), suction(1); zone_onehot(3) — целевая зона; extents(3)габариты (масштаб товара); grasp(3), grasp→ee(3)размерозависимая точка захвата (верх над ЦМ); contact(1)тактильный сигнал касания присоски; phase(1) — фаза задачи (был ли первый контакт, монотонно); recovery(1)режим восстановления после срыва; contact_conf(1) — гладкая тактильная уверенность [0,1].

Награда — контактно-ориентированная

Наивная награда «только по расстоянию» нестабильна для захвата: присоска подлетает → касается → лёгкий товар отталкивается → расстояние снова растёт → RL читает это как ошибку, хотя произошёл первый контакт. Поэтому награда переключает фазу по первому касанию (идея из tactile grasp refinement):

Фаза 1 — подвод (контакта ещё не было): единственный сигнал — сближение присоски с точкой захвата: Δ(−‖ee − grasp‖).

Первый контакт: разовый бонус +0.5 («нашёл объект»). Дальше расстояние ee→товар перестаёт влиять — толчок товара больше не штрафуется. Фаза задачи монотонна и не откатывается назад.

Фаза 2 — манипуляция (после контакта):

  • +0.01/шаг за удержание захвата (стабильный контакт, «не теряй товар»);
  • перенос ведётся по расстоянию товара (не EE) до точки зоны — оно корректно, т.к. схваченный товар движется вместе с рукой;
  • −slip_penalty (2.0) за проскальзывание/срыв (был захват, вакуум включён, товар потерян и не в зоне).

Терминально: +10 успех (товар в целевой зоне, отпущен, покоится), −5 чужая зона, −3 упал на пол; −0.01/шаг за время. Бонусы за контакт намеренно малы (не if force>thr: +100) — чтобы агент не «давил» в товар, а удерживал устойчивый захват.

Проверено: после первого контакта рост ‖ee−obj‖ даёт только −0.01 (штраф времени), а не большой штраф расстояния (scenario_drop_repick.py и тесты).

Сложный корнер-кейс: срыв и повторный захват (closed-loop regrasp)

Отдельный тяжёлый случай — когда товар срывается уже после захвата (частый slip лёгких/скользких деталей, vacuum_weak, резкое движение). Наивные варианты оба плохи: reset эпизода уничтожает learning signal, а «жёсткий откат фазы в approach» (наш прежний вариант) ломает причинность — политика воспринимает slip как перезапуск задачи с нуля, а не как «надо починить захват», и recovery не выучивается. Как в reset-free / tactile-regrasp / residual-VLA работах, сделано по формуле regrasp = тот же эпизод + та же фаза + изменённый reward + closed-loop коррекция:

  • фаза задачи не откатывается (phase/contacted монотонны);
  • вместо резкого сброса — гладкая contact_conf (растёт при контакте, плавно спадает без него) и флаг recovery_mode (оба в наблюдении);
  • при срыве включается recovery_mode и reward переключается на восстановление: слабое сведение присоски к товару (realign_weight·Δ(−‖ee−grasp‖)), штраф за скорость товара (−obj_vel_penalty·‖v‖, чтобы не гонять «убегающую» деталь), плюс +regrasp_bonus (0.5) за восстановленный захват;
  • эпизод завершается только при успехе, чужой зоне или падении на пол (вне досягаемости) — срыв «на столе» остаётся восстановимым в том же эпизоде.

Проверено (scenario_drop_repick.py + тесты): срыв → штраф −2.0, contacted остаётся True (фаза не откатилась), recovery_mode=True, contact_conf плавно спадает; восстановление захвата → +regrasp_bonus, выход из recovery — всё в пределах одного эпизода.

Корнер-кейсы в данных (инъекция отказов)

Чтобы в обучающих данных были неудачные захваты и повторные попытки, каждый эпизод с заданной вероятностью получает сценарий отказа (SCENARIOS в rl_env.py). Именно для этих случаев важна контактная награда и re-pick:

Сценарий Доля Что делает Какой отказ создаёт
normal 20% без возмущений
shift 20% товар смещён на 2–10 мм после осадки промах/смещение захвата
rotate 15% усиленный наклон/поворот позы неудобная грань для захвата
tcp_offset 15% смещение TCP/калибровки 2–8 мм в наблюдении прицел мимо → промах
friction 10% коэффициент трения товара ×[0.4,1.6] сдвиг/скольжение при касании
vacuum_delay 10% вакуум включается через 3–8 шагов «касание без захвата» → re-pick
vacuum_weak 10% сила прилипания ~30–110 Н срыв тяжёлого товара при подъёме

Проверено: сценарии применяются (трение 0.41, gain 327, задержка 4, bias −8 мм); vacuum_weak реально срывает тяжёлые товары (box_400, 4 кг) при подъёме, vacuum_delay требует держать вакуум заранее. SortingPickEnv(inject_scenarios= False) отключает инъекцию; curriculum=True усложняет рандомизацию по успеху.

Агент SAC: актёр и критик

Алгоритм — SAC (Soft Actor-Critic), off-policy actor-critic для непрерывного управления. Две сети:

  • Актёр (политика π) — MLP [256,256], вход = наблюдение (46), выход = параметры распределения действий: среднее μ(s) и log_std(s) по 7 осям; действие сэмплируется и сжимается tanh в [−1,1] = наше действие (7) (6 приращений суставов + вакуум). Стохастичность = встроенное исследование; на инференсе берётся детерминированно tanh(μ(s)).
  • Критик (две Q-сети Q₁,Q₂(s,a)) — MLP [256,256], вход = наблюдение + действие, выход = скалярная оценка ожидаемой суммарной будущей награды при выборе a в состоянии s. Именно критик «понимает», что подвод→захват→укладка ценны, а «ничего не делать» — нет.

Как учится критик. По переходам (s,a,r,s′) из replay buffer минимизируется TD-ошибка к цели Беллмана: y = r + γ·( minᵢ Q_targetᵢ(s′,a′) − α·log π(a′|s′) ), где a′∼π(·|s′), γ=0.98. Два критика и min борются с переоценкой Q; target-сети обновляются мягко (τ=0.02) для устойчивости; член −α·log π («soft») поощряет не схлопывать политику. Актёр обновляется в обратную сторону — максимизирует minᵢ Qᵢ(s, a∼π) + α·H(π) (действие репараметризовано, градиент течёт через критика в актёра).

Почему off-policy важно. Критик и актёр учатся на случайных выборках из буфера, а не только на свежих эпизодах — поэтому в буфер можно подложить демонстрации эксперта, и критик сразу видит успешные (s,a,r,s′).

Обучение: warm-start демонстрациями (важно)

Чистый SAC «с нуля» на этой задаче не обучается — застревает в оптимуме «ничего не делать»: координированную последовательность подвод→захват→укладка почти невозможно нащупать случайным исследованием, критик не видит ни одного успеха, и reward держится на уровне штрафа за время (ep_len≈300, timeout). Рабочий путь — warm-start демонстрациями (reset-free / SAC-from-Demonstrations):

  1. Скриптовый эксперт (src/robozone/expert.py) — конечный автомат на IK, выдаёт действия в формате среды (~74% успеха).
  2. Сбор демонстраций (collect_demos.py) — переходы (s,a,r,s′) успешных эпизодов (≈30.6k).
  3. Behavior Cloning актёра (train_bc_sac.py) — супервайзно обучаем актёра воспроизводить действия эксперта: MSE(tanh(μ(s)), a_expert). Ключевой нюанс: измерение вакуума бинарное — при обычном MSE усредняется к «выкл» и политика не захватывает; поэтому оно взвешено ×5. BC в одиночку даёт ~72–77%.
  4. Seed replay buffer — кладём демонстрации в буфер, чтобы критик сразу учился на успешных переходах, а не с нуля.
  5. Мягкое дообучение SAC поверх: низкий log_std (−2) и малый ent_coef (0.02, вместо «auto»), lr=1e-4 — иначе стохастичность и Q-максимизация SAC разрушают точную BC-политику.
python scripts/collect_demos.py --episodes 300 --out runs/demos.npz
python scripts/train_bc_sac.py --demos runs/demos.npz --bc-epochs 60 \
    --timesteps 250000 --ent-coef 0.02 --init-log-std -2.0 --outdir runs/sac_bc
python scripts/eval_rl.py --policy runs/sac_bc/final.zip --algo sac

Результат обученной политики (runs/sac_bc/final.zip, deterministic): 76% без сценариев отказов (B 100%, D 85%, C 25%) и 61% со сценариями (B 100%, D 57%, C 46%). Зона C (крупногабарит/тонкие) — ожидаемо самая сложная.

Честный вывод по online-дообучению: последующий online-SAC поверх BC ухудшает политику (актёр уходит от демонстраций к действиям с высоким Q при ещё не откалиброванном критике) — даже с малым ent_coef, низким log_std и BC-регуляризацией (--bc-coef). Это известная сложность offline→online RL. Решение реализовано в AWAC (раздел ниже) — advantage-weighted actor-critic, гарантирующий что актёр остаётся на данных. Итоговая BC политика — 76%/61%.

AWAC: offline→online RL без деградации BC

AWAC (Advantage-Weighted Actor-Critic) решает проблему SAC на offline→online задачах. Вместо Q-максимизации (которая уводит актёра далеко от данных), AWAC обновляет актёра как взвешенную регрессию к действиям из буфера:

L_π = − E_{s,a~buffer} [ log π(a|s) · exp( A(s,a) / λ ) ]
A(s,a) = Q(s,a) − V(s)

Веса exp(A/λ) усиливают действия с высоким преимуществом, но актёр остаётся близко к распределению демонстраций (гарантирует стабильность при плохой калибровке критика).

Пайплайн AWAC (runs/awac_v2):

5k BC инициализация → актёр ≈72% (как в train_bc_sac.py)
     ↓
30k offline AWAC → критик обучается на демо буфере, актёр уточняется
     ↓
50k online AWAC → взаимодействие среды со сценариями отказов, learned recovery
     ↓
Результат: ожидание 75–80% clean, 70+ scenarios (улучшение на recovery-сценариях)

Особенности AWAC:

  • Хорошо работает на recovery сценариях (vacuum_weak, vacuum_delay) — online видит отказы, учится восстанавливаться
  • Улучшает переадаптацию (shift, rotate, tcp_offset) — мелкие вариации в BC приносят преимущество
  • Гарантирует не деградировать относительно BC (advantage-weighted регуляризация)

Запуск и оценка:

python scripts/train_awac.py --demos runs/demos.npz \
    --offline-steps 30000 --online-steps 50000 --out runs/awac_v2

# Сравнительный отчёт на 8 сценариях (normal, shift, rotate, tcp_offset, friction, vacuum_delay, vacuum_weak):
python scripts/final_awac_report.py \
    --bc-policy runs/sac_bc/final.zip \
    --awac-policy runs/awac_v2/final.pt \
    --n-episodes 50

Запуск (базовый SAC — для сравнения, не обучается без warm-start)

pip install -r requirements-rl.txt                              # SB3 (+torch)
python scripts/train_rl.py --algo sac --timesteps 300000 --n-envs 4
python scripts/eval_rl.py --policy runs/sac_bc/final.zip --algo sac # оценка + видео
python scripts/scenario_drop_repick.py                          # сценарий срыва детали
python scripts/record_regrasp.py --out demo_pick.mp4            # видео regrasp (POV манипулятора)

demo_pick.mp4 — видео сценария regrasp от лица манипулятора (камера следует за захватом): подвод → захват → срыв → recovery → повторный захват → укладка, с онлайн-подписями фаз, флагов контакт/захват/recovery и contact_conf.

1. Что в этом репозитории

Часть Где Что делает
Определение + классификация src/robozone/classification.py габариты по OBB + признак «круг в сечении» (K=0.8) → категория/зона
Сцена симуляции sim/scene.xml, sim/cell_ur10e.xml, sim/objects.xml конвейер A, накопитель, зоны B/C/D, манипулятор UR10e + вакуумный захват
Ядро симуляции src/robozone/sim_core.py ленты, спавн товаров, вакуум, детекция зон
Исполнительная логика (IK) src/robozone/ik.py, scripts/demo_pick.py скриптовая политика захвата и укладки
RL-среда src/robozone/rl_env.py Gymnasium-среда pick-and-route для дообучения
ROS2-мост ros2_ws/src/robozone_sim/ узлы sim_node, classifier_node, orchestrator_node
Конвертация моделей scripts/convert_step.py STEP → STL (тестовый набор организаторов)

Соответствие осям УГТ: ось «определение/классификация» — раздел 4; ось «исполнительная часть» — разделы 5–6; связка — раздел 7.


2. Быстрый старт

# 0) окружение (Python 3.11). В этой машине готовый venv:
source ~/ros2_kilted/.venv/bin/activate
# зависимости (если ставить с нуля):
pip install mujoco gymnasium trimesh cascadio shapely scipy networkx rtree \
            pillow imageio imageio-ffmpeg

cd <репозиторий>
export PYTHONPATH=src

# 1) (однократно) конвертация STEP-моделей организаторов -> STL
python scripts/convert_step.py
python scripts/build_objects_xml.py         # sim/objects.xml из STL

# 2) классификация всего тестового набора (пишет categories.json)
python -m robozone.classification

# 3) сквозная демонстрация контура (скриптовая политика) + видео
python scripts/demo_pick.py --mode accumulator --video demo_route.mp4

# 4) видео сценария regrasp от лица манипулятора (demo_pick.mp4)
python scripts/record_regrasp.py --out demo_pick.mp4

# 5) проверка RL-среды (без обучения)
python scripts/eval_rl.py --policy random --episodes 12

Просмотр сцены интерактивно: python -m mujoco.viewer --mjcf=sim/scene.xml.


3. Постановка и схема участка

Мировая система координат: X — направление подачи (точка A → накопитель), Y — влево (в сторону зоны B), Z — вверх. Начало — центр накопителя. Рабочая зона 6000×10000 мм воспроизведена по схеме организаторов.

                                          [B] инфид сортировщика (фиксирован)
                                           ▲  питающая лента (диверторная)
   A ─ подающий конвейер 1 м/с ─▶ ╔═════╗  │
   (лента 500×700)                ║накоп║──┤ UR10e + вакуумный захват
                                  ╚═════╝  │
                                           ▼        ▼
                                     [C] ролл-кейдж   [D] ролл-кейдж
                                     негабарит        доупаковка
Точка Назначение Реализация в сцене
A подача товара лента 500 мм, верх 700 мм, скорость 1 м/с, заканчивается накопителем
B основной сортировщик питающая лента → инфид сортера (положение фиксировано)
C негабарит ролл-кейдж 1200×800×800
D доупаковка ролл-кейдж 1200×800×800

Ленты моделируются физически: тело ленты на слайд-шарнире движется velocity- актуатором, координата периодически «сматывается» — поверхность однородна, поэтому товары переносятся честным трением (бесконечная лента).


4. Часть определения и классификации товара

Категория определяется строго по правилам постановки (приоритет габаритов над формой):

  1. Габариты по минимальному ориентированному параллелепипеду (OBB) — инвариантно к позе товара на ленте. Если не в диапазоне (10×10×10 … 450×320×320) мм → C «Не подходит по габаритам».
  2. Круг в сечении: для набора сечений вдоль трёх главных осей OBB считается K = r_впис / R_опис (внешняя огибающая сечения). Если max K > 0.8D «Не подходит без доупаковки».
  3. Иначе → B «Подходит для сортировки».

Реализация: src/robozone/classification.py (trimesh + shapely). Результат по тестовому набору организаторов (assets/meshes/objects/categories.json):

Товар Зона K (макс. r/R) OBB, м Категория
box_300x200x200 B 0.720 0.301 × 0.201 × 0.200 подходит
detergent (моющее средство) B 0.742 0.278 × 0.260 × 0.180 подходит
lunchbox (ланчбокс) B 0.646 0.201 × 0.152 × 0.062 подходит
box_400x400x300 C 0.716 0.401 × 0.400 × 0.301 негабарит (>320 мм)
pen (ручка) C 0.842 0.149 × 0.013 × 0.009 негабарит (<10 мм по толщине)
pouf (пуфик) C 0.994 0.489 × 0.489 × 0.264 негабарит (>450 мм)
bag (мешок) D 0.896 0.202 × 0.175 × 0.170 круг в сечении
bottle (бутылка) D 0.995 0.305 × 0.091 × 0.091 круг в сечении
cylinder (цилиндр) D 0.867 0.435 × 0.050 × 0.043 круг в сечении
helmet (шлем) D 0.895 0.354 × 0.297 × 0.280 круг в сечении
plate (тарелка) D 0.998 0.209 × 0.209 × 0.027 круг в сечении

Приоритет соблюдён: box_400x400x300 и pouf имеют высокий K, но из-за габаритов отнесены к C (проверка габаритов выполняется первой). pen проходит по форме, но толщина 9 мм < 10 мм → C.

В симуляции категория подаётся исполнительной части как «выход CV-части» (ground truth из categories.json). Это позволяет отлаживать исполнительный контур независимо от зрения; при переносе на реальную систему узел classifier_node заменяется на инференс CV-модели по кадру камеры с тем же выходным контрактом (строка зоны B/C/D) — остальной контур не меняется. В сцене есть калиброванные камеры cam_overhead, cam_pick, cam_wrist.


5. Сцена и исполнительная часть

Манипулятор: UR10e (модель из MuJoCo Menagerie, BSD-3), 6 степеней свободы, имена суставов сохранены как у реального робота (shoulder_pan_joint … ) — критично для переноса. На фланец добавлен вакуумный захват: присоска + adhesion-актуатор suction (эмуляция вакуума, сила прилипания на контактах присоски). Выбор вакуумного захвата обоснован разнородностью потока (коробки, цилиндры, шлемы, тарелки) и требованием мягкого обращения — см. справочный раздел постановки.

Ключевые узлы:

  • подающий конвейер A (1 м/с) → накопитель (роликовый стол с бортами);
  • роботизированная ячейка UR10e на тумбе (верх на уровне ленты 700 мм);
  • питающая (диверторная) лента к инфиду сортировщика (зона B за пределами вылета руки — товар кладётся на ближний конец ленты, лента переносит к точке B);
  • ролл-кейджи C и D — открытые сверху, товар сбрасывается манипулятором.

Логика захвата и укладки (ik.py, sim_core.py):

  • точка присасывания — реальная верхняя грань товара в текущей позе (луч сверху вниз), а не по габаритам меша: товары ложатся плашмя (напр. ручка);
  • дифференциальная IK (damped least squares) по сайту присоски, ориентация оси захвата — вниз;
  • смещение захвата измеряется в момент прилипания и учитывается при укладке (placement_tip_target): B — укладка на ленту, C/D — сброс сверху в кейдж.

6. Демонстрация сквозного контура

python scripts/demo_pick.py --mode accumulator --video demo_pick.mp4
# один товар едет по ленте A -> накопитель:
python scripts/demo_pick.py --objects bottle --mode belt

Скриптовая политика для каждого товара: спавн → (доезд по ленте) → классификация → IK-захват присоской → перенос → укладка в зону B/C/D → возврат в домашнюю позу. Траектория укладки: подъём → перенос высоко над зоной → вертикальный спуск (устойчиво к застреваниям жадной IK).

Результат базовой скриптовой политики: 9/11, все три зоны (B: 3/3, D: 5/5, C: 1/3). Не удаются pen (9 мм — тонкая деталь для захвата сверху) и pouf (489 мм, мягкий крупногабарит) — задокументированные пограничные случаи. Подробности — REPORT.md.


7. Связка CV и исполнительной части (ROS2)

Мост построен на стандартных сообщениях ROS2 (без custom-msg — проще проверять и разворачивать). Три узла:

Узел Публикует Подписан на Сервисы
sim_node joint_states, ee_pose, suction_state, active_object, object_pose, TF position_command, suction_command, spawn reset, set_suction
classifier_node category, target_zone active_object
orchestrator_node position_command, suction_command joint_states, object_pose, suction_state, target_zone

Топик position_command (Float64MultiArray, 6 целевых позиций суставов) совместим по смыслу с forward_position_controller из ur_robot_driverзамена симуляции на реального робота не меняет остальной контур. Оркестратор считает кинематику на read-only копии модели (FK/якобиан) — на реальном роботе заменяется на URDF+KDL с тем же контрактом топиков.

Запуск полного контура:

# один раз: собрать ROS2-пакет
cd ros2_ws && colcon build --packages-select robozone_sim && source install/setup.zsh
export ROBOZONE_ROOT=<репозиторий>          # где лежит sim/scene.xml
ros2 launch robozone_sim sim_bringup.launch.py spawn_mode:=accumulator
# ручное управление без скриптовой политики (для RL):
ros2 launch robozone_sim sim_bringup.launch.py run_orchestrator:=false

Синхронизация по времени и cycle time

Лента A движется 1 м/с. CV-часть работает с опережением (товар классифицируется в накопителе до захвата), а положение товара к моменту захвата определяется по кадру камеры/позе — задержка инференса не смещает захват. Неоднозначные случаи (низкая уверенность) в реальной системе направляются в отдельный поток на повторный прогон.



9. Развёртывание в серверной среде проверки

Требования: Linux/macOS, Python 3.11, ROS2 (проверено на Kilted), пакеты из раздела 2. GPU не обязателен для проверки контура (нужен для обучения).

Параметры, которыми управляет эксперт (через launch/сервисы/топики):

  • spawn_mode = accumulator | belt; belt_speed (м/с);
  • /robozone/spawn (String) — подать конкретный товар или random;
  • /robozone/reset — сброс сцены; /robozone/set_suction — вакуум;
  • run_orchestrator:=false — отключить скриптовую политику для ручного/RL-управления.

Сценарии проверки: (1) классификация всего набора python -m robozone.classification; (2) сквозная укладка scripts/demo_pick.py; (3) RL-среда scripts/eval_rl.py; (4) ROS2-контур через launch.


10. Ограничения и развитие

  • Тонкие товары (ручка 9 мм) — вакуумный захват сверху ненадёжен; в реальной системе — щелевой/боковой захват или отдельный поток. Это пограничный случай, зафиксирован в проверках.
  • Классификация в симуляции использует ground-truth категории; CV-инференс по камере — следующий шаг (контракт узла уже задан).
  • Скриптовая политика — опорная; RL-политика обучается той же средой.
  • Захват предполагает доступную верхнюю грань; плотная упаковка в накопителе не моделируется.

11. Использованные инструменты

MuJoCo 3.10, Gymnasium, ROS2 Kilted (rclpy), UR10e из MuJoCo Menagerie (BSD-3), trimesh + cascadio (STEP→STL), shapely (сечения), NumPy/SciPy, Stable-Baselines3 (RL), imageio (видео). Полный список версий — в REPORT.md.

Структура репозитория

sim/                 сцены MuJoCo (scene, cell_ur10e, objects)
src/robozone/        classification, sim_core, ik, rl_env
scripts/             convert_step, build_objects_xml, demo_pick, train_rl, eval_rl
ros2_ws/src/robozone_sim/   ROS2-узлы, launch
assets/              меши UR10e и тестовых товаров, categories.json
models_files/        исходные STEP-модели организаторов

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages