def _save_ply( f, verts: torch.Tensor, faces: torch.LongTensor, verts_normals: torch.Tensor, decimal_places: Optional[int] = None, ) -> None: """ Internal implementation for saving 3D data to a .ply file. Args: f: File object to which the 3D data should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shsape (F, 3) giving faces. verts_normals: FloatTensor of shape (V, 3) giving vertex normals. decimal_places: Number of decimal places for saving. """ assert not len(verts) or (verts.dim() == 2 and verts.size(1) == 3) assert not len(faces) or (faces.dim() == 2 and faces.size(1) == 3) assert not len(verts_normals) or (verts_normals.dim() == 2 and verts_normals.size(1) == 3) print("ply\nformat ascii 1.0", file=f) print(f"element vertex {verts.shape[0]}", file=f) print("property float x", file=f) print("property float y", file=f) print("property float z", file=f) if verts_normals.numel() > 0: print("property float nx", file=f) print("property float ny", file=f) print("property float nz", file=f) print(f"element face {faces.shape[0]}", file=f) print("property list uchar int vertex_index", file=f) print("end_header", file=f) if not (len(verts) or len(faces)): warnings.warn("Empty 'verts' and 'faces' arguments provided") return if decimal_places is None: float_str = "%f" else: float_str = "%" + ".%df" % decimal_places vert_data = torch.cat((verts, verts_normals), dim=1) np.savetxt(f, vert_data.detach().numpy(), float_str) faces_array = faces.detach().numpy() _check_faces_indices(faces, max_index=verts.shape[0]) if len(faces_array): np.savetxt(f, faces_array, "3 %d %d %d")
def _write_off_data( 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, ) -> None: """ Internal implementation for saving 3D data to a .off file. Args: file: Binary file object to which the 3D data 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. """ nfaces = 0 if faces is None else faces.shape[0] file.write(f"off\n{verts.shape[0]} {nfaces} 0\n".encode("ascii")) if verts_colors is not None: verts = torch.cat((verts, verts_colors), dim=1) if decimal_places is None: float_str = "%f" else: float_str = "%" + ".%df" % decimal_places np.savetxt(file, verts.cpu().detach().numpy(), float_str) if faces is not None: _check_faces_indices(faces, max_index=verts.shape[0]) if faces_colors is not None: face_data = torch.cat( [ cast(torch.Tensor, faces).cpu().to(torch.float64), faces_colors.detach().cpu().to(torch.float64), ], dim=1, ) format = "3 %d %d %d" + " %f" * faces_colors.shape[1] np.savetxt(file, face_data.numpy(), format) elif faces is not None: np.savetxt(file, faces.cpu().detach().numpy(), "3 %d %d %d")
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 _save_ply( f, verts: torch.Tensor, faces: torch.LongTensor, verts_normals: torch.Tensor, ascii: bool, decimal_places: Optional[int] = None, ) -> None: """ Internal implementation for saving 3D data to a .ply file. Args: f: File object to which the 3D data should be written. verts: FloatTensor of shape (V, 3) giving vertex coordinates. faces: LongTensor of shsape (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. """ assert not len(verts) or (verts.dim() == 2 and verts.size(1) == 3) assert not len(faces) or (faces.dim() == 2 and faces.size(1) == 3) assert not len(verts_normals) or (verts_normals.dim() == 2 and verts_normals.size(1) == 3) if ascii: f.write(b"ply\nformat ascii 1.0\n") elif sys.byteorder == "big": f.write(b"ply\nformat binary_big_endian 1.0\n") else: f.write(b"ply\nformat binary_little_endian 1.0\n") f.write(f"element vertex {verts.shape[0]}\n".encode("ascii")) f.write(b"property float x\n") f.write(b"property float y\n") f.write(b"property float z\n") if verts_normals.numel() > 0: f.write(b"property float nx\n") f.write(b"property float ny\n") f.write(b"property float nz\n") f.write(f"element face {faces.shape[0]}\n".encode("ascii")) f.write(b"property list uchar int vertex_index\n") f.write(b"end_header\n") if not (len(verts) or len(faces)): warnings.warn("Empty 'verts' and 'faces' arguments provided") return vert_data = torch.cat((verts, verts_normals), dim=1).detach().numpy() if ascii: if decimal_places is None: float_str = "%f" else: float_str = "%" + ".%df" % decimal_places np.savetxt(f, vert_data, float_str) else: assert vert_data.dtype == np.float32 if isinstance(f, BytesIO): # tofile only works with real files, but is faster than this. f.write(vert_data.tobytes()) else: vert_data.tofile(f) faces_array = faces.detach().numpy() _check_faces_indices(faces, max_index=verts.shape[0]) if len(faces_array): if ascii: np.savetxt(f, faces_array, "3 %d %d %d") else: # rows are 13 bytes: a one-byte 3 followed by three four-byte face indices. faces_uints = np.full((len(faces_array), 13), 3, dtype=np.uint8) faces_uints[:, 1:] = faces_array.astype(np.uint32).view(np.uint8) if isinstance(f, BytesIO): f.write(faces_uints.tobytes()) else: faces_uints.tofile(f)
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_ply( f, *, path_manager: PathManager, return_vertex_colors: bool = False ) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor]]: """ 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 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. return_vertex_colors: whether to return vertex colors. Returns: verts: FloatTensor of shape (V, 3). faces: None or LongTensor of vertex indices, shape (F, 3). vertex_colors: None or FloatTensor of shape (V, 3), only if requested """ header, elements = _load_ply_raw(f, path_manager=path_manager) verts, vertex_colors = _get_verts(header, elements) face = elements.get("face", None) if face is not None: 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 face is None: faces = None elif not len(face): # pyre is happier when this condition is not joined to the # previous one with `or`. faces = None 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]]) faces = torch.tensor(face_list, dtype=torch.int64) if faces is not None: _check_faces_indices(faces, max_index=verts.shape[0]) if return_vertex_colors: return verts, faces, vertex_colors return verts, faces, None