gsply 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
gsply/__init__.py ADDED
@@ -0,0 +1,55 @@
1
+ """gsply - Fast Gaussian Splatting PLY I/O Library
2
+
3
+ A pure Python library for ultra-fast reading and writing of Gaussian splatting
4
+ PLY files in both uncompressed and compressed formats.
5
+
6
+ Basic Usage:
7
+ >>> import gsply
8
+ >>>
9
+ >>> # Read PLY file (auto-detect format) - returns GSData
10
+ >>> data = gsply.plyread("model.ply")
11
+ >>> print(f"Loaded {data.means.shape[0]} Gaussians")
12
+ >>> positions = data.means
13
+ >>> colors = data.sh0
14
+ >>>
15
+ >>> # Or unpack if needed
16
+ >>> means, scales, quats, opacities, sh0, shN = data[:6]
17
+ >>>
18
+ >>> # Zero-copy reading (1.65x faster, default behavior)
19
+ >>> data = gsply.plyread("model.ply", fast=True)
20
+ >>>
21
+ >>> # Safe copy reading (if you need independent arrays)
22
+ >>> data = gsply.plyread("model.ply", fast=False)
23
+ >>>
24
+ >>> # Write uncompressed PLY file
25
+ >>> gsply.plywrite("output.ply", data.means, data.scales, data.quats,
26
+ ... data.opacities, data.sh0, data.shN)
27
+ >>>
28
+ >>> # Write compressed PLY file (saves as "output.compressed.ply")
29
+ >>> gsply.plywrite("output.ply", data.means, data.scales, data.quats,
30
+ ... data.opacities, data.sh0, data.shN, compressed=True)
31
+ >>>
32
+ >>> # Detect format
33
+ >>> is_compressed, sh_degree = gsply.detect_format("model.ply")
34
+
35
+ Features:
36
+ - Zero dependencies (pure Python + numpy)
37
+ - SH degrees 0-3 support (14, 23, 38, 59 properties)
38
+ - Compressed format (PlayCanvas compatible)
39
+ - Ultra-fast (~3-5ms read, ~5-10ms write)
40
+ - Zero-copy optimization (1.65x faster reads)
41
+ - Auto-format detection
42
+
43
+ Performance (50K Gaussians):
44
+ - Read uncompressed (fast=True): ~3ms (SH degree 3, zero-copy)
45
+ - Read uncompressed (fast=False): ~5ms (SH degree 3, safe copies)
46
+ - Read compressed: ~30-50ms (with decompression)
47
+ - Write uncompressed: ~5-10ms
48
+ """
49
+
50
+ from gsply.reader import plyread, GSData
51
+ from gsply.writer import plywrite
52
+ from gsply.formats import detect_format
53
+
54
+ __version__ = "0.1.0"
55
+ __all__ = ["plyread", "GSData", "plywrite", "detect_format", "__version__"]
gsply/formats.py ADDED
@@ -0,0 +1,226 @@
1
+ """Format detection and constants for Gaussian splatting PLY files."""
2
+
3
+ from pathlib import Path
4
+ from typing import Tuple, Dict, List, Optional
5
+
6
+ # Property counts by SH degree
7
+ PROPERTY_COUNTS_BY_SH_DEGREE = {
8
+ 0: 14, # xyz(3) + f_dc(3) + opacity(1) + scales(3) + quats(4)
9
+ 1: 23, # +9 f_rest
10
+ 2: 38, # +24 f_rest
11
+ 3: 59, # +45 f_rest
12
+ }
13
+
14
+ # Expected property names in order for each SH degree
15
+ EXPECTED_PROPERTIES_BY_SH_DEGREE: Dict[int, List[str]] = {
16
+ 0: [
17
+ "x", "y", "z",
18
+ "f_dc_0", "f_dc_1", "f_dc_2",
19
+ "opacity",
20
+ "scale_0", "scale_1", "scale_2",
21
+ "rot_0", "rot_1", "rot_2", "rot_3"
22
+ ],
23
+ 1: [
24
+ "x", "y", "z",
25
+ "f_dc_0", "f_dc_1", "f_dc_2",
26
+ *[f"f_rest_{i}" for i in range(9)],
27
+ "opacity",
28
+ "scale_0", "scale_1", "scale_2",
29
+ "rot_0", "rot_1", "rot_2", "rot_3"
30
+ ],
31
+ 2: [
32
+ "x", "y", "z",
33
+ "f_dc_0", "f_dc_1", "f_dc_2",
34
+ *[f"f_rest_{i}" for i in range(24)],
35
+ "opacity",
36
+ "scale_0", "scale_1", "scale_2",
37
+ "rot_0", "rot_1", "rot_2", "rot_3"
38
+ ],
39
+ 3: [
40
+ "x", "y", "z",
41
+ "f_dc_0", "f_dc_1", "f_dc_2",
42
+ *[f"f_rest_{i}" for i in range(45)],
43
+ "opacity",
44
+ "scale_0", "scale_1", "scale_2",
45
+ "rot_0", "rot_1", "rot_2", "rot_3"
46
+ ],
47
+ }
48
+
49
+ # Compressed format constants
50
+ CHUNK_SIZE = 256
51
+ COMPRESSED_CHUNK_PROPERTIES = 18 # min/max bounds (6*3)
52
+ COMPRESSED_VERTEX_PROPERTIES = 4 # packed position, rotation, scale, color
53
+
54
+ # SH coefficient for color conversion
55
+ SH_C0 = 0.28209479177387814
56
+
57
+
58
+ def _parse_ply_header(file_path: Path) -> Tuple[Dict, int]:
59
+ """Parse PLY header to extract element definitions.
60
+
61
+ Args:
62
+ file_path: Path to PLY file
63
+
64
+ Returns:
65
+ Tuple of (elements_dict, header_size_bytes)
66
+
67
+ Raises:
68
+ ValueError: If PLY format is invalid
69
+ """
70
+ elements = {}
71
+ current_element = None
72
+ header_size = 0
73
+
74
+ with open(file_path, 'rb') as f:
75
+ line = f.readline()
76
+ header_size += len(line)
77
+
78
+ if line.strip() != b'ply':
79
+ raise ValueError("Not a valid PLY file")
80
+
81
+ while True:
82
+ line = f.readline()
83
+ header_size += len(line)
84
+ line_str = line.decode('ascii').strip()
85
+
86
+ if line_str == 'end_header':
87
+ break
88
+
89
+ parts = line_str.split()
90
+ if not parts:
91
+ continue
92
+
93
+ if parts[0] == 'element':
94
+ element_name = parts[1]
95
+ element_count = int(parts[2])
96
+ elements[element_name] = {
97
+ 'count': element_count,
98
+ 'properties': []
99
+ }
100
+ current_element = element_name
101
+
102
+ elif parts[0] == 'property' and current_element:
103
+ prop_type = parts[1]
104
+ prop_name = parts[2]
105
+ elements[current_element]['properties'].append((prop_type, prop_name))
106
+
107
+ return elements, header_size
108
+
109
+
110
+ def detect_format(file_path: str | Path) -> Tuple[bool, Optional[int]]:
111
+ """Detect PLY format type and SH degree.
112
+
113
+ Args:
114
+ file_path: Path to PLY file
115
+
116
+ Returns:
117
+ Tuple of (is_compressed, sh_degree)
118
+ - is_compressed: True if compressed format, False if uncompressed
119
+ - sh_degree: 0-3 for uncompressed, None for compressed or unknown
120
+
121
+ Example:
122
+ >>> is_compressed, sh_degree = detect_format("model.ply")
123
+ >>> if is_compressed:
124
+ ... print("Compressed format")
125
+ ... else:
126
+ ... print(f"Uncompressed SH degree {sh_degree}")
127
+ """
128
+ file_path = Path(file_path)
129
+
130
+ try:
131
+ elements, _ = _parse_ply_header(file_path)
132
+ except Exception:
133
+ return False, None
134
+
135
+ # Check for compressed format
136
+ if _is_compressed_format(elements):
137
+ return True, None
138
+
139
+ # Check for uncompressed format
140
+ if 'vertex' in elements:
141
+ vertex_props = elements['vertex']['properties']
142
+ property_count = len(vertex_props)
143
+
144
+ # Try to match against known SH degrees
145
+ for sh_degree, expected_count in PROPERTY_COUNTS_BY_SH_DEGREE.items():
146
+ if property_count == expected_count:
147
+ # Verify property names match
148
+ prop_names = [p[1] for p in vertex_props]
149
+ expected_names = EXPECTED_PROPERTIES_BY_SH_DEGREE[sh_degree]
150
+ if prop_names == expected_names:
151
+ return False, sh_degree
152
+
153
+ # Unknown format
154
+ return False, None
155
+
156
+
157
+ def _is_compressed_format(elements: Dict) -> bool:
158
+ """Check if elements dict represents compressed format.
159
+
160
+ Args:
161
+ elements: Parsed PLY header elements
162
+
163
+ Returns:
164
+ True if compressed format detected
165
+ """
166
+ # Must have chunk and vertex elements
167
+ if 'chunk' not in elements or 'vertex' not in elements:
168
+ return False
169
+
170
+ chunk_elem = elements['chunk']
171
+ vertex_elem = elements['vertex']
172
+
173
+ # Check chunk element (18 float properties)
174
+ chunk_props = chunk_elem['properties']
175
+ if len(chunk_props) != COMPRESSED_CHUNK_PROPERTIES:
176
+ return False
177
+
178
+ expected_chunk_names = [
179
+ "min_x", "min_y", "min_z", "max_x", "max_y", "max_z",
180
+ "min_scale_x", "min_scale_y", "min_scale_z",
181
+ "max_scale_x", "max_scale_y", "max_scale_z",
182
+ "min_r", "min_g", "min_b", "max_r", "max_g", "max_b"
183
+ ]
184
+
185
+ for i, (prop_type, prop_name) in enumerate(chunk_props):
186
+ if prop_type != 'float' or prop_name != expected_chunk_names[i]:
187
+ return False
188
+
189
+ # Check vertex element (4 uint properties)
190
+ vertex_props = vertex_elem['properties']
191
+ if len(vertex_props) < COMPRESSED_VERTEX_PROPERTIES:
192
+ return False
193
+
194
+ expected_vertex_names = [
195
+ "packed_position", "packed_rotation", "packed_scale", "packed_color"
196
+ ]
197
+
198
+ for i in range(COMPRESSED_VERTEX_PROPERTIES):
199
+ prop_type, prop_name = vertex_props[i]
200
+ if prop_type != 'uint' or prop_name != expected_vertex_names[i]:
201
+ return False
202
+
203
+ # Check chunk count matches splat count
204
+ num_chunks = chunk_elem['count']
205
+ num_vertices = vertex_elem['count']
206
+ expected_chunks = (num_vertices + CHUNK_SIZE - 1) // CHUNK_SIZE
207
+
208
+ if num_chunks != expected_chunks:
209
+ return False
210
+
211
+ return True
212
+
213
+
214
+ def get_sh_degree_from_property_count(property_count: int) -> Optional[int]:
215
+ """Get SH degree from property count.
216
+
217
+ Args:
218
+ property_count: Number of properties in vertex element
219
+
220
+ Returns:
221
+ SH degree (0-3) or None if unknown
222
+ """
223
+ for sh_degree, expected_count in PROPERTY_COUNTS_BY_SH_DEGREE.items():
224
+ if property_count == expected_count:
225
+ return sh_degree
226
+ return None
gsply/py.typed ADDED
@@ -0,0 +1 @@
1
+ # Marker file for PEP 561 - indicates this package supports type checking