예제 #1
0
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)
예제 #2
0
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
예제 #3
0
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
예제 #4
0
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
예제 #5
0
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