Module curlew.io

Functions for performing common IO operations.

Functions

def loadPLY(path)
Expand source code
def loadPLY(path):
    """
    Loads a PLY file from the specified path.
    """
    try:
        from plyfile import PlyData, PlyElement
    except:
        assert False, "Please install plyfile (pip install plyfile) to load PLY."
    data = PlyData.read(path) # load file!

    # extract data
    xyz = None
    rgb = None
    norm = None
    faces = None
    scalar = []
    scalar_names = []
    
    for e in data.elements:
        if 'face' in e.name.lower():
            faces = np.vstack( e['vertex_indices'])
        if 'vert' in e.name.lower():  # vertex data
            xyz = np.array([e['x'], e['y'], e['z']]).T
            if len(e.properties) > 3:  # vertices have more than just position
                names = e.data.dtype.names
                # colour?
                if 'red' in names and 'green' in names and 'blue' in names:
                    rgb = np.array([e['red'], e['green'], e['blue']], dtype=e['red'].dtype).T
                # normals?
                if 'nx' in names and 'ny' in names and 'nz' in names:
                    norm = np.array([e['nx'], e['ny'], e['nz']], dtype=e['nx'].dtype).T
                # load others as scalar
                mask = ['red', 'green', 'blue', 'nx', 'ny', 'nz', 'x', 'y', 'z']
                for n in names:
                    if not n in mask:
                        scalar_names.append(n)
                        scalar.append(e[n])
        elif 'color' in e.name.lower():  # rgb data
            rgb = np.array([e['r'], e['g'], e['b']], dtype=e['r'].dtype).T
        elif 'normals' in e.name.lower():  # normal data
            norm = np.array([e['x'], e['y'], e['z']], dtype=e['z'].dtype).T
        else:  # scalar data
            scalar_names.append(e.properties[0].name.strip().replace('scalar_',''))
            scalar.append(np.array(e[e.properties[0].name], dtype=e[e.properties[0].name].dtype))
    if len(scalar) > 0:
        scalar = np.vstack(scalar).T
    assert (not xyz is None) and (xyz.shape[0] > 0), "Error - PLY contains no geometry?"

    # TODO - also load faces if present
    
    # return everything needed
    out = dict( xyz = xyz, faces=faces, rgb=rgb, normals=norm, 
                attr=scalar, names=scalar_names )
    if len(scalar) == 0:
        del out['attr']
        del out['names']
    if rgb is None:
        del out['rgb']
    if norm is None:
        del out['normals']
    if faces is None:
        del out['faces']
    return out

Loads a PLY file from the specified path.

def saveOBJ(filename, xyz, rgb, faces)
Expand source code
def saveOBJ(filename, xyz, rgb, faces):
    """
    Writes a mesh to an OBJ file.

    Parameters
    ---------------
    filename : str
        Output file path.
    xyz : np.ndarray | list
        List of vertex positions [(x, y, z), ...]
    rgb: np.ndarray | list
        List of vertex colors [(r, g, b), ...] or [(r, g, b, a), ...] in [0, 1] or [0, 255]
    faces: np.ndarray | list
        List of faces [(i1, i2, i3), ...] with 0-based indices
    """
    with open(filename, 'w') as f:
        f.write("# OBJ file with vertex colors (stored in comments)\n")

        for i, v in enumerate(xyz):
            x, y, z = v
            color_str = ""
            if (rgb is not None) and i < len(rgb):
                color = rgb[i]
                # Normalize to 0-1 if in 0-255
                if max(color) > 1:
                    color = [c / 255.0 for c in color[:3]]
                else:
                    color = color[:3]
                color_str = " # color {:.4f} {:.4f} {:.4f}".format(*color)
            f.write("v {:.6f} {:.6f} {:.6f}{}\n".format(x, y, z, color_str))

        for face in faces:
            # OBJ format is 1-indexed
            f.write("f {}\n".format(' '.join(str(i + 1) for i in face)))

Writes a mesh to an OBJ file.

Parameters

filename : str
Output file path.
xyz : np.ndarray | list
List of vertex positions [(x, y, z), …]
rgb : np.ndarray | list
List of vertex colors [(r, g, b), …] or [(r, g, b, a), …] in [0, 1] or [0, 255]
faces : np.ndarray | list
List of faces [(i1, i2, i3), …] with 0-based indices
def savePLY(path, xyz, rgb=None, normals=None, attr=None, names=None, faces=None)
Expand source code
def savePLY(path, xyz, rgb=None, normals=None, attr=None, names=None, faces=None):
    """
    Write a point cloud and associated RGB and scalar fields to .ply.

    Parameters
    ---------------
    Path : str
        File path for the created (or overwritten) .ply file
    xyz : np.ndarray
        Array of xyz points to add to the PLY file
    rgb : np.ndarray
        Array of 0-255 RGB values associated with these points, or None.
    normals : np.ndarray
        Array of normal vectors associated with each point, or None.
    attr : np.ndarray
        Array of float32 values associated with these points, or None
    attr_names : list 
        List containing names for each of the passed attributes, or None.
    faces : np.ndarray, optional
        Array of triangular faces (F x 3) with vertex indices, or None.
    """

    # make directories if need be
    os.makedirs(os.path.dirname( path ), exist_ok=True )

    try:
        from plyfile import PlyData, PlyElement
    except:
        assert False, "Please install plyfile (`pip install plyfile`) to export to PLY."

    sfmt='f4' # use float32 precision

    # create structured data arrays and derived PlyElements
    vertex = np.array(list(zip(xyz[:, 0], xyz[:, 1], xyz[:, 2])),
                      dtype=[('x', 'double'), ('y', 'double'), ('z', 'double')])
    ply = [PlyElement.describe(vertex, 'vertices')]

    # create RGB elements
    if rgb is not None:
        if (np.max(rgb) <= 1):
            irgb = np.clip((rgb * 255),0,255).astype(np.uint8)
        else:
            irgb = np.clip(rgb,0,255).astype(np.uint8)

        # convert to structured arrays and create elements
        irgb = np.array(list(zip(irgb[:, 0], irgb[:, 1], irgb[:, 2])),
                        dtype=[('r', 'u1'), ('g', 'u1'), ('b', 'u1')])
        ply.append(PlyElement.describe(irgb, 'color'))  # create ply elements

    # normal vectors
    if normals is not None:
        # convert to structured arrays
        norm = np.array(list(zip(normals[:, 0], normals[:, 1], normals[:, 2])),
                        dtype=[('x', 'f4'), ('y', 'f4'), ('z', 'f4')])
        ply.append(PlyElement.describe(norm, 'normals'))  # create ply elements

    # attributes
    if attr is not None:
        if names is None:
            names = ["SF%d"%(i+1) for i in range(attr.shape[-1])]
        
        # map scalar fields to required type and build data arrays
        data = attr.astype(np.float32)
        for b in range(data.shape[-1]):
            n = names[b].strip().replace(' ', '_') #remove spaces from n
            if 'scalar' in n: #name already includes 'scalar'?
                ply.append(PlyElement.describe(data[:, b].astype([('%s' % n, sfmt)]), '%s' % n))
            else: #otherwise prepend it (so CloudCompare recognises this as a scalar field).
                ply.append(PlyElement.describe(data[:, b].astype([('scalar_%s' % n, sfmt)]), 'scalar_%s' % n))
    
    # Append faces if present
    if faces is not None and len(faces) > 0:
        faces = np.asarray(faces, dtype=np.int32)
        face_data = np.array(
            [(list(face),) for face in faces],
            dtype=[('vertex_indices', 'i4', (3,))]
        )
        ply.append(PlyElement.describe(face_data, 'face'))
    
    PlyData(ply).write(path) # and, finally, write everything :-) 

Write a point cloud and associated RGB and scalar fields to .ply.

Parameters

Path : str
File path for the created (or overwritten) .ply file
xyz : np.ndarray
Array of xyz points to add to the PLY file
rgb : np.ndarray
Array of 0-255 RGB values associated with these points, or None.
normals : np.ndarray
Array of normal vectors associated with each point, or None.
attr : np.ndarray
Array of float32 values associated with these points, or None
attr_names : list
List containing names for each of the passed attributes, or None.
faces : np.ndarray, optional
Array of triangular faces (F x 3) with vertex indices, or None.