Kyle Pearson commited on
Commit
0b9c87f
·
1 Parent(s): d8e93bd

decimate tool

Browse files
Files changed (1) hide show
  1. decimate.py +171 -0
decimate.py ADDED
@@ -0,0 +1,171 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ # filepath: decimate_splat.py
3
+ """
4
+ Decimate a Gaussian Splat PLY file and output as PLY or SPLAT format.
5
+ """
6
+
7
+ import argparse
8
+ import numpy as np
9
+ from io import BytesIO
10
+ from pathlib import Path
11
+ from plyfile import PlyData, PlyElement
12
+
13
+
14
+ def load_gaussian_ply(ply_file_path: str) -> PlyData:
15
+ """Load a Gaussian splat PLY file."""
16
+ return PlyData.read(ply_file_path)
17
+
18
+
19
+ def compute_importance_scores(vert) -> np.ndarray:
20
+ """
21
+ Compute importance scores for each Gaussian.
22
+ Higher scores = more important (larger and more opaque).
23
+ """
24
+ scales = np.exp(vert["scale_0"] + vert["scale_1"] + vert["scale_2"])
25
+ opacities = 1 / (1 + np.exp(-vert["opacity"]))
26
+ return scales * opacities
27
+
28
+
29
+ def decimate_ply(plydata: PlyData, keep_ratio: float) -> PlyData:
30
+ """
31
+ Decimate the PLY data by keeping only a fraction of the Gaussians.
32
+ Keeps the most important Gaussians based on scale and opacity.
33
+ """
34
+ vert = plydata["vertex"]
35
+ total_points = len(vert.data)
36
+ keep_count = max(1, int(total_points * keep_ratio))
37
+
38
+ # Compute importance and get indices of top Gaussians
39
+ importance = compute_importance_scores(vert)
40
+ sorted_indices = np.argsort(-importance)[:keep_count]
41
+
42
+ # Sort indices to maintain some spatial coherence
43
+ sorted_indices = np.sort(sorted_indices)
44
+
45
+ # Create new vertex data with only kept points
46
+ new_vertex_data = vert.data[sorted_indices]
47
+
48
+ # Create new PlyElement and PlyData
49
+ new_vertex_element = PlyElement.describe(new_vertex_data, "vertex")
50
+ new_plydata = PlyData([new_vertex_element])
51
+
52
+ return new_plydata
53
+
54
+
55
+ def convert_ply_to_splat(plydata: PlyData) -> bytes:
56
+ """
57
+ Convert PLY data to SPLAT format for the antimatter15 viewer.
58
+ Returns the splat data as bytes.
59
+ """
60
+ vert = plydata["vertex"]
61
+
62
+ sorted_indices = np.argsort(
63
+ -np.exp(vert["scale_0"] + vert["scale_1"] + vert["scale_2"])
64
+ / (1 + np.exp(-vert["opacity"]))
65
+ )
66
+
67
+ buffer = BytesIO()
68
+ for idx in sorted_indices:
69
+ v = plydata["vertex"][idx]
70
+ position = np.array([v["x"], v["y"], v["z"]], dtype=np.float32)
71
+ scales = np.exp(
72
+ np.array([v["scale_0"], v["scale_1"], v["scale_2"]], dtype=np.float32)
73
+ )
74
+ color = np.array([
75
+ 0.5 + 0.28209479177387814 * v["f_dc_0"],
76
+ 0.5 + 0.28209479177387814 * v["f_dc_1"],
77
+ 0.5 + 0.28209479177387814 * v["f_dc_2"],
78
+ 1 / (1 + np.exp(-v["opacity"])),
79
+ ])
80
+ rot = np.array([v["rot_0"], v["rot_1"], v["rot_2"], v["rot_3"]], dtype=np.float32)
81
+ buffer.write(position.tobytes())
82
+ buffer.write(scales.tobytes())
83
+ buffer.write((color * 255).clip(0, 255).astype(np.uint8).tobytes())
84
+ buffer.write(
85
+ ((rot / np.linalg.norm(rot)) * 128 + 128).clip(0, 255).astype(np.uint8).tobytes()
86
+ )
87
+
88
+ return buffer.getvalue()
89
+
90
+
91
+ def main():
92
+ parser = argparse.ArgumentParser(
93
+ description="Decimate a Gaussian Splat PLY file and output as PLY or SPLAT format."
94
+ )
95
+ parser.add_argument(
96
+ "input",
97
+ type=str,
98
+ help="Input PLY file path"
99
+ )
100
+ parser.add_argument(
101
+ "-o", "--output",
102
+ type=str,
103
+ help="Output file path (default: input_decimated.ply or .splat)"
104
+ )
105
+ parser.add_argument(
106
+ "-r", "--ratio",
107
+ type=float,
108
+ default=0.5,
109
+ help="Ratio of points to keep (0.0-1.0, default: 0.5)"
110
+ )
111
+ parser.add_argument(
112
+ "-f", "--format",
113
+ type=str,
114
+ choices=["ply", "splat"],
115
+ default="ply",
116
+ help="Output format: 'ply' or 'splat' (default: ply)"
117
+ )
118
+ parser.add_argument(
119
+ "-v", "--verbose",
120
+ action="store_true",
121
+ help="Print verbose output"
122
+ )
123
+
124
+ args = parser.parse_args()
125
+
126
+ # Validate ratio
127
+ if not 0.0 < args.ratio <= 1.0:
128
+ parser.error("Ratio must be between 0.0 (exclusive) and 1.0 (inclusive)")
129
+
130
+ # Determine output path
131
+ input_path = Path(args.input)
132
+ if args.output:
133
+ output_path = Path(args.output)
134
+ else:
135
+ suffix = ".splat" if args.format == "splat" else ".ply"
136
+ output_path = input_path.with_stem(f"{input_path.stem}_decimated").with_suffix(suffix)
137
+
138
+ if args.verbose:
139
+ print(f"Loading: {args.input}")
140
+
141
+ # Load PLY file
142
+ plydata = load_gaussian_ply(args.input)
143
+ original_count = len(plydata["vertex"].data)
144
+
145
+ if args.verbose:
146
+ print(f"Original point count: {original_count:,}")
147
+
148
+ # Decimate
149
+ decimated_plydata = decimate_ply(plydata, args.ratio)
150
+ new_count = len(decimated_plydata["vertex"].data)
151
+
152
+ if args.verbose:
153
+ print(f"Decimated point count: {new_count:,} ({args.ratio * 100:.1f}%)")
154
+
155
+ # Write output
156
+ if args.format == "splat":
157
+ splat_data = convert_ply_to_splat(decimated_plydata)
158
+ with open(output_path, "wb") as f:
159
+ f.write(splat_data)
160
+ else:
161
+ decimated_plydata.write(str(output_path))
162
+
163
+ if args.verbose:
164
+ print(f"Saved to: {output_path}")
165
+ else:
166
+ print(f"Decimated {original_count:,} → {new_count:,} points, saved to {output_path}")
167
+
168
+
169
+ if __name__ == "__main__":
170
+ main()
171
+