Collecting and annotating image data is one of the most resource-intensive tasks on any computer vision project. It can take months at a time to fully collect, analyze, and experiment with image streams at the level you need in order to compete in the current marketplace. Even after you’ve successfully collected data, you still have a constant stream of annotation errors, poorly framed images, small amounts of meaningful data in a sea of unwanted captures, and more. These major bottlenecks are why synthetic data creation needs to be in the toolkit of every modern engineer. By creating 3D representations of the objects we want to model, we can rapidly prototype algorithms while concurrently collecting live data.

In this post, I walk you through an example of using the open-source animation library Blender to build an end-to-end synthetic data pipeline, using chicken nuggets as an example. The following image is an illustration of the data generated in this blog post.

What is Blender?

Blender is an open-source 3D graphics software primarily used in animation, 3D printing, and virtual reality. It has an extremely comprehensive rigging, animation, and simulation suite that allows the creation of 3D worlds for nearly any computer vision use case. It also has an extremely active support community where most, if not all, user errors are solved.

Set up your local environment

We install two versions of Blender: one on a local machine with access to a GUI, and the other on an Amazon Elastic Compute Cloud (Amazon EC2) P2 instance.

Install Blender and ZPY

Install Blender from the Blender website.

Then complete the following steps:

  1. Run the following commands:
    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. Copy the necessary Python headers into the Blender version of Python so that you can use other non-Blender libraries:
    tar -xzf Python-3.10.2.tgz
    sudo cp Python-3.10.2/Include/* /bin/3.2/python/include/python3.10

  3. Override your Blender version and force installs so that the Blender-provided Python works:
    /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. Download zpy and install from source:
    git clone
    cd zpy
    vi requirements.txt

  5. Change the NumPy version to >=1.19.4 and scikit-image>=0.18.1 to make the install on 3.10.2 possible and so you don’t get any overwrites:

  6. To ensure compatibility with Blender 3.2, go into zpy/ and comment out the following two lines (for more information, refer to Blender 3.0 Failure #54):
    #scene.render.tile_x = tile_size
    #scene.render.tile_y = tile_size

  7. Next, install the zpy library:
    /bin/3.2/python/bin/python3.10 install --user
    /bin/3.2/python/bin/python3.10 -c "import zpy; print(zpy.__version__)"

  8. Download the add-ons version of zpy from the GitHub repo so you can actively run your instance:
    cd ~
    curl -O -L -C - ""
    sudo unzip -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. Save a file called in your /home directory and run the enablement command, because you don’t have a GUI to activate it:
    import bpy, os
    p = os.path.abspath('')
    bpy.ops.preferences.addon_install(overwrite=True, filepath=p)
    bpy.ops.wm.save_userpref() sudo blender -b -y --python

    If zpy-addon doesn’t install (for whatever reason), you can install it via the GUI.

  10. In Blender, on the Edit menu, choose Preferences.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  11. Choose Add-ons in the navigation pane and activate zpy.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

You should see a page open in the GUI, and you’ll be able to choose ZPY. This will confirm that Blender is loaded.

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

AliceVision and Meshroom

Install AliceVision and Meshrooom from their respective GitHub repos:


Your system should have ffmpeg, but if it doesn’t, you’ll need to download it.

Instant Meshes

You can either compile the library yourself or download the available pre-compiled binaries (which is what I did) for Instant Meshes.

Set up your AWS environment

Now we set up the AWS environment on an EC2 instance. We repeat the steps from the previous section, but only for Blender and zpy.

  1. On the Amazon EC2 console, choose Launch instances.
  2. Choose your AMI.There are a few options from here. We can either choose a standard Ubuntu image, pick a GPU instance, and then manually install the drivers and get everything set up, or we can take the easy route and start with a preconfigured Deep Learning AMI and only worry about installing Blender.For this post, I use the second option, and choose the latest version of the Deep Learning AMI for Ubuntu (Deep Learning AMI (Ubuntu 18.04) Version 61.0).
  3. For Instance type¸ choose p2.xlarge.
  4. If you don’t have a key pair, create a new one or choose an existing one.
  5. For this post, use the default settings for network and storage.
  6. Choose Launch instances.
  7. Choose Connect and find the instructions to log in to our instance from SSH on the SSH client tab.
  8. Connect with SSH: ssh -i "your-pem" [email protected]

Once you’ve connected to your instance, follow the same installation steps from the previous section to install Blender and zpy.

Data collection: 3D scanning our nugget

For this step, I use an iPhone to record a 360-degree video at a fairly slow pace around my nugget. I stuck a chicken nugget onto a toothpick and taped the toothpick to my countertop, and simply rotated my camera around the nugget to get as many angles as I could. The faster you film, the less likely you get good images to work with depending on the shutter speed.

After I finished filming, I sent the video to my email and extracted the video to a local drive. From there, I used ffmepg to chop the video into frames to make Meshroom ingestion much easier:

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

Open Meshroom and use the GUI to drag the nugget_images folder to the pane on the left. From there, choose Start and wait a few hours (or less) depending on the length of the video and if you have a CUDA-enabled machine.

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

You should see something like the following screenshot when it’s almost complete.

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Data collection: Blender manipulation

When our Meshroom reconstruction is complete, complete the following steps:

  1. Open the Blender GUI and on the File menu, choose Import, then choose Wavefront (.obj) to your created texture file from Meshroom.
    The file should be saved in path/to/MeshroomCache/Texturing/uuid-string/texturedMesh.obj.
  2. Load the file and observe the monstrosity that is your 3D object.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
    Here is where it gets a bit tricky.
  3. Scroll to the top right side and choose the Wireframe icon in Viewport Shading.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  4. Select your object on the right viewport and make sure it’s highlighted, scroll over to the main layout viewport, and either press Tab or manually choose Edit Mode.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  5. Next, maneuver the viewport in such a way as to allow yourself to be able to see your object with as little as possible behind it. You’ll have to do this a few times to really get it correct.
  6. Click and drag a bounding box over the object so that only the nugget is highlighted.
  7. After it’s highlighted like in the following screenshot, we separate our nugget from the 3D mass by left-clicking, choosing Separate, and then Selection.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
    We now move over to the right, where we should see two textured objects: texturedMesh and texturedMesh.001.
  8. Our new object should be texturedMesh.001, so we choose texturedMesh and choose Delete to remove the unwanted mass.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  9. Choose the object (texturedMesh.001) on the right, move to our viewer, and choose the object, Set Origin, and Origin to Center of Mass.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Now, if we want, we can move our object to the center of the viewport (or simply leave it where it is) and view it in all its glory. Notice the large black hole where we didn’t really get good film coverage from! We’re going to need to correct for this.

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

To clean our object of any pixel impurities, we export our object to an .obj file. Make sure to choose Selection Only when exporting.
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Data collection: Clean up with Instant Meshes

Now we have two problems: our image has a pixel gap creating by our poor filming that we need to clean up, and our image is incredibly dense (which will make generating images extremely time-consuming). To tackle both issues, we need to use a software called Instant Meshes to extrapolate our pixel surface to cover the black hole and also to shrink the total object to a smaller, less dense size.

  1. Open Instant Meshes and load our recently saved nugget.obj file.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  2. Under Orientation field, choose Solve.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  3. Under Position field, choose Solve.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,Here’s where it gets interesting. If you explore your object and notice that the criss-cross lines of the Position solver look disjointed, you can choose the comb icon under Orientation field and redraw the lines properly.
  4. Choose Solve for both Orientation field and Position field.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  5. If everything looks good, export the mesh, name it something like nugget_refined.obj, and save it to disk.

Data collection: Shake and bake!

Because our low-poly mesh doesn’t have any image texture associated with it and our high-poly mesh does, we either need to bake the high-poly texture onto the low-poly mesh, or create a new texture and assign it to our object. For sake of simplicity, we’re going to create an image texture from scratch and apply that to our nugget.

I used Google image search for nuggets and other fried things in order to get a high-res image of the surface of a fried object. I found a super high-res image of a fried cheese curd and made a new image full of the fried texture.
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
With this image, I’m ready to complete the following steps:

  1. Open Blender and load the new nugget_refined.obj the same way you loaded your initial object: on the File menu, choose Import, Wavefront (.obj), and choose the nugget_refined.obj file.
  2. Next, go to the Shading tab.
    At the bottom you should notice two boxes with the titles Principled BDSF and Material Output.
  3. On the Add menu, choose Texture and Image Texture.
    An Image Texture box should appear.
  4. Choose Open Image and load your fried texture image.
  5. Drag your mouse between Color in the Image Texture box and Base Color in the Principled BDSF box.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Now your nugget should be good to go!

Data collection: Create Blender environment variables

Now that we have our base nugget object, we need to create a few collections and environment variables to help us in our process.

  1. Left-click on the hand scene area and choose New Collection.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  2. Create the following collections: BACKGROUND, NUGGET, and SPAWNED.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  3. Drag the nugget to the NUGGET collection and rename it nugget_base.

Data collection: Create a plane

We’re going to create a background object from which our nuggets will be generated when we’re rendering images. In a real-world use case, this plane is where our nuggets are placed, such as a tray or bin.

  1. On the Add menu, choose Mesh and then Plane.
    From here, we move to the right side of the page and find the orange box (Object Properties).
  2. In the Transform pane, for XYZ Euler, set X to 46.968, Y to 46.968, and Z to 1.0.
  3. For both Location and Rotation, set X, Y, and Z to 0.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Data collection: Set the camera and axis

Next, we’re going to set our cameras up correctly so that we can generate images.

  1. On the Add menu, choose Empty and Plain Axis.
  2. Name the object Main Axis.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  3. Make sure our axis is 0 for all the variables (so it’s directly in the center).
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
  4. If you have a camera already created, drag that camera to under Main Axis.
  5. Choose Item and Transform.
  6. For Location, set X to 0, Y to 0, and Z to 100.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Data collection: Here comes the sun

Next, we add a Sun object.

  1. On the Add menu, choose Light and Sun.
    The location of this object doesn’t necessarily matter as long as it’s centered somewhere over the plane object we’ve set.
  2. Choose the green lightbulb icon in the bottom right pane (Object Data Properties) and set the strength to 5.0.
  3. Repeat the same procedure to add a Light object and put it in a random spot over the plane.
    Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Data collection: Download random backgrounds

To inject randomness into our images, we download as many random textures from as we can (for example, bricks). Download to a folder within your workspace called random_textures. I downloaded about 50.
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Generate images

Now we get to the fun stuff: generating images.

Image generation pipeline: Object3D and DensityController

Let’s start with some code definitions:

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 @ for v in] 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] 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__()

We first define a basic container Class with some important properties. This class mainly exists to allow us to create a BVH tree (a way to represent our nugget object in 3D space), where we’ll need to use the BVHTree.overlap method to see if two independent generated nugget objects are overlapping in our 3D space. More on this later.

The second piece of code is our density controller. This serves as a way to bound ourselves to the rules of reality and not the 3D world. For example, in the 3D Blender world, objects in Blender can exist inside each other; however, unless someone is performing some strange science on our chicken nuggets, we want to make sure no two nuggets are overlapping by a degree that makes it visually unrealistic.

We use our Plane object to spawn a set of bounded invisible cubes that can be queried at any given time to see if the space is occupied or not.
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
See the following code:

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

In the following snippet, we select the nugget and create a bounding cube around that nugget. This cube represents the size of a single pseudo-voxel of our psuedo-kdtree object. We need to use the bpy.context.view_layer.update() function because when this code is being run from inside a function or script vs. the blender-gui, it seems that the view_layer isn’t automatically updated.

 # 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.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 = 'CUBE1' bpy.context.view_layer.update() #copy transforms nug_dims =["nugget_base"].dimensions["CUBE1"].dimensions = nug_dims bpy.context.view_layer.update()["CUBE1"].location =["nugget_base"].location bpy.context.view_layer.update()["CUBE1"].rotation_euler =["nugget_base"].rotation_euler bpy.context.view_layer.update() print("bound_box.dimensions: ", bound_box.dimensions) print("bound_box.location:", bound_box.location)

Next, we slightly update our cube object so that its length and width are square, as opposed to the natural size of the nugget it was created from:

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

Now we use our updated cube object to create a plane that can volumetrically hold num_objects amount of nuggets:

 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' # move our plane to our background collection # current_collection = plane.users_collection link_object('PLANE', 'BACKGROUND') bpy.context.view_layer.update()

We take our plane object and create a giant cube of the same length and width as our plane, with the height of our nugget cube, CUBE1:

 # New Collection my_coll ="KDTREE") # Add collection to scene collection # 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' bpy.context.view_layer.update() current_collection = cube.users_collection link_object('cube', 'KDTREE') bpy.context.view_layer.update()

From here, we want to create voxels from our cube. We take the number of cubes we would to fit num_objects and then cut them from our cube object. We look for the upward-facing mesh-face of our cube, and then pick that face to make our cuts. See the following code:

 # 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 object.select_set(False) bpy.context.view_layer.update() for object in object.select_set(False)['cube'].select_set(True) # Blender 2.8x = 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 = mat = obj.matrix_world bm = bmesh.from_edit_mesh(me) up_face = None # select upwards facing cube-face # 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

Lastly, we calculate the center of the top-face of each cut we’ve made from our big cube and create actual cubes from those cuts. Each of these newly created cubes represents a single piece of space to spawn or move nuggets around our plane. See the following code:

 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_{}".format(f) link_object("cube_{}".format(f), 'KDTREE') #bpy.context.view_layer.update() bvhtrees[f] = { 'occupied' : 0, 'object' : Object3D(cube) } for object in object.select_set(False)['CUBE1'].select_set(True) # Blender 2.8x bpy.ops.object.delete() return bvhtrees

Next, we develop an algorithm that understands which cubes are occupied at any given time, finds which objects overlap with each other, and moves overlapping objects separately into unoccupied space. We won’t be able get rid of all overlaps entirely, but we can make it look real enough.
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,
See the following code:

 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

Image generation pipeline: Cool runnings

In this section, we break down what our run function is doing.

We initialize our DensityController and create something called a saver using the ImageSaver from zpy. This allows us to seemlessly save our rendered images to any location of our choosing. We then add our nugget category (and if we had more categories, we would add them here). See the following code:

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

Next, we need to make a source object from which we spawn copy nuggets from; in this case, it’s the nugget_base that we created:

 # Make a list of source nugget objects source_nugget_objects = [] for obj in zpy.objects.for_obj_in_collections( [["NUGGET"], ] ): assert(obj!=None) # pass on everything not named nugget if 'nugget_base' not in print('passing on {}'.format( continue zpy.objects.segment(obj, name="nugget", as_category=True) #color=nugget_seg_color print("zpy.objects.segment: check {}".format( source_nugget_objects.append(

Now that we have our base nugget, we’re going to save the world poses (locations) of all the other objects so that after each rendering run, we can use these saved poses to reinitialize a render. We also move our base nugget completely out of the way so that the kdtree doesn’t sense a space being occupied. Finally, we initialize our kdtree-cube objects. See the following code:

 # move nugget point up 10 z's so it won't collide with base-cube["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 =['Main Axis'] print('saving poses') # add some parameters to this # get the plane-3d object plane3d = Object3D(['Plane']) # generate kdtree cubes density_controller.generate_kdtree_cubes()

The following code collects our downloaded backgrounds from, where they’ll be used to be randomly projected onto our plane:

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

Here is where the magic begins. We first regenerate out kdtree-cubes for this run so that we can start fresh:

 # Run the sim. for step_idx in zpy.blender.step(): density_controller.generate_kdtree_cubes() objects3d = [] num_nuggets = random.randint(40, max_num_nuggets)"Spawning {num_nuggets} nuggets.") spawned_nugget_objects = [] for _ in range(num_nuggets):

We use our density controller to generate a random spawn point for our nugget, create a copy of nugget_base, and move the copy to the randomly generated spawn point:

 # 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)"Spawning a copy of source nugget {_name} at {spawn_point}") obj = zpy.objects.copy([_name],["SPAWNED"], is_copy=True, ) obj.location = spawn_point obj.matrix_world = mathutils.Matrix.Translation(spawn_point) spawned_nugget_objects.append(obj)

Next, we randomly jitter the size of the nugget, the mesh of the nugget, and the scale of the nugget so that no two nuggets look the same:

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

We turn our nugget copy into an Object3D object where we use the BVH tree functionality to see if our plane intersects or overlaps any face or vertices on our nugget copy. If we find an overlap with the plane, we simply move the nugget upwards on its Z axis. See the following code:

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

Now that all nuggets are created, we use our DensityController to move nuggets around so that we have a minimum number of overlaps, and those that do overlap aren’t hideous looking:

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

In the following code: we restore the Camera and Main Axis poses and randomly select how far the camera is to the Plane object:

 # 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(["Camera"].location == Vector((0,0,100))) assert(["Main Axis"].location == Vector((0,0,0))) assert(["Main Axis"].rotation_euler == Euler((0,0,0))) # alter the Z ditance with the camera["Camera"].location = (0, 0, random.uniform(0.75, 3.5)*100)

We decide how randomly we want the camera to travel along the Main Axis. Depending on if we want it to be mainly overhead or if we care very much about the angle from which it sees the board, we can adjust the top_down_mostly parameter depending on how well our training model is picking up the signal of “What even is a nugget anyway?”

 # alter the main-axis beta/gamma params top_down_mostly = False if top_down_mostly: zpy.objects.rotate(["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(["Main Axis"], rotation=( random.uniform(-1., 1.), random.uniform(-1., 1.), random.uniform(-1., 1.), ), ) print(["Main Axis"].rotation_euler) print(["Camera"].location)

In the following code, we do the same thing with the Sun object, and randomly pick a texture for the Plane object:

 # change the background material # Randomize texture of shelf, floors and walls for obj in["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), ), )["Sun"] = random.uniform(0.5, 7)

Finally, we hide all our objects that we don’t want to be rendered: the nugget_base and our entire cube structure:

# we hide the cube objects<br />for obj in # we hide the cube objects for obj in if 'cube' in 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["nugget_base"].hide_render = True zpy.objects.toggle_hidden(["nugget_base"], hidden=True)

Lastly, we use zpy to render our scene, save our images, and then save our annotations. For this post, I made some small changes to the zpy annotation library for my specific use case (annotation per image instead of one file per project), but you shouldn’t have to for the purpose of this 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(["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 = {}"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()


Run the headless creation script

Now that we have our saved Blender file, our created nugget, and all the supporting information, let’s zip our working directory and either scp it to our GPU machine or uploaded it via Amazon Simple Storage Service (Amazon S3) or another service:

tar cvf working_blender_dir.tar.gz working_blender_dir
scp -i "your.pem" working_blender_dir.tar.gz [email protected]:/home/ubuntu/working_blender_dir.tar.gz

Log in to your EC2 instance and decompress your working_blender folder:

tar xvf working_blender_dir.tar.gz

Now we create our data in all its glory:

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

The script should run for 500 images, and the data is saved in /path/to/working_blender_dir/nugget_data.

The following code shows a single annotation created with our dataset:

{ "metadata": { "description": "3D data of a nugget!", "contributor": "Matt Krzus", "url": "[email protected]", "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 ] },

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,


In this post, I demonstrated how to use the open-source animation library Blender to build an end-to-end synthetic data pipeline.

There are a ton of cool things you can do in Blender and AWS; hopefully this demo can help you on your next data-starved project!


About the Author

Hyperedge- . IoT, Embedded Systems, Artificial Intelligence,Matt Krzus is a Sr. Data Scientist at Amazon Web Service in the AWS Professional Services group

Read more about this on: AWS