def save( self, data: Pointclouds, path: Union[str, Path], path_manager: PathManager, binary: Optional[bool], decimal_places: Optional[int] = None, **kwargs, ) -> bool: if not endswith(path, self.known_suffixes): return False points = data.points_list()[0] features = data.features_list()[0] with _open_file(path, path_manager, "wb") as f: _save_ply( f=f, verts=points, verts_colors=features, verts_normals=torch.FloatTensor([]), faces=None, ascii=binary is False, decimal_places=decimal_places, ) return True
def read( self, path: PathOrStr, include_textures: bool, device, path_manager: PathManager, **kwargs, ) -> Optional[Meshes]: if not endswith(path, self.known_suffixes): return None with _open_file(path, path_manager, "rb") as f: data = _load_off_stream(f) verts = torch.from_numpy(data["verts"]).to(device) if "faces" in data: faces = torch.from_numpy(data["faces"]).to(dtype=torch.int64, device=device) else: faces = torch.zeros((0, 3), dtype=torch.int64, device=device) textures = None if "verts_colors" in data: if "faces_colors" in data: msg = "Faces colors ignored because vertex colors provided too." warnings.warn(msg) verts_colors = torch.from_numpy(data["verts_colors"]).to(device) textures = TexturesVertex([verts_colors]) elif "faces_colors" in data: faces_colors = torch.from_numpy(data["faces_colors"]).to(device) textures = TexturesAtlas([faces_colors[:, None, None, :]]) mesh = Meshes(verts=[verts.to(device)], faces=[faces.to(device)], textures=textures) return mesh
def save_obj( f, verts, faces, decimal_places: Optional[int] = None, path_manager: Optional[PathManager] = None, ): """ Save a mesh to an .obj file. Args: f: File (or path) to which the mesh should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shape (F, 3) giving faces. decimal_places: Number of decimal places for saving. path_manager: Optional PathManager for interpreting f if it is a str. """ if len(verts) and not (verts.dim() == 2 and verts.size(1) == 3): message = "Argument 'verts' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if len(faces) and not (faces.dim() == 2 and faces.size(1) == 3): message = "Argument 'faces' should either be empty or of shape (num_faces, 3)." raise ValueError(message) if path_manager is None: path_manager = PathManager() with _open_file(f, path_manager, "w") as f: return _save(f, verts, faces, decimal_places)
def save_ply( f, verts: torch.Tensor, faces: Optional[torch.LongTensor] = None, verts_normals: Optional[torch.Tensor] = None, ascii: bool = False, decimal_places: Optional[int] = None, path_manager: Optional[PathManager] = None, ) -> None: """ Save a mesh to a .ply file. Args: f: File (or path) to which the mesh should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shape (F, 3) giving faces. verts_normals: FloatTensor of shape (V, 3) giving vertex normals. ascii: (bool) whether to use the ascii ply format. decimal_places: Number of decimal places for saving if ascii=True. path_manager: PathManager for interpreting f if it is a str. """ if len(verts) and not (verts.dim() == 2 and verts.size(1) == 3): message = "Argument 'verts' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if ( faces is not None and len(faces) and not (faces.dim() == 2 and faces.size(1) == 3) ): message = "Argument 'faces' should either be empty or of shape (num_faces, 3)." raise ValueError(message) if ( verts_normals is not None and len(verts_normals) and not ( verts_normals.dim() == 2 and verts_normals.size(1) == 3 and verts_normals.size(0) == verts.size(0) ) ): message = "Argument 'verts_normals' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if path_manager is None: path_manager = PathManager() with _open_file(f, path_manager, "wb") as f: _save_ply( f, verts=verts, faces=faces, verts_normals=verts_normals, verts_colors=None, ascii=ascii, decimal_places=decimal_places, )
def _save_off( file, *, verts: torch.Tensor, verts_colors: Optional[torch.Tensor] = None, faces: Optional[torch.LongTensor] = None, faces_colors: Optional[torch.Tensor] = None, decimal_places: Optional[int] = None, path_manager: PathManager, ) -> None: """ Save a mesh to an ascii .off file. Args: file: File (or path) to which the mesh should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. verts_colors: FloatTensor of shape (V, C) giving vertex colors where C is 3 or 4. faces: LongTensor of shape (F, 3) giving faces. faces_colors: FloatTensor of shape (V, C) giving face colors where C is 3 or 4. decimal_places: Number of decimal places for saving. """ if len(verts) and not (verts.dim() == 2 and verts.size(1) == 3): message = "Argument 'verts' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if verts_colors is not None and 0 == len(verts_colors): verts_colors = None if faces_colors is not None and 0 == len(faces_colors): faces_colors = None if faces is not None and 0 == len(faces): faces = None if verts_colors is not None: if not (verts_colors.dim() == 2 and verts_colors.size(1) in [3, 4]): message = "verts_colors should have shape (num_faces, C)." raise ValueError(message) if verts_colors.shape[0] != verts.shape[0]: message = "verts_colors should have the same length as verts." raise ValueError(message) if faces is not None and not (faces.dim() == 2 and faces.size(1) == 3): message = "Argument 'faces' if present should have shape (num_faces, 3)." raise ValueError(message) if faces_colors is not None and faces is None: message = "Cannot have face colors without faces" raise ValueError(message) if faces_colors is not None: if not (faces_colors.dim() == 2 and faces_colors.size(1) in [3, 4]): message = "faces_colors should have shape (num_faces, C)." raise ValueError(message) if faces_colors.shape[0] != cast(torch.LongTensor, faces).shape[0]: message = "faces_colors should have the same length as faces." raise ValueError(message) with _open_file(file, path_manager, "wb") as f: _write_off_data(f, verts, verts_colors, faces, faces_colors, decimal_places)
def save_ply( f, verts: torch.Tensor, faces: Optional[torch.LongTensor] = None, verts_normals: Optional[torch.Tensor] = None, ascii: bool = False, decimal_places: Optional[int] = None, ) -> None: """ Save a mesh to a .ply file. Args: f: File (or path) to which the mesh should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shape (F, 3) giving faces. verts_normals: FloatTensor of shape (V, 3) giving vertex normals. ascii: (bool) whether to use the ascii ply format. decimal_places: Number of decimal places for saving if ascii=True. """ verts_normals = ( torch.tensor([], dtype=torch.float32, device=verts.device) if verts_normals is None else verts_normals ) faces = torch.LongTensor([]) if faces is None else faces if len(verts) and not (verts.dim() == 2 and verts.size(1) == 3): message = "Argument 'verts' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if len(faces) and not (faces.dim() == 2 and faces.size(1) == 3): message = "Argument 'faces' should either be empty or of shape (num_faces, 3)." raise ValueError(message) if len(verts_normals) and not ( verts_normals.dim() == 2 and verts_normals.size(1) == 3 and verts_normals.size(0) == verts.size(0) ): message = "Argument 'verts_normals' should either be empty or of shape (num_verts, 3)." raise ValueError(message) with _open_file(f, "wb") as f: _save_ply(f, verts, faces, verts_normals, ascii, decimal_places)
def save( self, data: Meshes, path: Union[str, Path], path_manager: PathManager, binary: Optional[bool], decimal_places: Optional[int] = None, **kwargs, ) -> bool: if not endswith(path, self.known_suffixes): return False verts = data.verts_list()[0] faces = data.faces_list()[0] if data.has_verts_normals(): verts_normals = data.verts_normals_list()[0] else: verts_normals = None if isinstance(data.textures, TexturesVertex): mesh_verts_colors = data.textures.verts_features_list()[0] n_colors = mesh_verts_colors.shape[1] if n_colors == 3: verts_colors = mesh_verts_colors else: warnings.warn( f"Texture will not be saved as it has {n_colors} colors, not 3." ) verts_colors = None else: verts_colors = None with _open_file(path, path_manager, "wb") as f: _save_ply( f=f, verts=verts, faces=faces, verts_colors=verts_colors, verts_normals=verts_normals, ascii=binary is False, decimal_places=decimal_places, ) return True
def _parse_mtl( f: str, path_manager: PathManager, device: Device = "cpu") -> Tuple[MaterialProperties, TextureFiles]: material_properties = {} texture_files = {} material_name = "" with _open_file(f, path_manager, "r") as f: for line in f: tokens = line.strip().split() if not tokens: continue if tokens[0] == "newmtl": material_name = tokens[1] material_properties[material_name] = {} elif tokens[0] == "map_Kd": # Diffuse texture map # Account for the case where filenames might have spaces filename = line.strip()[7:] texture_files[material_name] = filename elif tokens[0] == "Kd": # RGB diffuse reflectivity kd = np.array(tokens[1:4]).astype(np.float32) kd = torch.from_numpy(kd).to(device) material_properties[material_name]["diffuse_color"] = kd elif tokens[0] == "Ka": # RGB ambient reflectivity ka = np.array(tokens[1:4]).astype(np.float32) ka = torch.from_numpy(ka).to(device) material_properties[material_name]["ambient_color"] = ka elif tokens[0] == "Ks": # RGB specular reflectivity ks = np.array(tokens[1:4]).astype(np.float32) ks = torch.from_numpy(ks).to(device) material_properties[material_name]["specular_color"] = ks elif tokens[0] == "Ns": # Specular exponent ns = np.array(tokens[1:4]).astype(np.float32) ns = torch.from_numpy(ns).to(device) material_properties[material_name]["shininess"] = ns return material_properties, texture_files
def _load_ply_raw(f) -> Tuple[_PlyHeader, dict]: """ Load the data from a .ply file. Args: f: A binary or text file-like object (with methods read, readline, tell and seek), a pathlib path or a string containing a file name. If the ply file is binary, a text stream is not supported. It is recommended to use a binary stream. Returns: header: A _PlyHeader object describing the metadata in the ply file. elements: A dictionary of element names to values. If an element is regular, in the sense of having no lists or being one uniformly-sized list, then the value will be a 2D numpy array. If not, it is a list of the relevant property values. """ with _open_file(f, "rb") as f: header, elements = _load_ply_raw_stream(f) return header, elements
def load_meshes( path: PathOrStr, path_manager: PathManager, include_textures: bool = True, ) -> List[Tuple[Optional[str], Meshes]]: """ Loads all the meshes from the default scene in the given GLB file. and returns them separately. Args: path: path to read from path_manager: PathManager object for interpreting the path include_textures: whether to load textures Returns: List of (name, mesh) pairs, where the name is the optional name property from the GLB file, or None if it is absent, and the mesh is a Meshes object containing one mesh. """ with _open_file(path, path_manager, "rb") as f: loader = _GLTFLoader(cast(BinaryIO, f)) names_meshes_list = loader.load(include_textures=include_textures) return names_meshes_list
def load_obj( f, load_textures=True, create_texture_atlas: bool = False, texture_atlas_size: int = 4, texture_wrap: Optional[str] = "repeat", device="cpu", ): """ Load a mesh from a .obj file and optionally textures from a .mtl file. Currently this handles verts, faces, vertex texture uv coordinates, normals, texture images and material reflectivity values. Note .obj files are 1-indexed. The tensors returned from this function are 0-indexed. OBJ spec reference: http://www.martinreddy.net/gfx/3d/OBJ.spec Example .obj file format: :: # this is a comment v 1.000000 -1.000000 -1.000000 v 1.000000 -1.000000 1.000000 v -1.000000 -1.000000 1.000000 v -1.000000 -1.000000 -1.000000 v 1.000000 1.000000 -1.000000 vt 0.748573 0.750412 vt 0.749279 0.501284 vt 0.999110 0.501077 vt 0.999455 0.750380 vn 0.000000 0.000000 -1.000000 vn -1.000000 -0.000000 -0.000000 vn -0.000000 -0.000000 1.000000 f 5/2/1 1/2/1 4/3/1 f 5/1/1 4/3/1 2/4/1 The first character of the line denotes the type of input: :: - v is a vertex - vt is the texture coordinate of one vertex - vn is the normal of one vertex - f is a face Faces are interpreted as follows: :: 5/2/1 describes the first vertex of the first triange - 5: index of vertex [1.000000 1.000000 -1.000000] - 2: index of texture coordinate [0.749279 0.501284] - 1: index of normal [0.000000 0.000000 -1.000000] If there are faces with more than 3 vertices they are subdivided into triangles. Polygonal faces are assummed to have vertices ordered counter-clockwise so the (right-handed) normal points out of the screen e.g. a proper rectangular face would be specified like this: :: 0_________1 | | | | 3 ________2 The face would be split into two triangles: (0, 2, 1) and (0, 3, 2), both of which are also oriented counter-clockwise and have normals pointing out of the screen. Args: f: A file-like object (with methods read, readline, tell, and seek), a pathlib path or a string containing a file name. load_textures: Boolean indicating whether material files are loaded create_texture_atlas: Bool, If True a per face texture map is created and a tensor `texture_atlas` is also returned in `aux`. texture_atlas_size: Int specifying the resolution of the texture map per face when `create_texture_atlas=True`. A (texture_size, texture_size, 3) map is created per face. texture_wrap: string, one of ["repeat", "clamp"]. This applies when computing the texture atlas. If `texture_mode="repeat"`, for uv values outside the range [0, 1] the integer part is ignored and a repeating pattern is formed. If `texture_mode="clamp"` the values are clamped to the range [0, 1]. If None, then there is no transformation of the texture values. device: string or torch.device on which to return the new tensors. Returns: 6-element tuple containing - **verts**: FloatTensor of shape (V, 3). - **faces**: NamedTuple with fields: - verts_idx: LongTensor of vertex indices, shape (F, 3). - normals_idx: (optional) LongTensor of normal indices, shape (F, 3). - textures_idx: (optional) LongTensor of texture indices, shape (F, 3). This can be used to index into verts_uvs. - materials_idx: (optional) List of indices indicating which material the texture is derived from for each face. If there is no material for a face, the index is -1. This can be used to retrieve the corresponding values in material_colors/texture_images after they have been converted to tensors or Materials/Textures data structures - see textures.py and materials.py for more info. - **aux**: NamedTuple with fields: - normals: FloatTensor of shape (N, 3) - verts_uvs: FloatTensor of shape (T, 2), giving the uv coordinate per vertex. If a vertex is shared between two faces, it can have a different uv value for each instance. Therefore it is possible that the number of verts_uvs is greater than num verts i.e. T > V. vertex. - material_colors: if `load_textures=True` and the material has associated properties this will be a dict of material names and properties of the form: .. code-block:: python { material_name_1: { "ambient_color": tensor of shape (1, 3), "diffuse_color": tensor of shape (1, 3), "specular_color": tensor of shape (1, 3), "shininess": tensor of shape (1) }, material_name_2: {}, ... } If a material does not have any properties it will have an empty dict. If `load_textures=False`, `material_colors` will None. - texture_images: if `load_textures=True` and the material has a texture map, this will be a dict of the form: .. code-block:: python { material_name_1: (H, W, 3) image, ... } If `load_textures=False`, `texture_images` will None. - texture_atlas: if `load_textures=True` and `create_texture_atlas=True`, this will be a FloatTensor of the form: (F, texture_size, textures_size, 3) If the material does not have a texture map, then all faces will have a uniform white texture. Otherwise `texture_atlas` will be None. """ data_dir = "./" if isinstance(f, (str, bytes, os.PathLike)): # pyre-fixme[6]: Expected `_PathLike[Variable[typing.AnyStr <: [str, # bytes]]]` for 1st param but got `Union[_PathLike[typing.Any], bytes, str]`. data_dir = os.path.dirname(f) with _open_file(f, "r") as f: return _load_obj( f, data_dir, load_textures=load_textures, create_texture_atlas=create_texture_atlas, texture_atlas_size=texture_atlas_size, texture_wrap=texture_wrap, device=device, )
def load_mtl(f_mtl, material_names: List, data_dir: str, device="cpu"): """ Load texture images and material reflectivity values for ambient, diffuse and specular light (Ka, Kd, Ks, Ns). Args: f_mtl: a file like object of the material information. material_names: a list of the material names found in the .obj file. data_dir: the directory where the material texture files are located. Returns: material_colors: dict of properties for each material. If a material does not have any properties it will have an emtpy dict. { material_name_1: { "ambient_color": tensor of shape (1, 3), "diffuse_color": tensor of shape (1, 3), "specular_color": tensor of shape (1, 3), "shininess": tensor of shape (1) }, material_name_2: {}, ... } texture_images: dict of material names and texture images { material_name_1: (H, W, 3) image, ... } """ texture_files = {} material_colors = {} material_properties = {} texture_images = {} material_name = "" f_mtl, new_f = _open_file(f_mtl) lines = [line.strip() for line in f_mtl] for line in lines: if len(line.split()) != 0: if line.split()[0] == "newmtl": material_name = line.split()[1] material_colors[material_name] = {} if line.split()[0] == "map_Kd": # Texture map. texture_files[material_name] = line.split()[1] if line.split()[0] == "Kd": # RGB diffuse reflectivity kd = np.array(list(line.split()[1:4])).astype(np.float32) kd = torch.from_numpy(kd).to(device) material_colors[material_name]["diffuse_color"] = kd if line.split()[0] == "Ka": # RGB ambient reflectivity ka = np.array(list(line.split()[1:4])).astype(np.float32) ka = torch.from_numpy(ka).to(device) material_colors[material_name]["ambient_color"] = ka if line.split()[0] == "Ks": # RGB specular reflectivity ks = np.array(list(line.split()[1:4])).astype(np.float32) ks = torch.from_numpy(ks).to(device) material_colors[material_name]["specular_color"] = ks if line.split()[0] == "Ns": # Specular exponent ns = np.array(list(line.split()[1:4])).astype(np.float32) ns = torch.from_numpy(ns).to(device) material_colors[material_name]["shininess"] = ns if new_f: f_mtl.close() # Only keep the materials referenced in the obj. for name in material_names: if name in texture_files: # Load the texture image. filename = texture_files[name] filename_texture = os.path.join(data_dir, filename) if os.path.isfile(filename_texture): image = _read_image(filename_texture, format="RGB") / 255.0 image = torch.from_numpy(image) texture_images[name] = image else: msg = f"Texture file does not exist: {filename_texture}" warnings.warn(msg) if name in material_colors: material_properties[name] = material_colors[name] return material_properties, texture_images
def save_obj( f: PathOrStr, verts, faces, decimal_places: Optional[int] = None, path_manager: Optional[PathManager] = None, *, verts_uvs: Optional[torch.Tensor] = None, faces_uvs: Optional[torch.Tensor] = None, texture_map: Optional[torch.Tensor] = None, ) -> None: """ Save a mesh to an .obj file. Args: f: File (str or path) to which the mesh should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shape (F, 3) giving faces. decimal_places: Number of decimal places for saving. path_manager: Optional PathManager for interpreting f if it is a str. verts_uvs: FloatTensor of shape (V, 2) giving the uv coordinate per vertex. faces_uvs: LongTensor of shape (F, 3) giving the index into verts_uvs for each vertex in the face. texture_map: FloatTensor of shape (H, W, 3) representing the texture map for the mesh which will be saved as an image. The values are expected to be in the range [0, 1], """ if len(verts) and (verts.dim() != 2 or verts.size(1) != 3): message = "'verts' should either be empty or of shape (num_verts, 3)." raise ValueError(message) if len(faces) and (faces.dim() != 2 or faces.size(1) != 3): message = "'faces' should either be empty or of shape (num_faces, 3)." raise ValueError(message) if faces_uvs is not None and (faces_uvs.dim() != 2 or faces_uvs.size(1) != 3): message = "'faces_uvs' should either be empty or of shape (num_faces, 3)." raise ValueError(message) if verts_uvs is not None and (verts_uvs.dim() != 2 or verts_uvs.size(1) != 2): message = "'verts_uvs' should either be empty or of shape (num_verts, 2)." raise ValueError(message) if texture_map is not None and (texture_map.dim() != 3 or texture_map.size(2) != 3): message = "'texture_map' should either be empty or of shape (H, W, 3)." raise ValueError(message) if path_manager is None: path_manager = PathManager() save_texture = all([t is not None for t in [faces_uvs, verts_uvs, texture_map]]) output_path = Path(f) # Save the .obj file with _open_file(f, path_manager, "w") as f: if save_texture: # Add the header required for the texture info to be loaded correctly obj_header = "\nmtllib {0}.mtl\nusemtl mesh\n\n".format(output_path.stem) f.write(obj_header) _save( f, verts, faces, decimal_places, verts_uvs=verts_uvs, faces_uvs=faces_uvs, save_texture=save_texture, ) # Save the .mtl and .png files associated with the texture if save_texture: image_path = output_path.with_suffix(".png") mtl_path = output_path.with_suffix(".mtl") if isinstance(f, str): # Back to str for iopath interpretation. image_path = str(image_path) mtl_path = str(mtl_path) # Save texture map to output folder # pyre-fixme[16] # undefined attribute cpu texture_map = texture_map.detach().cpu() * 255.0 image = Image.fromarray(texture_map.numpy().astype(np.uint8)) with _open_file(image_path, path_manager, "wb") as im_f: image.save(im_f) # Create .mtl file with the material name and texture map filename # TODO: enable material properties to also be saved. with _open_file(mtl_path, path_manager, "w") as f_mtl: lines = f"newmtl mesh\n" f"map_Kd {output_path.stem}.png\n" f_mtl.write(lines)