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