Lucasgitguy-co's picture
Publish evaluated Forma3D Tiny 3 ABO continuation
3469f52 verified
Raw History Blame Contribute Delete
4.99 kB
# SPDX-License-Identifier: Apache-2.0
# Copyright 2026 Lucasgitguy-co
"""Activated Gaussian attributes and standard degree-zero 3DGS PLY IO.
Coordinates are preserved as supplied. Idol's *exported PLY* uses its y-up frame;
we do not apply Idol's internal z-up conversion a second time.
"""
from dataclasses import dataclass
from pathlib import Path
import numpy as np
from plyfile import PlyData, PlyElement
C0 = 0.28209479177387814
@dataclass
class Splats:
xyz: np.ndarray
rgb: np.ndarray
scale: np.ndarray
quat: np.ndarray # w,x,y,z
opacity: np.ndarray
def __post_init__(self):
n = len(self.xyz)
if n == 0:
raise ValueError("No Gaussians: refusing to write or train on an empty object.")
for name, width in [("xyz", 3), ("rgb", 3), ("scale", 3), ("quat", 4), ("opacity", 1)]:
a = np.asarray(getattr(self, name), dtype=np.float32).reshape(-1, width)
if a.shape != (n, width) or not np.isfinite(a).all():
raise ValueError(f"Invalid {name} shape or nonfinite values")
setattr(self, name, a)
if (self.scale <= 0).any():
raise ValueError("Gaussian scales must be positive")
if (self.opacity < 0).any() or (self.opacity > 1).any():
raise ValueError("Opacity must lie in [0,1]")
norm = np.linalg.norm(self.quat, axis=1, keepdims=True)
if (norm < 1e-8).any():
raise ValueError("Zero quaternion")
self.quat = self.quat / norm
def take(self, indices):
return Splats(**{k: getattr(self, k)[indices] for k in self.__dataclass_fields__})
def read_ply(path):
v = PlyData.read(str(path))["vertex"].data
required = ["x", "y", "z", "f_dc_0", "f_dc_1", "f_dc_2", "opacity",
"scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
if not set(required).issubset(v.dtype.names):
raise ValueError("Expected a Gaussian-splat PLY, not a mesh or ordinary point-cloud PLY")
col = lambda names: np.column_stack([v[k] for k in names]).astype(np.float32)
return Splats(col(["x", "y", "z"]), 0.5 + C0 * col([f"f_dc_{i}" for i in range(3)]),
np.exp(np.clip(col([f"scale_{i}" for i in range(3)]), -20, 5)),
col([f"rot_{i}" for i in range(4)]),
1 / (1 + np.exp(-np.clip(col(["opacity"]), -30, 30))))
def write_ply(path, splats):
path = Path(path)
path.parent.mkdir(parents=True, exist_ok=True)
names = ["x", "y", "z", "nx", "ny", "nz", "f_dc_0", "f_dc_1", "f_dc_2", "opacity",
"scale_0", "scale_1", "scale_2", "rot_0", "rot_1", "rot_2", "rot_3"]
opacity = np.clip(splats.opacity, 1e-6, 1 - 1e-6)
values = np.concatenate([splats.xyz, np.zeros_like(splats.xyz), (splats.rgb - 0.5) / C0,
np.log(opacity / (1 - opacity)), np.log(splats.scale), splats.quat], axis=1)
table = np.empty(len(values), dtype=[(k, "f4") for k in names])
for i, name in enumerate(names):
table[name] = values[:, i]
PlyData([PlyElement.describe(table, "vertex")], text=False).write(str(path))
def rotation_matrices(q):
w, x, y, z = q.T
return np.stack([1-2*(y*y+z*z), 2*(x*y-z*w), 2*(x*z+y*w),
2*(x*y+z*w), 1-2*(x*x+z*z), 2*(y*z-x*w),
2*(x*z-y*w), 2*(y*z+x*w), 1-2*(x*x+y*y)], axis=-1).reshape(-1, 3, 3)
def preview(splats, size=256, yaw=0.0):
"""CPU orthographic Gaussian preview; debugging only, not an evaluation renderer."""
from PIL import Image
c, s = np.cos(yaw), np.sin(yaw)
view = np.array([[c, 0, s], [0, 1, 0], [-s, 0, c]], dtype=np.float32)
xyz = splats.xyz @ view.T
rot = rotation_matrices(splats.quat)
cov = (rot * (splats.scale ** 2)[:, None, :]) @ rot.transpose(0, 2, 1)
cov = view[None] @ cov @ view.T[None]
pixels = size / 1.25
canvas = np.zeros((size, size, 3), dtype=np.float32)
# Back to front, camera on +z. Image y points down.
for i in np.argsort(xyz[:, 2]):
center = np.array([xyz[i, 0], -xyz[i, 1]]) * pixels + size / 2
flip = np.diag([1., -1.])
covariance = flip @ cov[i, :2, :2] @ flip * pixels ** 2 + np.eye(2) * 0.3
radius = min(size, int(np.ceil(3 * np.sqrt(np.linalg.eigvalsh(covariance).max()))))
lo = np.maximum(0, np.floor(center - radius).astype(int))
hi = np.minimum(size, np.ceil(center + radius + 1).astype(int))
if np.any(hi <= lo):
continue
yy, xx = np.mgrid[lo[1]:hi[1], lo[0]:hi[0]]
delta = np.stack([xx - center[0], yy - center[1]], axis=-1)
exponent = np.einsum("...i,ij,...j->...", delta, np.linalg.inv(covariance), delta)
alpha = (splats.opacity[i, 0] * np.exp(-0.5 * exponent)).clip(0, .99)[..., None]
patch = canvas[lo[1]:hi[1], lo[0]:hi[0]]
patch[:] = patch * (1-alpha) + np.clip(splats.rgb[i], 0, 1) * alpha
return Image.fromarray((canvas.clip(0, 1) * 255).astype(np.uint8))