|
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() |
|
|