import numpy as np import os from plyfile import PlyElement, PlyData import open3d as o3d def get_f_dc(colors): return RGB2SH(colors)[:, :, np.newaxis] def get_f_rest(points, max_sh_degree=3): f_rest_shape = (points.shape[0], (max_sh_degree + 1) ** 2 - 1, 3) return np.zeros(f_rest_shape) def get_opacity(points): return inverse_sigmoid(0.5 * np.ones((points.shape[0], 1))) def get_scales(points): scales = np.ones((points.shape[0], 3)) * 0.0015 scales[:, 2] = 1e-6 return np.log(scales) def get_rotation(normals): if normals is not None and np.any(normals): return normal2rotation(normals) else: return np.zeros((normals.shape[0], 4)) def RGB2SH(rgb): return (rgb - 0.5) / 0.28209479177387814 def inverse_sigmoid(x): return np.log(x / (1 - x)) def normal2rotation(n): n = n / np.linalg.norm(n, axis=1, keepdims=True) w0 = np.tile([[1, 0, 0]], (n.shape[0], 1)) R0 = w0 - np.sum(w0 * n, axis=1, keepdims=True) * n R0 *= np.sign(R0[:, :1]) R0 /= np.linalg.norm(R0, axis=1, keepdims=True) R1 = np.cross(n, R0) R1 *= np.sign(R1[:, 1:2]) * np.sign(n[:, 2:]) R = np.stack([R0, R1, n], axis=-1) q = rotmat2quaternion(R) return q def rotmat2quaternion(R, normalize=False): tr = R[:, 0, 0] + R[:, 1, 1] + R[:, 2, 2] + 1e-6 r = np.sqrt(1 + tr) / 2 q = np.stack([ r, (R[:, 2, 1] - R[:, 1, 2]) / (4 * r), (R[:, 0, 2] - R[:, 2, 0]) / (4 * r), (R[:, 1, 0] - R[:, 0, 1]) / (4 * r) ], axis=-1) if normalize: q /= np.linalg.norm(q, axis=-1, keepdims=True) return q def point2gs(path, pcd, scale=None, max_sh_degree=1): # Ensure the directory exists os.makedirs(os.path.dirname(path), exist_ok=True) # Get point cloud data xyz = np.asarray(pcd.points) normals = np.asarray(pcd.normals) if pcd.has_normals() else np.zeros_like(xyz) colors = np.asarray(pcd.colors) if pcd.has_colors() else np.ones_like(xyz) # Generate additional attributes f_dc = get_f_dc(colors).reshape(xyz.shape[0], -1) f_rest = get_f_rest(xyz, max_sh_degree).reshape(xyz.shape[0], -1) opacities = get_opacity(xyz) if scale is not None: scale = np.log(scale) else: scale = get_scales(xyz) rotation = get_rotation(normals) # Construct list of attributes attribute_names = ['x', 'y', 'z', 'nx', 'ny', 'nz'] attribute_names.extend([f'f_dc_{i}' for i in range(f_dc.shape[-1])]) attribute_names.extend([f'f_rest_{i}' for i in range(f_rest.shape[-1])]) attribute_names.append('opacity') attribute_names.extend([f'scale_{i}' for i in range(scale.shape[1])]) attribute_names.extend([f'rot_{i}' for i in range(rotation.shape[1])]) # Create dtype for numpy structured array dtype_full = [(attribute, 'f4') for attribute in attribute_names] # Combine all attributes attributes = np.concatenate(( xyz, normals, f_dc, f_rest, opacities, scale, rotation ), axis=1) # Ensure attributes match the dtype assert attributes.shape[1] == len(dtype_full), f"Mismatch in attribute count. Expected {len(dtype_full)}, got {attributes.shape[1]}" # Create structured array elements = np.empty(xyz.shape[0], dtype=dtype_full) elements[:] = list(map(tuple, attributes)) # Create PlyElement and save el = PlyElement.describe(elements, 'vertex') PlyData([el]).write(path)