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