| | from typing import Tuple, Dict |
| | import numpy as np |
| | from trimesh import grouping, util, remesh |
| | import struct |
| | import re |
| | from plyfile import PlyData, PlyElement |
| |
|
| |
|
| | def read_ply(filename): |
| | """ |
| | Read a PLY file and return vertices, triangle faces, and quad faces. |
| | |
| | Args: |
| | filename (str): The file path to read from. |
| | |
| | Returns: |
| | vertices (np.ndarray): Array of shape [N, 3] containing vertex positions. |
| | tris (np.ndarray): Array of shape [M, 3] containing triangle face indices (empty if none). |
| | quads (np.ndarray): Array of shape [K, 4] containing quad face indices (empty if none). |
| | """ |
| | with open(filename, 'rb') as f: |
| | |
| | header_bytes = b"" |
| | while True: |
| | line = f.readline() |
| | if not line: |
| | raise ValueError("PLY header not found") |
| | header_bytes += line |
| | if b"end_header" in line: |
| | break |
| | header = header_bytes.decode('utf-8') |
| | |
| | |
| | is_ascii = "ascii" in header |
| | |
| | |
| | vertex_match = re.search(r'element vertex (\d+)', header) |
| | if vertex_match: |
| | num_vertices = int(vertex_match.group(1)) |
| | else: |
| | raise ValueError("Vertex count not found in header") |
| | |
| | face_match = re.search(r'element face (\d+)', header) |
| | if face_match: |
| | num_faces = int(face_match.group(1)) |
| | else: |
| | raise ValueError("Face count not found in header") |
| | |
| | vertices = [] |
| | tris = [] |
| | quads = [] |
| | |
| | if is_ascii: |
| | |
| | for _ in range(num_vertices): |
| | line = f.readline().decode('utf-8').strip() |
| | if not line: |
| | continue |
| | parts = line.split() |
| | vertices.append([float(parts[0]), float(parts[1]), float(parts[2])]) |
| | |
| | |
| | for _ in range(num_faces): |
| | line = f.readline().decode('utf-8').strip() |
| | if not line: |
| | continue |
| | parts = line.split() |
| | count = int(parts[0]) |
| | indices = list(map(int, parts[1:])) |
| | if count == 3: |
| | tris.append(indices) |
| | elif count == 4: |
| | quads.append(indices) |
| | else: |
| | |
| | pass |
| | else: |
| | |
| | |
| | for _ in range(num_vertices): |
| | data = f.read(12) |
| | if len(data) < 12: |
| | raise ValueError("Insufficient vertex data") |
| | v = struct.unpack('<fff', data) |
| | vertices.append(v) |
| | |
| | |
| | for _ in range(num_faces): |
| | |
| | count_data = f.read(1) |
| | if len(count_data) < 1: |
| | raise ValueError("Failed to read face vertex count") |
| | count = struct.unpack('<B', count_data)[0] |
| | if count == 3: |
| | data = f.read(12) |
| | if len(data) < 12: |
| | raise ValueError("Insufficient data for triangle face") |
| | indices = struct.unpack('<3i', data) |
| | tris.append(indices) |
| | elif count == 4: |
| | data = f.read(16) |
| | if len(data) < 16: |
| | raise ValueError("Insufficient data for quad face") |
| | indices = struct.unpack('<4i', data) |
| | quads.append(indices) |
| | else: |
| | |
| | data = f.read(count * 4) |
| | |
| | raise ValueError(f"Unsupported face with {count} vertices") |
| | |
| | |
| | vertices = np.array(vertices, dtype=np.float32) |
| | tris = np.array(tris, dtype=np.int32) if len(tris) > 0 else np.empty((0, 3), dtype=np.int32) |
| | quads = np.array(quads, dtype=np.int32) if len(quads) > 0 else np.empty((0, 4), dtype=np.int32) |
| | |
| | return vertices, tris, quads |
| |
|
| |
|
| | def write_ply( |
| | filename: str, |
| | vertices: np.ndarray, |
| | tris: np.ndarray, |
| | quads: np.ndarray, |
| | vertex_colors: np.ndarray = None, |
| | ascii: bool = False |
| | ): |
| | """ |
| | Write a mesh to a PLY file, with the option to save in ASCII or binary format, |
| | and optional per-vertex colors. |
| | |
| | Args: |
| | filename (str): The filename to write to. |
| | vertices (np.ndarray): [N, 3] The vertex positions. |
| | tris (np.ndarray): [M, 3] The triangle indices. |
| | quads (np.ndarray): [K, 4] The quad indices. |
| | vertex_colors (np.ndarray, optional): [N, 3] or [N, 4] UInt8 colors for each vertex (RGB or RGBA). |
| | ascii (bool): If True, write in ASCII format; otherwise binary little-endian. |
| | """ |
| | import struct |
| |
|
| | num_vertices = len(vertices) |
| | num_faces = len(tris) + len(quads) |
| |
|
| | |
| | header_lines = [ |
| | "ply", |
| | f"format {'ascii 1.0' if ascii else 'binary_little_endian 1.0'}", |
| | f"element vertex {num_vertices}", |
| | "property float x", |
| | "property float y", |
| | "property float z", |
| | ] |
| |
|
| | |
| | has_color = vertex_colors is not None |
| | if has_color: |
| | |
| | header_lines += [ |
| | "property uchar red", |
| | "property uchar green", |
| | "property uchar blue", |
| | ] |
| | |
| | if vertex_colors.shape[1] == 4: |
| | header_lines.append("property uchar alpha") |
| |
|
| | header_lines += [ |
| | f"element face {num_faces}", |
| | "property list uchar int vertex_index", |
| | "end_header", |
| | "" |
| | ] |
| | header = "\n".join(header_lines) |
| |
|
| | mode = 'w' if ascii else 'wb' |
| | with open(filename, mode) as f: |
| | |
| | if ascii: |
| | f.write(header) |
| | else: |
| | f.write(header.encode('utf-8')) |
| |
|
| | |
| | for i, v in enumerate(vertices): |
| | if ascii: |
| | line = f"{v[0]} {v[1]} {v[2]}" |
| | if has_color: |
| | col = vertex_colors[i] |
| | line += ' ' + ' '.join(str(int(c)) for c in col) |
| | f.write(line + '\n') |
| | else: |
| | |
| | f.write(struct.pack('<fff', *v)) |
| | if has_color: |
| | col = vertex_colors[i] |
| | |
| | if col.shape[0] == 3: |
| | f.write(struct.pack('<BBB', *col)) |
| | else: |
| | f.write(struct.pack('<BBBB', *col)) |
| |
|
| | |
| | if ascii: |
| | for tri in tris: |
| | f.write(f"3 {tri[0]} {tri[1]} {tri[2]}\n") |
| | for quad in quads: |
| | f.write(f"4 {quad[0]} {quad[1]} {quad[2]} {quad[3]}\n") |
| | else: |
| | for tri in tris: |
| | f.write(struct.pack('<B3i', 3, *tri)) |
| | for quad in quads: |
| | f.write(struct.pack('<B4i', 4, *quad)) |
| | |
| |
|
| | def write_pbr_ply( |
| | filename: str, |
| | vertices: np.ndarray, |
| | faces: np.ndarray, |
| | base_color: np.ndarray, |
| | metallic: np.ndarray, |
| | roughness: np.ndarray, |
| | alpha: np.ndarray, |
| | ascii: bool = False |
| | ): |
| | """ |
| | Write a mesh to a PLY file, with the option to save in ASCII or binary format, |
| | and optional per-vertex colors. |
| | |
| | Args: |
| | filename (str): The filename to write to. |
| | vertices (np.ndarray): [N, 3] The vertex positions. |
| | faces (np.ndarray): [M, 3] The triangle indices. |
| | base_color (np.ndarray): [N, 3] UInt8 colors for each vertex (RGB). |
| | metallic (np.ndarray): [N] UInt8 values for metallicness. |
| | roughness (np.ndarray): [N] UInt8 values for roughness. |
| | alpha (np.ndarray): [N] UInt8 values for alpha. |
| | ascii (bool): If True, write in ASCII format; otherwise binary little-endian. |
| | """ |
| | vertex_dtype = [ |
| | ('x', 'f4'), ('y', 'f4'), ('z', 'f4'), |
| | ('red', 'u1'), ('green', 'u1'), ('blue', 'u1'), |
| | ('metallic', 'u1'), ('roughness', 'u1'), ('alpha', 'u1') |
| | ] |
| | |
| | vertex_data = np.empty(len(vertices), dtype=vertex_dtype) |
| | vertex_data['x'] = vertices[:, 0] |
| | vertex_data['y'] = vertices[:, 1] |
| | vertex_data['z'] = vertices[:, 2] |
| | vertex_data['red'] = base_color[:, 0] |
| | vertex_data['green'] = base_color[:, 1] |
| | vertex_data['blue'] = base_color[:, 2] |
| | vertex_data['metallic'] = metallic |
| | vertex_data['roughness'] = roughness |
| | vertex_data['alpha'] = alpha |
| | |
| | face_dtype = [ |
| | ('vertex_indices', 'i4', (3,)) |
| | ] |
| | |
| | face_data = np.empty(len(faces), dtype=face_dtype) |
| | face_data['vertex_indices'] = faces |
| | |
| | ply_data = PlyData([ |
| | PlyElement.describe(vertex_data,'vertex'), |
| | PlyElement.describe(face_data, 'face'), |
| | ], text=ascii) |
| | ply_data.write(filename) |
| |
|