Crie dados sintéticos para pipelines de visão computacional na AWS

Nó Fonte: 1726779

Coletar e anotar dados de imagem é uma das tarefas que mais consomem recursos em qualquer projeto de visão computacional. Pode levar meses para coletar, analisar e experimentar totalmente os fluxos de imagem no nível necessário para competir no mercado atual. Mesmo depois de coletar dados com sucesso, você ainda terá um fluxo constante de erros de anotação, imagens mal enquadradas, pequenas quantidades de dados significativos em um mar de capturas indesejadas e muito mais. Esses principais gargalos são o motivo pelo qual a criação de dados sintéticos precisa estar no kit de ferramentas de todo engenheiro moderno. Ao criar representações 3D dos objetos que queremos modelar, podemos rapidamente prototipar algoritmos enquanto coletamos dados ao vivo simultaneamente.

Neste post, apresento um exemplo de uso da biblioteca de animação de código aberto Blender para criar um pipeline de dados sintéticos de ponta a ponta, usando nuggets de frango como exemplo. A imagem a seguir é uma ilustração dos dados gerados nesta postagem do blog.

O que é Blender?

liqüidificador é um software gráfico 3D de código aberto usado principalmente em animação, impressão 3D e realidade virtual. Ele possui um conjunto de rigging, animação e simulação extremamente abrangente que permite a criação de mundos 3D para praticamente qualquer caso de uso de visão computacional. Ele também possui uma comunidade de suporte extremamente ativa, onde a maioria, se não todos, os erros do usuário são resolvidos.

Configure seu ambiente local

Instalamos duas versões do Blender: uma em uma máquina local com acesso a uma GUI, e outra em um Amazon Elastic Compute Nuvem (Amazon EC2) instância P2.

Instale o Blender e o ZPY

Instale o Blender a partir do Site do Blender.

Em seguida, conclua as seguintes etapas:

  1. Execute os seguintes comandos:
    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. Copie os cabeçalhos necessários do Python na versão do Blender do Python para que você possa usar outras bibliotecas que não sejam do 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. Substitua sua versão do Blender e force as instalações para que o Python fornecido pelo Blender funcione:
    /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. Baixar zpy e instale a partir da fonte:
    git clone https://github.com/ZumoLabs/zpy
    cd zpy
    vi requirements.txt

  5. Altere a versão do NumPy para >=1.19.4 e scikit-image>=0.18.1 para fazer a instalação 3.10.2 possível e para que você não sobrescreva:
    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. Para garantir a compatibilidade com o Blender 3.2, acesse zpy/render.py e comente as duas linhas a seguir (para mais informações, consulte Falha do Blender 3.0 #54):
    #scene.render.tile_x = tile_size
    #scene.render.tile_y = tile_size

  7. Em seguida, instale o zpy biblioteca:
    /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. Baixe a versão complementar do zpy do GitHub repo para que você possa executar ativamente sua instância:
    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. Salve um arquivo chamado enable_zpy_addon.py na sua /home diretório e execute o comando de ativação, porque você não tem uma GUI para ativá-lo:
    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 não instala (por qualquer motivo), você pode instalá-lo através da GUI.

  10. No Blender, no Editar menu, escolha Preferencias.
  11. Escolha Add-ons no painel de navegação e ative zpy.

Você deverá ver uma página aberta na GUI e poderá escolher ZPY. Isso confirmará que o Blender está carregado.

AliceVision e Meshroom

Instale o AliceVision e o Meshroom de seus respectivos repositórios do GitHub:

FFmpeg

Seu sistema deve ter ffmpeg, mas se isso não acontecer, você precisará download .

Malhas instantâneas

Você pode compilar a biblioteca você mesmo ou baixar os binários pré-compilados disponíveis (que foi o que eu fiz) para Malhas instantâneas.

Configure seu ambiente AWS

Agora configuramos o ambiente da AWS em uma instância do EC2. Repetimos os passos da seção anterior, mas apenas para Blender e zpy.

  1. No console do Amazon EC2, escolha Iniciar instâncias.
  2. Escolha sua AMI. Existem algumas opções aqui. Podemos escolher uma imagem padrão do Ubuntu, escolher uma instância de GPU e, em seguida, instalar manualmente os drivers e configurar tudo, ou podemos seguir o caminho mais fácil e começar com uma AMI de Deep Learning pré-configurada e nos preocupar apenas em instalar o Blender. post, eu uso a segunda opção e escolho a versão mais recente do Deep Learning AMI para Ubuntu (Versão de Deep Learning AMI (Ubuntu 18.04) 61.0).
  3. Escolha Tipo de instância¸ escolher p2.xlarg.
  4. Se você não tiver um par de chaves, crie um novo ou escolha um existente.
  5. Para esta postagem, use as configurações padrão para rede e armazenamento.
  6. Escolha Iniciar instâncias.
  7. Escolha Contato e encontre as instruções para fazer login em nossa instância do SSH no Cliente SSH aba.
  8. Conecte-se com SSH: ssh -i "your-pem" ubuntu@IPADDRESS.YOUR-REGION.compute.amazonaws.com

Depois de se conectar à sua instância, siga as mesmas etapas de instalação da seção anterior para instalar o Blender e zpy.

Coleta de dados: digitalização 3D de nossa pepita

Para esta etapa, uso um iPhone para gravar um vídeo de 360 ​​graus em um ritmo bastante lento em torno da minha pepita. Enfiei um nugget de frango em um palito e prendi o palito na minha bancada, e simplesmente girei minha câmera ao redor do nugget para obter o máximo de ângulos que conseguisse. Quanto mais rápido você filmar, menor a probabilidade de obter boas imagens para trabalhar, dependendo da velocidade do obturador.

Depois que terminei de filmar, enviei o vídeo para o meu e-mail e extraí o vídeo para uma unidade local. A partir daí, usei ffmepg para cortar o vídeo em quadros para facilitar muito a ingestão do Meshroom:

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

Abra o Meshroom e use a GUI para arrastar o nugget_images pasta para o painel à esquerda. A partir daí, escolha Início e espere algumas horas (ou menos) dependendo da duração do vídeo e se você tiver uma máquina habilitada para CUDA.

Você deve ver algo como a captura de tela a seguir quando estiver quase completo.

Coleta de dados: manipulação do Blender

Quando a reconstrução do Meshroom estiver concluída, conclua as etapas a seguir:

  1. Abra a GUI do Blender e no Envie o menu, escolha importação, Em seguida, escolha Frente de onda (obj) para o arquivo de textura criado do Meshroom.
    O arquivo deve ser salvo em path/to/MeshroomCache/Texturing/uuid-string/texturedMesh.obj.
  2. Carregue o arquivo e observe a monstruosidade que é seu objeto 3D.

    Aqui é onde fica um pouco complicado.
  3. Role até o canto superior direito e escolha o wireframe ícone em Sombreamento da janela de visualização.
  4. Selecione seu objeto na viewport direita e certifique-se de que esteja realçado, role até a viewport de layout principal e pressione Aba ou escolha manualmente Modo de edição.
  5. Em seguida, manobre a janela de visualização de forma a permitir que você veja seu objeto com o mínimo possível atrás dele. Você terá que fazer isso algumas vezes para realmente acertar.
  6. Clique e arraste uma caixa delimitadora sobre o objeto para que apenas a pepita seja realçada.
  7. Depois de destacado como na captura de tela a seguir, separamos nossa pepita da massa 3D clicando com o botão esquerdo, escolhendo Separado, e depois Seleção.

    Agora vamos para a direita, onde devemos ver dois objetos texturizados: texturedMesh e texturedMesh.001.
  8. Nosso novo objeto deve ser texturedMesh.001, então escolhemos texturedMesh e escolha Apagar para remover a massa indesejada.
  9. Escolha o objeto (texturedMesh.001) à direita, vá para o nosso visualizador e escolha o objeto, Definir origem e Origem ao centro de massa.

Agora, se quisermos, podemos mover nosso objeto para o centro da viewport (ou simplesmente deixá-lo onde está) e visualizá-lo em toda a sua glória. Observe o grande buraco negro de onde não conseguimos uma boa cobertura de filme! Vamos precisar corrigir isso.

Para limpar nosso objeto de qualquer impureza de pixel, exportamos nosso objeto para um arquivo .obj. Certifique-se de escolher Apenas seleção ao exportar.

Coleta de dados: limpe com malhas instantâneas

Agora temos dois problemas: nossa imagem tem uma lacuna de pixel criada por nossa má filmagem que precisamos limpar, e nossa imagem é incrivelmente densa (o que tornará a geração de imagens extremamente demorada). Para resolver os dois problemas, precisamos usar um software chamado Instant Meshes para extrapolar nossa superfície de pixel para cobrir o buraco negro e também reduzir o objeto total para um tamanho menor e menos denso.

  1. Abra as malhas instantâneas e carregue nossos arquivos salvos recentemente nugget.obj arquivo.
  2. Debaixo Campo de orientação, escolha Resolver.
  3. Debaixo Campo de posição, escolha Resolver.
    Aqui é onde fica interessante. Se você explorar seu objeto e perceber que as linhas cruzadas do Solucionador de posição parecem desconexas, você pode escolher o ícone de pente em Campo de orientação e redesenhe as linhas corretamente.
  4. Escolha Resolver tanto Campo de orientação e Campo de posição.
  5. Se tudo estiver bem, exporte a malha, nomeie-a como nugget_refined.obj, e salve-o em disco.

Coleta de dados: Agite e asse!

Como nossa malha low-poly não tem nenhuma textura de imagem associada a ela e nossa malha high-poly tem, precisamos assar a textura high-poly na malha low-poly ou criar uma nova textura e atribuí-la a nosso objeto. Para simplificar, vamos criar uma textura de imagem do zero e aplicá-la ao nosso nugget.

Usei a pesquisa de imagens do Google para pepitas e outras coisas fritas para obter uma imagem de alta resolução da superfície de um objeto frito. Encontrei uma imagem super em alta resolução de um queijo coalho frito e fiz uma nova imagem cheia de textura frita.

Com esta imagem, estou pronto para concluir as seguintes etapas:

  1. Abra o Blender e carregue o novo nugget_refined.obj da mesma forma que você carregou seu objeto inicial: no Envie o menu, escolha importação, Frente de onda (obj)e escolha o nugget_refined.obj arquivo.
  2. Em seguida, vá para o Sombreamento aba.
    Na parte inferior você deve observar duas caixas com os títulos Princípio BDSF e Saída de Materiais.
  3. No Adicionar menu, escolha Textura e Textura da imagem.
    An Textura da imagem caixa deve aparecer.
  4. Escolha Abrir imagem e carregue sua imagem de textura frita.
  5. Arraste o mouse entre Cor no Textura da imagem caixa e Cor base no Princípio BDSF caixa.

Agora sua pepita deve estar pronta!

Coleta de dados: crie variáveis ​​de ambiente do Blender

Agora que temos nosso objeto nugget base, precisamos criar algumas coleções e variáveis ​​de ambiente para nos ajudar em nosso processo.

  1. Clique com o botão esquerdo na área da cena da mão e escolha Nova coleção.
  2. Crie as seguintes coleções: ORIGEM, PEPITA e GERADO.
  3. Arraste a pepita para o PEPITA coleção e renomeie-a pepita_base.

Coleta de dados: criar um avião

Vamos criar um objeto de fundo a partir do qual nossos nuggets serão gerados quando estivermos renderizando imagens. Em um caso de uso do mundo real, esse plano é onde nossas pepitas são colocadas, como uma bandeja ou caixa.

  1. No Adicionar menu, escolha Malha e depois Avião.
    A partir daqui, vamos para o lado direito da página e encontramos a caixa laranja (Propriedades do objeto).
  2. No Transformar painel, para XYZ Eulerconjunto X para 46.968, Y para 46.968, e Z para 1.0.
  3. Para ambos Localização e rotaçãoconjunto X, Y e Z para 0.

Coleta de dados: Defina a câmera e o eixo

Em seguida, vamos configurar nossas câmeras corretamente para que possamos gerar imagens.

  1. No Adicionar menu, escolha vazio e Eixo Simples.
  2. Nomeie o objeto Eixo Principal.
  3. Certifique-se de que nosso eixo seja 0 para todas as variáveis ​​(para que fique diretamente no centro).
  4. Se você já tiver uma câmera criada, arraste-a para o eixo principal.
  5. Escolha item e Transformar.
  6. Escolha Localizaçãoconjunto X para 0, Y para 0, e Z para 100.

Coleta de dados: Lá vem o sol

Em seguida, adicionamos um objeto Sun.

  1. No Adicionar menu, escolha Claro e Espreguiçadeiras.
    A localização deste objeto não importa necessariamente, desde que esteja centralizado em algum lugar sobre o objeto plano que definimos.
  2. Escolha o ícone de lâmpada verde no painel inferior direito (Propriedades de dados do objeto) e defina a intensidade para 5.0.
  3. Repita o mesmo procedimento para adicionar um Claro objeto e colocá-lo em um ponto aleatório sobre o plano.

Coleta de dados: baixe fundos aleatórios

Para injetar aleatoriedade em nossas imagens, baixamos tantas texturas aleatórias de textura.ninja como podemos (por exemplo, tijolos). Faça o download para uma pasta em seu espaço de trabalho chamada random_textures. Baixei cerca de 50.

Gerar imagens

Agora vamos ao que interessa: gerar imagens.

Pipeline de geração de imagens: Object3D e DensityController

Vamos começar com algumas definições de código:

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__()

Primeiro definimos uma classe de contêiner básica com algumas propriedades importantes. Esta classe existe principalmente para nos permitir criar uma árvore BVH (uma forma de representar nosso objeto pepita no espaço 3D), onde precisaremos usar o BVHTree.overlap método para ver se dois objetos pepita gerados independentes estão sobrepostos em nosso espaço 3D. Mais sobre isso mais tarde.

A segunda parte do código é o nosso controlador de densidade. Isso serve como uma maneira de nos vincularmos às regras da realidade e não ao mundo 3D. Por exemplo, no mundo do Blender 3D, os objetos no Blender podem existir uns dentro dos outros; no entanto, a menos que alguém esteja realizando alguma ciência estranha em nossos nuggets de frango, queremos garantir que não haja dois nuggets sobrepostos em um grau que o torne visualmente irreal.

Nós usamos nosso Plane objeto para gerar um conjunto de cubos invisíveis limitados que podem ser consultados a qualquer momento para ver se o espaço está ocupado ou não.


Veja o seguinte código:

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  
        """

No snippet a seguir, selecionamos o nugget e criamos um cubo delimitador em torno desse nugget. Este cubo representa o tamanho de um único pseudo-voxel do nosso objeto psuedo-kdtree. Precisamos usar o bpy.context.view_layer.update() função porque quando este código está sendo executado de dentro de uma função ou script versus o blender-gui, parece que o view_layer não é atualizado automaticamente.

        # 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)

Em seguida, atualizamos ligeiramente nosso objeto cubo para que seu comprimento e largura sejam quadrados, em oposição ao tamanho natural do nugget do qual foi criado:

        # 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.

Agora usamos nosso objeto de cubo atualizado para criar um plano que pode conter volumetricamente num_objects quantidade de pepitas:

        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()

Pegamos nosso objeto plano e criamos um cubo gigante do mesmo comprimento e largura do nosso plano, com a altura do nosso cubo pepita, CUBO1:

        # 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()

A partir daqui, queremos criar voxels do nosso cubo. Tomamos o número de cubos que caberiam num_objects e, em seguida, corte-os do nosso objeto cubo. Procuramos a face de malha voltada para cima do nosso cubo e, em seguida, escolhemos essa face para fazer nossos cortes. Veja o seguinte código:

        # 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

Por fim, calculamos o centro da face superior de cada corte que fizemos do nosso cubo grande e criamos cubos reais a partir desses cortes. Cada um desses cubos recém-criados representa um único pedaço de espaço para gerar ou mover pepitas pelo nosso avião. Veja o seguinte código:

        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

Em seguida, desenvolvemos um algoritmo que entende quais cubos estão ocupados em um determinado momento, descobre quais objetos se sobrepõem e move objetos sobrepostos separadamente para o espaço desocupado. Não conseguiremos nos livrar de todas as sobreposições inteiramente, mas podemos fazer com que pareça real o suficiente.



Veja o seguinte código:

    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

Pipeline de geração de imagens: corridas legais

Nesta seção, detalhamos o que nossos run função está fazendo.

Inicializamos nosso DensityController e crie algo chamado saver usando o ImageSaver da zpy. Isso nos permite salvar facilmente nossas imagens renderizadas em qualquer local de nossa escolha. Em seguida, adicionamos nosso nugget categoria (e se tivéssemos mais categorias, nós as adicionaríamos aqui). Veja o seguinte código:

@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")

Em seguida, precisamos criar um objeto de origem a partir do qual geramos os nuggets de cópia; neste caso, é o nugget_base que criamos:

    # 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)

Agora que temos nosso nugget base, vamos salvar as poses do mundo (locais) de todos os outros objetos para que, após cada execução de renderização, possamos usar essas poses salvas para reinicializar uma renderização. Também movemos nosso nugget base completamente para fora do caminho para que o kdtree não perceba que um espaço está sendo ocupado. Finalmente, inicializamos nossos objetos kdtree-cube. Veja o seguinte código:

    # 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()

O código a seguir coleta nossos planos de fundo baixados do texture.ninja, onde eles serão usados ​​para serem projetados aleatoriamente em nosso avião:

    # 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)
    ]

Aqui é onde a magia começa. Primeiro, regeneramos kdtree-cubes para esta execução para que possamos começar de novo:

    # 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):

Usamos nosso controlador de densidade para gerar um ponto de spawn aleatório para nosso nugget, criamos uma cópia de nugget_base, e mova a cópia para o ponto de spawn gerado aleatoriamente:

            # 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)

Em seguida, agitamos aleatoriamente o tamanho da pepita, a malha da pepita e a escala da pepita para que não haja duas pepitas iguais:

            # 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)          

Transformamos nossa cópia de pepita em um Object3D objeto onde usamos a funcionalidade de árvore BVH para ver se nosso plano cruza ou se sobrepõe a qualquer face ou vértice em nossa cópia do nugget. Se encontrarmos uma sobreposição com o plano, simplesmente movemos a pepita para cima em seu eixo Z. Veja o seguinte código:

            # 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)

Agora que todos os nuggets foram criados, usamos nosso DensityController para mover as pepitas para que tenhamos um número mínimo de sobreposições, e aquelas que se sobrepõem não são horríveis:

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

No código a seguir: restauramos o Camera e Main Axis poses e selecione aleatoriamente a que distância a câmera está Plane objeto:

        # 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)

Nós decidimos quão aleatoriamente queremos que a câmera viaje ao longo do Main Axis. Dependendo se queremos que ele fique principalmente acima da cabeça ou se nos importamos muito com o ângulo de visão do tabuleiro, podemos ajustar o top_down_mostly parâmetro dependendo de quão bem nosso modelo de treinamento está captando o sinal de “O que é mesmo uma pepita de qualquer maneira?”

        # 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)

No código a seguir, fazemos a mesma coisa com o Sun objeto e escolha aleatoriamente uma textura para o Plane objeto:

        # 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)

Por fim, ocultamos todos os nossos objetos que não queremos que sejam renderizados: o nugget_base e toda a nossa estrutura de cubos:

# 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)

Por último, usamos zpy para renderizar nossa cena, salvar nossas imagens e depois salvar nossas anotações. Para este post, fiz algumas pequenas alterações no zpy biblioteca de anotações para meu caso de uso específico (anotação por imagem em vez de um arquivo por projeto), mas você não deve precisar para os propósitos deste post).

        # 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()

Voila!

Execute o script de criação sem cabeça

Agora que temos nosso arquivo Blender salvo, nosso nugget criado e todas as informações de suporte, vamos compactar nosso diretório de trabalho e scp para nossa máquina GPU ou carregou via Serviço de armazenamento simples da Amazon (Amazon S3) ou outro serviço:

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

Faça login em sua instância do EC2 e descompacte sua pasta working_blender:

tar xvf working_blender_dir.tar.gz

Agora criamos nossos dados em toda a sua glória:

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

O script deve ser executado para 500 imagens e os dados são salvos em /path/to/working_blender_dir/nugget_data.

O código a seguir mostra uma única anotação criada com nosso conjunto de dados:

{
    "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
            ]
        },
...
...
...

Conclusão

Neste post, demonstrei como usar a biblioteca de animação de código aberto Blender para construir um pipeline de dados sintéticos de ponta a ponta.

Há muitas coisas legais que você pode fazer no Blender e na AWS; Espero que esta demonstração possa ajudá-lo em seu próximo projeto carente de dados!

Referências


Sobre o autor

Matt Krzus é cientista de dados sênior da Amazon Web Service no grupo AWS Professional Services

Carimbo de hora:

Mais de Aprendizado de máquina da AWS