Kyle Pearson
decimate tool
0b9c87f
Raw
History Blame Contribute Delete
5.25 kB
#!/usr/bin/env python3
# filepath: decimate_splat.py
"""
Decimate a Gaussian Splat PLY file and output as PLY or SPLAT format.
"""
import argparse
import numpy as np
from io import BytesIO
from pathlib import Path
from plyfile import PlyData, PlyElement
def load_gaussian_ply(ply_file_path: str) -> PlyData:
"""Load a Gaussian splat PLY file."""
return PlyData.read(ply_file_path)
def compute_importance_scores(vert) -> np.ndarray:
"""
Compute importance scores for each Gaussian.
Higher scores = more important (larger and more opaque).
"""
scales = np.exp(vert["scale_0"] + vert["scale_1"] + vert["scale_2"])
opacities = 1 / (1 + np.exp(-vert["opacity"]))
return scales * opacities
def decimate_ply(plydata: PlyData, keep_ratio: float) -> PlyData:
"""
Decimate the PLY data by keeping only a fraction of the Gaussians.
Keeps the most important Gaussians based on scale and opacity.
"""
vert = plydata["vertex"]
total_points = len(vert.data)
keep_count = max(1, int(total_points * keep_ratio))
# Compute importance and get indices of top Gaussians
importance = compute_importance_scores(vert)
sorted_indices = np.argsort(-importance)[:keep_count]
# Sort indices to maintain some spatial coherence
sorted_indices = np.sort(sorted_indices)
# Create new vertex data with only kept points
new_vertex_data = vert.data[sorted_indices]
# Create new PlyElement and PlyData
new_vertex_element = PlyElement.describe(new_vertex_data, "vertex")
new_plydata = PlyData([new_vertex_element])
return new_plydata
def convert_ply_to_splat(plydata: PlyData) -> bytes:
"""
Convert PLY data to SPLAT format for the antimatter15 viewer.
Returns the splat data as bytes.
"""
vert = plydata["vertex"]
sorted_indices = np.argsort(
-np.exp(vert["scale_0"] + vert["scale_1"] + vert["scale_2"])
/ (1 + np.exp(-vert["opacity"]))
)
buffer = BytesIO()
for idx in sorted_indices:
v = plydata["vertex"][idx]
position = np.array([v["x"], v["y"], v["z"]], dtype=np.float32)
scales = np.exp(
np.array([v["scale_0"], v["scale_1"], v["scale_2"]], dtype=np.float32)
)
color = np.array([
0.5 + 0.28209479177387814 * v["f_dc_0"],
0.5 + 0.28209479177387814 * v["f_dc_1"],
0.5 + 0.28209479177387814 * v["f_dc_2"],
1 / (1 + np.exp(-v["opacity"])),
])
rot = np.array([v["rot_0"], v["rot_1"], v["rot_2"], v["rot_3"]], dtype=np.float32)
buffer.write(position.tobytes())
buffer.write(scales.tobytes())
buffer.write((color * 255).clip(0, 255).astype(np.uint8).tobytes())
buffer.write(
((rot / np.linalg.norm(rot)) * 128 + 128).clip(0, 255).astype(np.uint8).tobytes()
)
return buffer.getvalue()
def main():
parser = argparse.ArgumentParser(
description="Decimate a Gaussian Splat PLY file and output as PLY or SPLAT format."
)
parser.add_argument(
"input",
type=str,
help="Input PLY file path"
)
parser.add_argument(
"-o", "--output",
type=str,
help="Output file path (default: input_decimated.ply or .splat)"
)
parser.add_argument(
"-r", "--ratio",
type=float,
default=0.5,
help="Ratio of points to keep (0.0-1.0, default: 0.5)"
)
parser.add_argument(
"-f", "--format",
type=str,
choices=["ply", "splat"],
default="ply",
help="Output format: 'ply' or 'splat' (default: ply)"
)
parser.add_argument(
"-v", "--verbose",
action="store_true",
help="Print verbose output"
)
args = parser.parse_args()
# Validate ratio
if not 0.0 < args.ratio <= 1.0:
parser.error("Ratio must be between 0.0 (exclusive) and 1.0 (inclusive)")
# Determine output path
input_path = Path(args.input)
if args.output:
output_path = Path(args.output)
else:
suffix = ".splat" if args.format == "splat" else ".ply"
output_path = input_path.with_stem(f"{input_path.stem}_decimated").with_suffix(suffix)
if args.verbose:
print(f"Loading: {args.input}")
# Load PLY file
plydata = load_gaussian_ply(args.input)
original_count = len(plydata["vertex"].data)
if args.verbose:
print(f"Original point count: {original_count:,}")
# Decimate
decimated_plydata = decimate_ply(plydata, args.ratio)
new_count = len(decimated_plydata["vertex"].data)
if args.verbose:
print(f"Decimated point count: {new_count:,} ({args.ratio * 100:.1f}%)")
# Write output
if args.format == "splat":
splat_data = convert_ply_to_splat(decimated_plydata)
with open(output_path, "wb") as f:
f.write(splat_data)
else:
decimated_plydata.write(str(output_path))
if args.verbose:
print(f"Saved to: {output_path}")
else:
print(f"Decimated {original_count:,}{new_count:,} points, saved to {output_path}")
if __name__ == "__main__":
main()