def run(self):
        if self.config.has_param('path') and self.config.has_param('paths'):
            raise Exception(
                "Objectloader can not use path and paths in the same module!")
        if self.config.has_param('path'):
            file_path = resolve_path(self.config.get_string("path"))
            loaded_objects = load_obj(filepath=file_path)
        elif self.config.has_param('paths'):
            file_paths = self.config.get_list('paths')
            loaded_objects = []
            # the file paths are mapped here to object names
            cache_objects = {}
            for file_path in file_paths:
                resolved_file_path = resolve_path(file_path)
                current_objects = load_obj(filepath=resolved_file_path,
                                           cached_objects=cache_objects)
                loaded_objects.extend(current_objects)
        else:
            raise Exception(
                "Loader module needs either a path or paths config value")

        if not loaded_objects:
            raise Exception(
                "No objects have been loaded here, check the config.")

        # Set the add_properties of all imported objects
        self._set_properties(loaded_objects)
    def build_convex_decomposition_collision_shape(
            self,
            vhacd_path: str,
            temp_dir: str = None,
            cache_dir: str = "blenderproc_resources/decomposition_cache"):
        """ Builds a collision shape of the object by decomposing it into near convex parts using V-HACD

        :param vhacd_path: The directory in which vhacd should be installed or is already installed.
        :param temp_dir: The temp dir to use for storing the object files created by v-hacd.
        :param cache_dir: If a directory is given, convex decompositions are stored there named after the meshes hash. If the same mesh is decomposed a second time, the result is loaded from the cache and the actual decomposition is skipped.
        """
        if platform == "win32":
            raise Exception("This is currently not supported under Windows")

        if temp_dir is None:
            temp_dir = Utility.get_temporary_directory()

        # Decompose the object
        parts = convex_decomposition(self,
                                     temp_dir,
                                     resolve_path(vhacd_path),
                                     cache_dir=resolve_path(cache_dir))
        parts = [MeshObject(p) for p in parts]

        # Make the convex parts children of this object, enable their rigid body component and hide them
        for part in parts:
            part.set_parent(self)
            part.enable_rigidbody(True, "CONVEX_HULL")
            part.hide()
def load_front3d(json_path: str,
                 future_model_path: str,
                 front_3D_texture_path: str,
                 label_mapping: LabelIdMapping,
                 ceiling_light_strength: float = 0.8,
                 lamp_light_strength: float = 7.0) -> List[MeshObject]:
    """ Loads the 3D-Front scene specified by the given json file.

    :param json_path: Path to the json file, where the house information is stored.
    :param future_model_path: Path to the models used in the 3D-Front dataset.
    :param front_3D_texture_path: Path to the 3D-FRONT-texture folder.
    :param label_mapping: A dict which maps the names of the objects to ids.
    :param ceiling_light_strength: Strength of the emission shader used in the ceiling.
    :param lamp_light_strength: Strength of the emission shader used in each lamp.
    :return: The list of loaded mesh objects.
    """
    json_path = resolve_path(json_path)
    future_model_path = resolve_path(future_model_path)
    front_3D_texture_path = resolve_path(front_3D_texture_path)

    if not os.path.exists(json_path):
        raise Exception("The given path does not exists: {}".format(json_path))
    if not json_path.endswith(".json"):
        raise Exception(
            "The given path does not point to a .json file: {}".format(
                json_path))
    if not os.path.exists(future_model_path):
        raise Exception("The 3D future model path does not exist: {}".format(
            future_model_path))

    # load data from json file
    with open(json_path, "r") as json_file:
        data = json.load(json_file)

    if "scene" not in data:
        raise Exception(
            "There is no scene data in this json file: {}".format(json_path))

    created_objects = Front3DLoader._create_mesh_objects_from_file(
        data, front_3D_texture_path, ceiling_light_strength, label_mapping,
        json_path)

    all_loaded_furniture = Front3DLoader._load_furniture_objs(
        data, future_model_path, lamp_light_strength, label_mapping)
    category_ids = np.unique(
        [obj.get_cp("category_id") for obj in all_loaded_furniture])
    print("furniture categories", category_ids)
    objects_to_add = Front3DLoader._move_and_duplicate_furniture(
        data, all_loaded_furniture)
    category_ids = np.unique(
        [obj.get_cp("category_id") for obj in objects_to_add])
    print("objects_to_add categories", category_ids)
    created_objects += objects_to_add

    # add an identifier to the obj
    for obj in created_objects:
        obj.set_cp("is_3d_front", True)

    return created_objects
Exemple #4
0
    def __init__(self, config):
        LoaderInterface.__init__(self, config)

        self._file_path = resolve_path(self.config.get_string("file_path"))

        self._texture_folder = resolve_path(
            self.config.get_string("texture_folder"))

        # the default unknown texture folder is not included inside of the scenenet texture folder
        default_unknown_texture_folder = os.path.join(self._texture_folder,
                                                      "unknown")
        # the textures in this folder are used, if the object has no available texture
        self._unknown_texture_folder = resolve_path(
            self.config.get_string("unknown_texture_folder",
                                   default_unknown_texture_folder))
Exemple #5
0
    def _adjust_material_nodes(mat: Material, adjustments: Dict[str, str]):
        """ Adjust the material node of the given material according to the given adjustments.

        Textures or diffuse colors will be changed according to the given material_adjustments.

        :param mat: The blender material.
        :param adjustments: A dict containing a new "diffuse" color or a new "texture" path
        """

        if "diffuse" in adjustments:
            principle_node = mat.get_the_one_node_with_type("BsdfPrincipled")
            principle_node.inputs[
                'Base Color'].default_value = Utility.hex_to_rgba(
                    adjustments["diffuse"])

        if "texture" in adjustments:
            image_path = os.path.join(SuncgLoader._suncg_dir, "texture",
                                      adjustments["texture"])
            image_path = resolve_path(image_path)

            if os.path.exists(image_path + ".png"):
                image_path += ".png"
            else:
                image_path += ".jpg"

            image_node = mat.get_the_one_node_with_type("ShaderNodeTexImage")
            if os.path.exists(image_path):
                image_node.image = bpy.data.images.load(image_path,
                                                        check_existing=True)
            else:
                print(
                    "Warning: Cannot load texture, path does not exist: {}, remove image node again"
                    .format(image_path))
                mat.remove_node(image_node)
Exemple #6
0
def load_pix3d(used_category: str, data_path: str = 'resources/pix3d') -> List[MeshObject]:
    """ Loads one random Pix3D object from the given category.

    :param used_category: The category to use for example: 'bed', check the data_path/model folder for more categories.
                          Available: ['bed', 'bookcase', 'chair', 'desk', 'misc', 'sofa', 'table', 'tool', 'wardrobe']
    :param data_path: The path to the Pix3D folder.
    :return: The list of loaded mesh objects.
    """
    data_path = resolve_path(data_path)
    files_with_fitting_category = Pix3DLoader.get_files_with_category(used_category, data_path)

    selected_obj = random.choice(files_with_fitting_category)
    loaded_obj = load_obj(selected_obj)

    Pix3DLoader._correct_materials(loaded_obj)

    # removes the x axis rotation found in all ShapeNet objects, this is caused by importing .obj files
    # the object has the same pose as before, just that the rotation_euler is now [0, 0, 0]
    for obj in loaded_obj:
        obj.persist_transformation_into_mesh(location=False, rotation=True, scale=False)

    # move the origin of the object to the world origin and on top of the X-Y plane
    # makes it easier to place them later on, this does not change the `.location`
    for obj in loaded_obj:
        obj.move_origin_to_bottom_mean_point()
    bpy.ops.object.select_all(action='DESELECT')

    return loaded_obj
    def _collect_arguments_from_file(self, path, file_format,
                                     number_of_arguments):
        """ Reads in all lines of the given file and returns them as a list of lists of arguments

        This method also checks is the lines match the configured file format.

        :param path: The path of the file.
        :param file_format: Specifies how the arguments should be mapped to parameters.
        :param number_of_arguments: The total number of arguments required per line.
        :return: A list of lists of arguments
        """
        arguments = []
        if path != "":
            with open(resolve_path(path)) as f:
                lines = f.readlines()
                # remove all empty lines
                lines = [line for line in lines if len(line.strip()) > 3]

                for line in lines:
                    # Split line into separate arguments
                    line_args = line.strip().split()
                    # Make sure the arguments match the configured file format
                    if len(line_args) != number_of_arguments:
                        raise Exception(
                            "A line in the given cam pose file does not match the configured file format:\n"
                            + line.strip() + " (Number of values: " +
                            str(len(line_args)) + ")\n" + str(file_format) +
                            " (Number of values: " + str(number_of_arguments) +
                            ")")

                    # Parse arguments in line using json. (In this way "test" will be mapped to a string, while 42 will be mapped to an integer)
                    arguments.append([json.loads(x) for x in line_args])

        return arguments
 def __init__(self, config):
     LoaderInterface.__init__(self, config)
     self.house_path = resolve_path(self.config.get_string("path"))
     suncg_folder_path = os.path.join(os.path.dirname(self.house_path),
                                      "../..")
     self.suncg_dir = self.config.get_string("suncg_path",
                                             suncg_folder_path)
Exemple #9
0
    def setup_utility_paths(temp_dir: str):
        """ Set utility paths: Temp dir and working dir.

        :param temp_dir: Path to temporary directory where Blender saves output. Default is shared memory.
        """
        from blenderproc.python.utility.Utility import Utility, resolve_path

        Utility.temp_dir = resolve_path(temp_dir)
        os.makedirs(Utility.temp_dir, exist_ok=True)
    def _default_init(self):
        """
        These operations are called during all modules inits
        """
        self._output_dir = resolve_path(
            self.config.get_string("output_dir", ""))
        os.makedirs(self._output_dir, exist_ok=True)

        self._temp_dir = Utility.get_temporary_directory()

        self._avoid_output = self.config.get_bool("avoid_output", False)
Exemple #11
0
    def __init__(self, config: Config):
        LoaderInterface.__init__(self, config)

        self.mapping_file = resolve_path(
            self.config.get_string(
                "mapping_file",
                resolve_resource(
                    os.path.join("front_3D", "3D_front_mapping.csv"))))
        if not os.path.exists(self.mapping_file):
            raise Exception("The mapping file could not be found: {}".format(
                self.mapping_file))
    def run(self):
        if self.config.get_bool("use_all_materials", False) and self.config.has_param("used_assets"):
            raise Exception("It is impossible to use all materials and selected a certain list of assets!")

        load_ccmaterials(
            folder_path=resolve_path(self.config.get_string("folder_path", resolve_resource("cctextures"))),
            used_assets=self.config.get_list("used_assets", []),
            preload=self.config.get_bool("preload", False),
            fill_used_empty_materials=self.config.get_bool("fill_used_empty_materials", False),
            add_custom_properties=self.config.get_raw_dict("add_custom_properties", {}),
            use_all_materials=self.config.get_bool("use_all_materials", False)
        )
Exemple #13
0
    def __init__(self, config):
        LoaderInterface.__init__(self, config)

        self._data_dir = resolve_path(
            self.config.get_string("data_dir", resolve_resource("IKEA")))

        if self.config.has_param("category"):
            self._obj_categories = self.config.get_raw_value("category", None)
        else:
            self._obj_categories = None

        self._obj_style = self.config.get_raw_value("style", None)
Exemple #14
0
def load_shapenet(data_path: str,
                  used_synset_id: str,
                  used_source_id: str = "",
                  move_object_origin: bool = True) -> MeshObject:
    """ This loads an object from ShapeNet based on the given synset_id, which specifies the category of objects to use.

    From these objects one is randomly sampled and loaded.

    Todo: not good:
    Note: if this module is used with another loader that loads objects with semantic mapping, make sure the other module is loaded first in the config file.

    :param data_path: The path to the ShapeNetCore.v2 folder.
    :param used_synset_id: The synset id for example: '02691156', check the data_path folder for more ids.
    :param used_source_id: Object identifier of the a particular ShapeNet category, see inside any ShapeNet category for identifiers
    :param move_object_origin: Moves the object center to the bottom of the bounding box in Z direction and also in the middle of the X and Y plane, this does not change the `.location` of the object. Default: True
    :return: The loaded mesh object.
    """
    data_path = resolve_path(data_path)
    taxonomy_file_path = os.path.join(data_path, "taxonomy.json")

    files_with_fitting_synset = ShapeNetLoader._get_files_with_synset(
        used_synset_id, used_source_id, taxonomy_file_path, data_path)
    selected_obj = random.choice(files_with_fitting_synset)
    loaded_objects = load_obj(selected_obj)

    # In shapenet every .obj file only contains one object, make sure that is the case
    if len(loaded_objects) != 1:
        raise Exception(
            "The ShapeNetLoader expects every .obj file to contain exactly one object, however the file "
            + selected_obj + " contained " + str(len(loaded_objects)) +
            " objects.")
    obj = loaded_objects[0]

    obj.set_cp("used_synset_id", used_synset_id)
    obj.set_cp("used_source_id", pathlib.PurePath(selected_obj).parts[-3])

    ShapeNetLoader._correct_materials(obj)

    # removes the x axis rotation found in all ShapeNet objects, this is caused by importing .obj files
    # the object has the same pose as before, just that the rotation_euler is now [0, 0, 0]
    obj.persist_transformation_into_mesh(location=False,
                                         rotation=True,
                                         scale=False)

    # check if the move_to_world_origin flag is set
    if move_object_origin:
        # move the origin of the object to the world origin and on top of the X-Y plane
        # makes it easier to place them later on, this does not change the `.location`
        obj.move_origin_to_bottom_mean_point()
    bpy.ops.object.select_all(action='DESELECT')

    return obj
Exemple #15
0
    def __init__(self, config_path, args, temp_dir, avoid_output=False):
        """
        Inits the pipeline, by calling the constructors of all modules mentioned in the config.

        :param config_path: path to the config
        :param args: arguments which were provided to the run.py and are specified in the config file
        :param temp_dir: the directory where to put temporary files during the execution
        :param avoid_output: if this is true, all modules (renderers and writers) skip producing output. With this it is possible to debug \
                               properly.
        """

        config_parser = ConfigParser(silent=True)
        config = config_parser.parse(resolve_path(config_path), args)

        # Setup pip packages specified in config
        SetupUtility.setup_pip(config["setup"]["pip"] if "pip" in config["setup"] else [])

        if avoid_output:
            GlobalStorage.add_to_config_before_init("avoid_output", True)

        Utility.temp_dir = resolve_path(temp_dir)
        os.makedirs(Utility.temp_dir, exist_ok=True)

        self.modules = Utility.initialize_modules(config["modules"])
 def __init__(self, config):
     LoaderInterface.__init__(self, config)
     self._data_path = resolve_path(
         self.config.get_string("data_path", resolve_resource("AMASS")))
     # Body Model Specs
     self._used_body_model_gender = self.config.get_string(
         "body_model_gender", random.choice(["male", "female", "neutral"]))
     # These numbers are based on a recommendation from the authors. refer to visualization tutorial from the
     # authors: https://github.com/nghorbani/amass/blob/master/notebooks/01-AMASS_Visualization.ipynb
     self._num_betas = 10  # number of body parameters
     self._num_dmpls = 8  # number of DMPL parameters
     # Pose Specs
     self._used_sub_dataset_id = self.config.get_string("sub_dataset_id")
     self._used_subject_id = self.config.get_string("subject_id", "")
     self._used_sequence_id = self.config.get_int("sequence_id", -1)
     self._used_frame_id = self.config.get_int("frame_id", -1)
    def _add_parameter(self, param_name: str, default_path: str, environment_key: str):
        """
        Adds an parameter to the object, the name of the parameter is defined by the param_name. The default_path is
        only used if it exists, if it does not exists the environment_key is used. An error is thrown if both do
        not exist.

        :param param_name: Name of the new parameter
        :param default_path: Default path used for this parameter
        :param environment_key: Environment key which has to be set if the default path does not exist
        """
        setattr(self, param_name, abspath(join(self._main_folder, default_path)))
        if not exists(getattr(self, param_name)):
            if environment_key in os.environ:
                setattr(self, param_name, resolve_path(os.environ[environment_key]))
            if not exists(getattr(self, param_name)):
                raise Exception(f"The env variable: \"{environment_key}\" is empty or does not exist and the default "
                                f"path does also not exist: {default_path}")
Exemple #18
0
    def _read_model_category_mapping(path: str):
        """ Reads in the model category mapping csv.

        :param path: The path to the csv file.
        """
        object_label_map = {}
        object_fine_grained_label_map = {}
        object_coarse_grained_label_map = {}

        with open(resolve_path(path), 'r') as csvfile:
            reader = csv.DictReader(csvfile)
            for row in reader:
                object_label_map[row["model_id"]] = row["nyuv2_40class"]
                object_fine_grained_label_map[
                    row["model_id"]] = row["fine_grained_class"]
                object_coarse_grained_label_map[
                    row["model_id"]] = row["coarse_grained_class"]

        return object_label_map, object_fine_grained_label_map, object_coarse_grained_label_map
Exemple #19
0
    def _load_and_postprocess(self, file_path, key, version="1.0.0"):
        """
        Loads an image and post process it.

        :param file_path: Image path. Type: string.
        :param key: The image's key with regards to the hdf5 file. Type: string.
        :param version: The version number original data. Type: String. Default: 1.0.0.
        :return: The post-processed image that was loaded using the file path.
        """
        data = WriterUtility.load_output_file(resolve_path(file_path),
                                              self.write_alpha_channel,
                                              remove=False)
        data, new_key, new_version = self._apply_postprocessing(
            key, data, version)
        if isinstance(data, np.ndarray):
            print("Key: " + key + " - shape: " + str(data.shape) +
                  " - dtype: " + str(data.dtype) + " - path: " + file_path)
        else:
            print("Key: " + key + " - path: " + file_path)
        return data, new_key, new_version
def load_texture(path: str, colorspace: str = "sRGB") -> List[bpy.types.Texture]:
    """ Loads images and creates image textures.

    Depending on the form of the provided path:
    1. Loads an image, creates an image texture, and assigns the loaded image to the texture, when a path to an
    image is provided.
    2. Loads images and for each creates a texture, and assing an image to this texture, if a path to a
    folder with images is provided.

    NOTE: Same image file can be loaded once to avoid unnecessary overhead. If you really need the same image in
    different colorspaces, then have a copy per desired colorspace and load them in different instances of this Loader.

    :param path: The path to the folder with assets/to the asset.
    :param colorspace: Colorspace type to assign to loaded assets. Available: ['Filmic Log', 'Linear', 'Linear ACES', 'Non-Color', 'Raw', 'sRGB', 'XYZ'].
    :return: The list of created textures.
    """
    path = resolve_path(path)
    image_paths = TextureLoader._resolve_paths(path)
    textures = TextureLoader._load_and_create(image_paths, colorspace)

    return textures
Exemple #21
0
    def run(self):
        """ Samples a path to an object.

        :return: A path to object. Type: string.
        """
        # get path to folder
        path = resolve_path(self.config.get_string("path"))

        # get list of paths
        paths = glob(path)

        if self.config.has_param("return_all"):
            return paths
        elif self.config.has_param("random_samples"):
            return random.choices(paths,
                                  k=self.config.get_int("random_samples"))
        else:
            # chose a random one
            chosen_path = choice(paths)

            return chosen_path
    def _write_frames(chunks_dir: str,
                      dataset_objects: list,
                      depths: List[np.ndarray] = [],
                      colors: List[np.ndarray] = [],
                      color_file_format: str = "PNG",
                      depth_scale: float = 1.0,
                      frames_per_chunk: int = 1000,
                      m2mm: bool = True,
                      ignore_dist_thres: float = 100.,
                      save_world2cam: bool = True,
                      jpg_quality: int = 95):
        """Write each frame's ground truth into chunk directory in BOP format

        :param chunks_dir: Path to the output directory of the current chunk.
        :param dataset_objects: Save annotations for these objects.
        :param depths: List of depth images in m to save
        :param colors: List of color images to save
        :param color_file_format: File type to save color images. Available: "PNG", "JPEG"
        :param jpg_quality: If color_file_format is "JPEG", save with the given quality.
        :param depth_scale: Multiply the uint16 output depth image with this factor to get depth in mm. Used to trade-off between depth accuracy 
            and maximum depth value. Default corresponds to 65.54m maximum depth and 1mm accuracy.
        :param ignore_dist_thres: Distance between camera and object after which object is ignored. Mostly due to failed physics.
        :param m2mm: Original bop annotations and models are in mm. If true, we convert the gt annotations to mm here. This
            is needed if BopLoader option mm2m is used.
        :param frames_per_chunk: Number of frames saved in each chunk (called scene in BOP) 
        """

        # Format of the depth images.
        depth_ext = '.png'

        rgb_tpath = os.path.join(chunks_dir, '{chunk_id:06d}', 'rgb',
                                 '{im_id:06d}' + '{im_type}')
        depth_tpath = os.path.join(chunks_dir, '{chunk_id:06d}', 'depth',
                                   '{im_id:06d}' + depth_ext)
        chunk_camera_tpath = os.path.join(chunks_dir, '{chunk_id:06d}',
                                          'scene_camera.json')
        chunk_gt_tpath = os.path.join(chunks_dir, '{chunk_id:06d}',
                                      'scene_gt.json')

        # Paths to the already existing chunk folders (such folders may exist
        # when appending to an existing dataset).
        chunk_dirs = sorted(glob.glob(os.path.join(chunks_dir, '*')))
        chunk_dirs = [d for d in chunk_dirs if os.path.isdir(d)]

        # Get ID's of the last already existing chunk and frame.
        curr_chunk_id = 0
        curr_frame_id = 0
        if len(chunk_dirs):
            last_chunk_dir = sorted(chunk_dirs)[-1]
            last_chunk_gt_fpath = os.path.join(last_chunk_dir, 'scene_gt.json')
            chunk_gt = BopWriterUtility._load_json(last_chunk_gt_fpath,
                                                   keys_to_int=True)

            # Last chunk and frame ID's.
            last_chunk_id = int(os.path.basename(last_chunk_dir))
            last_frame_id = int(sorted(chunk_gt.keys())[-1])

            # Current chunk and frame ID's.
            curr_chunk_id = last_chunk_id
            curr_frame_id = last_frame_id + 1
            if curr_frame_id % frames_per_chunk == 0:
                curr_chunk_id += 1
                curr_frame_id = 0

        # Initialize structures for the GT annotations and camera info.
        chunk_gt = {}
        chunk_camera = {}
        if curr_frame_id != 0:
            # Load GT and camera info of the chunk we are appending to.
            chunk_gt = BopWriterUtility._load_json(
                chunk_gt_tpath.format(chunk_id=curr_chunk_id),
                keys_to_int=True)
            chunk_camera = BopWriterUtility._load_json(
                chunk_camera_tpath.format(chunk_id=curr_chunk_id),
                keys_to_int=True)

        # Go through all frames.
        num_new_frames = bpy.context.scene.frame_end - bpy.context.scene.frame_start

        if len(depths) != len(colors) != num_new_frames:
            raise Exception(
                "The amount of images stored in the depths/colors does not correspond to the amount"
                "of images specified by frame_start to frame_end.")

        for frame_id in range(bpy.context.scene.frame_start,
                              bpy.context.scene.frame_end):
            # Activate frame.
            bpy.context.scene.frame_set(frame_id)

            # Reset data structures and prepare folders for a new chunk.
            if curr_frame_id == 0:
                chunk_gt = {}
                chunk_camera = {}
                os.makedirs(
                    os.path.dirname(
                        rgb_tpath.format(chunk_id=curr_chunk_id,
                                         im_id=0,
                                         im_type='PNG')))
                os.makedirs(
                    os.path.dirname(
                        depth_tpath.format(chunk_id=curr_chunk_id, im_id=0)))

            # Get GT annotations and camera info for the current frame.

            # Output translation gt in m or mm
            unit_scaling = 1000. if m2mm else 1.

            chunk_gt[curr_frame_id] = BopWriterUtility._get_frame_gt(
                dataset_objects, unit_scaling, ignore_dist_thres)
            chunk_camera[curr_frame_id] = BopWriterUtility._get_frame_camera(
                save_world2cam, depth_scale, unit_scaling)

            if colors:
                color_rgb = colors[frame_id]
                color_bgr = color_rgb[..., ::-1].copy()
                if color_file_format == 'PNG':
                    rgb_fpath = rgb_tpath.format(chunk_id=curr_chunk_id,
                                                 im_id=curr_frame_id,
                                                 im_type='.png')
                    cv2.imwrite(rgb_fpath, color_bgr)
                elif color_file_format == 'JPEG':
                    rgb_fpath = rgb_tpath.format(chunk_id=curr_chunk_id,
                                                 im_id=curr_frame_id,
                                                 im_type='.jpg')
                    cv2.imwrite(rgb_fpath, color_bgr,
                                [int(cv2.IMWRITE_JPEG_QUALITY), jpg_quality])
            else:
                rgb_output = Utility.find_registered_output_by_key("colors")
                if rgb_output is None:
                    raise Exception("RGB image has not been rendered.")
                color_ext = '.png' if rgb_output['path'].endswith(
                    'png') else '.jpg'
                # Copy the resulting RGB image.
                rgb_fpath = rgb_tpath.format(chunk_id=curr_chunk_id,
                                             im_id=curr_frame_id,
                                             im_type=color_ext)
                shutil.copyfile(rgb_output['path'] % frame_id, rgb_fpath)

            if depths:
                depth = depths[frame_id]
            else:
                # Load the resulting dist image.
                dist_output = Utility.find_registered_output_by_key("distance")
                if dist_output is None:
                    raise Exception("Distance image has not been rendered.")
                distance = WriterUtility.load_output_file(resolve_path(
                    dist_output['path'] % frame_id),
                                                          remove=False)
                depth = dist2depth(distance)

            # Scale the depth to retain a higher precision (the depth is saved
            # as a 16-bit PNG image with range 0-65535).
            depth_mm = 1000.0 * depth  # [m] -> [mm]
            depth_mm_scaled = depth_mm / float(depth_scale)

            # Save the scaled depth image.
            depth_fpath = depth_tpath.format(chunk_id=curr_chunk_id,
                                             im_id=curr_frame_id)
            BopWriterUtility._save_depth(depth_fpath, depth_mm_scaled)

            # Save the chunk info if we are at the end of a chunk or at the last new frame.
            if ((curr_frame_id + 1) % frames_per_chunk == 0) or \
                    (frame_id == num_new_frames - 1):

                # Save GT annotations.
                BopWriterUtility._save_json(
                    chunk_gt_tpath.format(chunk_id=curr_chunk_id), chunk_gt)

                # Save camera info.
                BopWriterUtility._save_json(
                    chunk_camera_tpath.format(chunk_id=curr_chunk_id),
                    chunk_camera)

                # Update ID's.
                curr_chunk_id += 1
                curr_frame_id = 0
            else:
                curr_frame_id += 1
Exemple #23
0
def load_haven_mat(folder_path: str = "resources/haven",
                   used_assets: list = [],
                   preload: bool = False,
                   fill_used_empty_materials: bool = False,
                   add_cp: dict = {}):
    """ Loads all specified haven textures from the given directory.

    :param folder_path: The path to the downloaded haven.
    :param used_assets: A list of all asset names, you want to use. The asset-name must not be typed in completely, only the
                        beginning the name starts with. By default all assets will be loaded, specified by an empty list.
    :param preload: If set true, only the material names are loaded and not the complete material.
    :param fill_used_empty_materials: If set true, the preloaded materials, which are used are now loaded completely.
    :param add_cp: A dictionary of materials and the respective properties.
    """
    # makes the integration of complex materials easier
    addon_utils.enable("node_wrangler")

    folder_path = resolve_path(folder_path)

    if preload and fill_used_empty_materials:
        raise Exception(
            "Preload and fill used empty materials can not be done at the same time, check config!"
        )
    if os.path.exists(folder_path) and os.path.isdir(folder_path):
        for asset in os.listdir(folder_path):
            if used_assets:
                skip_this_one = True
                for used_asset in used_assets:
                    if asset.startswith(used_asset):
                        skip_this_one = False
                        break
                if skip_this_one:
                    continue
            current_path = os.path.join(folder_path, asset)
            if os.path.isdir(current_path):
                # find the current base_image_path by search for _diff_, this make it independent of the used res
                all_paths = glob.glob(os.path.join(current_path, "*.jpg"))
                base_image_path = ""
                for path in all_paths:
                    if "_diff_" in path:
                        base_image_path = path
                        break
                if not os.path.exists(base_image_path):
                    continue

                # if the material was already created it only has to be searched
                if fill_used_empty_materials:
                    new_mat = MaterialLoaderUtility.find_cc_material_by_name(
                        asset, add_cp)
                else:
                    new_mat = MaterialLoaderUtility.create_new_cc_material(
                        asset, add_cp)
                if preload:
                    # if preload then the material is only created but not filled
                    continue
                elif fill_used_empty_materials and not MaterialLoaderUtility.is_material_used(
                        new_mat):
                    # now only the materials, which have been used should be filled
                    continue

                # construct all image paths
                # the images path contain the words named in this list, but some of them are differently
                # capitalized, e.g. Nor, NOR, NoR, ...
                used_elements = [
                    "ao", "spec", "rough", "nor", "disp", "bump", "alpha"
                ]
                final_paths = {}
                for ele in used_elements:
                    new_path = base_image_path.replace("diff", ele).lower()
                    found_path = ""
                    for path in all_paths:
                        if path.lower() == new_path:
                            found_path = path
                            break
                    final_paths[ele] = found_path

                # create material based on these image paths
                HavenMaterialLoader.create_material(
                    new_mat, base_image_path, final_paths["ao"],
                    final_paths["spec"], final_paths["rough"],
                    final_paths["alpha"], final_paths["nor"],
                    final_paths["disp"], final_paths["bump"])
    else:
        raise Exception(
            "The folder path does not exist: {}".format(folder_path))
Exemple #24
0
def load_suncg(
        house_path: str,
        label_mapping: LabelIdMapping,
        suncg_dir: Optional[str] = None) -> List[Union[Entity, MeshObject]]:
    """ Loads a house.json file into blender.

    - Loads all objects files specified in the house.json file.
    - Orders them hierarchically (level -> room -> object)
    - Writes metadata into the custom properties of each object

    :param house_path: The path to the house.json file which should be loaded.
    :param suncg_dir: The path to the suncg root directory which should be used for loading objects, rooms, textures etc.
    :return: The list of loaded mesh objects.
    """
    # If not suncg root directory has been given, determine it via the given house directory.
    if suncg_dir is None:
        suncg_dir = os.path.join(os.path.dirname(house_path), "../..")

    SuncgLoader._suncg_dir = suncg_dir
    SuncgLoader._collection_of_loaded_objs = {}
    # there are only two types of materials, textures and diffuse
    SuncgLoader._collection_of_loaded_mats = {"texture": {}, "diffuse": {}}

    with open(resolve_path(house_path), "r") as f:
        config = json.load(f)

    object_label_map, object_fine_grained_label_map, object_coarse_grained_label_map = SuncgLoader._read_model_category_mapping(
        resolve_resource(os.path.join('suncg', 'Better_labeling_for_NYU.csv')))

    house_id = config["id"]
    loaded_objects = []

    for level in config["levels"]:
        # Build empty level object which acts as a parent for all rooms on the level
        level_obj = create_empty("Level#" + level["id"])
        level_obj.set_cp("type", "Level")
        if "bbox" in level:
            level_obj.set_cp("bbox",
                             SuncgLoader._correct_bbox_frame(level["bbox"]))
        else:
            print(
                "Warning: The level with id " + level["id"] +
                " is missing the bounding box attribute in the given house.json file!"
            )
        loaded_objects.append(level_obj)

        room_per_object: Dict[int, Entity] = {}

        for node in level["nodes"]:
            # Skip invalid nodes (This is the same behavior as in the SUNCG Toolbox)
            if "valid" in node and node["valid"] == 0:
                continue

            # Metadata is directly stored in the objects custom data
            metadata = {"type": node["type"], "is_suncg": True}

            if "modelId" in node:
                metadata["modelId"] = node["modelId"]

                if node["modelId"] in object_fine_grained_label_map:
                    metadata[
                        "fine_grained_class"] = object_fine_grained_label_map[
                            node["modelId"]]
                    metadata[
                        "coarse_grained_class"] = object_coarse_grained_label_map[
                            node["modelId"]]
                    metadata["category_id"] = label_mapping.id_from_label(
                        object_label_map[node["modelId"]])

            if "bbox" in node:
                metadata["bbox"] = SuncgLoader._correct_bbox_frame(
                    node["bbox"])

            if "transform" in node:
                transform = Matrix(
                    [node["transform"][i * 4:(i + 1) * 4] for i in range(4)])
                # Transpose, as given transform matrix was col-wise, but blender expects row-wise
                transform.transpose()
            else:
                transform = None

            if "materials" in node:
                material_adjustments = node["materials"]
            else:
                material_adjustments = []

            # Lookup if the object belongs to a room
            object_id = int(node["id"].split("_")[-1])
            if object_id in room_per_object:
                parent = room_per_object[object_id]
            else:
                parent = level_obj

            if node["type"] == "Room":
                loaded_objects += SuncgLoader._load_room(
                    node, metadata, material_adjustments, transform, house_id,
                    level_obj, room_per_object, label_mapping)
            elif node["type"] == "Ground":
                loaded_objects += SuncgLoader._load_ground(
                    node, metadata, material_adjustments, transform, house_id,
                    parent, label_mapping)
            elif node["type"] == "Object":
                loaded_objects += SuncgLoader._load_object(
                    node, metadata, material_adjustments, transform, parent)
            elif node["type"] == "Box":
                loaded_objects += SuncgLoader._load_box(
                    node, material_adjustments, transform, parent,
                    label_mapping)
    SuncgLoader._rename_materials()
    return loaded_objects
Exemple #25
0
def load_blend(path: str, obj_types: Optional[Union[List[str], str]] = None, name_regrex: Optional[str] = None,
               data_blocks: Union[List[str], str] = "objects") -> List[Entity]:
    """
    Loads entities (everything that can be stored in a .blend file's folders, see Blender's documentation for
    bpy.types.ID for more info) that match a name pattern from a specified .blend file's section/datablock.

    :param path: Path to a .blend file.
    :param obj_types: The type of objects to load. This parameter is only relevant when `data_blocks` is set to `"objects"`.
                      Available options are: ['mesh', 'curve', 'hair', 'armature', 'empty', 'light', 'camera']
    :param name_regrex: Regular expression representing a name pattern of entities' (everything that can be stored in a .blend
                     file's folders, see Blender's documentation for bpy.types.ID for more info) names.
    :param data_blocks: The datablock or a list of datablocks which should be loaded from the given .blend file.
                        Available options are: ['armatures', 'cameras', 'curves', 'hairs', 'images', 'lights', 'materials', 'meshes', 'objects', 'textures']
    :return: The list of loaded mesh objects.
    """
    if obj_types is None:
        obj_types = ["mesh", "empty"]
    # get a path to a .blend file
    path = resolve_path(path)
    data_blocks = BlendLoader._validate_and_standardizes_configured_list(data_blocks, BlendLoader.valid_datablocks,
                                                                         "data block")
    obj_types = BlendLoader._validate_and_standardizes_configured_list(obj_types, BlendLoader.valid_object_types,
                                                                       "object type")

    # Remember which orphans existed beforehand
    orphans_before = collect_all_orphan_datablocks()

    # Start importing blend file. All objects that should be imported need to be copied from "data_from" to "data_to"
    with bpy.data.libraries.load(path) as (data_from, data_to):
        for data_block in data_blocks:
            # Verify that the given data block is valid
            if hasattr(data_from, data_block):
                # Find all entities of this data block that match the specified pattern
                data_to_entities = []
                for entity_name in getattr(data_from, data_block):
                    if not name_regrex or re.fullmatch(name_regrex, entity_name) is not None:
                        data_to_entities.append(entity_name)
                # Import them
                setattr(data_to, data_block, data_to_entities)
                print("Imported " + str(len(data_to_entities)) + " " + data_block)
            else:
                raise Exception("No such data block: " + data_block)

    # Go over all imported objects again
    loaded_objects: List[Entity] = []
    for data_block in data_blocks:
        # Some adjustments that only affect objects
        if data_block == "objects":
            for obj in getattr(data_to, data_block):
                # Check that the object type is desired
                if obj.type.lower() in obj_types:
                    # Link objects to the scene
                    bpy.context.collection.objects.link(obj)
                    if obj.type == 'MESH':
                        loaded_objects.append(MeshObject(obj))
                    elif obj.type == 'LIGHT':
                        loaded_objects.append(Light(blender_obj=obj))
                    else:
                        loaded_objects.append(Entity(obj))

                    # If a camera was imported
                    if obj.type == 'CAMERA':
                        # Make it the active camera in the scene
                        bpy.context.scene.camera = obj

                        # Find the maximum frame number of its key frames
                        max_keyframe = -1
                        if obj.animation_data is not None:
                            fcurves = obj.animation_data.action.fcurves
                            for curve in fcurves:
                                keyframe_points = curve.keyframe_points
                                for keyframe in keyframe_points:
                                    max_keyframe = max(max_keyframe, keyframe.co[0])

                        # Set frame_end to the next free keyframe
                        bpy.context.scene.frame_end = max_keyframe + 1
                else:
                    # Remove object again if its type is not desired
                    bpy.data.objects.remove(obj, do_unlink=True)
            print("Selected " + str(len(loaded_objects)) + " of the loaded objects by type")
        else:
            loaded_objects.extend(getattr(data_to, data_block))

    # As some loaded objects were deleted again due to their type, we need also to remove the dependent datablocks that were also loaded and are now orphans
    BlendLoader._purge_added_orphans(orphans_before, data_to)
    return loaded_objects
def extract_floor(mesh_objects: List[MeshObject], compare_angle_degrees: float = 7.5, compare_height: float = 0.15,
                  up_vector_upwards: bool = True, height_list_path: str = None,
                  new_name_for_object: str = "Floor", should_skip_if_object_is_already_there: bool = False) \
        -> List[MeshObject]:
    """ Extracts floors in the following steps:
    1. Searchs for the specified object.
    2. Splits the surfaces which point upwards at a specified level away.

    :param mesh_objects: Objects to where all polygons will be extracted.
    :param compare_angle_degrees: Maximum difference between the up vector and the current polygon normal in degrees.
    :param compare_height: Maximum difference in Z direction between the polygons median point and the specified height of the room.
    :param up_vector_upwards: If this is True the `up_vec` points upwards -> [0, 0, 1] if not it points downwards: [0, 0, -1] in world coordinates. This vector is used for the `compare_angle_degrees` option.
    :param height_list_path: Path to a file with height values. If none is provided, a ceiling and floor is automatically detected. \
                             This might fail. The height_list_values can be specified in a list like fashion in the file: [0.0, 2.0]. \
                             These values are in the same size the dataset is in, which is usually meters. The content must always be \
                             a list, e.g. [0.0].
    :param new_name_for_object: Name for the newly created object, which faces fulfill the given parameters.
    :param should_skip_if_object_is_already_there: If this is true no extraction will be done, if an object is there, which has the same name as
                                                   name_for_split_obj, which would be used for the newly created object.
    :return: The extracted floor objects.
    """
    # set the up_vector
    up_vec = mathutils.Vector([0, 0, 1])
    if not up_vector_upwards:
        up_vec *= -1.0

    height_list = []
    if height_list_path is not None:
        height_file_path = resolve_path(height_list_path)
        with open(height_file_path) as file:
            import ast
            height_list = [float(val) for val in ast.literal_eval(file.read())]

    object_names = [
        obj.name for obj in bpy.context.scene.objects if obj.type == "MESH"
    ]

    def clean_up_name(name: str):
        """
        Clean up the given name from Floor1 to floor

        :param name: given name
        :return: str: cleaned up name
        """
        name = ''.join([i for i in name if not i.isdigit()])  # remove digits
        name = name.lower().replace(".",
                                    "").strip()  # remove dots and whitespace
        return name

    object_names = [clean_up_name(name) for name in object_names]
    if should_skip_if_object_is_already_there and new_name_for_object.lower(
    ) in object_names:
        # if should_skip is True and if there is an object, which name is the same as the one for the newly
        # split object, than the execution is skipped
        return []

    newly_created_objects = []
    for obj in mesh_objects:
        obj.edit_mode()
        bm = obj.mesh_as_bmesh()
        bpy.ops.mesh.select_all(action='DESELECT')

        if height_list:
            counter = 0
            for height_val in height_list:
                counter = FloorExtractor.select_at_height_value(
                    bm, height_val, compare_height, up_vec,
                    compare_angle_degrees, obj.get_local2world_mat())

            if counter:
                obj.update_from_bmesh(bm)
                bpy.ops.mesh.separate(type='SELECTED')
        else:
            # no height list was provided, try to estimate them on its own

            # first get a list of all height values of the median points, which are inside of the defined
            # compare angle range
            list_of_median_poses: Union[List[float], np.ndarray] = [
                FloorExtractor._get_median_face_pose(
                    f, obj.get_local2world_mat())[2] for f in bm.faces
                if FloorExtractor._check_face_angle(f, obj.get_local2world_mat(
                ), up_vec, compare_angle_degrees)
            ]
            if not list_of_median_poses:
                print(
                    "Object with name: {} is skipped no faces were relevant, try with "
                    "flipped up_vec".format(obj.get_name()))
                list_of_median_poses = [
                    FloorExtractor._get_median_face_pose(
                        f, obj.get_local2world_mat())[2] for f in bm.faces
                    if FloorExtractor._check_face_angle(
                        f, obj.get_local2world_mat(), -up_vec,
                        compare_angle_degrees)
                ]
                if not list_of_median_poses:
                    print("Still no success for: {} skip object.".format(
                        obj.get_name()))
                    bpy.ops.object.mode_set(mode='OBJECT')
                    bpy.ops.object.select_all(action='DESELECT')
                    continue

                successful_up_vec = -up_vec
            else:
                successful_up_vec = up_vec

            list_of_median_poses = np.reshape(list_of_median_poses, (-1, 1))
            if np.var(list_of_median_poses) < 1e-4:
                # All faces are already correct
                height_value = np.mean(list_of_median_poses)
            else:
                ms = MeanShift(bandwidth=0.2, bin_seeding=True)
                ms.fit(list_of_median_poses)

                # if the up vector is negative the maximum value is searched
                if up_vector_upwards:
                    height_value = np.min(ms.cluster_centers_)
                else:
                    height_value = np.max(ms.cluster_centers_)

            counter = FloorExtractor.select_at_height_value(
                bm, height_value, compare_height, successful_up_vec,
                compare_angle_degrees, obj.get_local2world_mat())

            if counter:
                obj.update_from_bmesh(bm)
                bpy.ops.mesh.separate(type='SELECTED')
            selected_objects = bpy.context.selected_objects
            if selected_objects:
                if len(selected_objects) == 2:
                    selected_objects = [
                        o for o in selected_objects
                        if o != bpy.context.view_layer.objects.active
                    ]
                    selected_objects[0].name = new_name_for_object
                    newly_created_objects.append(
                        MeshObject(selected_objects[0]))
                else:
                    raise Exception(
                        "There is more than one selection after splitting, this should not happen!"
                    )
            else:
                raise Exception("No floor object was constructed!")

        obj.object_mode()

    return newly_created_objects
    def run(self):

        max_distance = self.config.get_float("max_distance", 24)
        normal_len = self.config.get_float("normal_length", 0.1)

        path_to_hdf5 = resolve_path(self.config.get_string("path_to_hdf5"))
        data = {}
        with h5py.File(path_to_hdf5, "r") as file:
            for key in file.keys():
                data[key] = np.array(file[key])

        if "normals" not in data:
            raise Exception(
                "The hdf5 container does not contain normals: {}".format(
                    path_to_hdf5))
        if "distance" not in data:
            raise Exception(
                "The hdf5 container does not contain distance data: {}".format(
                    path_to_hdf5))
        if "campose" not in data:
            raise Exception(
                "The hdf5 container does not contain a camera pose: {}".format(
                    path_to_hdf5))

        normal_img = data["normals"]
        distance_img = data["distance"]
        campose = eval(data["campose"])[0]
        cam_ob = bpy.context.scene.camera
        cam = cam_ob.data
        cam_ob.location = campose["location"]
        cam_ob.rotation_euler = campose["rotation_euler"]
        cam.angle_x = campose["fov_x"]
        cam.angle_y = campose["fov_y"]
        bpy.context.scene.render.resolution_x = distance_img.shape[1]
        bpy.context.scene.render.resolution_y = distance_img.shape[0]
        bpy.context.view_layer.update()

        cam_matrix = cam_ob.matrix_world
        # This might not be completely correct, but the fov_y value is off
        x_angle = cam.angle_x * 0.5

        tan_angle_x = math.tan(x_angle)
        tan_angle_y = math.tan(x_angle) * (float(distance_img.shape[0]) /
                                           distance_img.shape[1])
        vertices = []
        normals = []
        rot_mat_camera = mathutils.Matrix([[ele for ele in rows[:3]]
                                           for rows in cam_ob.matrix_world[:3]
                                           ])
        for i in range(distance_img.shape[0]):
            for j in range(distance_img.shape[1]):
                if len(distance_img.shape) == 2:
                    distance_value = distance_img[i, j]
                else:
                    distance_value = distance_img[i, j, 0]
                if distance_value < max_distance:
                    # Convert principal point cx,cy in px to blender cam shift in proportion to larger image dim
                    x_center = float(j - distance_img.shape[1] *
                                     0.5) / distance_img.shape[1] * 2
                    y_center = float(i - distance_img.shape[0] *
                                     0.5) / distance_img.shape[0] * 2 * -1
                    coord_x = x_center * tan_angle_x
                    coord_y = y_center * tan_angle_y
                    beam = mathutils.Vector([coord_x, coord_y, -1])
                    beam.normalize()
                    beam *= distance_value

                    location = cam_matrix @ beam
                    vertices.append(mathutils.Vector(location))
                    normal = mathutils.Vector([(ele - 0.5) * 2.0
                                               for ele in normal_img[i, j]])
                    normal = rot_mat_camera @ normal
                    normals.append(normal)

        add_object_only_with_direction_vectors(vertices,
                                               normals,
                                               radius=normal_len,
                                               name='NewVertexObject')