| import io |
| import struct |
| from contextlib import contextmanager |
| from typing import BinaryIO, Iterator, Optional |
|
|
| import numpy as np |
|
|
|
|
| def write_ply( |
| raw_f: BinaryIO, |
| coords: np.ndarray, |
| rgb: Optional[np.ndarray] = None, |
| faces: Optional[np.ndarray] = None, |
| ): |
| """ |
| Write a PLY file for a mesh or a point cloud. |
| |
| :param coords: an [N x 3] array of floating point coordinates. |
| :param rgb: an [N x 3] array of vertex colors, in the range [0.0, 1.0]. |
| :param faces: an [N x 3] array of triangles encoded as integer indices. |
| """ |
| with buffered_writer(raw_f) as f: |
| f.write(b"ply\n") |
| f.write(b"format binary_little_endian 1.0\n") |
| f.write(bytes(f"element vertex {len(coords)}\n", "ascii")) |
| f.write(b"property float x\n") |
| f.write(b"property float y\n") |
| f.write(b"property float z\n") |
| if rgb is not None: |
| f.write(b"property uchar red\n") |
| f.write(b"property uchar green\n") |
| f.write(b"property uchar blue\n") |
| if faces is not None: |
| f.write(bytes(f"element face {len(faces)}\n", "ascii")) |
| f.write(b"property list uchar int vertex_index\n") |
| f.write(b"end_header\n") |
|
|
| if rgb is not None: |
| rgb = (rgb * 255.499).round().astype(int) |
| vertices = [ |
| (*coord, *rgb) |
| for coord, rgb in zip( |
| coords.tolist(), |
| rgb.tolist(), |
| ) |
| ] |
| format = struct.Struct("<3f3B") |
| for item in vertices: |
| f.write(format.pack(*item)) |
| else: |
| format = struct.Struct("<3f") |
| for vertex in coords.tolist(): |
| f.write(format.pack(*vertex)) |
|
|
| if faces is not None: |
| format = struct.Struct("<B3I") |
| for tri in faces.tolist(): |
| f.write(format.pack(len(tri), *tri)) |
|
|
|
|
| @contextmanager |
| def buffered_writer(raw_f: BinaryIO) -> Iterator[io.BufferedIOBase]: |
| if isinstance(raw_f, io.BufferedIOBase): |
| yield raw_f |
| else: |
| f = io.BufferedWriter(raw_f) |
| yield f |
| f.flush() |
|
|