Створюйте синтетичні дані для конвеєрів комп’ютерного зору на AWS

Вихідний вузол: 1726779

Збір і анотування даних зображень є одним із найбільш ресурсомістких завдань у будь-якому проекті комп’ютерного зору. Можуть знадобитися місяці, щоб повністю зібрати, проаналізувати та поекспериментувати з потоками зображень на потрібному рівні, щоб конкурувати на поточному ринку. Навіть після того, як ви успішно зібрали дані, у вас все ще є постійний потік помилок анотацій, погано оформлених зображень, невеликих обсягів значущих даних у морі небажаних знімків тощо. Саме через ці основні вузькі місця створення синтетичних даних має бути в наборі інструментів кожного сучасного інженера. Створюючи тривимірні представлення об’єктів, які ми хочемо змоделювати, ми можемо швидко створювати прототипи алгоритмів, одночасно збираючи живі дані.

У цій публікації я розповім вам про приклад використання бібліотеки анімації з відкритим кодом Blender для створення наскрізного конвеєра синтетичних даних, використовуючи як приклад курячі нагетси. Наступне зображення є ілюстрацією даних, згенерованих у цій публікації блогу.

Що таке блендер?

змішувач це програмне забезпечення для 3D-графіки з відкритим вихідним кодом, яке в основному використовується в анімації, 3D-друкі та віртуальній реальності. Він має надзвичайно всеосяжний набір обладнання, анімації та моделювання, який дозволяє створювати 3D-світи майже для будь-якого використання комп’ютерного зору. Він також має надзвичайно активну спільноту підтримки, де більшість, якщо не всі помилки користувачів вирішуються.

Налаштуйте своє місцеве середовище

Ми встановлюємо дві версії Blender: одну на локальну машину з доступом до GUI, а іншу на Обчислювальна хмара Amazon Elastic (Amazon EC2) Примірник P2.

Встановіть Blender і ZPY

Встановіть Blender з Веб-сайт Blender.

Потім виконайте наступні кроки:

  1. Виконайте такі команди:
    wget https://mirrors.ocf.berkeley.edu/blender/release/Blender3.2/blender-3.2.0-linux-x64.tar.xz
    sudo tar -Jxf blender-3.2.0-linux-x64.tar.xz --strip-components=1 -C /bin
    rm -rf blender*
    
    /bin/3.2/python/bin/python3.10 -m ensurepip
    /bin/3.2/python/bin/python3.10 -m pip install --upgrade pip

  2. Скопіюйте необхідні заголовки Python у версію Python для Blender, щоб ви могли використовувати інші бібліотеки, що не належать до Blender:
    wget https://www.python.org/ftp/python/3.10.2/Python-3.10.2.tgz
    tar -xzf Python-3.10.2.tgz
    sudo cp Python-3.10.2/Include/* /bin/3.2/python/include/python3.10

  3. Перевизначте свою версію Blender і примусово встановіть, щоб наданий Blender Python працював:
    /bin/3.2/python/bin/python3.10 -m pip install pybind11 pythran Cython numpy==1.22.1
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U Pillow --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U scipy --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U shapely --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U scikit-image --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U gin-config --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U versioneer --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U shapely --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U ptvsd --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U ptvseabornsd --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U zmq --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U pyyaml --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U requests --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U click --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U table-logger --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U tqdm --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U pydash --force
    sudo /bin/3.2/python/bin/python3.10 -m pip install -U matplotlib --force

  4. Завантажити zpy і встановити з джерела:
    git clone https://github.com/ZumoLabs/zpy
    cd zpy
    vi requirements.txt

  5. Змініть версію NumPy на >=1.19.4 та scikit-image>=0.18.1 щоб виконати інсталяцію 3.10.2 можливо, щоб ви не отримували жодних перезаписів:
    numpy>=1.19.4
    gin-config>=0.3.0
    versioneer
    scikit-image>=0.18.1
    shapely>=1.7.1
    ptvsd>=4.3.2
    seaborn>=0.11.0
    zmq
    pyyaml
    requests
    click
    table-logger>=0.3.6
    tqdm
    pydash

  6. Щоб переконатися в сумісності з Blender 3.2, перейдіть до zpy/render.py і закоментуйте наступні два рядки (додаткову інформацію див Помилка Blender 3.0 №54):
    #scene.render.tile_x = tile_size
    #scene.render.tile_y = tile_size

  7. Потім встановіть zpy бібліотека:
    /bin/3.2/python/bin/python3.10 setup.py install --user
    /bin/3.2/python/bin/python3.10 -c "import zpy; print(zpy.__version__)"

  8. Завантажте версію доповнень zpy від GitHub репо щоб ви могли активно запускати свій екземпляр:
    cd ~
    curl -O -L -C - "https://github.com/ZumoLabs/zpy/releases/download/v1.4.1rc9/zpy_addon-v1.4.1rc9.zip"
    sudo unzip zpy_addon-v1.4.1rc9.zip -d /bin/3.2/scripts/addons/
    mkdir .config/blender/
    mkdir .config/blender/3.2
    mkdir .config/blender/3.2/scripts
    mkdir .config/blender/3.2/scripts/addons/
    mkdir .config/blender/3.2/scripts/addons/zpy_addon/
    sudo cp -r zpy/zpy_addon/* .config/blender/3.2/scripts/addons/zpy_addon/

  9. Збережіть файл під назвою enable_zpy_addon.py у вашому /home і виконайте команду ввімкнення, оскільки у вас немає графічного інтерфейсу для його активації:
    import bpy, os
    p = os.path.abspath('zpy_addon-v1.4.1rc9.zip')
    bpy.ops.preferences.addon_install(overwrite=True, filepath=p)
    bpy.ops.preferences.addon_enable(module='zpy_addon')
    bpy.ops.wm.save_userpref()
    
    sudo blender -b -y --python enable_zpy_addon.py

    If zpy-addon не встановлюється (з будь-якої причини), ви можете встановити його через GUI.

  10. У Blender, на Редагувати меню, виберіть переваги.
  11. Вибирати доповнення на панелі навігації та активуйте zpy.

Ви повинні побачити сторінку, відкриту в GUI, і ви зможете вибрати ZPY. Це підтвердить, що Blender завантажено.

AliceVision і Meshroom

Встановіть AliceVision і Meshroomom із відповідних сховищ GitHub:

FFmpeg

Ваша система повинна мати ffmpeg, але якщо цього не станеться, вам доведеться це зробити скачати його.

Миттєві сітки

Ви можете скомпілювати бібліотеку самостійно або завантажити доступні попередньо скомпільовані двійкові файли (що я і зробив) для Миттєві сітки.

Налаштуйте середовище AWS

Тепер ми налаштовуємо середовище AWS на примірнику EC2. Ми повторюємо кроки з попереднього розділу, але тільки для Blender і zpy.

  1. На консолі Amazon EC2 виберіть Запуск екземплярів.
  2. Виберіть свій AMI. Тут є кілька варіантів. Ми можемо або вибрати стандартний образ Ubuntu, вибрати екземпляр графічного процесора, а потім вручну встановити драйвери та все налаштувати, або ми можемо вибрати простий шлях і почати з попередньо налаштованого Deep Learning AMI і турбуватися лише про встановлення Blender. Для цього публікації, я використовую другий варіант і вибираю останню версію Deep Learning AMI для Ubuntu (Версія Deep Learning AMI (Ubuntu 18.04). 61.0).
  3. для Тип екземпляра¸ вибрати p2.xlarge.
  4. Якщо у вас немає пари ключів, створити новий або виберіть існуючий.
  5. Для цієї публікації використовуйте налаштування за замовчуванням для мережі та сховища.
  6. Вибирати Запуск екземплярів.
  7. Вибирати З'єднуватися і знайдіть інструкції щодо входу в наш екземпляр із SSH на SSH клієнт Вкладка.
  8. Підключіться за допомогою SSH: ssh -i "your-pem" ubuntu@IPADDRESS.YOUR-REGION.compute.amazonaws.com

Підключившись до свого екземпляра, виконайте ті самі кроки з попереднього розділу, щоб інсталювати Blender і zpy.

Збір даних: 3D сканування нашого самородка

Для цього кроку я використовую iPhone, щоб записати 360-градусне відео в досить повільному темпі навколо мого самородка. Я настромив курячий нагетс на зубочистку, приклеїв зубочистку до стільниці та просто обертав камеру навколо нагетса, щоб отримати якомога більше ракурсів. Чим швидше ви знімаєте, тим менша ймовірність отримати хороші зображення залежно від витримки.

Після того, як я закінчив зйомку, я надіслав відео на свою електронну пошту та розпакував відео на локальний диск. Звідти я використовував ffmepg щоб порізати відео на кадри, щоб полегшити прийом Meshroom:

mkdir nugget_images
ffmpeg -i VIDEO.mov ffmpeg nugget_images/nugget_%06d.jpg

Відкрийте Meshroom і використовуйте графічний інтерфейс, щоб перетягнути nugget_images папку на панель ліворуч. Звідти вибирайте Start і зачекайте кілька годин (або менше) залежно від тривалості відео та наявності машини з підтримкою CUDA.

Ви повинні побачити щось на кшталт наступного знімка екрана, коли він буде майже завершений.

Збір даних: маніпуляції блендером

Після завершення реконструкції Meshroom виконайте наступні кроки:

  1. Відкрийте GUI Blender і на філе меню, виберіть Імпортувати, Потім виберіть Хвильовий фронт (.obj) до створеного файлу текстури з Meshroom.
    Файл має бути збережено в path/to/MeshroomCache/Texturing/uuid-string/texturedMesh.obj.
  2. Завантажте файл і спостерігайте за чудовиськом, яким є ваш 3D-об’єкт.

    Ось тут стає трохи складніше.
  3. Прокрутіть до верхнього правого боку та виберіть Каркас значок в Затінення вікна перегляду.
  4. Виберіть об’єкт у правому вікні перегляду та переконайтеся, що він виділений, перейдіть до основного вікна перегляду макета та натисніть таб або вибрати вручну Режим редагування.
  5. Далі маневруйте вікном перегляду таким чином, щоб дозволити собі бачити свій об’єкт якомога менше за ним. Вам доведеться зробити це кілька разів, щоб справді все було правильно.
  6. Натисніть і перетягніть обмежувальну рамку над об’єктом, щоб виділити лише самородок.
  7. Після того, як він буде виділений, як на наступному знімку екрана, ми відокремлюємо наш самородок від 3D-маси, клацнувши лівою кнопкою миші та вибравши Окремий, А потім вибір.

    Тепер ми переходимо праворуч, де маємо побачити два текстурованих об’єкти: texturedMesh та texturedMesh.001.
  8. Наш новий об'єкт має бути texturedMesh.001, тому вибираємо texturedMesh І вибирай видаляти щоб видалити непотрібну масу.
  9. Виберіть об'єкт (texturedMesh.001) праворуч перейдіть до нашого засобу перегляду та виберіть об’єкт, Встановити Початок та Початок до центру мас.

Тепер, якщо ми хочемо, ми можемо перемістити наш об’єкт у центр вікна перегляду (або просто залишити його там, де він є) і побачити його у всій красі. Зверніть увагу на велику чорну діру, звідки ми не отримали хорошого кінопокриття! Нам потрібно буде це виправити.

Щоб очистити наш об’єкт від піксельних домішок, ми експортуємо наш об’єкт у файл .obj. Обов’язково вибирайте Лише вибір при експорті.

Збір даних: очищення за допомогою миттєвих сіток

Тепер у нас є дві проблеми: у нашому зображенні є піксельна щілина, утворена нашою поганою зйомкою, яку нам потрібно очистити, і наше зображення неймовірно щільне (що зробить створення зображень надзвичайно трудомістким). Щоб вирішити обидві проблеми, нам потрібно використовувати програмне забезпечення під назвою Instant Meshes, щоб екстраполювати поверхню пікселів, щоб покрити чорну діру, а також зменшити загальний об’єкт до меншого розміру з меншою щільністю.

  1. Відкрийте Instant Meshes і завантажте нещодавно збережені nugget.obj файлу.
  2. під Поле орієнтаціївиберіть Вирішити.
  3. під Поле позиціївиберіть Вирішити.
    Ось тут стає цікаво. Якщо ви досліджуєте свій об’єкт і помічаєте, що перехресні лінії засобу визначення позиції виглядають роз’єднаними, ви можете вибрати піктограму гребінця під Поле орієнтації і правильно перемалюйте лінії.
  4. Вибирати Вирішити як для Поле орієнтації та Поле позиції.
  5. Якщо все виглядає добре, експортуйте сітку, назвіть її приблизно так nugget_refined.obj, і збережіть його на диску.

Збір даних: струсіть і запікайте!

Оскільки наша низькополігональна сітка не має жодної текстури зображення, пов’язаної з нею, а наша високополігональна сітка має, нам потрібно або запекти високополіграфічну текстуру на низькополігональній сітці, або створити нову текстуру та призначити її наш об'єкт. Для простоти ми створимо текстуру зображення з нуля та застосуємо її до нашого самородка.

Я використовував пошук зображень Google для нагетсів та інших смажених речей, щоб отримати зображення поверхні смаженого предмета у високій роздільній здатності. Я знайшов зображення смаженої сирної маси з надвисокою роздільною здатністю та створив нове зображення, повне смаженої текстури.

З цим зображенням я готовий виконати наступні кроки:

  1. Відкрийте Blender і завантажте новий nugget_refined.obj так само, як ви завантажили свій початковий об’єкт: на філе меню, виберіть Імпортувати, Хвильовий фронт (.obj)і виберіть nugget_refined.obj файлу.
  2. Далі, перейдіть до Затінення Вкладка.
    Внизу ви повинні помітити дві рамки з заголовками Принциповий BDSF та Вихід матеріалу.
  3. на додавати меню, виберіть текстура та Текстура зображення.
    An Текстура зображення повинно з’явитися поле.
  4. Вибирати Відкрити зображення і завантажте зображення смаженої текстури.
  5. Перетягніть мишу між ними Колір в Текстура зображення коробка і Базовий колір в Принциповий BDSF коробка

Тепер ваш самородок готовий!

Збір даних: Створення змінних середовища Blender

Тепер, коли у нас є базовий об’єкт самородка, нам потрібно створити кілька колекцій і змінних середовища, які допоможуть нам у нашому процесі.

  1. Клацніть лівою кнопкою миші область сцени руки та виберіть Нова колекція.
  2. Створіть такі колекції: ІСТОРІЯ, НАРОДЖЕННЯ та ПОРОДИЛАСЯ.
  3. Перетягніть самородок до НАРОДЖЕННЯ колекцію та перейменуйте її nugget_base.

Збір даних: створити площину

Ми збираємося створити фоновий об’єкт, з якого генеруватимуться наші самородки під час відтворення зображень. У реальному випадку використання на цій площині розміщуються наші самородки, наприклад, піднос або контейнер.

  1. на додавати меню, виберіть меш , А потім Літак.
    Звідси ми переходимо в праву частину сторінки та знаходимо помаранчеву рамку (Властивості об'єкта).
  2. У Перетворення панель, для XYZ Ейлер, набір X до 46.968, Y до 46.968, і Z в 1.0.
  3. Для обох Місце розташування та Обертання, набір X, Y та Z в 0.

Збір даних: установіть камеру та вісь

Далі ми збираємося правильно налаштувати наші камери, щоб ми могли створювати зображення.

  1. на додавати меню, виберіть порожній та Звичайна вісь.
  2. Назвіть предмет Головна вісь.
  3. Переконайтеся, що наша вісь дорівнює 0 для всіх змінних (тобто вона знаходиться безпосередньо в центрі).
  4. Якщо у вас уже є створена камера, перетягніть її в розділ «Головна вісь».
  5. Вибирати пункт та Перетворення.
  6. для Місце розташування, набір X до 0, Y до 0, і Z в 100.

Збір даних: Ось сонце

Далі ми додаємо об’єкт Sun.

  1. на додавати меню, виберіть Light та Sun.
    Розташування цього об’єкта не обов’язково має значення, якщо він знаходиться в центрі десь над плоским об’єктом, який ми встановили.
  2. Виберіть зелену піктограму лампочки на нижній правій панелі (Властивості даних об’єкта) і встановіть міцність 5.0.
  3. Повторіть ту саму процедуру, щоб додати a Light предмет і помістіть його в довільне місце над площиною.

Збір даних: завантажуйте випадкові фони

Щоб зробити зображення випадковими, ми завантажуємо якомога більше випадкових текстур texture.ninja як ми можемо (наприклад, цегли). Завантажте в папку у вашій робочій області під назвою random_textures. Я завантажив близько 50.

Створення зображень

Тепер ми переходимо до найцікавішого: створення зображень.

Конвеєр генерації зображень: Object3D і DensityController

Почнемо з деяких визначень коду:

class Object3D:
	'''
	object container to store mesh information about the
	given object

	Returns
	the Object3D object
	'''
	def __init__(self, object: Union[bpy.types.Object, str]):
		"""Creates a Object3D object.

		Args:
		obj (Union[bpy.types.Object, str]): Scene object (or it's name)
		"""
		self.object = object
		self.obj_poly = None
		self.mat = None
		self.vert = None
		self.poly = None
		self.bvht = None
		self.calc_mat()
		self.calc_world_vert()
		self.calc_poly()
		self.calc_bvht()

	def calc_mat(self) -> None:
		"""store an instance of the object's matrix_world"""
		self.mat = self.object.matrix_world

	def calc_world_vert(self) -> None:
		"""calculate the verticies from object's matrix_world perspective"""
		self.vert = [self.mat @ v.co for v in self.object.data.vertices]
		self.obj_poly = np.array(self.vert)

	def calc_poly(self) -> None:
		"""store an instance of the object's polygons"""
		self.poly = [p.vertices for p in self.object.data.polygons]

	def calc_bvht(self) -> None:
		"""create a BVHTree from the object's polygon"""
		self.bvht = BVHTree.FromPolygons( self.vert, self.poly )

	def regenerate(self) -> None:
		"""reinstantiate the object's variables;
		used when the object is manipulated after it's creation"""
		self.calc_mat()
		self.calc_world_vert()
		self.calc_poly()
		self.calc_bvht()

	def __repr__(self):
		return "Object3D: " + self.object.__repr__()

Спочатку ми визначимо базовий клас контейнера з деякими важливими властивостями. Цей клас в основному існує для того, щоб дозволити нам створити дерево BVH (спосіб представлення нашого об’єкта самородка в 3D-просторі), де нам потрібно буде використовувати BVHTree.overlap метод, щоб побачити, чи два незалежні згенеровані об’єкти-самородки перекриваються в нашому 3D-просторі. Про це пізніше.

Друга частина коду — це контролер щільності. Це служить способом прив’язати себе до правил реальності, а не 3D-світу. Наприклад, у світі 3D Blender об’єкти в Blender можуть існувати один в одному; однак, якщо хтось не проводить якусь дивну науку з нашими курячими нагетсами, ми хочемо переконатися, що жодні два нагетси не перекриваються настільки, щоб зробити це візуально нереалістичним.

Ми використовуємо наш Plane об’єкт для створення набору обмежених невидимих ​​кубів, які можна запитувати в будь-який момент часу, щоб побачити, чи зайнятий простір чи ні.


Дивіться наступний код:

class DensityController:
    """Container that controlls the spacial relationship between 3D objects

    Returns:
        DensityController: The DensityController object.
    """
    def __init__(self):
        self.bvhtrees = None
        self.overlaps = None
        self.occupied = None
        self.unoccupied = None
        self.objects3d = []

    def auto_generate_kdtree_cubes(
        self,
        num_objects: int = 100, # max size of nuggets
    ) -> None:
        """
        function to generate physical kdtree cubes given a plane of -resize- size
        this allows us to access each cube's overlap/occupancy status at any given
        time
        
        creates a KDTree collection, a cube, a set of individual cubes, and the 
        BVHTree object for each individual cube

        Args:
            resize (Tuple[float]): the size of a cube to create XYZ.
            cuts (int): how many cuts are made to the cube face
                12 cuts == 13 Rows x 13 Columns  
        """

У наступному фрагменті ми вибираємо самородок і створюємо обмежувальний куб навколо цього самородка. Цей куб представляє розмір одного псевдовокселя нашого об’єкта псевдо-kdtree. Нам потрібно використовувати bpy.context.view_layer.update() оскільки коли цей код виконується зсередини функції чи сценарію, а не з інтерфейсу blender-ui, здається, що view_layer не оновлюється автоматично.

        # read the nugget,
        # see how large the cube needs to be to encompass a single nugget
        # then touch a parameter to allow it to be smaller or larger (eg more touching)
        bpy.context.view_layer.objects.active = bpy.context.scene.objects.get('nugget_base')
        bpy.ops.object.origin_set(type='ORIGIN_GEOMETRY', center='BOUNDS')
        #create a cube for the bounding box
        bpy.ops.mesh.primitive_cube_add(location=Vector((0,0,0))) 
        #our new cube is now the active object, so we can keep track of it in a variable:
        bound_box = bpy.context.active_object
        bound_box.name = 'CUBE1'
        bpy.context.view_layer.update()
        #copy transforms
        nug_dims = bpy.data.objects["nugget_base"].dimensions
        bpy.data.objects["CUBE1"].dimensions = nug_dims
        bpy.context.view_layer.update()
        bpy.data.objects["CUBE1"].location = bpy.data.objects["nugget_base"].location
        bpy.context.view_layer.update()
        bpy.data.objects["CUBE1"].rotation_euler = bpy.data.objects["nugget_base"].rotation_euler
        bpy.context.view_layer.update()
        print("bound_box.dimensions: ", bound_box.dimensions)
        print("bound_box.location:", bound_box.location)

Далі ми трохи оновлюємо наш об’єкт-куб, щоб його довжина та ширина були квадратними, на відміну від природного розміру самородка, з якого він був створений:

        # this cube created isn't always square, but we're going to make it square
        # to fit into our 
        x, y, z = bound_box.dimensions
        v = max(x, y)
        if np.round(v) < v:
            v = np.round(v)+1
        bb_x, bb_y = v, v
        bound_box.dimensions = Vector((v, v, z))
        bpy.context.view_layer.update()
        print("bound_box.dimensions updated: ", bound_box.dimensions)
        # now we generate a plane
        # calc the size of the plane given a max number of boxes.

Тепер ми використовуємо наш оновлений об’єкт-куб, щоб створити площину, яка може зберігати об’єм num_objects кількість нагетсів:

        x, y, z = bound_box.dimensions
        bb_loc = bound_box.location
        bb_rot_eu = bound_box.rotation_euler
        min_area = (x*y)*num_objects
        min_length = min_area / num_objects
        print(min_length)
        # now we generate a plane
        # calc the size of the plane given a max number of boxes.
        bpy.ops.mesh.primitive_plane_add(location=Vector((0,0,0)), size = min_length)
        plane = bpy.context.selected_objects[0]
        plane.name = 'PLANE'
        # move our plane to our background collection
        # current_collection = plane.users_collection
        link_object('PLANE', 'BACKGROUND')
        bpy.context.view_layer.update()

Ми беремо наш плоский об’єкт і створюємо гігантський куб такої ж довжини та ширини, як наша площина, з висотою нашого самородкового куба, КУБ1:

        # New Collection
        my_coll = bpy.data.collections.new("KDTREE")
        # Add collection to scene collection
        bpy.context.scene.collection.children.link(my_coll)
        # now we generate cubes based on the size of the plane.
        bpy.ops.mesh.primitive_cube_add(location=Vector((0,0,0)), size = min_length)
        bpy.context.view_layer.update()
        cube = bpy.context.selected_objects[0]
        cube_dimensions = cube.dimensions
        bpy.context.view_layer.update()
        cube.dimensions = Vector((cube_dimensions[0], cube_dimensions[1], z))
        bpy.context.view_layer.update()
        cube.location = bb_loc
        bpy.context.view_layer.update()
        cube.rotation_euler = bb_rot_eu
        bpy.context.view_layer.update()
        cube.name = 'cube'
        bpy.context.view_layer.update()
        current_collection = cube.users_collection
        link_object('cube', 'KDTREE')
        bpy.context.view_layer.update()

Звідси ми хочемо створити вокселі з нашого куба. Беремо стільки кубиків, скільки хочемо помістити num_objects а потім виріжте їх із нашого об’єкта куба. Ми шукаємо спрямовану вгору сітчасту грань нашого куба, а потім вибираємо цю грань, щоб зробити розрізи. Перегляньте наступний код:

        # get the bb volume and make the proper cuts to the object 
        bb_vol = x*y*z
        cube_vol = cube_dimensions[0]*cube_dimensions[1]*cube_dimensions[2]
        n_cubes = cube_vol / bb_vol
        cuts = n_cubes / ((x+y) / 2)
        cuts = int(np.round(cuts)) - 1 # 
        # select the cube
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.context.view_layer.update()
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.data.objects['cube'].select_set(True) # Blender 2.8x
        bpy.context.view_layer.objects.active = bpy.context.scene.objects.get('cube')
        # set to edit mode
        bpy.ops.object.mode_set(mode='EDIT', toggle=False)
        print('edit mode success')
        # get face_data
        context = bpy.context
        obj = context.edit_object
        me = obj.data
        mat = obj.matrix_world
        bm = bmesh.from_edit_mesh(me)
        up_face = None
        # select upwards facing cube-face
        # https://blender.stackexchange.com/questions/43067/get-a-face-selected-pointing-upwards
        for face in bm.faces:
            if (face.normal-UP_VECTOR).length < EPSILON:
                up_face = face
                break
        assert(up_face)
        # subdivide the edges to get the perfect kdtree cubes
        bmesh.ops.subdivide_edges(bm,
                edges=up_face.edges,
                use_grid_fill=True,
                cuts=cuts)
        bpy.context.view_layer.update()
        # get the center point of each face

Нарешті, ми обчислюємо центр верхньої грані кожного розрізу, який ми зробили з нашого великого куба, і створюємо справжні куби з цих розрізів. Кожен із цих щойно створених кубів представляє окремий шматок простору для створення або переміщення самородків навколо нашого літака. Перегляньте наступний код:

        face_data = {}
        sizes = []
        for f, face in enumerate(bm.faces): 
            face_data[f] = {}
            face_data[f]['calc_center_bounds'] = face.calc_center_bounds()
            loc = mat @ face_data[f]['calc_center_bounds']
            face_data[f]['loc'] = loc
            sizes.append(loc[-1])
        # get the most common cube-z; we use this to determine the correct loc
        counter = Counter()
        counter.update(sizes)
        most_common = counter.most_common()[0][0]
        cube_loc = mat @ cube.location
        # get out of edit mode
        bpy.ops.object.mode_set(mode='OBJECT', toggle=False)
        # go to new colection
        bvhtrees = {}
        for f in face_data:
            loc = face_data[f]['loc']
            loc = mat @ face_data[f]['calc_center_bounds']
            print(loc)
            if loc[-1] == most_common:
                # set it back down to the floor because the face is elevated to the
                # top surface of the cube
                loc[-1] = cube_loc[-1]
                bpy.ops.mesh.primitive_cube_add(location=loc, size = x)
                cube = bpy.context.selected_objects[0]
                cube.dimensions = Vector((x, y, z))
                # bpy.context.view_layer.update()
                cube.name = "cube_{}".format(f)
                #my_coll.objects.link(cube)
                link_object("cube_{}".format(f), 'KDTREE')
                #bpy.context.view_layer.update()
                bvhtrees[f] = {
                    'occupied' : 0,
                    'object' : Object3D(cube)
                }
        for object in bpy.data.objects:
            object.select_set(False)
        bpy.data.objects['CUBE1'].select_set(True) # Blender 2.8x
        bpy.ops.object.delete()
        return bvhtrees

Далі ми розробляємо алгоритм, який визначає, які куби зайняті в будь-який момент часу, визначає, які об’єкти накладаються один на одного, і переміщує об’єкти, що перекриваються, окремо в вільний простір. Ми не зможемо повністю позбутися всіх накладень, але ми можемо зробити так, щоб це виглядало досить реально.



Дивіться наступний код:

    def find_occupied_space(
        self, 
        objects3d: List[Object3D],
    ) -> None:
        """
        discover which cube's bvhtree is occupied in our kdtree space

        Args:
            list of Object3D objects

        """
        count = 0
        occupied = []
        for i in self.bvhtrees:
            bvhtree = self.bvhtrees[i]['object']
            for object3d in objects3d:
                if object3d.bvht.overlap(bvhtree.bvht):
                    self.bvhtrees[i]['occupied'] = 1

    def find_overlapping_objects(
        self, 
        objects3d: List[Object3D],
    ) -> List[Tuple[int]]:
        """
        returns which Object3D objects are overlapping

        Args:
            list of Object3D objects
        
        Returns:
            List of indicies from objects3d that are overlap
        """
        count = 0
        overlaps = []
        for i, x_object3d in enumerate(objects3d):
            for ii, y_object3d in enumerate(objects3d[i+1:]):
                if x_object3d.bvht.overlap(y_object3d.bvht):
                    overlaps.append((i, ii))
        return overlaps

    def calc_most_overlapped(
        self,
        overlaps: List[Tuple[int]]
    ) -> List[Tuple[int]]:
        """
        Algorithm to count the number of edges each index has
        and return a sorted list from most->least with the number
        of edges each index has. 

        Args:
            list of indicies that are overlapping
        
        Returns:
            list of indicies with the total number of overlapps they have 
            [index, count]
        """
        keys = {}
        for x,y in overlaps:
            if x not in keys:
                keys[x] = 0
            if y not in keys:
                keys[y] = 0
            keys[x]+=1
            keys[y]+=1
        # sort by most edges first
        index_counts = sorted(keys.items(), key=lambda x: x[1])[::-1]
        return index_counts
    
    def get_random_unoccupied(
        self
    ) -> Union[int,None]:
        """
        returns a randomly chosen unoccuped kdtree cube

        Return
            either the kdtree cube's key or None (meaning all spaces are
            currently occupied)
            Union[int,None]
        """
        unoccupied = []
        for i in self.bvhtrees:
            if not self.bvhtrees[i]['occupied']:
                unoccupied.append(i)
        if unoccupied:
            random.shuffle(unoccupied)
            return unoccupied[0]
        else:
            return None

    def regenerate(
        self,
        iterable: Union[None, List[Object3D]] = None
    ) -> None:
        """
        this function recalculates each objects world-view information
        we default to None, which means we're recalculating the self.bvhtree cubes

        Args:
            iterable (None or List of Object3D objects). if None, we default to
            recalculating the kdtree
        """
        if isinstance(iterable, list):
            for object in iterable:
                object.regenerate()
        else:
            for idx in self.bvhtrees:
                self.bvhtrees[idx]['object'].regenerate()
                self.update_tree(idx, occupied=0)       

    def process_trees_and_objects(
        self,
        objects3d: List[Object3D],
    ) -> List[Tuple[int]]:
        """
        This function finds all overlapping objects within objects3d,
        calculates the objects with the most overlaps, searches within
        the kdtree cube space to see which cubes are occupied. It then returns 
        the edge-counts from the most overlapping objects

        Args:
            list of Object3D objects
        Returns
            this returns the output of most_overlapped
        """
        overlaps = self.find_overlapping_objects(objects3d)
        most_overlapped = self.calc_most_overlapped(overlaps)
        self.find_occupied_space(objects3d)
        return most_overlapped

    def move_objects(
        self, 
        objects3d: List[Object3D],
        most_overlapped: List[Tuple[int]],
        z_increase_offset: float = 2.,
    ) -> None:
        """
        This function iterates through most-overlapped, and uses 
        the index to extract the matching object from object3d - it then
        finds a random unoccupied kdtree cube and moves the given overlapping
        object to that space. It does this for each index from the most-overlapped
        function

        Args:
            objects3d: list of Object3D objects
            most_overlapped: a list of tuples (index, count) - where index relates to
                where it's found in objects3d and count - how many times it overlaps 
                with other objects
            z_increase_offset: this value increases the Z value of the object in order to
                make it appear as though it's off the floor. If you don't augment this value
                the object looks like it's 'inside' the ground plane
        """
        for idx, cnt in most_overlapped:
            object3d = objects3d[idx]
            unoccupied_idx = self.get_random_unoccupied()
            if unoccupied_idx:
                object3d.object.location =  self.bvhtrees[unoccupied_idx]['object'].object.location
                # ensure the nuggest is above the groundplane
                object3d.object.location[-1] = z_increase_offset
                self.update_tree(unoccupied_idx, occupied=1)
    
    def dynamic_movement(
        self, 
        objects3d: List[Object3D],
        tries: int = 100,
        z_offset: float = 2.,
    ) -> None:
        """
        This function resets all objects to get their current positioning
        and randomly moves objects around in an attempt to avoid any object
        overlaps (we don't want two objects to be spawned in the same position)

        Args:
            objects3d: list of Object3D objects
            tries: int the number of times we want to move objects to random spaces
                to ensure no overlaps are present.
            z_offset: this value increases the Z value of the object in order to
                make it appear as though it's off the floor. If you don't augment this value
                the object looks like it's 'inside' the ground plane (see `move_objects`)
        """
    
        # reset all objects
        self.regenerate(objects3d)
        # regenerate bvhtrees
        self.regenerate(None)

        most_overlapped = self.process_trees_and_objects(objects3d)
        attempts = 0
        while most_overlapped:
            if attempts>=tries:
                break
            self.move_objects(objects3d, most_overlapped, z_offset)
            attempts+=1
            # recalc objects
            self.regenerate(objects3d)
            # regenerate bvhtrees
            self.regenerate(None)
            # recalculate overlaps
            most_overlapped = self.process_trees_and_objects(objects3d)

    def generate_spawn_point(
        self,
    ) -> Vector:
        """
        this function generates a random spawn point by finding which
        of the kdtree-cubes are unoccupied, and returns one of those

        Returns
            the Vector location of the kdtree-cube that's unoccupied
        """
        idx = self.get_random_unoccupied()
        print(idx)
        self.update_tree(idx, occupied=1)
        return self.bvhtrees[idx]['object'].object.location

    def update_tree(
        self,
        idx: int,
        occupied: int,
    ) -> None:
        """
        this function updates the given state (occupied vs. unoccupied) of the
        kdtree given the idx

        Args:
            idx: int
            occupied: int
        """
        self.bvhtrees[idx]['occupied'] = occupied

Конвеєр генерації зображень: Cool runs

У цьому розділі ми розбираємо наші run функція виконується.

Ініціалізуємо нашу DensityController і створіть щось під назвою заставка за допомогою ImageSaver від zpy. Це дозволяє нам легко зберігати наші візуалізовані зображення в будь-якому вибраному нами місці. Потім ми додаємо свій nugget категорія (і якби у нас було більше категорій, ми б додали їх сюди). Перегляньте наступний код:

@gin.configurable("run")
@zpy.blender.save_and_revert
def run(
    max_num_nuggets: int = 100,
    jitter_mesh: bool = True,
    jitter_nugget_scale: bool = True,
    jitter_material: bool = True,
    jitter_nugget_material: bool = False,
    number_of_random_materials: int = 50,
    nugget_texture_path: str = os.getcwd()+"/nugget_textures",
    annotations_path = os.getcwd()+'/nugget_data',
):
    """
    Main run function.
    """
    density_controller = DensityController()
    # Random seed results in unique behavior
    zpy.blender.set_seed(random.randint(0,1000000000))

    # Create the saver object
    saver = zpy.saver_image.ImageSaver(
        description="Image of the randomized Amazon nuggets",
        output_dir=annotations_path,
    )
    saver.add_category(name="nugget")

Далі нам потрібно створити вихідний об’єкт, з якого ми створимо копію самородків; у цьому випадку це nugget_base які ми створили:

    # Make a list of source nugget objects
    source_nugget_objects = []
    for obj in zpy.objects.for_obj_in_collections(
        [
            bpy.data.collections["NUGGET"],
        ]
    ):
        assert(obj!=None)

        # pass on everything not named nugget
        if 'nugget_base' not in obj.name:
            print('passing on {}'.format(obj.name))
            continue
        zpy.objects.segment(obj, name="nugget", as_category=True) #color=nugget_seg_color
        print("zpy.objects.segment: check {}".format(obj.name))
        source_nugget_objects.append(obj.name)

Тепер, коли у нас є базовий самородок, ми збираємося зберегти світові пози (розташування) усіх інших об’єктів, щоб після кожного запуску візуалізації ми могли використовувати ці збережені пози для повторної ініціалізації візуалізації. Ми також повністю переміщаємо наш базовий самородок, щоб kdtree не відчувало, що простір зайнято. Нарешті, ми ініціалізуємо наші об’єкти kdtree-cube. Перегляньте наступний код:

    # move nugget point up 10 z's so it won't collide with base-cube
    bpy.data.objects["nugget_base"].location[-1] = 10

    # Save the position of the camera and light
    # create light and camera
    zpy.objects.save_pose("Camera")
    zpy.objects.save_pose("Sun")
    zpy.objects.save_pose("Plane")
    zpy.objects.save_pose("Main Axis")
    axis = bpy.data.objects['Main Axis']
    print('saving poses')
    # add some parameters to this 

    # get the plane-3d object
    plane3d = Object3D(bpy.data.objects['Plane'])

    # generate kdtree cubes
    density_controller.generate_kdtree_cubes()

Наступний код збирає наші завантажені фони з texture.ninja, де вони будуть використані для випадкового проектування на нашу площину:

    # Pre-create a bunch of random textures
    #random_materials = [
    #    zpy.material.random_texture_mat() for _ in range(number_of_random_materials)
    #]
    p = os.path.abspath(os.getcwd()+'/random_textures')
    print(p)
    random_materials = []
    for x in os.listdir(p):
        texture_path = Path(os.path.join(p,x))
        y = zpy.material.make_mat_from_texture(texture_path, name=texture_path.stem)
        random_materials.append(y)
    #print(random_materials[0])

    # Pre-create a bunch of random textures
    random_nugget_materials = [
        random_nugget_texture_mat(Path(nugget_texture_path)) for _ in range(number_of_random_materials)
    ]

Ось де починається магія. Спочатку ми регенеруємо kdtree-куби для цього запуску, щоб ми могли почати заново:

    # Run the sim.
    for step_idx in zpy.blender.step():
        density_controller.generate_kdtree_cubes()

        objects3d = []
        num_nuggets = random.randint(40, max_num_nuggets)
        log.info(f"Spawning {num_nuggets} nuggets.")
        spawned_nugget_objects = []
        for _ in range(num_nuggets):

Ми використовуємо наш контролер щільності, щоб створити випадкову точку появи для нашого самородка, створити копію nugget_baseі перемістіть копію до випадково згенерованої точки появи:

            # Choose location to spawn nuggets
            spawn_point = density_controller.generate_spawn_point()
            # manually spawn above the floor
            # spawn_point[-1] = 1.8 #2.0

            # Pick a random object to spawn
            _name = random.choice(source_nugget_objects)
            log.info(f"Spawning a copy of source nugget {_name} at {spawn_point}")
            obj = zpy.objects.copy(
                bpy.data.objects[_name],
                collection=bpy.data.collections["SPAWNED"],
                is_copy=True,
            )

            obj.location = spawn_point
            obj.matrix_world = mathutils.Matrix.Translation(spawn_point)
            spawned_nugget_objects.append(obj)

Далі ми випадково змінюємо розмір самородка, сітку самородка та масштаб самородка, щоб жодні два самородки не виглядали однаково:

            # Segment the newly spawned nugget as an instance
            zpy.objects.segment(obj)

            # Jitter final pose of the nugget a little
            zpy.objects.jitter(
                obj,
                rotate_range=(
                    (0.0, 0.0),
                    (0.0, 0.0),
                    (-math.pi * 2, math.pi * 2),
                ),
            )

            if jitter_nugget_scale:
                # Jitter the scale of each nugget
                zpy.objects.jitter(
                    obj,
                    scale_range=(
                        (0.8, 2.0), #1.2
                        (0.8, 2.0), #1.2
                        (0.8, 2.0), #1.2
                    ),
                )

            if jitter_mesh:
                # Jitter (deform) the mesh of each nugget
                zpy.objects.jitter_mesh(
                    obj=obj,
                    scale=(
                        random.uniform(0.01, 0.03),
                        random.uniform(0.01, 0.03),
                        random.uniform(0.01, 0.03),
                    ),
                )

            if jitter_nugget_material:
                # Jitter the material (apperance) of each nugget
                for i in range(len(obj.material_slots)):
                    obj.material_slots[i].material = random.choice(random_nugget_materials)
                    zpy.material.jitter(obj.material_slots[i].material)          

Ми перетворюємо нашу копію самородка в Object3D об’єкт, де ми використовуємо функціональні можливості дерева BVH, щоб побачити, чи наша площина перетинає або перекриває будь-яку грань або вершини на нашій копії самородка. Якщо ми знаходимо перекриття з площиною, ми просто переміщуємо самородок вгору по його осі Z. Перегляньте наступний код:

            # create 3d obj for movement
            nugget3d = Object3D(obj)

            # make sure the bottom most part of the nugget is NOT
            # inside the plane-object       
            plane_overlap(plane3d, nugget3d)

            objects3d.append(nugget3d)

Тепер, коли всі самородки створено, ми використовуємо наші DensityController щоб переміщати самородки так, щоб у нас була мінімальна кількість перекривань, а ті, які перекриваються, не виглядали жахливо:

        # ensure objects aren't on top of each other
        density_controller.dynamic_movement(objects3d)

У наступному коді: ми відновлюємо Camera та Main Axis позах і випадковим чином виберіть відстань камери до Plane об’єкт:

        # Return camera to original position
        zpy.objects.restore_pose("Camera")
        zpy.objects.restore_pose("Main Axis")
        zpy.objects.restore_pose("Camera")
        zpy.objects.restore_pose("Main Axis")

        # assert these are the correct versions...
        assert(bpy.data.objects["Camera"].location == Vector((0,0,100)))
        assert(bpy.data.objects["Main Axis"].location == Vector((0,0,0)))
        assert(bpy.data.objects["Main Axis"].rotation_euler == Euler((0,0,0)))

        # alter the Z ditance with the camera
        bpy.data.objects["Camera"].location = (0, 0, random.uniform(0.75, 3.5)*100)

Ми вирішуємо, як випадково ми хочемо, щоб камера рухалася вздовж Main Axis. Залежно від того, чи хочемо ми, щоб він був переважно над головою, чи нам дуже важливий кут, під яким він бачить дошку, ми можемо налаштувати top_down_mostly залежно від того, наскільки добре наша навчальна модель вловлює сигнал «Що взагалі таке самородок?»

        # alter the main-axis beta/gamma params
        top_down_mostly = False 
        if top_down_mostly:
            zpy.objects.rotate(
                bpy.data.objects["Main Axis"],
                rotation=(
                    random.uniform(0.05, 0.05),
                    random.uniform(0.05, 0.05),
                    random.uniform(0.05, 0.05),
                ),
            )
        else:
            zpy.objects.rotate(
                bpy.data.objects["Main Axis"],
                rotation=(
                    random.uniform(-1., 1.),
                    random.uniform(-1., 1.),
                    random.uniform(-1., 1.),
                ),
            )

        print(bpy.data.objects["Main Axis"].rotation_euler)
        print(bpy.data.objects["Camera"].location)

У наступному коді ми робимо те ж саме з Sun і навмання виберіть текстуру для Plane об’єкт:

        # change the background material
        # Randomize texture of shelf, floors and walls
        for obj in bpy.data.collections["BACKGROUND"].all_objects:
            for i in range(len(obj.material_slots)):
                # TODO
                # Pick one of the random materials
                obj.material_slots[i].material = random.choice(random_materials)
                if jitter_material:
                    zpy.material.jitter(obj.material_slots[i].material)
                # Sets the material relative to the object
                obj.material_slots[i].link = "OBJECT"
        # Pick a random hdri (from the local textures folder for background background)
        zpy.hdris.random_hdri()
        # Return light to original position
        zpy.objects.restore_pose("Sun")

        # Jitter the light position
        zpy.objects.jitter(
            "Sun",
            translate_range=(
                (-5, 5),
                (-5, 5),
                (-5, 5),
            ),
        )
        bpy.data.objects["Sun"].data.energy = random.uniform(0.5, 7)

Нарешті, ми ховаємо всі наші об’єкти, які ми не хочемо відображати: nugget_base і вся наша структура куба:

# we hide the cube objects
for obj in # we hide the cube objects for obj in bpy.data.objects: if 'cube' in obj.name: obj.hide_render = True try: zpy.objects.toggle_hidden(obj, hidden=True) except: # deal with this exception here... pass # we hide our base nugget object bpy.data.objects["nugget_base"].hide_render = True zpy.objects.toggle_hidden(bpy.data.objects["nugget_base"], hidden=True)

Нарешті, ми використовуємо zpy щоб відобразити нашу сцену, збережіть наші зображення, а потім збережіть наші анотації. Для цієї публікації я вніс деякі невеликі зміни в zpy бібліотека анотацій для мого конкретного випадку використання (анотація до зображення замість одного файлу для проекту), але вам не потрібно це робити для цілей цієї публікації).

        # create the image name
        image_uuid = str(uuid.uuid4())

        # Name for each of the output images
        rgb_image_name = format_image_string(image_uuid, 'rgb')
        iseg_image_name = format_image_string(image_uuid, 'iseg')
        depth_image_name = format_image_string(image_uuid, 'depth')

        zpy.render.render(
            rgb_path=saver.output_dir / rgb_image_name,
            iseg_path=saver.output_dir / iseg_image_name,
            depth_path=saver.output_dir / depth_image_name,
        )

        # Add images to saver
        saver.add_image(
            name=rgb_image_name,
            style="default",
            output_path=saver.output_dir / rgb_image_name,
            frame=step_idx,
        )
    
        saver.add_image(
            name=iseg_image_name,
            style="segmentation",
            output_path=saver.output_dir / iseg_image_name,
            frame=step_idx,
        )
        saver.add_image(
            name=depth_image_name,
            style="depth",
            output_path=saver.output_dir / depth_image_name,
            frame=step_idx,
        )

        # ideally in this thread, we'll open the anno file
        # and write to it directly, saving it after each generation
        for obj in spawned_nugget_objects:
            # Add annotation to segmentation image
            saver.add_annotation(
                image=rgb_image_name,
                category="nugget",
                seg_image=iseg_image_name,
                seg_color=tuple(obj.seg.instance_color),
            )

        # Delete the spawned nuggets
        zpy.objects.empty_collection(bpy.data.collections["SPAWNED"])

        # Write out annotations
        saver.output_annotated_images()
        saver.output_meta_analysis()

        # # ZUMO Annotations
        _output_zumo = _OutputZUMO(saver=saver, annotation_filename = Path(image_uuid + ".zumo.json"))
        _output_zumo.output_annotations()
        # change the name here..
        saver.output_annotated_images()
        saver.output_meta_analysis()

        # remove the memory of the annotation to free RAM
        saver.annotations = []
        saver.images = {}
        saver.image_name_to_id = {}
        saver.seg_annotations_color_to_id = {}

    log.info("Simulation complete.")

if __name__ == "__main__":

    # Set the logger levels
    zpy.logging.set_log_levels("info")

    # Parse the gin-config text block
    # hack to read a specific gin config
    parse_config_from_file('nugget_config.gin')

    # Run the sim
    run()

Вуаля!

Запустіть безголовий сценарій створення

Тепер, коли у нас є збережений файл Blender, створений самородок і вся допоміжна інформація, давайте заархівуємо наш робочий каталог і scp на нашу машину GPU або завантажив через Служба простого зберігання Amazon (Amazon S3) або інший сервіс:

tar cvf working_blender_dir.tar.gz working_blender_dir
scp -i "your.pem" working_blender_dir.tar.gz ubuntu@EC2-INSTANCE.compute.amazonaws.com:/home/ubuntu/working_blender_dir.tar.gz

Увійдіть у свій екземпляр EC2 і розпакуйте папку working_blender:

tar xvf working_blender_dir.tar.gz

Тепер ми створюємо наші дані у всій їх красі:

blender working_blender_dir/nugget.blend --background --python working_blender_dir/create_synthetic_nuggets.py

Сценарій має працювати для 500 зображень, і дані зберігаються в /path/to/working_blender_dir/nugget_data.

Наступний код показує одну анотацію, створену з нашим набором даних:

{
    "metadata": {
        "description": "3D data of a nugget!",
        "contributor": "Matt Krzus",
        "url": "krzum@amazon.com",
        "year": "2021",
        "date_created": "20210924_000000",
        "save_path": "/home/ubuntu/working_blender_dir/nugget_data"
    },
    "categories": {
        "0": {
            "name": "nugget",
            "supercategories": [],
            "subcategories": [],
            "color": [
                0.0,
                0.0,
                0.0
            ],
            "count": 6700,
            "subcategory_count": [],
            "id": 0
        }
    },
    "images": {
        "0": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "style": "default",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.rgb.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 0
        },
        "1": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "style": "segmentation",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.iseg.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 1
        },
        "2": {
            "name": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "style": "depth",
            "output_path": "/home/ubuntu/working_blender_dir/nugget_data/a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "relative_path": "a0bb1fd3-c2ec-403c-aacf-07e0c07f4fdd.depth.png",
            "frame": 97,
            "width": 640,
            "height": 480,
            "id": 2
        }
    },
    "annotations": [
        {
            "image_id": 0,
            "category_id": 0,
            "id": 0,
            "seg_color": [
                1.0,
                0.6000000238418579,
                0.9333333373069763
            ],
            "color": [
                1.0,
                0.6,
                0.9333333333333333
            ],
            "segmentation": [
                [
                    299.0,
                    308.99,
                    292.0,
                    308.99,
                    283.01,
                    301.0,
                    286.01,
                    297.0,
                    285.01,
                    294.0,
                    288.01,
                    285.0,
                    283.01,
                    275.0,
                    287.0,
                    271.01,
                    294.0,
                    271.01,
                    302.99,
                    280.0,
                    305.99,
                    286.0,
                    305.99,
                    303.0,
                    302.0,
                    307.99,
                    299.0,
                    308.99
                ]
            ],
            "bbox": [
                283.01,
                271.01,
                22.980000000000018,
                37.98000000000002
            ],
            "area": 667.0802000000008,
            "bboxes": [
                [
                    283.01,
                    271.01,
                    22.980000000000018,
                    37.98000000000002
                ]
            ],
            "areas": [
                667.0802000000008
            ]
        },
        {
            "image_id": 0,
            "category_id": 0,
            "id": 1,
            "seg_color": [
                1.0,
                0.4000000059604645,
                1.0
            ],
            "color": [
                1.0,
                0.4,
                1.0
            ],
            "segmentation": [
                [
                    241.0,
                    273.99,
                    236.0,
                    271.99,
                    234.0,
                    273.99,
                    230.01,
                    270.0,
                    232.01,
                    268.0,
                    231.01,
                    263.0,
                    233.01,
                    261.0,
                    229.0,
                    257.99,
                    225.0,
                    257.99,
                    223.01,
                    255.0,
                    225.01,
                    253.0,
                    227.01,
                    246.0,
                    235.0,
                    239.01,
                    238.0,
                    239.01,
                    240.0,
                    237.01,
                    247.0,
                    237.01,
                    252.99,
                    245.0,
                    253.99,
                    252.0,
                    246.99,
                    269.0,
                    241.0,
                    273.99
                ]
            ],
            "bbox": [
                223.01,
                237.01,
                30.980000000000018,
                36.98000000000002
            ],
            "area": 743.5502000000008,
            "bboxes": [
                [
                    223.01,
                    237.01,
                    30.980000000000018,
                    36.98000000000002
                ]
            ],
            "areas": [
                743.5502000000008
            ]
        },
...
...
...

Висновок

У цій публікації я продемонстрував, як використовувати бібліотеку анімації з відкритим кодом Blender для створення наскрізного конвеєра синтетичних даних.

У Blender і AWS є маса цікавих речей; Сподіваюся, ця демонстрація допоможе вам у вашому наступному проекті, де бракує даних!

посилання


Про автора

Мет Крзус є старшим спеціалістом з обробки даних Amazon Web Service у групі професійних послуг AWS

Часова мітка:

Більше від AWS Машинне навчання