Example #1
0
    def __init__(self):
        self.obj_name_dict: Dict[int, str] = {}
        
        # Set up the object transform data.
        object_setup_data = json.loads(Path("object_setup.json").read_text())
        self.object_setups: List[_ObjectSetup] = []
        for o in object_setup_data:
            combo = _ObjectSetup(id=int(o),
                                 model_name=object_setup_data[o]["model_name"],
                                 position=object_setup_data[o]["position"],
                                 rotation=object_setup_data[o]["rotation"],
                                 scale=object_setup_data[o]["scale"])
            self.object_setups.append(combo)

        # Parse the default objects.csv spreadsheet. 
        self.object_audio_data = PyImpact.get_object_info()

        self.temp_amp = 0.1

        # Keep track of the current trial number, for logging purposes.
        self.current_trial_num = 0

        # Fetch the ball and board model's records; we will need them later to change its material.
        self.special_models = ModelLibrarian(library="models_special.json")
        self.full_models = ModelLibrarian(library="models_full.json")
        self.ball_record = self.special_models.get_record("prim_sphere")
        self.board_record = self.full_models.get_record("wood_board")

        # Set path to write out logging info.
        self.root_dest_dir = Path("dist/mode_properties_logs")
        if not self.root_dest_dir.exists():
            self.root_dest_dir.mkdir(parents=True)

        super().__init__()
Example #2
0
class AudioInitData(RigidbodyInitData):
    """
    A subclass of `RigidbodyInitData` that includes [audio values](py_impact.md#objectinfo).
    Physics values are derived from these audio values.
    """

    _DYNAMIC_FRICTION = {AudioMaterial.ceramic: 0.47,
                         AudioMaterial.hardwood: 0.35,
                         AudioMaterial.wood: 0.35,
                         AudioMaterial.cardboard: 0.47,
                         AudioMaterial.glass: 0.65,
                         AudioMaterial.metal: 0.43}
    _STATIC_FRICTION = {AudioMaterial.ceramic: 0.47,
                        AudioMaterial.hardwood: 0.4,
                        AudioMaterial.wood: 0.4,
                        AudioMaterial.cardboard: 0.47,
                        AudioMaterial.glass: 0.65,
                        AudioMaterial.metal: 0.52}
    AUDIO = PyImpact.get_object_info()

    def __init__(self, name: str, library: str = "models_core.json", scale_factor: Dict[str, float] = None, position: Dict[str, float] = None, rotation: Dict[str, float] = None, kinematic: bool = False, gravity: bool = True, audio: ObjectInfo = None):
        """
        :param name: The name of the model.
        :param library: The filename of the library containing the model's record.
        :param scale_factor: The [scale factor](../api/command_api.md#scale_object).
        :param position: The initial position. If None, defaults to: `{"x": 0, "y": 0, "z": 0`}.
        :param rotation: The initial rotation as Euler angles or a quaternion. If None, defaults to: `{"w": 1, "x": 0, "y": 0, "z": 0}`
        :param kinematic: If True, the object will be [kinematic](../api/command_api.md#set_kinematic_state).
        :param gravity: If True, the object won't respond to [gravity](../api/command_api.md#set_kinematic_state).
        :param audio: If None, derive physics data from the audio data in `PyImpact.get_object_info()` (if the object isn't in this dictionary, this constructor will throw an error). If not None, use these values instead of the default audio values.
        """

        if audio is None:
            self.audio = AudioInitData.AUDIO[name]
        else:
            self.audio = audio
        super().__init__(name=name, library=library, scale_factor=scale_factor, position=position, rotation=rotation,
                         kinematic=kinematic, gravity=gravity, mass=self.audio.mass,
                         dynamic_friction=AudioInitData._DYNAMIC_FRICTION[self.audio.material],
                         static_friction=AudioInitData._STATIC_FRICTION[self.audio.material],
                         bounciness=self.audio.bounciness)

    def get_commands(self) -> Tuple[int, List[dict]]:
        """
        :return: Tuple: The ID of the object; a list of commands to create the object: `[add_object, rotate_object_to, scale_object, set_kinematic_state, set_object_collision_detection_mode, set_mass, set_physic_material]`
        """

        return super().get_commands()
Example #3
0
    def __init__(self):
        self.obj_name_dict: Dict[int, str] = {}

        # Set up the object transform data.
        object_setup_data = json.loads(Path("object_setup.json").read_text())
        self.object_setups: List[_ObjectSetup] = []
        for o in object_setup_data:
            combo = _ObjectSetup(id=int(o),
                                 model_name=object_setup_data[o]["model_name"],
                                 position=object_setup_data[o]["position"],
                                 rotation=object_setup_data[o]["rotation"],
                                 scale=object_setup_data[o]["scale"])
            self.object_setups.append(combo)

        # Parse the default objects.csv spreadsheet.
        self.object_audio_data = PyImpact.get_object_info()

        # Fetch the ball and board model's records; we will need them later to change its material.
        self.special_models = ModelLibrarian(library="models_special.json")
        self.full_models = ModelLibrarian(library="models_full.json")
        self.ball_record = self.special_models.get_record("prim_sphere")
        self.board_record = self.full_models.get_record("wood_board")

        super().__init__()
Example #4
0
    def do_trial(self):
        # Initialize PyImpact and pass in the "master gain" amplitude value. This value must be betweem 0 and 1.
        # The relative amplitudes of all scene objects involved in collisions will be scaled relative to this value.
        # Note -- if this value is too high, waveform clipping can occur and the resultant audio will be distorted.
        # For this reason, the value used here is considerably smaller than the corresponding value used in the
        # impact_sounds.py example controller. Here we have a large number of closely-occuring collisions resulting in
        # a rapid series of "clustered" impact sounds, as opposed to a single object falling from a height;
        # using a higher value such as the 0.5 used in the example controller will definitely result in unpleasant
        # distortion of the audio.
        # Note that logging is also enabled.

        # Keep track of trial number.
        self.current_trial_num += 1

        # Create folder for this trial's logging info.
        dest_dir = self.root_dest_dir.joinpath(str(self.current_trial_num))
        if not dest_dir.exists():
            dest_dir.mkdir(parents=True)
        dest_dir_str = str(dest_dir.resolve())

        p = PyImpact(0.25, logging=True)

        self.add_all_objects()

        # "Aim" the ball at the monkey and apply the force.
        # Note that this force value was arrived at through a number of trial-and-error iterations.
        resp = self.communicate([{"$type": "object_look_at_position",
                                  "id": 0,
                                  "position": {"x": -12.95, "y": 1.591, "z": -5.1}},
                                 {"$type": "apply_force_magnitude_to_object",
                                  "id": 0,
                                  "magnitude": 98.0}])

        for i in range(400):
            collisions, environment_collision, rigidbodies = PyImpact.get_collisions(resp)
            # Sort the objects by mass.
            masses: Dict[int, float] = {}
            for j in range(rigidbodies.get_num()):
                masses.update({rigidbodies.get_id(j): rigidbodies.get_mass(j)})

            # If there was a collision, create an impact sound.
            if len(collisions) > 0 and PyImpact.is_valid_collision(collisions[0]):
                collider_id = collisions[0].get_collider_id()
                collidee_id = collisions[0].get_collidee_id()
                # The "target" object should have less mass than the "other" object.
                if masses[collider_id] < masses[collidee_id]:
                    target_id = collider_id
                    other_id = collidee_id
                else:
                    target_id = collidee_id
                    other_id = collider_id

                target_name = self.obj_name_dict[target_id]
                other_name = self.obj_name_dict[other_id]

                impact_sound_command = p.get_impact_sound_command(
                    collision=collisions[0],
                    rigidbodies=rigidbodies,
                    target_id=target_id,
                    target_mat=self.object_audio_data[target_name].material,
                    other_id=other_id,
                    other_mat=self.object_audio_data[other_name].material,
                    target_amp=self.object_audio_data[target_name].amp,
                    other_amp=self.object_audio_data[other_name].amp,
                    resonance=self.object_audio_data[target_name].resonance)
                resp = self.communicate(impact_sound_command)
            # Continue to run the trial.
            else:
                resp = self.communicate([])

        # Get the logging info for this trial and write it out.
        log = p.get_log()
        json_dest = dest_dir.joinpath("mode_properties_log.json")
        json_dest.write_text(json.dumps(log, indent=2))

        for obj_setup in self.object_setups:
            self.communicate({"$type": "destroy_object", "id": obj_setup.id})
Example #5
0
    def trial(self):
        """
        Select random objects and collide them to produce impact sounds.
        """

        p = PyImpact(initial_amp=0.25)
        # Set the environment audio materials.
        floor = AudioMaterial.ceramic
        wall = AudioMaterial.wood

        # Create an empty room.
        # Set the screen size.
        # Adjust framerate so that physics is closer to realtime.
        commands = [
            TDWUtils.create_empty_room(12, 12), {
                "$type": "destroy_all_objects"
            }, {
                "$type": "set_screen_size",
                "width": 1024,
                "height": 1024
            }, {
                "$type": "set_target_framerate",
                "framerate": 100
            }
        ]
        # Create the avatar.
        commands.extend(
            TDWUtils.create_avatar(avatar_type="A_Img_Caps_Kinematic",
                                   position={
                                       "x": 1,
                                       "y": 1.2,
                                       "z": 1.2
                                   },
                                   look_at=TDWUtils.VECTOR3_ZERO))
        # Add the audio sensor.
        commands.extend([{
            "$type": "add_audio_sensor",
            "avatar_id": "a"
        }, {
            "$type": "set_focus_distance",
            "focus_distance": 2
        }])

        # Select a random pair of objects.
        obj1_names = [
            "trapezoidal_table", "glass_table_round", "yellow_side_chair",
            "table_square", "marble_table"
        ]
        obj2_names = ["vase_06", "spoon1", "glass3", "jug02"]
        obj1_name = random.choice(obj1_names)
        obj2_name = random.choice(obj2_names)

        # Get initialization data from the default audio data (which includes mass, friction values, etc.).
        obj1_init_data = AudioInitData(name=obj1_name)
        obj2_init_data = AudioInitData(name=obj2_name,
                                       position={
                                           "x": 0,
                                           "y": 2,
                                           "z": 0
                                       },
                                       rotation={
                                           "x": 135,
                                           "y": 0,
                                           "z": 30
                                       })
        # Convert the initialization data to commands.
        obj1_id, obj1_commands = obj1_init_data.get_commands()
        obj2_id, obj2_commands = obj2_init_data.get_commands()

        # Cache the IDs and names of each object for PyImpact.
        object_names = {obj1_id: obj1_name, obj2_id: obj2_name}
        p.set_default_audio_info(object_names=object_names)

        # Add the objects.
        commands.extend(obj1_commands)
        commands.extend(obj2_commands)
        # Apply a small force to the dropped object.
        # Request collision and rigidbody output data.
        commands.extend([{
            "$type": "apply_force_to_object",
            "force": {
                "x": 0,
                "y": -0.01,
                "z": 0
            },
            "id": obj2_id
        }, {
            "$type": "send_collisions",
            "enter": True,
            "stay": False,
            "exit": False,
            "collision_types": ["obj", "env"]
        }, {
            "$type": "send_rigidbodies",
            "frequency": "always",
            "ids": [obj2_id, obj1_id]
        }])
        # Send all of the commands.
        resp = self.communicate(commands)

        # Iterate through 200 frames.
        # Every frame, listen for collisions, and parse the output data.
        for i in range(200):
            # Use PyImpact to generate audio from the output data and then convert the audio to TDW commands.
            # If no audio is generated, then `commands` is an empty list.
            commands = p.get_audio_commands(resp=resp, floor=floor, wall=wall)
            # Send the commands to TDW in order to play the audio.
            resp = self.communicate(commands)
Example #6
0
    def trial(self):
        """
        Select random objects and collide them to produce impact sounds.
        """

        p = PyImpact(initial_amp=0.5)

        # Destroy all objects currently in the scene.
        # Set the screen size.
        # Adjust physics timestep for more real-time physics behavior.
        commands = [{
            "$type": "destroy_all_objects"
        }, {
            "$type": "set_screen_size",
            "width": 1024,
            "height": 1024
        }, {
            "$type": "set_time_step",
            "time_step": 0.02
        }]
        # Create the avatar.
        commands.extend(
            TDWUtils.create_avatar(avatar_type="A_Img_Caps_Kinematic",
                                   position={
                                       "x": 1,
                                       "y": 1.2,
                                       "z": 1.2
                                   },
                                   look_at=TDWUtils.VECTOR3_ZERO))

        # Add the audio sensor.
        # Set the target framerate.
        # Make sure that post-processing is enabled and render quality is set to max.
        commands.extend([{
            "$type": "add_audio_sensor",
            "avatar_id": "a"
        }, {
            "$type": "set_target_framerate",
            "framerate": 60
        }, {
            "$type": "set_post_process",
            "value": True
        }, {
            "$type": "set_focus_distance",
            "focus_distance": 2
        }, {
            "$type": "set_render_quality",
            "render_quality": 5
        }])

        # Select a random pair of objects.
        objects = PyImpact.get_object_info()
        obj1_names = [
            "trapezoidal_table", "glass_table_round", "yellow_side_chair",
            "table_square", "marble_table"
        ]
        obj2_names = ["vase_06", "spoon1", "glass3", "jug02"]
        obj1_name = random.choice(obj1_names)
        obj2_name = random.choice(obj2_names)
        obj1_id = 0
        obj2_id = 1

        # Add the objects.
        # Set their masses from the audio info data.
        # Set a physics material for the second object.
        # Apply a force to the second object.
        # Listen for collisions, and object properties.
        commands.extend([
            self.get_add_object(model_name=obj1_name,
                                object_id=obj1_id,
                                library=objects[obj1_name].library), {
                                    "$type": "set_mass",
                                    "id": obj1_id,
                                    "mass": objects[obj1_name].mass
                                },
            self.get_add_object(model_name=obj2_name,
                                object_id=obj2_id,
                                library=objects[obj2_name].library,
                                rotation={
                                    "x": 135,
                                    "y": 0,
                                    "z": 30
                                },
                                position={
                                    "x": 0,
                                    "y": 2,
                                    "z": 0
                                }), {
                                    "$type": "set_mass",
                                    "id": obj2_id,
                                    "mass": objects[obj2_name].mass
                                }, {
                                    "$type": "set_physic_material",
                                    "id": obj2_id,
                                    "bounciness":
                                    objects[obj2_name].bounciness,
                                    "dynamic_friction": 0.8
                                }, {
                                    "$type": "apply_force_to_object",
                                    "force": {
                                        "x": 0,
                                        "y": -0.01,
                                        "z": 0
                                    },
                                    "id": obj2_id
                                }, {
                                    "$type": "send_collisions",
                                    "enter": True,
                                    "stay": False,
                                    "exit": False
                                }, {
                                    "$type": "send_rigidbodies",
                                    "frequency": "always",
                                    "ids": [obj2_id, obj1_id]
                                }
        ])

        # Send all of the commands.
        resp = self.communicate(commands)

        # Iterate through 200 frames.
        # Every frame, listen for collisions, and parse the output data.
        for i in range(200):
            collisions, environment_collision, rigidbodies = PyImpact.get_collisions(
                resp)
            # If there was a collision, create an impact sound.
            if len(collisions) > 0 and PyImpact.is_valid_collision(
                    collisions[0]):
                impact_sound_command = p.get_impact_sound_command(
                    collision=collisions[0],
                    rigidbodies=rigidbodies,
                    target_id=obj2_id,
                    target_mat=objects[obj2_name].material.name,
                    target_amp=objects[obj2_name].amp,
                    other_id=obj1_id,
                    other_amp=objects[obj1_name].amp,
                    other_mat=objects[obj1_name].material.name,
                    resonance=objects[obj1_name].resonance)
                resp = self.communicate(impact_sound_command)
            # Continue to run the trial.
            else:
                resp = self.communicate([])

        # Stop listening for collisions and rigidbodies.
        self.communicate([{
            "$type": "send_collisions",
            "frequency": "never"
        }, {
            "$type": "send_rigidbodies",
            "frequency": "never"
        }])
Example #7
0
    def trial(self, scene: Scene, record: ModelRecord, output_path: Path,
              scene_index: int) -> None:
        """
        Run a trial in a scene that has been initialized.

        :param scene: Data for the current scene.
        :param record: The model's metadata record.
        :param output_path: Write the .wav file to this path.
        :param scene_index: The scene identifier.
        """

        self.py_impact.reset(initial_amp=0.05)

        # Initialize the scene, positioning objects, furniture, etc.
        resp = self.communicate(scene.initialize_scene(self))
        center = scene.get_center(self)

        max_y = scene.get_max_y()

        # The object's initial position.
        o_x = RNG.uniform(center["x"] - 0.15, center["x"] + 0.15)
        o_y = RNG.uniform(max_y - 0.5, max_y)
        o_z = RNG.uniform(center["z"] - 0.15, center["z"] + 0.15)
        # Physics values.
        mass = self.object_info[record.name].mass + RNG.uniform(
            self.object_info[record.name].mass * -0.15,
            self.object_info[record.name].mass * 0.15)
        static_friction = RNG.uniform(0.1, 0.3)
        dynamic_friction = RNG.uniform(0.7, 0.9)
        # Angles of rotation.
        yaw = RNG.uniform(-30, 30)
        pitch = RNG.uniform(0, 45)
        roll = RNG.uniform(-45, 45)
        # The force applied to the object.
        force = RNG.uniform(0, 5)
        # The avatar's position.
        a_r = RNG.uniform(1.5, 2.2)
        a_x = center["x"] + a_r
        a_y = RNG.uniform(1.5, 3)
        a_z = center["z"] + a_r
        cam_angle_min, cam_angle_max = scene.get_camera_angles()
        theta = np.radians(RNG.uniform(cam_angle_min, cam_angle_max))
        a_x = np.cos(theta) * (a_x - center["x"]) - np.sin(theta) * (
            a_z - center["z"]) + center["x"]
        a_z = np.sin(theta) * (a_x - center["x"]) + np.cos(theta) * (
            a_z - center["z"]) + center["z"]

        o_id = 0
        # Create the object and apply a force.
        commands = [{
            "$type": "add_object",
            "name": record.name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": {
                "x": o_x,
                "y": o_y,
                "z": o_z
            },
            "category": record.wcategory,
            "id": o_id
        }, {
            "$type": "set_mass",
            "id": o_id,
            "mass": mass
        }, {
            "$type": "set_physic_material",
            "id": o_id,
            "bounciness": self.object_info[record.name].bounciness,
            "static_friction": static_friction,
            "dynamic_friction": dynamic_friction
        }, {
            "$type": "rotate_object_by",
            "angle": yaw,
            "id": o_id,
            "axis": "yaw",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": pitch,
            "id": o_id,
            "axis": "pitch",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": roll,
            "id": o_id,
            "axis": "roll",
            "is_world": True
        }, {
            "$type": "apply_force_magnitude_to_object",
            "magnitude": force,
            "id": o_id
        }, {
            "$type": "send_rigidbodies",
            "frequency": "always"
        }, {
            "$type": "send_collisions",
            "enter": True,
            "exit": False,
            "stay": False,
            "collision_types": ["obj", "env"]
        }, {
            "$type": "send_transforms",
            "frequency": "always"
        }]
        # Parse bounds data to get the centroid of all objects currently in the scene.
        bounds = Bounds(resp[0])
        if bounds.get_num() == 0:
            look_at = {"x": center["x"], "y": 0.1, "z": center["z"]}
        else:
            centers = []
            for i in range(bounds.get_num()):
                centers.append(bounds.get_center(i))
            centers_x, centers_y, centers_z = zip(*centers)
            centers_len = len(centers_x)
            look_at = {
                "x": sum(centers_x) / centers_len,
                "y": sum(centers_y) / centers_len,
                "z": sum(centers_z) / centers_len
            }
        # Add the avatar.
        # Set the position at a given distance (r) from the center of the scene.
        # Rotate around that position to a random angle constrained by the scene's min and max angles.
        commands.extend([{
            "$type": "teleport_avatar_to",
            "position": {
                "x": a_x,
                "y": a_y,
                "z": a_z
            }
        }, {
            "$type": "look_at_position",
            "position": look_at
        }])

        # Send the commands.
        resp = self.communicate(commands)

        AudioUtils.start(output_path=output_path, until=(0, 10))

        # Loop until all objects are sleeping.
        done = False
        while not done and AudioUtils.is_recording():
            commands = []
            collisions, environment_collisions, rigidbodies = PyImpact.get_collisions(
                resp)
            # Create impact sounds from object-object collisions.
            for collision in collisions:
                if PyImpact.is_valid_collision(collision):
                    # Get the audio material and amp.
                    collider_id = collision.get_collider_id()
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    collidee_id = collision.get_collider_id()
                    collidee_material, collidee_amp = self._get_object_info(
                        collidee_id, Scene.OBJECT_IDS, record.name)
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collidee_id,
                        target_amp=collidee_amp,
                        target_mat=collidee_material.name,
                        other_id=collider_id,
                        other_mat=collider_material.name,
                        other_amp=collider_amp,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # Create impact sounds from object-environment collisions.
            for collision in environment_collisions:
                collider_id = collision.get_object_id()
                if self._get_velocity(rigidbodies, collider_id) > 0:
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    surface_material = scene.get_surface_material()
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collider_id,
                        target_amp=collider_amp,
                        target_mat=collider_material.name,
                        other_id=-1,
                        other_amp=0.01,
                        other_mat=surface_material.name,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # If there were no collisions, check for movement. If nothing is moving, the trial is done.
            if len(commands) == 0:
                transforms = AudioDataset._get_transforms(resp)
                done = True
                for i in range(rigidbodies.get_num()):
                    if self._is_moving(rigidbodies.get_id(i), transforms,
                                       rigidbodies):
                        done = False
                        break
            # Continue the trial.
            if not done:
                resp = self.communicate(commands)

        # Stop listening for anything except audio data..
        resp = self.communicate([{
            "$type": "send_rigidbodies",
            "frequency": "never"
        }, {
            "$type": "send_transforms",
            "frequency": "never"
        }, {
            "$type": "send_collisions",
            "enter": False,
            "exit": False,
            "stay": False,
            "collision_types": []
        }, {
            "$type": "send_audio_sources",
            "frequency": "always"
        }])
        # Wait for the audio to finish.
        done = False
        while not done and AudioUtils.is_recording():
            done = True
            for r in resp[:-1]:
                if OutputData.get_data_type_id(r) == "audi":
                    audio_sources = AudioSources(r)
                    for i in range(audio_sources.get_num()):
                        if audio_sources.get_is_playing(i):
                            done = False
            if not done:
                resp = self.communicate([])
        # Cleanup.
        commands = [{
            "$type": "send_audio_sources",
            "frequency": "never"
        }, {
            "$type": "destroy_object",
            "id": o_id
        }]
        for scene_object_id in Scene.OBJECT_IDS:
            commands.append({"$type": "destroy_object", "id": scene_object_id})
        self.communicate(commands)

        # Insert the trial's values into the database.
        self.db_c.execute(
            "INSERT INTO sound20k VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            (output_path.name, scene_index, a_x, a_y, a_z, o_x, o_y, o_z, mass,
             static_friction, dynamic_friction, yaw, pitch, roll, force))
        self.conn.commit()
Example #8
0
    def __init__(self,
                 output_dir: Path = Path("D:/audio_dataset"),
                 total: int = 28602,
                 port: int = 1071):
        """
        :param output_dir: The output directory for the files.
        :param port: The socket port.
        :param total: The total number of files per sub-set.
        """

        self.total = total

        self.output_dir = output_dir
        if not self.output_dir.exists():
            self.output_dir.mkdir(parents=True)

        db_path = self.output_dir.joinpath('results.db')
        self.conn = sqlite3.connect(str(db_path.resolve()))
        self.db_c = self.conn.cursor()
        # Sound20K table.
        if self.db_c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sound20k'").\
                fetchone() is None:
            self.db_c.execute(
                "CREATE TABLE sound20k (path text, scene integer, cam_x real, cam_y real, cam_z real,"
                "obj_x real, obj_y real, obj_z real, mass real, static_friction real, dynamic_friction "
                "real, yaw real, pitch real, roll real, force real)")
        # Scenes table.
        if self.db_c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='scenes'").\
                fetchone() is None:
            self.db_c.execute(
                "CREATE TABLE scenes (id integer, commands text)")

        self.py_impact = PyImpact()

        self.object_info = PyImpact.get_object_info()
        sound20k_object_info = PyImpact.get_object_info(
            Path("models/object_info.csv"))
        for obj_info in sound20k_object_info:
            if obj_info in self.object_info:
                continue
            else:
                self.object_info.update(
                    {obj_info: sound20k_object_info[obj_info]})

        self.libs: Dict[str, ModelLibrarian] = {}
        # Load all model libraries into memory.
        for lib_name in ModelLibrarian.get_library_filenames():
            self.libs.update({lib_name: ModelLibrarian(lib_name)})
        # Add the custom model library.
        self.libs.update({
            "models/models.json":
            ModelLibrarian(str(Path("models/models.json").resolve()))
        })

        super().__init__(port=port)

        # Global settings.
        self.communicate([{
            "$type": "set_screen_size",
            "width": 256,
            "height": 256
        }, {
            "$type": "set_time_step",
            "time_step": 0.02
        }, {
            "$type": "set_target_framerate",
            "framerate": 60
        }, {
            "$type": "set_physics_solver_iterations",
            "iterations": 20
        }])
Example #9
0
class AudioDataset(Controller):
    def __init__(self,
                 output_dir: Path = Path("D:/audio_dataset"),
                 total: int = 28602,
                 port: int = 1071):
        """
        :param output_dir: The output directory for the files.
        :param port: The socket port.
        :param total: The total number of files per sub-set.
        """

        self.total = total

        self.output_dir = output_dir
        if not self.output_dir.exists():
            self.output_dir.mkdir(parents=True)

        db_path = self.output_dir.joinpath('results.db')
        self.conn = sqlite3.connect(str(db_path.resolve()))
        self.db_c = self.conn.cursor()
        # Sound20K table.
        if self.db_c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='sound20k'").\
                fetchone() is None:
            self.db_c.execute(
                "CREATE TABLE sound20k (path text, scene integer, cam_x real, cam_y real, cam_z real,"
                "obj_x real, obj_y real, obj_z real, mass real, static_friction real, dynamic_friction "
                "real, yaw real, pitch real, roll real, force real)")
        # Scenes table.
        if self.db_c.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='scenes'").\
                fetchone() is None:
            self.db_c.execute(
                "CREATE TABLE scenes (id integer, commands text)")

        self.py_impact = PyImpact()

        self.object_info = PyImpact.get_object_info()
        sound20k_object_info = PyImpact.get_object_info(
            Path("models/object_info.csv"))
        for obj_info in sound20k_object_info:
            if obj_info in self.object_info:
                continue
            else:
                self.object_info.update(
                    {obj_info: sound20k_object_info[obj_info]})

        self.libs: Dict[str, ModelLibrarian] = {}
        # Load all model libraries into memory.
        for lib_name in ModelLibrarian.get_library_filenames():
            self.libs.update({lib_name: ModelLibrarian(lib_name)})
        # Add the custom model library.
        self.libs.update({
            "models/models.json":
            ModelLibrarian(str(Path("models/models.json").resolve()))
        })

        super().__init__(port=port)

        # Global settings.
        self.communicate([{
            "$type": "set_screen_size",
            "width": 256,
            "height": 256
        }, {
            "$type": "set_time_step",
            "time_step": 0.02
        }, {
            "$type": "set_target_framerate",
            "framerate": 60
        }, {
            "$type": "set_physics_solver_iterations",
            "iterations": 20
        }])

    def remove_output_directory(self) -> None:
        """
        Delete the old directory.
        """

        dir_util.remove_tree(str(self.output_dir.resolve()))

    def process_sub_set(self, name: str, models_mat_file: str,
                        init_commands: List[dict],
                        scenes: List[Scene]) -> None:
        """
        Process a sub-set of the complete dataset (e.g. all of Sound20K).

        :param name: The name of the sub-set.
        :param models_mat_file: The models per material data filename.
        :param init_commands: The commands used to initialize the entire process (this is sent only once).
        :param scenes: The scenes that can be loaded.
        """

        print(name)
        # Load models by wnid.
        materials: Dict[str, List[str]] = loads(
            Path(f"models/{models_mat_file}.json").read_text(encoding="utf-8"))
        num_per_material = int(self.total / len(materials))

        # Load the scene.
        self.communicate(init_commands)

        pbar = tqdm(total=self.total)

        for material in materials:
            pbar.set_description(material)
            self.process_material(root_dir=self.output_dir.joinpath(name),
                                  scenes=scenes,
                                  material=material,
                                  models=materials[material],
                                  num_total=num_per_material,
                                  pbar=pbar)
        pbar.close()

    def sound20k_set(self) -> None:
        """
        Generate a dataset analogous to Sound20K.
        """

        sound20k_init_commands = [
            {
                "$type": "load_scene"
            },
            TDWUtils.create_empty_room(12, 12), {
                "$type": "set_proc_gen_walls_scale",
                "walls": TDWUtils.get_box(12, 12),
                "scale": {
                    "x": 1,
                    "y": 4,
                    "z": 1
                }
            }, {
                "$type": "set_reverb_space_simple",
                "env_id": 0,
                "reverb_floor_material": "parquet",
                "reverb_ceiling_material": "acousticTile",
                "reverb_front_wall_material": "smoothPlaster",
                "reverb_back_wall_material": "smoothPlaster",
                "reverb_left_wall_material": "smoothPlaster",
                "reverb_right_wall_material": "smoothPlaster"
            }, {
                "$type": "create_avatar",
                "type": "A_Img_Caps_Kinematic",
                "id": "a"
            }, {
                "$type": "add_environ_audio_sensor"
            }, {
                "$type": "toggle_image_sensor"
            }
        ]

        self.process_sub_set("Sound20K", "models_per_material_sound20k",
                             sound20k_init_commands, get_sound20k_scenes())

    def tdw_set(self) -> None:
        self.process_sub_set("TDW", "models_per_material_tdw", [],
                             get_tdw_scenes())

    def process_material(self, root_dir: Path, scenes: List[Scene],
                         models: List[str], material: str, num_total: int,
                         pbar: Optional[tqdm]) -> None:
        """
        Generate .wav files from all models with the material.

        :param root_dir: The root output directory.
        :param scenes: The scenes that a trial can use.
        :param models: The names of the models in the category and their libraries.
        :param num_total: The total number of files to generate for this category.
        :param material: The name of the material.
        :param pbar: The progress bar.
        """

        num_images_per_model = int(num_total / len(models))
        num_scenes_per_model = int(num_images_per_model / len(scenes))

        # The number of files generated for the wnid.
        count = 0
        # The number of files generated for the current model.
        model_count = 0
        # The model being used to generate files.
        model_index = 0
        # The number of files for the current model that have used the current scene.
        scene_count = 0
        # The scene being used to generate files.
        scene_index = 0

        output_dir = root_dir.joinpath(material)
        if not output_dir.exists():
            output_dir.mkdir(parents=True)

        while count < num_total:
            obj_name = models[model_index]
            filename = output_dir.joinpath(
                TDWUtils.zero_padding(count, 4) + ".wav")
            # Get the expected output path.
            output_path = output_dir.joinpath(filename)

            # Do a trial if the file doesn't exist yet.
            if not output_path.exists():
                try:
                    self.trial(
                        scene=scenes[scene_index],
                        record=self.libs[self.object_info[
                            models[model_index]].library].get_record(obj_name),
                        output_path=output_path,
                        scene_index=scene_index)
                finally:
                    # Stop recording audio.
                    AudioUtils.stop()

            count += 1
            # Iterate through scenes.
            scene_count += 1
            if scene_count > num_scenes_per_model:
                scene_id = scenes[scene_index].get_id()
                # Add the scene to the database.
                scene_db = self.db_c.execute("SELECT * FROM scenes WHERE id=?",
                                             (scene_id, )).fetchone()
                if scene_db is None:
                    self.db_c.execute(
                        "INSERT INTO scenes VALUES(?,?)",
                        (scene_id,
                         json.dumps(
                             scenes[scene_index].initialize_scene(self))))
                    self.conn.commit()
                scene_index += 1
                scene_count = 0
                if scene_index >= len(scenes):
                    scene_index = 0
            # Iterate through models.
            model_count += 1
            if model_count > num_images_per_model:
                model_index += 1
                if model_index >= len(models):
                    model_index = 0
                model_count = 0
                # If this is a new model, reset the scene count.
                scene_index = 0
                scene_count = 0
                # Unload the asset bundles because we are done with this model.
                self.communicate({"$type": "unload_asset_bundles"})
            if pbar is not None:
                pbar.update(1)

    def trial(self, scene: Scene, record: ModelRecord, output_path: Path,
              scene_index: int) -> None:
        """
        Run a trial in a scene that has been initialized.

        :param scene: Data for the current scene.
        :param record: The model's metadata record.
        :param output_path: Write the .wav file to this path.
        :param scene_index: The scene identifier.
        """

        self.py_impact.reset(initial_amp=0.05)

        # Initialize the scene, positioning objects, furniture, etc.
        resp = self.communicate(scene.initialize_scene(self))
        center = scene.get_center(self)

        max_y = scene.get_max_y()

        # The object's initial position.
        o_x = RNG.uniform(center["x"] - 0.15, center["x"] + 0.15)
        o_y = RNG.uniform(max_y - 0.5, max_y)
        o_z = RNG.uniform(center["z"] - 0.15, center["z"] + 0.15)
        # Physics values.
        mass = self.object_info[record.name].mass + RNG.uniform(
            self.object_info[record.name].mass * -0.15,
            self.object_info[record.name].mass * 0.15)
        static_friction = RNG.uniform(0.1, 0.3)
        dynamic_friction = RNG.uniform(0.7, 0.9)
        # Angles of rotation.
        yaw = RNG.uniform(-30, 30)
        pitch = RNG.uniform(0, 45)
        roll = RNG.uniform(-45, 45)
        # The force applied to the object.
        force = RNG.uniform(0, 5)
        # The avatar's position.
        a_r = RNG.uniform(1.5, 2.2)
        a_x = center["x"] + a_r
        a_y = RNG.uniform(1.5, 3)
        a_z = center["z"] + a_r
        cam_angle_min, cam_angle_max = scene.get_camera_angles()
        theta = np.radians(RNG.uniform(cam_angle_min, cam_angle_max))
        a_x = np.cos(theta) * (a_x - center["x"]) - np.sin(theta) * (
            a_z - center["z"]) + center["x"]
        a_z = np.sin(theta) * (a_x - center["x"]) + np.cos(theta) * (
            a_z - center["z"]) + center["z"]

        o_id = 0
        # Create the object and apply a force.
        commands = [{
            "$type": "add_object",
            "name": record.name,
            "url": record.get_url(),
            "scale_factor": record.scale_factor,
            "position": {
                "x": o_x,
                "y": o_y,
                "z": o_z
            },
            "category": record.wcategory,
            "id": o_id
        }, {
            "$type": "set_mass",
            "id": o_id,
            "mass": mass
        }, {
            "$type": "set_physic_material",
            "id": o_id,
            "bounciness": self.object_info[record.name].bounciness,
            "static_friction": static_friction,
            "dynamic_friction": dynamic_friction
        }, {
            "$type": "rotate_object_by",
            "angle": yaw,
            "id": o_id,
            "axis": "yaw",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": pitch,
            "id": o_id,
            "axis": "pitch",
            "is_world": True
        }, {
            "$type": "rotate_object_by",
            "angle": roll,
            "id": o_id,
            "axis": "roll",
            "is_world": True
        }, {
            "$type": "apply_force_magnitude_to_object",
            "magnitude": force,
            "id": o_id
        }, {
            "$type": "send_rigidbodies",
            "frequency": "always"
        }, {
            "$type": "send_collisions",
            "enter": True,
            "exit": False,
            "stay": False,
            "collision_types": ["obj", "env"]
        }, {
            "$type": "send_transforms",
            "frequency": "always"
        }]
        # Parse bounds data to get the centroid of all objects currently in the scene.
        bounds = Bounds(resp[0])
        if bounds.get_num() == 0:
            look_at = {"x": center["x"], "y": 0.1, "z": center["z"]}
        else:
            centers = []
            for i in range(bounds.get_num()):
                centers.append(bounds.get_center(i))
            centers_x, centers_y, centers_z = zip(*centers)
            centers_len = len(centers_x)
            look_at = {
                "x": sum(centers_x) / centers_len,
                "y": sum(centers_y) / centers_len,
                "z": sum(centers_z) / centers_len
            }
        # Add the avatar.
        # Set the position at a given distance (r) from the center of the scene.
        # Rotate around that position to a random angle constrained by the scene's min and max angles.
        commands.extend([{
            "$type": "teleport_avatar_to",
            "position": {
                "x": a_x,
                "y": a_y,
                "z": a_z
            }
        }, {
            "$type": "look_at_position",
            "position": look_at
        }])

        # Send the commands.
        resp = self.communicate(commands)

        AudioUtils.start(output_path=output_path, until=(0, 10))

        # Loop until all objects are sleeping.
        done = False
        while not done and AudioUtils.is_recording():
            commands = []
            collisions, environment_collisions, rigidbodies = PyImpact.get_collisions(
                resp)
            # Create impact sounds from object-object collisions.
            for collision in collisions:
                if PyImpact.is_valid_collision(collision):
                    # Get the audio material and amp.
                    collider_id = collision.get_collider_id()
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    collidee_id = collision.get_collider_id()
                    collidee_material, collidee_amp = self._get_object_info(
                        collidee_id, Scene.OBJECT_IDS, record.name)
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collidee_id,
                        target_amp=collidee_amp,
                        target_mat=collidee_material.name,
                        other_id=collider_id,
                        other_mat=collider_material.name,
                        other_amp=collider_amp,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # Create impact sounds from object-environment collisions.
            for collision in environment_collisions:
                collider_id = collision.get_object_id()
                if self._get_velocity(rigidbodies, collider_id) > 0:
                    collider_material, collider_amp = self._get_object_info(
                        collider_id, Scene.OBJECT_IDS, record.name)
                    surface_material = scene.get_surface_material()
                    impact_sound_command = self.py_impact.get_impact_sound_command(
                        collision=collision,
                        rigidbodies=rigidbodies,
                        target_id=collider_id,
                        target_amp=collider_amp,
                        target_mat=collider_material.name,
                        other_id=-1,
                        other_amp=0.01,
                        other_mat=surface_material.name,
                        play_audio_data=False)
                    commands.append(impact_sound_command)
            # If there were no collisions, check for movement. If nothing is moving, the trial is done.
            if len(commands) == 0:
                transforms = AudioDataset._get_transforms(resp)
                done = True
                for i in range(rigidbodies.get_num()):
                    if self._is_moving(rigidbodies.get_id(i), transforms,
                                       rigidbodies):
                        done = False
                        break
            # Continue the trial.
            if not done:
                resp = self.communicate(commands)

        # Stop listening for anything except audio data..
        resp = self.communicate([{
            "$type": "send_rigidbodies",
            "frequency": "never"
        }, {
            "$type": "send_transforms",
            "frequency": "never"
        }, {
            "$type": "send_collisions",
            "enter": False,
            "exit": False,
            "stay": False,
            "collision_types": []
        }, {
            "$type": "send_audio_sources",
            "frequency": "always"
        }])
        # Wait for the audio to finish.
        done = False
        while not done and AudioUtils.is_recording():
            done = True
            for r in resp[:-1]:
                if OutputData.get_data_type_id(r) == "audi":
                    audio_sources = AudioSources(r)
                    for i in range(audio_sources.get_num()):
                        if audio_sources.get_is_playing(i):
                            done = False
            if not done:
                resp = self.communicate([])
        # Cleanup.
        commands = [{
            "$type": "send_audio_sources",
            "frequency": "never"
        }, {
            "$type": "destroy_object",
            "id": o_id
        }]
        for scene_object_id in Scene.OBJECT_IDS:
            commands.append({"$type": "destroy_object", "id": scene_object_id})
        self.communicate(commands)

        # Insert the trial's values into the database.
        self.db_c.execute(
            "INSERT INTO sound20k VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
            (output_path.name, scene_index, a_x, a_y, a_z, o_x, o_y, o_z, mass,
             static_friction, dynamic_friction, yaw, pitch, roll, force))
        self.conn.commit()

    def _get_object_info(self, o_id: int, object_ids: Dict[int, str],
                         drop_name: str) -> Tuple[AudioMaterial, float]:
        """
        :param o_id: The object ID.
        :param object_ids: The scene object IDs.
        :param drop_name: The name of the dropped object.

        :return: The audio material and amp associated with the object.
        """

        if o_id in object_ids:
            return self.object_info[object_ids[
                o_id]].material, self.object_info[object_ids[o_id]].amp
        else:
            return self.object_info[drop_name].material, self.object_info[
                drop_name].amp

    @staticmethod
    def _get_transforms(resp: List[bytes]) -> Transforms:
        """
        :param resp: The output data response.

        :return: Transforms data.
        """

        for r in resp[:-1]:
            if OutputData.get_data_type_id(r) == "tran":
                return Transforms(r)
        raise Exception("Transforms output data not found!")

    @staticmethod
    def _get_velocity(rigidbodies: Rigidbodies, o_id: int) -> float:
        """
        :param rigidbodies: The rigidbody data.
        :param o_id: The ID of the object.

        :return: The velocity magnitude of the object.
        """

        for i in range(rigidbodies.get_num()):
            if rigidbodies.get_id(i) == o_id:
                return np.linalg.norm(rigidbodies.get_velocity(i))

    @staticmethod
    def _is_moving(o_id: int, transforms: Transforms,
                   rigidbodies: Rigidbodies) -> bool:
        """
        :param o_id: The ID of the object.
        :param transforms: The Transforms output data.
        :param rigidbodies: The Rigidbodies output data.

        :return: True if the object is still moving.
        """

        y: Optional[float] = None
        sleeping: bool = False

        for i in range(transforms.get_num()):
            if transforms.get_id(i) == o_id:
                y = transforms.get_position(i)[1]
                break
        assert y is not None, f"y value is none for {o_id}"

        for i in range(rigidbodies.get_num()):
            if rigidbodies.get_id(i) == o_id:
                sleeping = rigidbodies.get_sleeping(i)
                break
        # If the object isn't sleeping, it is still moving.
        # If the object fell into the abyss, we don't count it as moving (to prevent an infinitely long simulation).
        return not sleeping and y > -10
Example #10
0
class Scene(ABC):
    """
    A recipe to initialize a scene.
    """

    _MODEL_LIBRARY_PATH = str(Path("models/models.json").resolve())

    # A list of object IDs for the scene objects and the model names.
    OBJECT_IDS: Dict[int, str] = {}
    _OBJECT_INFO = PyImpact.get_object_info()
    # Append custom data.
    _custom_object_info = PyImpact.get_object_info(Path("models/object_info.csv"))
    for obj in _custom_object_info:
        _OBJECT_INFO.update({obj: _custom_object_info[obj]})
    _SCENE_IDS: Dict[str, int] = {}
    _SCENE_INDEX = 0

    def initialize_scene(self, c: Controller) -> List[dict]:
        """
        Add these commands to the beginning of the list of initialization commands.

        :param c: The controller.

        :return: A list of commands to initialize a scene.
        """

        # Clean up all objects.
        Scene.OBJECT_IDS.clear()

        # Custom commands to initialize the scene.
        commands = self._initialize_scene(c)[:]

        # Send bounds data (for the new objects).
        commands.append({"$type": "send_bounds",
                         "frequency": "once"})

        return commands

    @abstractmethod
    def _initialize_scene(self, c: Controller) -> List[dict]:
        """
        :param c: The controller.

        :return: A list of commands to initialize a scene.
        """

        raise Exception()

    @abstractmethod
    def get_center(self, c: Controller) -> Dict[str, float]:
        """
        :param c: The controller.

        :return: The "center" of the scene, as a Vector3 dictionary.
        """

        raise Exception()

    @abstractmethod
    def get_max_y(self) -> float:
        """
        :return: The maximum y value for the camera (avatar) and starting height for an object.
        """

        raise Exception()

    @staticmethod
    def _init_object(c: Controller, name: str, pos: Dict[str, float], rot: Dict[str, float]) -> List[dict]:
        """
        :param c: The controller.
        :param name: The name of the model.
        :param pos: The initial position of the model.
        :param rot: The initial rotation of the model.

        :return: A list of commands to instantiate an object from ObjectInfo values.
        """

        o_id = c.get_unique_id()
        Scene.OBJECT_IDS.update({o_id: name})
        info = Scene._OBJECT_INFO[name]
        return [c.get_add_object(name,
                                 object_id=o_id,
                                 position=pos,
                                 rotation=rot,
                                 library=info.library),
                {"$type": "set_mass",
                 "id": o_id,
                 "mass": info.mass},
                {"$type": "set_physic_material",
                 "id": o_id,
                 "bounciness": info.bounciness,
                 "static_friction": 0.1,
                 "dynamic_friction": 0.8}]

    @staticmethod
    def get_camera_angles() -> Tuple[float, float]:
        """
        :return: Range of valid camera angles.
        """

        return 0, 360

    @abstractmethod
    def get_surface_material(self) -> AudioMaterial:
        """
        :return: The audio material of the surface.
        """

        raise Exception()

    def _get_name(self) -> str:
        """
        :return: The name of this scene; used for indexing.
        """

        return type(self).__name__

    def get_id(self) -> int:
        """
        :return: The unique ID of this type of scene.
        """

        name = self._get_name()
        # Index the name.
        if name not in Scene._SCENE_IDS:
            Scene._SCENE_IDS.update({name: Scene._SCENE_INDEX})
            Scene._SCENE_INDEX += 1
        return Scene._SCENE_IDS[name]
class FrameData:
    """
    Per-frame data that an avatar can use to decide what action to do next.

    Access this data from the [StickyMittenAvatarController](sma_controller.md):

    ```python
    from sticky_mitten_avatar import StickyMittenAvatarController, Arm

    c = StickyMittenAvatarController()
    c.init_scene()

    # Look towards the left arm.
    c.rotate_camera_by(pitch=70, yaw=-45)

    c.reach_for_target(target={"x": -0.2, "y": 0.21, "z": 0.385}, arm=Arm.left)

    # Save each image from the start of the most recent API action to the end.
    for frame in c.frames:
        frame.save_images(output_directory="dist")
    c.end()
    ```

    ***

    ## Fields

    ### Visual

    - `image_pass` Rendered image of the scene as a numpy array.

     ![](images/pass_masks/img_0.jpg)

    - `id_pass` Image pass of object color segmentation as a numpy array.

     ![](images/pass_masks/id_0.png)

    - `depth_pass` Image pass of depth values per pixel as a numpy array.

     ![](images/pass_masks/depth_simple_0.png)

    - `projection_matrix` The [camera projection matrix](https://github.com/threedworld-mit/tdw/blob/master/Documentation/api/output_data.md#cameramatrices) of the avatar's camera as a numpy array.
    - `camera_matrix` The [camera matrix](https://github.com/threedworld-mit/tdw/blob/master/Documentation/api/output_data.md#cameramatrices) of the avatar's camera as a numpy array.

    ### Audio

    - `audio` A list of tuples of audio generated by impacts. The first element in the tuple is a [`Base64Sound` object](https://github.com/threedworld-mit/tdw/blob/master/Documentation/python/py_impact.md#base64sound).
              The second element is the ID of the "target" (smaller) object.

    ```python
    for frame in c.frames:
        for audio in frame.audio:
            # Get the audio data.
            wav_data = audio[0].bytes
            # Get the position of the object that generated the audio data.
            object_id = audio[1]
            position = frame.object_transforms[object_id].position
    ```

    ### Objects

    - `object_transforms` The dictionary of object [transform data](transform.md). Key = the object ID.

    ```python
    # Print the position of each object per frame.
    for frame in c.frames:
        for object_id in frame.object_transforms:
            print(frame.object_transforms[object_id].position)
    ```

    ### Avatar

    - `avatar_transform` The [transform data](transform.md) of the avatar.

    ```python
    for frame in c.frames:
        avatar_position = frame.avatar_transform.position
    ```

    - `avatar_body_part_transforms` The [transform data](transform.md) of each body part of the avatar. Key = body part ID.

    ```python
    for frame in c.frames:
        # Get the position and segmentation color of each body part.
        for body_part_id in frame.avatar_body_part_transforms:
            position = frame.avatar_body_part_transforms[body_part_id]
            segmentation_color = c.static_avatar_data[body_part_id]
    ```


    - `avatar_object_collisions` A dictionary of objects the avatar collided with. Key = body part ID. Value = A list of object IDs.

    ```python
    for frame in c.frames:
        for body_part_id in frame.avatar_object_collisions:
            body_part = c.static_avatar_info[body_part_id]
            object_ids = frame.avatar_object_collisions[body_part_id]
            for object_id in object_ids:
                print(body_part.name + " collided with object " + str(object_id))
    ```

    - `avatar_env_collisions`  A list of body part IDs that collided with the environment (such as a wall).

    ```python
    for frame in c.frames:
        for body_part_id in frame.avatar_env_collisions:
            body_part = c.static_avatar_info[body_part_id]
            print(body_part.name + " collided with the environment.")
    ```

    - `held_objects` A dictionary of IDs of objects held in each mitten. Key = arm:

    ```python
    from sticky_mitten_avatar import StickyMittenAvatarController, Arm

    c = StickyMittenAvatarController()

    # Your code here.

    # Prints all objects held by the left mitten at the last frame.
    print(c.frames[-1].held_objects[Arm.left])
    ```

    ***

    ## Functions
    """

    _P = PyImpact(initial_amp=0.01)
    _SURFACE_MATERIAL: AudioMaterial = AudioMaterial.hardwood

    def __init__(self, resp: List[bytes], objects: Dict[int, StaticObjectInfo],
                 avatar: Avatar):
        """
        :param resp: The response from the build.
        :param objects: Static object info per object. Key = the ID of the object in the scene.
        :param avatar: The avatar in the scene.
        """

        self._frame_count = Controller.get_frame(resp[-1])

        self.audio: List[Tuple[Base64Sound, int]] = list()
        collisions, env_collisions, rigidbodies = FrameData._P.get_collisions(
            resp=resp)

        # Record avatar collisions.
        if avatar is not None:
            self.avatar_object_collisions = avatar.collisions
            self.avatar_env_collisions = avatar.env_collisions
            self.held_objects = {
                Arm.left: avatar.frame.get_held_left(),
                Arm.right: avatar.frame.get_held_right()
            }
        else:
            self.avatar_object_collisions = None
            self.avatar_env_collisions = None
            self.held_objects = None

        # Get the object transform data.
        self.object_transforms: Dict[int, Transform] = dict()
        tr = get_data(resp=resp, d_type=Transforms)
        for i in range(tr.get_num()):
            o_id = tr.get_id(i)
            self.object_transforms[o_id] = Transform(
                position=np.array(tr.get_position(i)),
                rotation=np.array(tr.get_rotation(i)),
                forward=np.array(tr.get_forward(i)))

        # Get camera matrix data.
        matrices = get_data(resp=resp, d_type=CameraMatrices)
        self.projection_matrix = matrices.get_projection_matrix()
        self.camera_matrix = matrices.get_camera_matrix()

        # Get the transform data of the avatar.
        self.avatar_transform = Transform(
            position=np.array(avatar.frame.get_position()),
            rotation=np.array(avatar.frame.get_rotation()),
            forward=np.array(avatar.frame.get_forward()))
        self.avatar_body_part_transforms: Dict[int, Transform] = dict()
        for i in range(avatar.frame.get_num_body_parts()):
            self.avatar_body_part_transforms[avatar.frame.get_body_part_id(
                i)] = Transform(
                    position=np.array(avatar.frame.get_body_part_position(i)),
                    rotation=np.array(avatar.frame.get_body_part_rotation(i)),
                    forward=np.array(avatar.frame.get_body_part_forward(i)))

        # Get the audio of each collision.
        for coll in collisions:
            if not FrameData._P.is_valid_collision(coll):
                continue

            collider_id = coll.get_collider_id()
            collidee_id = coll.get_collidee_id()

            collider_info: Optional[ObjectInfo] = None
            collidee_info: Optional[ObjectInfo] = None

            if collider_id in objects:
                collider_info = objects[collider_id].audio
            # Check if the object is a body part.
            else:
                if collider_id in avatar.body_parts_static:
                    collider_info = avatar.body_parts_static[collider_id].audio
            if collidee_id in objects:
                collidee_info = objects[collidee_id].audio
            # Check if the object is a body part.
            else:
                if collidee_id in avatar.body_parts_static:
                    collidee_info = avatar.body_parts_static[collidee_id].audio

            # If either object isn't a cached object, don't try to add audio.
            if collider_info is None or collidee_info is None:
                continue

            if collider_info.mass < collidee_info.mass:
                target_id = collider_id
                target_amp = collider_info.amp
                target_mat = collider_info.material.name
                other_id = collidee_id
                other_amp = collidee_info.amp
                other_mat = collider_info.material.name
            else:
                target_id = collidee_id
                target_amp = collidee_info.amp
                target_mat = collidee_info.material.name
                other_id = collider_id
                other_amp = collider_info.amp
                other_mat = collider_info.material.name
            rel_amp = other_amp / target_amp
            audio = FrameData._P.get_sound(coll, rigidbodies, other_id,
                                           other_mat, target_id, target_mat,
                                           rel_amp)
            self.audio.append((audio, target_id))
        # Get the audio of each environment collision.
        for coll in env_collisions:
            collider_id = coll.get_object_id()
            if collider_id not in objects:
                continue
            v = FrameData._get_velocity(rigidbodies, collider_id)
            if (v is not None) and (v > 0):
                collider_info = objects[collider_id].audio
                audio = FrameData._P.get_sound(
                    coll, rigidbodies, 1, FrameData._SURFACE_MATERIAL.name,
                    collider_id, collider_info.material.name, 0.01)
                self.audio.append((audio, collider_id))
        # Get the image data.
        self.id_pass: Optional[np.array] = None
        self.depth_pass: Optional[np.array] = None
        self.image_pass: Optional[np.array] = None
        for i in range(0, len(resp) - 1):
            if OutputData.get_data_type_id(resp[i]) == "imag":
                images = Images(resp[i])
                for j in range(images.get_num_passes()):
                    if images.get_pass_mask(j) == "_id":
                        self.id_pass = images.get_image(j)
                    elif images.get_pass_mask(j) == "_depth_simple":
                        self.depth_pass = images.get_image(j)
                    elif images.get_pass_mask(j) == "_img":
                        self.image_pass = images.get_image(j)

    @staticmethod
    def set_surface_material(surface_material: AudioMaterial) -> None:
        """
        Set the surface material of the scene.

        :param surface_material: The floor's [audio material](https://github.com/threedworld-mit/tdw/blob/master/Documentation/python/py_impact.md#audiomaterialenum).
        """

        FrameData._SURFACE_MATERIAL = surface_material

    def save_images(self, output_directory: Union[str, Path]) -> None:
        """
        Save the ID pass (segmentation colors) and the depth pass to disk.
        Images will be named: `[frame_number]_[pass_name].[extension]`
        For example, the depth pass on the first frame will be named: `00000000_depth.png`
        The image pass is a jpg file and the other passes are png files.

        :param output_directory: The directory that the images will be saved to.
        """

        if isinstance(output_directory, str):
            output_directory = Path(output_directory)
        if not output_directory.exists():
            output_directory.mkdir(parents=True)
        prefix = TDWUtils.zero_padding(self._frame_count, 8)
        # Save each image.
        for image, pass_name, ext in zip(
            [self.image_pass, self.id_pass, self.depth_pass],
            ["img", "id", "depth"], ["jpg", "png", "png"]):
            p = output_directory.joinpath(f"{prefix}_{pass_name}.{ext}")
            with p.open("wb") as f:
                f.write(image)

    def get_pil_images(self) -> dict:
        """
        Convert each image pass to PIL images.

        :return: A dictionary of PIL images. Key = the name of the pass (img, id, depth)
        """

        print(type(Image.open(BytesIO(self.image_pass))))

        return {
            "img": Image.open(BytesIO(self.image_pass)),
            "id": Image.open(BytesIO(self.id_pass)),
            "depth": Image.open(BytesIO(self.depth_pass))
        }

    @staticmethod
    def _get_velocity(rigidbodies: Rigidbodies, o_id: int) -> float:
        """
        :param rigidbodies: The rigidbody data.
        :param o_id: The ID of the object.

        :return: The velocity magnitude of the object.
        """

        for i in range(rigidbodies.get_num()):
            if rigidbodies.get_id(i) == o_id:
                return np.linalg.norm(rigidbodies.get_velocity(i))