Download decimate.py from pearsonkyle/Gaussian-Splat-Generator: direct link, hf CLI and curl.
- Browser
- Download file 5.25 kB
-
https://huggingface.co/spaces/pearsonkyle/Gaussian-Splat-Generator/resolve/main/decimate.py
- Command line
-
hf download hf://spaces/pearsonkyle/Gaussian-Splat-Generator/decimate.py
-
curl -L -o decimate.py https://huggingface.co/spaces/pearsonkyle/Gaussian-Splat-Generator/resolve/main/decimate.py
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() | |