def _format_faces_indices(faces_indices, max_index: int, device, pad_value=None): """ Format indices and check for invalid values. Indices can refer to values in one of the face properties: vertices, textures or normals. See comments of the load_obj function for more details. Args: faces_indices: List of ints of indices. max_index: Max index for the face property. pad_value: if any of the face_indices are padded, specify the value of the padding (e.g. -1). This is only used for texture indices indices where there might not be texture information for all the faces. Returns: faces_indices: List of ints of indices. Raises: ValueError if indices are not in a valid range. """ faces_indices = _make_tensor( faces_indices, cols=3, dtype=torch.int64, device=device ) if pad_value is not None: # pyre-fixme[28]: Unexpected keyword argument `dim`. mask = faces_indices.eq(pad_value).all(dim=-1) # Change to 0 based indexing. faces_indices[(faces_indices > 0)] -= 1 # Negative indexing counts from the end. faces_indices[(faces_indices < 0)] += max_index if pad_value is not None: # pyre-fixme[61]: `mask` is undefined, or not always defined. faces_indices[mask] = pad_value return _check_faces_indices(faces_indices, max_index, pad_value)
def load_ply(f, path_manager: Optional[PathManager] = None): """ Load the data from a .ply file. Example .ply file format: ply format ascii 1.0 { ascii/binary, format version number } comment made by Greg Turk { comments keyword specified, like all lines } comment this file is a cube element vertex 8 { define "vertex" element, 8 of them in file } property float x { vertex contains float "x" coordinate } property float y { y coordinate is also a vertex property } property float z { z coordinate, too } element face 6 { there are 6 "face" elements in the file } property list uchar int vertex_index { "vertex_indices" is a list of ints } end_header { delimits the end of the header } 0 0 0 { start of vertex list } 0 0 1 0 1 1 0 1 0 1 0 0 1 0 1 1 1 1 1 1 0 4 0 1 2 3 { start of face list } 4 7 6 5 4 4 0 4 5 1 4 1 5 6 2 4 2 6 7 3 4 3 7 4 0 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 in the binary ply format rather than the text ply format, then a text stream is not supported. It is easiest to use a binary stream in all cases. path_manager: PathManager for loading if f is a str. Returns: verts: FloatTensor of shape (V, 3). faces: LongTensor of vertex indices, shape (F, 3). """ if path_manager is None: path_manager = PathManager() header, elements = _load_ply_raw(f, path_manager=path_manager) vertex = elements.get("vertex", None) if vertex is None: raise ValueError("The ply file has no vertex element.") face = elements.get("face", None) if face is None: raise ValueError("The ply file has no face element.") if len(vertex) and (not isinstance(vertex, np.ndarray) or vertex.ndim != 2 or vertex.shape[1] != 3): raise ValueError("Invalid vertices in file.") verts = _make_tensor(vertex, cols=3, dtype=torch.float32) face_head = next(head for head in header.elements if head.name == "face") if len(face_head.properties ) != 1 or face_head.properties[0].list_size_type is None: raise ValueError("Unexpected form of faces data.") # face_head.properties[0].name is usually "vertex_index" or "vertex_indices" # but we don't need to enforce this. if not len(face): # pyre-fixme[28]: Unexpected keyword argument `size`. faces = torch.zeros(size=(0, 3), dtype=torch.int64) elif isinstance(face, np.ndarray) and face.ndim == 2: # Homogeneous elements if face.shape[1] < 3: raise ValueError("Faces must have at least 3 vertices.") face_arrays = [ face[:, [0, i + 1, i + 2]] for i in range(face.shape[1] - 2) ] faces = torch.LongTensor(np.vstack(face_arrays)) else: face_list = [] for face_item in face: if face_item.ndim != 1: raise ValueError("Bad face data.") if face_item.shape[0] < 3: raise ValueError("Faces must have at least 3 vertices.") for i in range(face_item.shape[0] - 2): face_list.append( [face_item[0], face_item[i + 1], face_item[i + 2]]) # pyre-fixme[6]: Expected `dtype` for 3rd param but got `Type[torch.int64]`. faces = _make_tensor(face_list, cols=3, dtype=torch.int64) _check_faces_indices(faces, max_index=verts.shape[0]) return verts, faces
def _load_obj( f_obj, *, data_dir, load_textures: bool = True, create_texture_atlas: bool = False, texture_atlas_size: int = 4, texture_wrap: Optional[str] = "repeat", path_manager: PathManager, device="cpu", ): """ Load a mesh from a file-like object. See load_obj function more details. Any material files associated with the obj are expected to be in the directory given by data_dir. """ if texture_wrap is not None and texture_wrap not in ["repeat", "clamp"]: msg = "texture_wrap must be one of ['repeat', 'clamp'] or None, got %s" raise ValueError(msg % texture_wrap) ( verts, normals, verts_uvs, faces_verts_idx, faces_normals_idx, faces_textures_idx, faces_materials_idx, material_names, mtl_path, ) = _parse_obj(f_obj, data_dir) verts = _make_tensor(verts, cols=3, dtype=torch.float32, device=device) # (V, 3) normals = _make_tensor(normals, cols=3, dtype=torch.float32, device=device) # (N, 3) verts_uvs = _make_tensor(verts_uvs, cols=2, dtype=torch.float32, device=device) # (T, 2) faces_verts_idx = _format_faces_indices(faces_verts_idx, verts.shape[0], device=device) # Repeat for normals and textures if present. if len(faces_normals_idx): faces_normals_idx = _format_faces_indices(faces_normals_idx, normals.shape[0], device=device, pad_value=-1) if len(faces_textures_idx): faces_textures_idx = _format_faces_indices(faces_textures_idx, verts_uvs.shape[0], device=device, pad_value=-1) if len(faces_materials_idx): faces_materials_idx = torch.tensor(faces_materials_idx, dtype=torch.int64, device=device) texture_atlas = None material_colors, texture_images = _load_materials( material_names, mtl_path, data_dir=data_dir, load_textures=load_textures, path_manager=path_manager, device=device, ) if create_texture_atlas: # Using the images and properties from the # material file make a per face texture map. # Create an array of strings of material names for each face. # If faces_materials_idx == -1 then that face doesn't have a material. idx = faces_materials_idx.cpu().numpy() face_material_names = np.array(material_names)[idx] # (F,) face_material_names[idx == -1] = "" # Construct the atlas. texture_atlas = make_mesh_texture_atlas( material_colors, texture_images, face_material_names, faces_textures_idx, verts_uvs, texture_atlas_size, texture_wrap, ) faces = _Faces( verts_idx=faces_verts_idx, normals_idx=faces_normals_idx, textures_idx=faces_textures_idx, materials_idx=faces_materials_idx, ) aux = _Aux( normals=normals if len(normals) else None, verts_uvs=verts_uvs if len(verts_uvs) else None, material_colors=material_colors, texture_images=texture_images, texture_atlas=texture_atlas, ) return verts, faces, aux
def _load_obj( f_obj, data_dir, load_textures: bool = True, create_texture_atlas: bool = False, texture_atlas_size: int = 4, texture_wrap: Optional[str] = "repeat", device="cpu", ): """ Load a mesh from a file-like object. See load_obj function more details. Any material files associated with the obj are expected to be in the directory given by data_dir. """ if texture_wrap is not None and texture_wrap not in ["repeat", "clamp"]: msg = "texture_wrap must be one of ['repeat', 'clamp'] or None, got %s" raise ValueError(msg % texture_wrap) lines = [line.strip() for line in f_obj] verts = [] normals = [] verts_uvs = [] faces_verts_idx = [] faces_normals_idx = [] faces_textures_idx = [] material_names = [] faces_materials_idx = [] f_mtl = None materials_idx = -1 # startswith expects each line to be a string. If the file is read in as # bytes then first decode to strings. if lines and isinstance(lines[0], bytes): lines = [el.decode("utf-8") for el in lines] for line in lines: tokens = line.strip().split() if line.startswith("mtllib"): if len(tokens) < 2: raise ValueError("material file name is not specified") # NOTE: only allow one .mtl file per .obj. # Definitions for multiple materials can be included # in this one .mtl file. f_mtl = os.path.join(data_dir, line.split()[1]) elif len(tokens) and tokens[0] == "usemtl": material_name = tokens[1] # materials are often repeated for different parts # of a mesh. if material_name not in material_names: material_names.append(material_name) materials_idx = len(material_names) - 1 else: materials_idx = material_names.index(material_name) elif line.startswith("v "): # Line is a vertex. vert = [float(x) for x in tokens[1:4]] if len(vert) != 3: msg = "Vertex %s does not have 3 values. Line: %s" raise ValueError(msg % (str(vert), str(line))) verts.append(vert) elif line.startswith("vt "): # Line is a texture. tx = [float(x) for x in tokens[1:3]] if len(tx) != 2: raise ValueError( "Texture %s does not have 2 values. Line: %s" % (str(tx), str(line)) ) verts_uvs.append(tx) elif line.startswith("vn "): # Line is a normal. norm = [float(x) for x in tokens[1:4]] if len(norm) != 3: msg = "Normal %s does not have 3 values. Line: %s" raise ValueError(msg % (str(norm), str(line))) normals.append(norm) elif line.startswith("f "): # Line is a face. # Update face properties info. _parse_face( line, tokens, materials_idx, faces_verts_idx, faces_normals_idx, faces_textures_idx, faces_materials_idx, ) verts = _make_tensor(verts, cols=3, dtype=torch.float32, device=device) # (V, 3) normals = _make_tensor( normals, cols=3, dtype=torch.float32, device=device ) # (N, 3) verts_uvs = _make_tensor( verts_uvs, cols=2, dtype=torch.float32, device=device ) # (T, 2) faces_verts_idx = _format_faces_indices( faces_verts_idx, verts.shape[0], device=device ) # Repeat for normals and textures if present. if len(faces_normals_idx) > 0: faces_normals_idx = _format_faces_indices( faces_normals_idx, normals.shape[0], device=device, pad_value=-1 ) if len(faces_textures_idx) > 0: faces_textures_idx = _format_faces_indices( faces_textures_idx, verts_uvs.shape[0], device=device, pad_value=-1 ) if len(faces_materials_idx) > 0: faces_materials_idx = torch.tensor( faces_materials_idx, dtype=torch.int64, device=device ) # Load materials material_colors, texture_images, texture_atlas = None, None, None if load_textures: if (len(material_names) > 0) and (f_mtl is not None): # pyre-fixme[6]: Expected `Union[_PathLike[typing.Any], bytes, str]` for # 1st param but got `Optional[str]`. if os.path.isfile(f_mtl): # Texture mode uv wrap material_colors, texture_images = load_mtl( f_mtl, material_names, data_dir, device=device ) if create_texture_atlas: # Using the images and properties from the # material file make a per face texture map. # Create an array of strings of material names for each face. # If faces_materials_idx == -1 then that face doesn't have a material. idx = faces_materials_idx.cpu().numpy() face_material_names = np.array(material_names)[idx] # (F,) face_material_names[idx == -1] = "" texture_atlas = None if len(verts_uvs) > 0: # Get the uv coords for each vert in each face faces_verts_uvs = verts_uvs[faces_textures_idx] # (F, 3, 2) # Construct the atlas. texture_atlas = make_mesh_texture_atlas( material_colors, texture_images, face_material_names, faces_verts_uvs, texture_atlas_size, texture_wrap, ) else: warnings.warn(f"Mtl file does not exist: {f_mtl}") elif len(material_names) > 0: warnings.warn("No mtl file provided") faces = _Faces( verts_idx=faces_verts_idx, normals_idx=faces_normals_idx, textures_idx=faces_textures_idx, materials_idx=faces_materials_idx, ) aux = _Aux( normals=normals if len(normals) > 0 else None, verts_uvs=verts_uvs if len(verts_uvs) > 0 else None, material_colors=material_colors, texture_images=texture_images, texture_atlas=texture_atlas, ) return verts, faces, aux
def _get_verts(header: _PlyHeader, elements: dict) -> Tuple[torch.Tensor, Optional[torch.Tensor]]: """ Get the vertex locations and colors from a parsed ply file. Args: header, elements: as returned from load_ply_raw. Returns: verts: FloatTensor of shape (V, 3). vertex_colors: None or FloatTensor of shape (V, 3). """ vertex = elements.get("vertex", None) if vertex is None: raise ValueError("The ply file has no vertex element.") if not isinstance(vertex, list): raise ValueError("Invalid vertices in file.") vertex_head = next(head for head in header.elements if head.name == "vertex") point_idxs, color_idxs = _get_verts_column_indices(vertex_head) # Case of no vertices if vertex_head.count == 0: verts = torch.zeros((0, 3), dtype=torch.float32) if color_idxs is None: return verts, None return verts, torch.zeros((0, 3), dtype=torch.float32) # Simple case where the only data is the vertices themselves if (len(vertex) == 1 and isinstance(vertex[0], np.ndarray) and vertex[0].ndim == 2 and vertex[0].shape[1] == 3): return _make_tensor(vertex[0], cols=3, dtype=torch.float32), None vertex_colors = None if len(vertex) == 1: # This is the case where the whole vertex element has one type, # so it was read as a single array and we can index straight into it. verts = torch.tensor(vertex[0][:, point_idxs], dtype=torch.float32) if color_idxs is not None: vertex_colors = torch.tensor(vertex[0][:, color_idxs], dtype=torch.float32) else: # The vertex element is heterogeneous. It was read as several arrays, # part by part, where a part is a set of properties with the same type. # For each property (=column in the file), we store in # prop_to_partnum_col its partnum (i.e. the index of what part it is # in) and its column number (its index within its part). prop_to_partnum_col = [(partnum, col) for partnum, array in enumerate(vertex) for col in range(array.shape[1])] verts = torch.empty(size=(vertex_head.count, 3), dtype=torch.float32) for axis in range(3): partnum, col = prop_to_partnum_col[point_idxs[axis]] verts.numpy()[:, axis] = vertex[partnum][:, col] # Note that in the previous line, we made the assignment # as numpy arrays by casting verts. If we took the (more # obvious) method of converting the right hand side to # torch, then we might have an extra data copy because # torch wants contiguity. The code would be like: # if not vertex[partnum].flags["C_CONTIGUOUS"]: # vertex[partnum] = np.ascontiguousarray(vertex[partnum]) # verts[:, axis] = torch.tensor((vertex[partnum][:, col])) if color_idxs is not None: vertex_colors = torch.empty(size=(vertex_head.count, 3), dtype=torch.float32) for color in range(3): partnum, col = prop_to_partnum_col[color_idxs[color]] vertex_colors.numpy()[:, color] = vertex[partnum][:, col] return verts, vertex_colors